Extend sort functionality for policy status UI (#5078)
This commit is contained in:
@@ -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";
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+6
-10
@@ -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)
|
||||
);
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ const PoliciesListWrapper = ({
|
||||
columns={generateTableHeaders()}
|
||||
data={generateDataSet(policyHostsList)}
|
||||
isLoading={isLoading}
|
||||
defaultSortHeader={"name"}
|
||||
defaultSortHeader={"query_results"}
|
||||
defaultSortDirection={"asc"}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
|
||||
+3
-6
@@ -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);
|
||||
|
||||
+7
-3
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user