From fa8bfbd79617a4d8aa6471f04d70e7b6299b23e8 Mon Sep 17 00:00:00 2001 From: gillespi314 <73313222+gillespi314@users.noreply.github.com> Date: Wed, 13 Apr 2022 11:08:37 -0500 Subject: [PATCH] Extend sort functionality for policy status UI (#5078) --- .../TableContainer/DataTable/DataTable.tsx | 30 ++-- .../QueryResultsRow/QueryResultsRow.tsx | 24 --- .../queries/QueryResultsRow/index.ts | 1 - frontend/interfaces/campaign.ts | 21 +-- frontend/interfaces/host.ts | 9 +- .../PolicyQueriesErrorsListWrapper.tsx | 4 +- .../PolicyQueriesErrorsTableConfig.tsx | 16 +- .../PolicyQueriesListWrapper.tsx | 2 +- .../PolicyQueriesTableConfig.tsx | 9 +- .../components/QueryResults/QueryResults.tsx | 2 +- .../QueryResults/QueryResultsTableConfig.tsx | 10 +- frontend/utilities/campaign_helpers/index.ts | 140 ++++++++++-------- frontend/utilities/sort/sort_functions.ts | 22 ++- 13 files changed, 143 insertions(+), 147 deletions(-) delete mode 100644 frontend/components/queries/QueryResultsRow/QueryResultsRow.tsx delete mode 100644 frontend/components/queries/QueryResultsRow/index.ts diff --git a/frontend/components/TableContainer/DataTable/DataTable.tsx b/frontend/components/TableContainer/DataTable/DataTable.tsx index 32e903a4b6..23ecc6b9ee 100644 --- a/frontend/components/TableContainer/DataTable/DataTable.tsx +++ b/frontend/components/TableContainer/DataTable/DataTable.tsx @@ -173,23 +173,25 @@ const DataTable = ({ // with custom `sortTypes` defined for this `useTable` instance sortTypes: React.useMemo( () => ({ - caseInsensitive: (a: any, b: any, id: any) => { - let valueA = a.values[id]; - let valueB = b.values[id]; + caseInsensitive: ( + a: { values: Record }, + b: { values: Record }, + id: string + ) => sort.caseInsensitiveAsc(a.values[id], b.values[id]), - valueA = isString(valueA) ? valueA.toLowerCase() : valueA; - valueB = isString(valueB) ? valueB.toLowerCase() : valueB; + dateStrings: ( + a: { values: Record }, + b: { values: Record }, + id: string + ) => sort.dateStringsAsc(a.values[id], b.values[id]), - if (valueB > valueA) { - return -1; - } - if (valueB < valueA) { - return 1; - } - return 0; + hasLength: ( + a: { values: Record }, + b: { values: Record }, + id: string + ) => { + return sort.hasLength(a.values[id], b.values[id]); }, - dateStrings: (a: any, b: any, id: any) => - sort.dateStringsAsc(a.values[id], b.values[id]), }), [] ), diff --git a/frontend/components/queries/QueryResultsRow/QueryResultsRow.tsx b/frontend/components/queries/QueryResultsRow/QueryResultsRow.tsx deleted file mode 100644 index eec938e98b..0000000000 --- a/frontend/components/queries/QueryResultsRow/QueryResultsRow.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import { omit } from "lodash"; - -import { ICampaignQueryResult } from "interfaces/campaign"; - -interface IQueryResultsRowProps { - queryResult: ICampaignQueryResult; -} - -const QueryResultsRow = ({ queryResult }: IQueryResultsRowProps) => { - const { host_hostname: hostHostname } = queryResult; - const queryColumns: any = omit(queryResult, ["host_hostname"]); - - return ( - - {hostHostname} - {Object.keys(queryColumns).map((col) => { - return {queryColumns[col]}; - })} - - ); -}; - -export default React.memo(QueryResultsRow); diff --git a/frontend/components/queries/QueryResultsRow/index.ts b/frontend/components/queries/QueryResultsRow/index.ts deleted file mode 100644 index 1dec1118d3..0000000000 --- a/frontend/components/queries/QueryResultsRow/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./QueryResultsRow"; diff --git a/frontend/interfaces/campaign.ts b/frontend/interfaces/campaign.ts index f0ac62b130..33492be72e 100644 --- a/frontend/interfaces/campaign.ts +++ b/frontend/interfaces/campaign.ts @@ -11,20 +11,10 @@ export default PropTypes.shape({ online: PropTypes.number, }); -export interface ICampaignQueryResult { - build_distro: string; - build_platform: string; - config_hash: string; - config_valid: string; - extensions: string; +export interface ICampaignError { host_hostname: string; - instance_id: string; - pid: string; - platform_mask: string; - start_time: string; - uuid: string; - version: string; - watcher: string; + osquery_version: string; + error: string; } export interface ICampaign { @@ -32,7 +22,7 @@ export interface ICampaign { [key: string]: any; }; created_at: string; - errors: any; + errors: ICampaignError[]; hosts: IHost[]; hosts_count: { total: number; @@ -41,7 +31,7 @@ export interface ICampaign { }; id: number; query_id: number; - query_results: ICampaignQueryResult[]; + query_results: unknown[]; status: string; totals: { count: number; @@ -53,6 +43,7 @@ export interface ICampaign { user_id: number; } +// TODO: review use of ICampaignState to see if legacy code can be removed export interface ICampaignState { campaign: ICampaign; observerShowSql: boolean; diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts index 4343c76a20..f52df5dd0e 100644 --- a/frontend/interfaces/host.ts +++ b/frontend/interfaces/host.ts @@ -97,15 +97,10 @@ export interface IPackStats { export interface IHostPolicyQuery { id: number; hostname: string; + query_results?: unknown[]; status?: string; } -export interface IHostPolicyQueryError { - host_hostname: string; - osquery_version: string; - error: string; -} - interface IGeoLocation { country_iso: string; city_name: string; @@ -170,6 +165,6 @@ export interface IHost { munki?: IMunkiData; mdm?: IMDMData; policies: IHostPolicy[]; - query_results?: []; + query_results?: unknown[]; geolocation?: IGeoLocation; } diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsListWrapper.tsx b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsListWrapper.tsx index 09c55efb11..e7d1cc2196 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsListWrapper.tsx +++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsListWrapper.tsx @@ -1,8 +1,8 @@ import React from "react"; import { noop } from "lodash"; -import { IHostPolicyQueryError } from "interfaces/host"; import TableContainer from "components/TableContainer"; +import { ICampaignError } from "interfaces/campaign"; import { generateTableHeaders, generateDataSet, @@ -12,7 +12,7 @@ const baseClass = "policies-queries-list-wrapper"; const noPolicyQueries = "no-policy-queries"; interface IPoliciesListWrapperProps { - errorsList: IHostPolicyQueryError[]; + errorsList: ICampaignError[]; isLoading: boolean; resultsTitle?: string; canAddOrRemovePolicy?: boolean; diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsTableConfig.tsx b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsTableConfig.tsx index 23d05d2f42..1ddb49ab29 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsTableConfig.tsx +++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsListWrapper/PolicyQueriesErrorsTableConfig.tsx @@ -4,12 +4,10 @@ import React from "react"; import { memoize } from "lodash"; -// @ts-ignore -import TextCell from "components/TableContainer/DataTable/TextCell/TextCell"; -import { IHostPolicyQueryError } from "interfaces/host"; +import { ICampaignError } from "interfaces/campaign"; import sortUtils from "utilities/sort"; -// TODO functions for paths math e.g., path={PATHS.MANAGE_HOSTS + getParams(cellProps.row.original)} +import TextCell from "components/TableContainer/DataTable/TextCell/TextCell"; interface IHeaderProps { column: { @@ -23,7 +21,7 @@ interface ICellProps { value: string; }; row: { - original: IHostPolicyQueryError; + original: ICampaignError; }; } @@ -51,8 +49,8 @@ const generateTableHeaders = (): IDataColumn[] => { ), }, { - title: "OSQuery Version", - Header: "OSQuery Version", + title: "Osquery version", + Header: "Osquery version", disableSortBy: true, accessor: "osquery_version", Cell: (cellProps: ICellProps): JSX.Element => ( @@ -73,9 +71,7 @@ const generateTableHeaders = (): IDataColumn[] => { }; const generateDataSet = memoize( - ( - policyHostsErrorsList: IHostPolicyQueryError[] = [] - ): IHostPolicyQueryError[] => { + (policyHostsErrorsList: ICampaignError[] = []): ICampaignError[] => { policyHostsErrorsList = policyHostsErrorsList.sort((a, b) => sortUtils.caseInsensitiveAsc(a.host_hostname, b.host_hostname) ); diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesListWrapper.tsx b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesListWrapper.tsx index fd7309e864..a8ad91efa1 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesListWrapper.tsx +++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesListWrapper.tsx @@ -43,7 +43,7 @@ const PoliciesListWrapper = ({ columns={generateTableHeaders()} data={generateDataSet(policyHostsList)} isLoading={isLoading} - defaultSortHeader={"name"} + defaultSortHeader={"query_results"} defaultSortDirection={"asc"} showMarkAllPages={false} isAllPagesSelected={false} diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesTableConfig.tsx b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesTableConfig.tsx index 8cc2a93bf9..676435e8a6 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesTableConfig.tsx +++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesListWrapper/PolicyQueriesTableConfig.tsx @@ -14,8 +14,6 @@ import sortUtils from "utilities/sort"; import PassIcon from "../../../../../../assets/images/icon-check-circle-green-16x16@2x.png"; import FailIcon from "../../../../../../assets/images/icon-exclamation-circle-red-16x16@2x.png"; -// TODO functions for paths math e.g., path={PATHS.MANAGE_HOSTS + getParams(cellProps.row.original)} - interface IHeaderProps { column: ColumnInstance & IDataColumn; } @@ -49,14 +47,13 @@ const generateTableHeaders = (): IDataColumn[] => { ), + disableSortBy: false, accessor: "hostname", Cell: (cellProps: ICellProps): JSX.Element => ( ), - disableSortBy: false, }, { title: "Status", @@ -64,9 +61,10 @@ const generateTableHeaders = (): IDataColumn[] => { ), + disableSortBy: false, + sortType: "hasLength", accessor: "query_results", Cell: (cellProps: ICellProps): JSX.Element => ( <> @@ -83,7 +81,6 @@ const generateTableHeaders = (): IDataColumn[] => { )} ), - disableSortBy: false, }, ]; return tableHeaders; diff --git a/frontend/pages/policies/PolicyPage/components/QueryResults/QueryResults.tsx b/frontend/pages/policies/PolicyPage/components/QueryResults/QueryResults.tsx index 97320161de..7604e48c8f 100644 --- a/frontend/pages/policies/PolicyPage/components/QueryResults/QueryResults.tsx +++ b/frontend/pages/policies/PolicyPage/components/QueryResults/QueryResults.tsx @@ -55,7 +55,7 @@ const QueryResults = ({ const { hosts: hostsOnline, hosts_count: hostsCount, errors } = campaign || {}; - const totalRowsCount = get(campaign, ["query_results", "length"], 0); + const totalRowsCount = get(campaign, ["hosts_count", "successful"], 0); const [pageTitle, setPageTitle] = useState(PAGE_TITLES.RUNNING); const [navTabIndex, setNavTabIndex] = useState(0); diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx b/frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx index 7aaf8cfc5b..3d191585c1 100644 --- a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx @@ -2,6 +2,7 @@ // disable this rule as it was throwing an error in Header and Cell component // definitions for the selection row for some reason when we dont really need it. import React from "react"; +import { isPlainObject } from "lodash"; import { CellProps, @@ -11,7 +12,6 @@ import { HeaderProps, TableInstance, } from "react-table"; -import { ICampaignQueryResult } from "interfaces/campaign"; import DefaultColumnFilter from "components/TableContainer/DataTable/DefaultColumnFilter"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; @@ -39,13 +39,17 @@ const _unshiftHostname = (headers: IDataColumn[]) => { return newHeaders; }; -const resultsTableHeaders = (results: ICampaignQueryResult[]): Column[] => { +const resultsTableHeaders = (results: unknown[]): Column[] => { // Table headers are derived from the shape of the first result. // Note: It is possible that results may vary from the shape of the first result. // For example, different versions of osquery may have new columns in a table // However, this is believed to be a very unlikely scenario and there have been // no reported issues. - const keys = results[0] ? Object.keys(results[0]) : []; + const shape = results[0]; + const keys = + shape && typeof shape === "object" && isPlainObject(shape) + ? Object.keys(shape) + : []; const headers = keys.map((key) => { return { id: key, diff --git a/frontend/utilities/campaign_helpers/index.ts b/frontend/utilities/campaign_helpers/index.ts index be9f18b255..9e45c66513 100644 --- a/frontend/utilities/campaign_helpers/index.ts +++ b/frontend/utilities/campaign_helpers/index.ts @@ -1,97 +1,119 @@ -const updateCampaignStateFromTotals = (campaign: any, { data }: any) => { +import { ICampaign, ICampaignState } from "interfaces/campaign"; +import { IHost } from "interfaces/host"; + +interface IResult { + type: "result"; + data: { + distributed_query_execution_id: number; + error: string | null; + host: IHost; + rows: unknown[]; + }; +} +interface IStatus { + type: "status"; + data: { + actual_results: number; + expected_result: number; + status: string; + }; +} + +interface ITotals { + type: "totals"; + data: { + count: number; + missing_in_action: number; + offline: number; + online: number; + }; +} + +type ISocketData = IResult | IStatus | ITotals; + +const updateCampaignStateFromTotals = ( + campaign: ICampaign, + { data: totals }: ITotals +) => { return { - campaign: { ...campaign, totals: data }, + campaign: { ...campaign, totals }, }; }; -const updateCampaignStateFromResults = (campaign: any, { data }: any) => { - const queryResults = campaign.query_results || []; - const errors = campaign.errors || []; - const hosts = campaign.hosts || []; - const { host, rows, error } = data; - host.query_results = rows; - const { hosts_count: hostsCount } = campaign; - const newHosts = [...hosts, host]; - const newQueryResults = [...queryResults, ...rows]; - let newHostsCount; +const updateCampaignStateFromResults = ( + campaign: ICampaign, + { data }: IResult +) => { + const { + errors = [], + hosts = [], + hosts_count: hostsCount = { total: 0, failed: 0, successful: 0 }, + query_results: queryResults = [], + } = campaign; + const { error, host, rows = [] } = data; + let newErrors; - // Host's with osquery version above 4.4.0 receive an error message - // when the live query fails. - if (error) { + let newHosts; + let newHostsCount; + + if (error || error === "") { const newFailed = hostsCount.failed + 1; const newTotal = hostsCount.successful + newFailed; - newHostsCount = { - successful: hostsCount.successful, - failed: newFailed, - total: newTotal, - }; - - newErrors = [ - ...errors, + newErrors = errors.concat([ { - host_hostname: host.hostname, - osquery_version: host.osquery_version, - error, - }, - ]; - // Host's with osquery version below 4.4.0 receive an empty error message - // when the live query fails so we create our own message. - } else if (error === "") { - const newFailed = hostsCount.failed + 1; - const newTotal = hostsCount.successful + newFailed; - - newHostsCount = { - successful: hostsCount.successful, - failed: newFailed, - total: newTotal, - }; - newErrors = [ - ...errors, - { - host_hostname: host.hostname, - osquery_version: host.osquery_version, + host_hostname: host?.hostname, + osquery_version: host?.osquery_version, error: + error || + // Hosts with osquery version below 4.4.0 receive an empty error message + // when the live query fails so we create our own message. "Error details require osquery 4.4.0+ (Launcher does not provide error details)", }, - ]; + ]); + newHostsCount = { + successful: hostsCount.successful, + failed: newFailed, + total: newTotal, + }; + newHosts = hosts; } else { const newSuccessful = hostsCount.successful + 1; const newTotal = hostsCount.failed + newSuccessful; + newErrors = [...errors]; newHostsCount = { successful: newSuccessful, failed: hostsCount.failed, total: newTotal, }; - newErrors = [...errors]; + const newHost = { ...host, query_results: rows }; + newHosts = hosts.concat(newHost); } return { campaign: { ...campaign, - hosts: newHosts, - query_results: newQueryResults, - hosts_count: newHostsCount, errors: newErrors, + hosts: newHosts, + hosts_count: newHostsCount, + query_results: [...queryResults, ...rows], }, }; }; -const updateCampaignStateFromStatus = (campaign: any, { data }: any) => { - const { status } = data; - const updatedCampaign = { ...campaign, status }; - +const updateCampaignStateFromStatus = ( + campaign: ICampaign, + { data: { status } }: IStatus +) => { return { - campaign: updatedCampaign, - queryIsRunning: data !== "finished", + campaign: { ...campaign, status }, + queryIsRunning: status !== "finished", }; }; -export const updateCampaignState = (socketData: any) => { - return (prevState: any) => { - const { campaign } = prevState; - +export const updateCampaignState = (socketData: ISocketData) => { + return ({ campaign }: ICampaignState) => { switch (socketData.type) { case "totals": return updateCampaignStateFromTotals(campaign, socketData); diff --git a/frontend/utilities/sort/sort_functions.ts b/frontend/utilities/sort/sort_functions.ts index 4c9aef53ef..5271561fa4 100644 --- a/frontend/utilities/sort/sort_functions.ts +++ b/frontend/utilities/sort/sort_functions.ts @@ -1,6 +1,6 @@ -const caseInsensitiveAsc = (a: string, b: string): number => { - a = a.toLowerCase(); - b = b.toLowerCase(); +const caseInsensitiveAsc = (a: any, b: any): number => { + a = typeof a === "string" ? a.toLowerCase() : a; + b = typeof b === "string" ? b.toLowerCase() : b; if (a < b) { return -1; @@ -36,4 +36,18 @@ const dateStringsAsc = (a: string, b: string): number => { return 0; }; -export default { caseInsensitiveAsc, dateStringsAsc }; +const hasLength = (a: unknown[], b: unknown[]): number => { + if (!a?.length && b?.length) { + return -1; + } + if (a?.length && !b?.length) { + return 1; + } + return 0; +}; + +export default { + caseInsensitiveAsc, + dateStringsAsc, + hasLength, +};