Extend sort functionality for policy status UI (#5078)

This commit is contained in:
gillespi314
2022-04-13 11:08:37 -05:00
committed by GitHub
parent a4be69d9d1
commit fa8bfbd796
13 changed files with 143 additions and 147 deletions
@@ -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<string, unknown> },
b: { values: Record<string, unknown> },
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<string, string> },
b: { values: Record<string, string> },
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<string, unknown[]> },
b: { values: Record<string, unknown[]> },
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]),
}),
[]
),
@@ -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 (
<tr>
<td>{hostHostname}</td>
{Object.keys(queryColumns).map((col) => {
return <td key={col}>{queryColumns[col]}</td>;
})}
</tr>
);
};
export default React.memo(QueryResultsRow);
@@ -1 +0,0 @@
export { default } from "./QueryResultsRow";
+6 -15
View File
@@ -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;
+2 -7
View File
@@ -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;
}
@@ -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;
@@ -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)
);
@@ -43,7 +43,7 @@ const PoliciesListWrapper = ({
columns={generateTableHeaders()}
data={generateDataSet(policyHostsList)}
isLoading={isLoading}
defaultSortHeader={"name"}
defaultSortHeader={"query_results"}
defaultSortDirection={"asc"}
showMarkAllPages={false}
isAllPagesSelected={false}
@@ -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[] => {
<HeaderCell
value={headerProps.column.title || headerProps.column.id}
isSortedDesc={headerProps.column.isSortedDesc}
disableSortBy={false}
/>
),
disableSortBy: false,
accessor: "hostname",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={cellProps.cell.value} />
),
disableSortBy: false,
},
{
title: "Status",
@@ -64,9 +61,10 @@ const generateTableHeaders = (): IDataColumn[] => {
<HeaderCell
value={headerProps.column.title || headerProps.column.id}
isSortedDesc={headerProps.column.isSortedDesc}
disableSortBy={false}
/>
),
disableSortBy: false,
sortType: "hasLength",
accessor: "query_results",
Cell: (cellProps: ICellProps): JSX.Element => (
<>
@@ -83,7 +81,6 @@ const generateTableHeaders = (): IDataColumn[] => {
)}
</>
),
disableSortBy: false,
},
];
return tableHeaders;
@@ -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<string>(PAGE_TITLES.RUNNING);
const [navTabIndex, setNavTabIndex] = useState(0);
@@ -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,
+81 -59
View File
@@ -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);
+18 -4
View File
@@ -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,
};