From 02fa778788b3f96c90eec783bbbe163823b73eed Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 7 Mar 2022 15:10:23 -0500 Subject: [PATCH] Refactor Query/Policy UX (#4334) --- changes/issue-3432-2075-improve-live-ui | 1 + cypress/integration/free/admin.spec.ts | 2 +- cypress/integration/free/maintainer.spec.ts | 2 +- .../pages/policies/PolicyPage/PolicyPage.tsx | 4 + .../pages/policies/PolicyPage/_styles.scss | 8 ++ .../components/QueryResults/QueryResults.tsx | 51 +++++++----- .../components/QueryResults/_styles.scss | 79 +++++++++++-------- .../policies/PolicyPage/screens/RunQuery.tsx | 3 + .../PolicyPage/screens/SelectTargets.tsx | 25 +++++- .../pages/queries/QueryPage/QueryPage.tsx | 4 + frontend/pages/queries/QueryPage/_styles.scss | 8 ++ .../components/QueryResults/QueryResults.tsx | 53 ++++++++----- .../components/QueryResults/_styles.scss | 51 ++++-------- .../queries/QueryPage/screens/RunQuery.tsx | 3 + .../QueryPage/screens/SelectTargets.tsx | 26 +++++- 15 files changed, 202 insertions(+), 118 deletions(-) create mode 100644 changes/issue-3432-2075-improve-live-ui diff --git a/changes/issue-3432-2075-improve-live-ui b/changes/issue-3432-2075-improve-live-ui new file mode 100644 index 0000000000..d950cab4b8 --- /dev/null +++ b/changes/issue-3432-2075-improve-live-ui @@ -0,0 +1 @@ +* Improved UX around live query and live policy \ No newline at end of file diff --git a/cypress/integration/free/admin.spec.ts b/cypress/integration/free/admin.spec.ts index aa74f7c203..65c7db28f2 100644 --- a/cypress/integration/free/admin.spec.ts +++ b/cypress/integration/free/admin.spec.ts @@ -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 }); diff --git a/cypress/integration/free/maintainer.spec.ts b/cypress/integration/free/maintainer.spec.ts index 32808fba92..b68156c980 100644 --- a/cypress/integration/free/maintainer.spec.ts +++ b/cypress/integration/free/maintainer.spec.ts @@ -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 }); diff --git a/frontend/pages/policies/PolicyPage/PolicyPage.tsx b/frontend/pages/policies/PolicyPage/PolicyPage.tsx index 9b14a58c30..3d83541ea7 100644 --- a/frontend/pages/policies/PolicyPage/PolicyPage.tsx +++ b/frontend/pages/policies/PolicyPage/PolicyPage.tsx @@ -82,6 +82,7 @@ const PolicyPage = ({ const [step, setStep] = useState(QUERIES_PAGE_STEPS[1]); const [selectedTargets, setSelectedTargets] = useState([]); + const [targetsTotalCount, setTargetsTotalCount] = useState(0); const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(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) { diff --git a/frontend/pages/policies/PolicyPage/_styles.scss b/frontend/pages/policies/PolicyPage/_styles.scss index d39ea89128..50a75dc6ad 100644 --- a/frontend/pages/policies/PolicyPage/_styles.scss +++ b/frontend/pages/policies/PolicyPage/_styles.scss @@ -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 { diff --git a/frontend/pages/policies/PolicyPage/components/QueryResults/QueryResults.tsx b/frontend/pages/policies/PolicyPage/components/QueryResults/QueryResults.tsx index 6689ac5edc..9897f3a1d9 100644 --- a/frontend/pages/policies/PolicyPage/components/QueryResults/QueryResults.tsx +++ b/frontend/pages/policies/PolicyPage/components/QueryResults/QueryResults.tsx @@ -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) => 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(PAGE_TITLES.RUNNING); const [navTabIndex, setNavTabIndex] = useState(0); + const [ + targetsRespondedPercent, + setTargetsRespondedPercent, + ] = useState(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 Yes. Hosts that responded with no results are marked No. + + {totalRowsCount} result{totalRowsCount !== 1 && "s"} + { return (
+ {errors && ( + + {errors.length} error{errors.length !== 1 && "s"} + + )}
{isQueryFinished ? renderFinishedButtons() : renderStopQueryButton()} diff --git a/frontend/pages/policies/PolicyPage/components/QueryResults/_styles.scss b/frontend/pages/policies/PolicyPage/components/QueryResults/_styles.scss index 633c208f9b..cd31afcb3b 100644 --- a/frontend/pages/policies/PolicyPage/components/QueryResults/_styles.scss +++ b/frontend/pages/policies/PolicyPage/components/QueryResults/_styles.scss @@ -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; + } } diff --git a/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx b/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx index 494cbd314a..7459bc910c 100644 --- a/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx +++ b/frontend/pages/policies/PolicyPage/screens/RunQuery.tsx @@ -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} /> ); }; diff --git a/frontend/pages/policies/PolicyPage/screens/SelectTargets.tsx b/frontend/pages/policies/PolicyPage/screens/SelectTargets.tsx index a0c470d94e..8c901444ac 100644 --- a/frontend/pages/policies/PolicyPage/screens/SelectTargets.tsx +++ b/frontend/pages/policies/PolicyPage/screens/SelectTargets.tsx @@ -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>; + setTargetsTotalCount: React.Dispatch>; + targetsTotalCount: number; } const DEBOUNCE_DELAY = 500; @@ -60,7 +63,11 @@ const TargetPillSelector = ({ data-selected={isSelected} onClick={(e) => onClick(entity)(e)} > - + {displayText()} {entity.count} @@ -73,6 +80,7 @@ const SelectTargets = ({ goToQueryEditor, goToRunQuery, setSelectedTargets, + setTargetsTotalCount, }: ISelectTargetsProps): JSX.Element => { const [allHostsLabels, setAllHostsLabels] = useState(null); const [platformLabels, setPlatformLabels] = useState(null); @@ -131,6 +139,10 @@ const SelectTargets = ({ } ); + useEffect(() => { + setTargetsTotalCount(targets?.targetsTotalCount || 0); + }, [targets]); + const handleClickCancel = () => { setSelectedTargets([]); goToQueryEditor(); @@ -289,8 +301,15 @@ const SelectTargets = ({
{!!targets?.targetsTotalCount && ( <> - {targets?.targetsTotalCount} targets selected  ( - {targets?.targetsOnlinePercent}% online) + {targets?.targetsTotalCount} targets + selected  ({targets?.targetsOnlinePercent}%  + have recently checked
into Fleet`} + > + online +
+ ){" "} )}
diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/QueryPage/QueryPage.tsx index 2044da9629..852abd3f27 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.tsx +++ b/frontend/pages/queries/QueryPage/QueryPage.tsx @@ -64,6 +64,7 @@ const QueryPage = ({ ); const [step, setStep] = useState(QUERIES_PAGE_STEPS[1]); const [selectedTargets, setSelectedTargets] = useState([]); + const [targetsTotalCount, setTargetsTotalCount] = useState(0); const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(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) { diff --git a/frontend/pages/queries/QueryPage/_styles.scss b/frontend/pages/queries/QueryPage/_styles.scss index 8c0bae69b3..6a2bd1abaa 100644 --- a/frontend/pages/queries/QueryPage/_styles.scss +++ b/frontend/pages/queries/QueryPage/_styles.scss @@ -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 { diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx b/frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx index 7bb986da12..7061f01d1d 100644 --- a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx @@ -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) => 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(PAGE_TITLES.RUNNING); - const [navTabIndex, setNavTabIndex] = useState(0); + const [ + targetsRespondedPercent, + setTargetsRespondedPercent, + ] = useState(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 ( -
+
+ + {totalRowsCount} result{totalRowsCount !== 1 && "s"} +
{isQueryFinished ? renderFinishedButtons() : renderStopQueryButton()} diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss b/frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss index 615a2bdb96..fe4f848233 100644 --- a/frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss +++ b/frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss @@ -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; + } } diff --git a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx b/frontend/pages/queries/QueryPage/screens/RunQuery.tsx index 7cfc32cddf..960ad9119d 100644 --- a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx +++ b/frontend/pages/queries/QueryPage/screens/RunQuery.tsx @@ -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} /> ); }; diff --git a/frontend/pages/queries/QueryPage/screens/SelectTargets.tsx b/frontend/pages/queries/QueryPage/screens/SelectTargets.tsx index c64a8102ae..62282bdcc4 100644 --- a/frontend/pages/queries/QueryPage/screens/SelectTargets.tsx +++ b/frontend/pages/queries/QueryPage/screens/SelectTargets.tsx @@ -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>; + setTargetsTotalCount: React.Dispatch>; + targetsTotalCount: number; } const DEBOUNCE_DELAY = 500; @@ -71,7 +74,11 @@ const TargetPillSelector = ({ data-selected={isSelected} onClick={(e) => onClick(entity)(e)} > - + {displayText()} {entity.count} @@ -85,6 +92,8 @@ const SelectTargets = ({ goToQueryEditor, goToRunQuery, setSelectedTargets, + setTargetsTotalCount, + targetsTotalCount, }: ISelectTargetsProps): JSX.Element => { const [allHostsLabels, setAllHostsLabels] = useState(null); const [platformLabels, setPlatformLabels] = useState(null); @@ -145,6 +154,10 @@ const SelectTargets = ({ } ); + useEffect(() => { + setTargetsTotalCount(targets?.targetsTotalCount || 0); + }, [targets]); + const handleClickCancel = () => { setSelectedTargets([]); goToQueryEditor(); @@ -307,8 +320,15 @@ const SelectTargets = ({
{!!targets?.targetsTotalCount && ( <> - {targets?.targetsTotalCount} targets selected  ( - {targets?.targetsOnlinePercent}% online) + {targets?.targetsTotalCount} hosts + targeted  ({targets?.targetsOnlinePercent}%  + have recently checked
into Fleet`} + > + online +
+ ){" "} )}