mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Refactor Query/Policy UX (#4334)
This commit is contained in:
parent
52db2812be
commit
02fa778788
15 changed files with 202 additions and 118 deletions
1
changes/issue-3432-2075-improve-live-ui
Normal file
1
changes/issue-3432-2075-improve-live-ui
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Improved UX around live query and live policy
|
||||
|
|
@ -257,7 +257,7 @@ describe(
|
|||
cy.findByText(/run query/i).click({ force: true });
|
||||
cy.findByText(/select targets/i).should("exist");
|
||||
cy.findByText(/all hosts/i).click();
|
||||
cy.findByText(/targets selected/i).should("exist"); // target count
|
||||
cy.findByText(/hosts targeted/i).should("exist"); // target count
|
||||
cy.findByText(/run/i).click();
|
||||
cy.findByText(/querying selected hosts/i).should("exist"); // target count
|
||||
});
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ describe(
|
|||
cy.findByText(/run query/i).click({ force: true });
|
||||
cy.findByText(/select targets/i).should("exist");
|
||||
cy.findByText(/all hosts/i).click();
|
||||
cy.findByText(/targets selected/i).should("exist"); // target count
|
||||
cy.findByText(/hosts targeted/i).should("exist"); // target count
|
||||
cy.findByText(/run/i).click();
|
||||
cy.findByText(/querying selected hosts/i).should("exist"); // target count
|
||||
});
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ const PolicyPage = ({
|
|||
|
||||
const [step, setStep] = useState<string>(QUERIES_PAGE_STEPS[1]);
|
||||
const [selectedTargets, setSelectedTargets] = useState<ITarget[]>([]);
|
||||
const [targetsTotalCount, setTargetsTotalCount] = useState<number>(0);
|
||||
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState<boolean>(true);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
|
||||
const [
|
||||
|
|
@ -215,6 +216,8 @@ const PolicyPage = ({
|
|||
goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
|
||||
goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]),
|
||||
setSelectedTargets,
|
||||
targetsTotalCount,
|
||||
setTargetsTotalCount,
|
||||
};
|
||||
|
||||
const step3Opts = {
|
||||
|
|
@ -223,6 +226,7 @@ const PolicyPage = ({
|
|||
policyIdForEdit,
|
||||
setSelectedTargets,
|
||||
goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
|
||||
targetsTotalCount,
|
||||
};
|
||||
|
||||
switch (step) {
|
||||
|
|
|
|||
|
|
@ -124,6 +124,9 @@
|
|||
img {
|
||||
max-width: 12px;
|
||||
}
|
||||
.plus-icon {
|
||||
padding-right: 3px;
|
||||
}
|
||||
.selector-name {
|
||||
margin-left: 8px;
|
||||
font-size: $x-small;
|
||||
|
|
@ -153,10 +156,15 @@
|
|||
&__targets-total-count {
|
||||
margin-left: 16px;
|
||||
font-size: $x-small;
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.icon-tooltip {
|
||||
margin-left: $pad-small;
|
||||
}
|
||||
}
|
||||
&__page-loading {
|
||||
.loading-spinner {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import Button from "components/buttons/Button"; // @ts-ignore
|
|||
import Spinner from "components/Spinner";
|
||||
import TabsWrapper from "components/TabsWrapper";
|
||||
import InfoBanner from "components/InfoBanner";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import PolicyQueryListWrapper from "../PolicyQueriesListWrapper/PolicyQueriesListWrapper";
|
||||
import PolicyQueriesErrorsListWrapper from "../PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsListWrapper";
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ interface IQueryResultsProps {
|
|||
onStopQuery: (evt: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
setSelectedTargets: (value: ITarget[]) => void;
|
||||
goToQueryEditor: () => void;
|
||||
targetsTotalCount: number;
|
||||
}
|
||||
|
||||
const baseClass = "query-results";
|
||||
|
|
@ -48,22 +50,27 @@ const QueryResults = ({
|
|||
onStopQuery,
|
||||
setSelectedTargets,
|
||||
goToQueryEditor,
|
||||
targetsTotalCount,
|
||||
}: IQueryResultsProps): JSX.Element => {
|
||||
const { hosts: hostsOnline, hosts_count: hostsCount, errors } =
|
||||
campaign || {};
|
||||
|
||||
const totalHostsOnline = get(campaign, ["totals", "online"], 0);
|
||||
const totalHostsOffline = get(campaign, ["totals", "offline"], 0);
|
||||
const totalRowsCount = get(campaign, ["query_results", "length"], 0);
|
||||
const onlineTotalText = `${totalRowsCount} result${
|
||||
totalRowsCount === 1 ? "" : "s"
|
||||
}`;
|
||||
const errorsTotalText = `${errors?.length || 0} result${
|
||||
errors?.length === 1 ? "" : "s"
|
||||
}`;
|
||||
|
||||
const [pageTitle, setPageTitle] = useState<string>(PAGE_TITLES.RUNNING);
|
||||
const [navTabIndex, setNavTabIndex] = useState(0);
|
||||
const [
|
||||
targetsRespondedPercent,
|
||||
setTargetsRespondedPercent,
|
||||
] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const calculatePercent =
|
||||
Math.round(
|
||||
((totalRowsCount + errors?.length) / targetsTotalCount) * 100
|
||||
) || 0;
|
||||
setTargetsRespondedPercent(calculatePercent);
|
||||
}, [totalRowsCount, errors]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isQueryFinished) {
|
||||
|
|
@ -152,13 +159,16 @@ const QueryResults = ({
|
|||
Host that responded with results are marked <strong>Yes</strong>.
|
||||
Hosts that responded with no results are marked <strong>No</strong>.
|
||||
</InfoBanner>
|
||||
<span className={`${baseClass}__results-count`}>
|
||||
{totalRowsCount} result{totalRowsCount !== 1 && "s"}
|
||||
</span>
|
||||
<Button
|
||||
className={`${baseClass}__export-btn`}
|
||||
onClick={onExportQueryResults}
|
||||
variant="text-link"
|
||||
>
|
||||
<>
|
||||
Export hosts <img alt="" src={DownloadIcon} />
|
||||
Export results <img alt="" src={DownloadIcon} />
|
||||
</>
|
||||
</Button>
|
||||
<PolicyQueryListWrapper
|
||||
|
|
@ -173,6 +183,11 @@ const QueryResults = ({
|
|||
const renderErrorsTable = () => {
|
||||
return (
|
||||
<div className={`${baseClass}__error-table-container`}>
|
||||
{errors && (
|
||||
<span className={`${baseClass}__error-count`}>
|
||||
{errors.length} error{errors.length !== 1 && "s"}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
className={`${baseClass}__export-btn`}
|
||||
onClick={onExportErrorsResults}
|
||||
|
|
@ -234,15 +249,15 @@ const QueryResults = ({
|
|||
<div className={`${baseClass}__wrapper`}>
|
||||
<h1>{pageTitle}</h1>
|
||||
<div className={`${baseClass}__text-wrapper`}>
|
||||
<span className={`${baseClass}__text-online`}>
|
||||
Online: {totalHostsOnline} hosts / {onlineTotalText}
|
||||
</span>
|
||||
<span className={`${baseClass}__text-offline`}>
|
||||
Offline: {totalHostsOffline} hosts / 0 results
|
||||
</span>
|
||||
<span className={`${baseClass}__text-error`}>
|
||||
Errors: {hostsCount.failed} hosts / {errorsTotalText}
|
||||
</span>
|
||||
<span>{targetsTotalCount}</span> hosts targeted (
|
||||
{targetsRespondedPercent}%
|
||||
<TooltipWrapper
|
||||
tipContent={`
|
||||
Hosts that respond may<br /> return results, errors, or <br />no results`}
|
||||
>
|
||||
responded
|
||||
</TooltipWrapper>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
{isQueryFinished ? renderFinishedButtons() : renderStopQueryButton()}
|
||||
|
|
|
|||
|
|
@ -8,47 +8,19 @@
|
|||
&__text-wrapper {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
font-size: $x-small;
|
||||
|
||||
span {
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
span:not(:last-of-type) {
|
||||
margin-bottom: $pad-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__text-online {
|
||||
&:before {
|
||||
background-color: $ui-success;
|
||||
border-radius: 100%;
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
margin-right: $pad-small;
|
||||
width: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__text-offline {
|
||||
&:before {
|
||||
background-color: $ui-fleet-black-25;
|
||||
border-radius: 100%;
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
margin-right: $pad-small;
|
||||
width: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__text-error {
|
||||
&:before {
|
||||
background-color: $ui-error;
|
||||
border-radius: 100%;
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
margin-right: $pad-small;
|
||||
width: 8px;
|
||||
.icon-tooltip {
|
||||
margin-left: $pad-small;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,4 +48,41 @@
|
|||
top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
&__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.data-table__wrapper {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.data-table-container .data-table thead th {
|
||||
min-width: 140px;
|
||||
padding-left: 0px;
|
||||
padding-right: $pad-large;
|
||||
border-right: 0;
|
||||
|
||||
&:first-of-type {
|
||||
padding-left: $pad-large;
|
||||
}
|
||||
}
|
||||
|
||||
.data-table-container .data-table tbody td {
|
||||
padding-left: 0px;
|
||||
padding-right: $pad-large;
|
||||
|
||||
&:first-of-type {
|
||||
padding-left: $pad-large;
|
||||
}
|
||||
}
|
||||
|
||||
&__results-count,
|
||||
&__error-count {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
margin-right: $pad-medium;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface IRunQueryProps {
|
|||
selectedTargets: ITarget[];
|
||||
setSelectedTargets: (value: ITarget[]) => void;
|
||||
goToQueryEditor: () => void;
|
||||
targetsTotalCount: number;
|
||||
}
|
||||
|
||||
const RunQuery = ({
|
||||
|
|
@ -29,6 +30,7 @@ const RunQuery = ({
|
|||
selectedTargets,
|
||||
setSelectedTargets,
|
||||
goToQueryEditor,
|
||||
targetsTotalCount,
|
||||
}: IRunQueryProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
|
@ -212,6 +214,7 @@ const RunQuery = ({
|
|||
setSelectedTargets={setSelectedTargets}
|
||||
goToQueryEditor={goToQueryEditor}
|
||||
policyName={storedPolicy?.name}
|
||||
targetsTotalCount={targetsTotalCount}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { IHost } from "interfaces/host";
|
|||
import TargetsInput from "components/TargetsInput";
|
||||
import Button from "components/buttons/Button";
|
||||
import Spinner from "components/Spinner";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import PlusIcon from "../../../../../assets/images/icon-plus-purple-32x32@2x.png";
|
||||
import CheckIcon from "../../../../../assets/images/icon-check-purple-32x32@2x.png";
|
||||
import ExternalURLIcon from "../../../../../assets/images/icon-external-url-12x12@2x.png";
|
||||
|
|
@ -33,6 +34,8 @@ interface ISelectTargetsProps {
|
|||
goToQueryEditor: () => void;
|
||||
goToRunQuery: () => void;
|
||||
setSelectedTargets: React.Dispatch<React.SetStateAction<ITarget[]>>;
|
||||
setTargetsTotalCount: React.Dispatch<React.SetStateAction<number>>;
|
||||
targetsTotalCount: number;
|
||||
}
|
||||
|
||||
const DEBOUNCE_DELAY = 500;
|
||||
|
|
@ -60,7 +63,11 @@ const TargetPillSelector = ({
|
|||
data-selected={isSelected}
|
||||
onClick={(e) => onClick(entity)(e)}
|
||||
>
|
||||
<img alt="" src={isSelected ? CheckIcon : PlusIcon} />
|
||||
<img
|
||||
className={isSelected ? "check-icon" : "plus-icon"}
|
||||
alt=""
|
||||
src={isSelected ? CheckIcon : PlusIcon}
|
||||
/>
|
||||
<span className="selector-name">{displayText()}</span>
|
||||
<span className="selector-count">{entity.count}</span>
|
||||
</button>
|
||||
|
|
@ -73,6 +80,7 @@ const SelectTargets = ({
|
|||
goToQueryEditor,
|
||||
goToRunQuery,
|
||||
setSelectedTargets,
|
||||
setTargetsTotalCount,
|
||||
}: ISelectTargetsProps): JSX.Element => {
|
||||
const [allHostsLabels, setAllHostsLabels] = useState<ILabel[] | null>(null);
|
||||
const [platformLabels, setPlatformLabels] = useState<ILabel[] | null>(null);
|
||||
|
|
@ -131,6 +139,10 @@ const SelectTargets = ({
|
|||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTargetsTotalCount(targets?.targetsTotalCount || 0);
|
||||
}, [targets]);
|
||||
|
||||
const handleClickCancel = () => {
|
||||
setSelectedTargets([]);
|
||||
goToQueryEditor();
|
||||
|
|
@ -289,8 +301,15 @@ const SelectTargets = ({
|
|||
<div className={`${baseClass}__targets-total-count`}>
|
||||
{!!targets?.targetsTotalCount && (
|
||||
<>
|
||||
<span>{targets?.targetsTotalCount}</span> targets selected (
|
||||
{targets?.targetsOnlinePercent}% online)
|
||||
<span>{targets?.targetsTotalCount}</span> targets
|
||||
selected ({targets?.targetsOnlinePercent}%
|
||||
<TooltipWrapper
|
||||
tipContent={`
|
||||
Hosts are online if they<br /> have recently checked <br />into Fleet`}
|
||||
>
|
||||
online
|
||||
</TooltipWrapper>
|
||||
){" "}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ const QueryPage = ({
|
|||
);
|
||||
const [step, setStep] = useState<string>(QUERIES_PAGE_STEPS[1]);
|
||||
const [selectedTargets, setSelectedTargets] = useState<ITarget[]>([]);
|
||||
const [targetsTotalCount, setTargetsTotalCount] = useState<number>(0);
|
||||
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState<boolean>(true);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
|
||||
const [
|
||||
|
|
@ -195,6 +196,8 @@ const QueryPage = ({
|
|||
goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
|
||||
goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]),
|
||||
setSelectedTargets,
|
||||
targetsTotalCount,
|
||||
setTargetsTotalCount,
|
||||
};
|
||||
|
||||
const step3Opts = {
|
||||
|
|
@ -203,6 +206,7 @@ const QueryPage = ({
|
|||
queryIdForEdit,
|
||||
setSelectedTargets,
|
||||
goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
|
||||
targetsTotalCount,
|
||||
};
|
||||
|
||||
switch (step) {
|
||||
|
|
|
|||
|
|
@ -124,6 +124,9 @@
|
|||
img {
|
||||
max-width: 12px;
|
||||
}
|
||||
.plus-icon {
|
||||
padding-right: 3px;
|
||||
}
|
||||
.selector-name {
|
||||
margin-left: 8px;
|
||||
font-size: $x-small;
|
||||
|
|
@ -153,10 +156,15 @@
|
|||
&__targets-total-count {
|
||||
margin-left: 16px;
|
||||
font-size: $x-small;
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.icon-tooltip {
|
||||
margin-left: $pad-small;
|
||||
}
|
||||
}
|
||||
&__page-loading {
|
||||
.loading-spinner {
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ import { ICampaign, ICampaignQueryResult } from "interfaces/campaign";
|
|||
import { ITarget } from "interfaces/target";
|
||||
|
||||
import Button from "components/buttons/Button"; // @ts-ignore
|
||||
|
||||
import Spinner from "components/Spinner";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import TabsWrapper from "components/TabsWrapper";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import DownloadIcon from "../../../../../../assets/images/icon-download-12x12@2x.png";
|
||||
|
||||
import resultsTableHeaders from "./QueryResultsTableConfig";
|
||||
|
|
@ -26,6 +26,7 @@ interface IQueryResultsProps {
|
|||
onStopQuery: (evt: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
setSelectedTargets: (value: ITarget[]) => void;
|
||||
goToQueryEditor: () => void;
|
||||
targetsTotalCount: number;
|
||||
}
|
||||
|
||||
const baseClass = "query-results";
|
||||
|
|
@ -46,23 +47,27 @@ const QueryResults = ({
|
|||
onStopQuery,
|
||||
setSelectedTargets,
|
||||
goToQueryEditor,
|
||||
targetsTotalCount,
|
||||
}: IQueryResultsProps): JSX.Element => {
|
||||
const { hosts_count: hostsCount, query_results: queryResults, errors } =
|
||||
campaign || {};
|
||||
|
||||
const totalHostsOnline = get(campaign, ["totals", "online"], 0);
|
||||
const totalHostsOffline = get(campaign, ["totals", "offline"], 0);
|
||||
const totalRowsCount = get(campaign, ["query_results", "length"], 0);
|
||||
const onlineTotalText = `${totalRowsCount} result${
|
||||
totalRowsCount === 1 ? "" : "s"
|
||||
}`;
|
||||
const errorsTotalText = `${errors?.length || 0} result${
|
||||
errors?.length === 1 ? "" : "s"
|
||||
}`;
|
||||
|
||||
const [pageTitle, setPageTitle] = useState<string>(PAGE_TITLES.RUNNING);
|
||||
|
||||
const [navTabIndex, setNavTabIndex] = useState(0);
|
||||
const [
|
||||
targetsRespondedPercent,
|
||||
setTargetsRespondedPercent,
|
||||
] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const calculatePercent =
|
||||
Math.round(
|
||||
((totalRowsCount + errors?.length) / targetsTotalCount) * 100
|
||||
) || 0;
|
||||
setTargetsRespondedPercent(calculatePercent);
|
||||
}, [totalRowsCount, errors]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isQueryFinished) {
|
||||
|
|
@ -163,7 +168,10 @@ const QueryResults = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${baseClass}__results-table-container`}>
|
||||
<span className={`${baseClass}__results-count`}>
|
||||
{totalRowsCount} result{totalRowsCount !== 1 && "s"}
|
||||
</span>
|
||||
<Button
|
||||
className={`${baseClass}__export-btn`}
|
||||
onClick={onExportQueryResults}
|
||||
|
|
@ -181,6 +189,11 @@ const QueryResults = ({
|
|||
const renderErrorsTable = () => {
|
||||
return (
|
||||
<div className={`${baseClass}__error-table-container`}>
|
||||
{errors && (
|
||||
<span className={`${baseClass}__error-count`}>
|
||||
{errors.length} error{errors.length !== 1 && "s"}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
className={`${baseClass}__export-btn`}
|
||||
onClick={onExportErrorsResults}
|
||||
|
|
@ -240,15 +253,15 @@ const QueryResults = ({
|
|||
<div className={`${baseClass}__wrapper`}>
|
||||
<h1>{pageTitle}</h1>
|
||||
<div className={`${baseClass}__text-wrapper`}>
|
||||
<span className={`${baseClass}__text-online`}>
|
||||
Online: {totalHostsOnline} hosts / {onlineTotalText}
|
||||
</span>
|
||||
<span className={`${baseClass}__text-offline`}>
|
||||
Offline: {totalHostsOffline} hosts / 0 results
|
||||
</span>
|
||||
<span className={`${baseClass}__text-error`}>
|
||||
Errors: {hostsCount.failed} hosts / {errorsTotalText}
|
||||
</span>
|
||||
<span>{targetsTotalCount}</span> hosts targeted (
|
||||
{targetsRespondedPercent}%
|
||||
<TooltipWrapper
|
||||
tipContent={`
|
||||
Hosts that respond may<br /> return results, errors, or <br />no results`}
|
||||
>
|
||||
responded
|
||||
</TooltipWrapper>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
{isQueryFinished ? renderFinishedButtons() : renderStopQueryButton()}
|
||||
|
|
|
|||
|
|
@ -4,47 +4,19 @@
|
|||
&__text-wrapper {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
font-size: $x-small;
|
||||
|
||||
span {
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
span:not(:last-of-type) {
|
||||
margin-bottom: $pad-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__text-online {
|
||||
&:before {
|
||||
background-color: $ui-success;
|
||||
border-radius: 100%;
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
margin-right: $pad-small;
|
||||
width: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__text-offline {
|
||||
&:before {
|
||||
background-color: $ui-fleet-black-25;
|
||||
border-radius: 100%;
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
margin-right: $pad-small;
|
||||
width: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__text-error {
|
||||
&:before {
|
||||
background-color: $ui-error;
|
||||
border-radius: 100%;
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
margin-right: $pad-small;
|
||||
width: 8px;
|
||||
.icon-tooltip {
|
||||
margin-left: $pad-small;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,8 +48,6 @@
|
|||
}
|
||||
|
||||
.table-container {
|
||||
padding-top: $pad-large;
|
||||
|
||||
&__header {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -106,4 +76,11 @@
|
|||
padding-left: $pad-large;
|
||||
}
|
||||
}
|
||||
|
||||
&__results-count,
|
||||
&__error-count {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
margin-right: $pad-medium;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ interface IRunQueryProps {
|
|||
queryIdForEdit: number | null;
|
||||
setSelectedTargets: (value: ITarget[]) => void;
|
||||
goToQueryEditor: () => void;
|
||||
targetsTotalCount: number;
|
||||
}
|
||||
|
||||
const RunQuery = ({
|
||||
|
|
@ -32,6 +33,7 @@ const RunQuery = ({
|
|||
queryIdForEdit,
|
||||
setSelectedTargets,
|
||||
goToQueryEditor,
|
||||
targetsTotalCount,
|
||||
}: IRunQueryProps): JSX.Element | null => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
|
@ -214,6 +216,7 @@ const RunQuery = ({
|
|||
isQueryFinished={isQueryFinished}
|
||||
setSelectedTargets={setSelectedTargets}
|
||||
goToQueryEditor={goToQueryEditor}
|
||||
targetsTotalCount={targetsTotalCount}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { IHost } from "interfaces/host";
|
|||
import TargetsInput from "components/TargetsInput";
|
||||
import Button from "components/buttons/Button";
|
||||
import Spinner from "components/Spinner";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import PlusIcon from "../../../../../assets/images/icon-plus-purple-32x32@2x.png";
|
||||
import CheckIcon from "../../../../../assets/images/icon-check-purple-32x32@2x.png";
|
||||
import ExternalURLIcon from "../../../../../assets/images/icon-external-url-12x12@2x.png";
|
||||
|
|
@ -39,6 +40,8 @@ interface ISelectTargetsProps {
|
|||
goToQueryEditor: () => void;
|
||||
goToRunQuery: () => void;
|
||||
setSelectedTargets: React.Dispatch<React.SetStateAction<ITarget[]>>;
|
||||
setTargetsTotalCount: React.Dispatch<React.SetStateAction<number>>;
|
||||
targetsTotalCount: number;
|
||||
}
|
||||
|
||||
const DEBOUNCE_DELAY = 500;
|
||||
|
|
@ -71,7 +74,11 @@ const TargetPillSelector = ({
|
|||
data-selected={isSelected}
|
||||
onClick={(e) => onClick(entity)(e)}
|
||||
>
|
||||
<img alt="" src={isSelected ? CheckIcon : PlusIcon} />
|
||||
<img
|
||||
className={isSelected ? "check-icon" : "plus-icon"}
|
||||
alt=""
|
||||
src={isSelected ? CheckIcon : PlusIcon}
|
||||
/>
|
||||
<span className="selector-name">{displayText()}</span>
|
||||
<span className="selector-count">{entity.count}</span>
|
||||
</button>
|
||||
|
|
@ -85,6 +92,8 @@ const SelectTargets = ({
|
|||
goToQueryEditor,
|
||||
goToRunQuery,
|
||||
setSelectedTargets,
|
||||
setTargetsTotalCount,
|
||||
targetsTotalCount,
|
||||
}: ISelectTargetsProps): JSX.Element => {
|
||||
const [allHostsLabels, setAllHostsLabels] = useState<ILabel[] | null>(null);
|
||||
const [platformLabels, setPlatformLabels] = useState<ILabel[] | null>(null);
|
||||
|
|
@ -145,6 +154,10 @@ const SelectTargets = ({
|
|||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTargetsTotalCount(targets?.targetsTotalCount || 0);
|
||||
}, [targets]);
|
||||
|
||||
const handleClickCancel = () => {
|
||||
setSelectedTargets([]);
|
||||
goToQueryEditor();
|
||||
|
|
@ -307,8 +320,15 @@ const SelectTargets = ({
|
|||
<div className={`${baseClass}__targets-total-count`}>
|
||||
{!!targets?.targetsTotalCount && (
|
||||
<>
|
||||
<span>{targets?.targetsTotalCount}</span> targets selected (
|
||||
{targets?.targetsOnlinePercent}% online)
|
||||
<span>{targets?.targetsTotalCount}</span> hosts
|
||||
targeted ({targets?.targetsOnlinePercent}%
|
||||
<TooltipWrapper
|
||||
tipContent={`
|
||||
Hosts are online if they<br /> have recently checked <br />into Fleet`}
|
||||
>
|
||||
online
|
||||
</TooltipWrapper>
|
||||
){" "}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue