From 36971b4cb81d4f3dfad2f0e51ecb2ca8bdd102b0 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 1 Sep 2021 18:08:20 -0400 Subject: [PATCH] Host Details Page: Query performance column (#1867) --- changes/1867-query-performance-ui | 1 + cypress/integration/all/app/hosts.spec.ts | 3 + .../DataTable/PillCell/PillCell.tsx | 97 +++++++++++++++++++ .../DataTable/PillCell/index.ts | 1 + .../TableContainer/DataTable/_styles.scss | 12 +++ frontend/fleet/helpers.ts | 2 +- frontend/interfaces/query_stats.ts | 10 +- .../PackTable/PackTableConfig.tsx | 49 +++++++++- .../pages/hosts/HostDetailsPage/_styles.scss | 45 ++++++++- frontend/styles/var/colors.scss | 1 + 10 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 changes/1867-query-performance-ui create mode 100644 frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx create mode 100644 frontend/components/TableContainer/DataTable/PillCell/index.ts diff --git a/changes/1867-query-performance-ui b/changes/1867-query-performance-ui new file mode 100644 index 0000000000..dff6a6804f --- /dev/null +++ b/changes/1867-query-performance-ui @@ -0,0 +1 @@ +Renders query performance information on host details page pack section \ No newline at end of file diff --git a/cypress/integration/all/app/hosts.spec.ts b/cypress/integration/all/app/hosts.spec.ts index 6bf12e2d44..39ae559ab3 100644 --- a/cypress/integration/all/app/hosts.spec.ts +++ b/cypress/integration/all/app/hosts.spec.ts @@ -45,6 +45,9 @@ describe( cy.get("input[disabled]").should("have.value", contents); }); + // ensure load + cy.wait(5000); // eslint-disable-line cypress/no-unnecessary-waiting + // Wait until the host becomes available (usually immediate in local // testing, but may vary by environment). cy.waitUntil( diff --git a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx new file mode 100644 index 0000000000..5bb307a31e --- /dev/null +++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx @@ -0,0 +1,97 @@ +import React from "react"; +import classnames from "classnames"; + +import ReactTooltip from "react-tooltip"; + +interface IPillCellProps { + value: [string, number]; +} + +const generateClassTag = (rawValue: string): string => { + return rawValue.replace(" ", "-").toLowerCase(); +}; + +const PillCell = (props: IPillCellProps): JSX.Element => { + const { value } = props; + + const pillClassName = classnames( + "data-table__pill", + `data-table__pill--${generateClassTag(value[0])}` + ); + + const disable = () => { + switch (value[0]) { + case "Minimal": + return false; + case "Considerate": + return false; + case "Excessive": + return false; + case "Denylisted": + return false; + default: + return true; + } + }; + + const tooltipText = () => { + switch (value[0]) { + case "Minimal": + return ( + <> + Running this query very
+ frequently has little to no
impact on your device’s
+ performance. + + ); + case "Considerate": + return ( + <> + Running this query
frequently can have a
noticeable + impact on your
device’s performance. + + ); + case "Excessive": + return ( + <> + Running this query, even
infrequently, can have a
+ significant impact on your
device’s performance. + + ); + case "Denylisted": + return ( + <> + This query has been
stopped from running
because of + excessive
resource consumption. + + ); + default: + return null; + } + }; + + return ( + <> +
+ {value[0]} +
+ + + {tooltipText()} + + + + ); +}; + +export default PillCell; diff --git a/frontend/components/TableContainer/DataTable/PillCell/index.ts b/frontend/components/TableContainer/DataTable/PillCell/index.ts new file mode 100644 index 0000000000..a37cf0a4b8 --- /dev/null +++ b/frontend/components/TableContainer/DataTable/PillCell/index.ts @@ -0,0 +1 @@ +export { default } from "./PillCell"; diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index dd923f1959..005b48dc4f 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -149,6 +149,18 @@ color: $core-fleet-black; } + &__pill { + color: $core-fleet-black; + font-weight: $bold; + padding: 4px 12px; + border-radius: 29px; + + span { + border-radius: 29px; + background-color: $core-fleet-purple; + } + } + &__status { color: $core-fleet-blue; text-transform: capitalize; diff --git a/frontend/fleet/helpers.ts b/frontend/fleet/helpers.ts index 6bb09b4839..6d49a9b009 100644 --- a/frontend/fleet/helpers.ts +++ b/frontend/fleet/helpers.ts @@ -545,7 +545,7 @@ export const humanQueryLastRun = (lastRun: string): string => { // Handles the case when a query has never been ran. // July 28, 2016 is the date of the initial commit to fleet/fleet. if (lastRun < "2016-07-28T00:00:00Z") { - return "Never"; + return "Has not run"; } return moment(lastRun).fromNow(); diff --git a/frontend/interfaces/query_stats.ts b/frontend/interfaces/query_stats.ts index 2b941990d2..dad3fb7a92 100644 --- a/frontend/interfaces/query_stats.ts +++ b/frontend/interfaces/query_stats.ts @@ -25,13 +25,13 @@ export interface IQueryStats { description: string; pack_name: string; pack_id: number; - average_memory?: number; - denylisted?: boolean; - executions?: number; + average_memory: number; + denylisted: boolean; + executions: number; interval: number; last_executed: string; output_size?: number; - system_time?: number; - user_time?: number; + system_time: number; + user_time: number; wall_time?: number; } diff --git a/frontend/pages/hosts/HostDetailsPage/PackTable/PackTableConfig.tsx b/frontend/pages/hosts/HostDetailsPage/PackTable/PackTableConfig.tsx index c2df894483..a8cfdf9d70 100644 --- a/frontend/pages/hosts/HostDetailsPage/PackTable/PackTableConfig.tsx +++ b/frontend/pages/hosts/HostDetailsPage/PackTable/PackTableConfig.tsx @@ -2,8 +2,10 @@ import React from "react"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; import TextCell from "components/TableContainer/DataTable/TextCell"; +import PillCell from "components/TableContainer/DataTable/PillCell"; import { IQueryStats } from "interfaces/query_stats"; import { humanQueryLastRun, secondsToHms } from "fleet/helpers"; +import IconToolTip from "components/IconToolTip"; interface IHeaderProps { column: { @@ -33,8 +35,30 @@ interface IDataColumn { interface IPackTable extends IQueryStats { frequency: string; last_run: string; + performance: (string | number)[]; } +const performanceIndicator = (scheduledQuery: IQueryStats): string => { + if (scheduledQuery.executions === 0) { + return "Undetermined"; + } + if (scheduledQuery.denylisted === true) { + return "Denylisted"; + } + + const indicator = + (scheduledQuery.user_time + scheduledQuery.system_time) / + scheduledQuery.executions; + + if (indicator < 2000) { + return "Minimal"; + } + if (indicator >= 2000 && indicator <= 4000) { + return "Considerate"; + } + return "Excessive"; +}; + // NOTE: cellProps come from react-table // more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties const generatePackTableHeaders = (): IDataColumn[] => { @@ -55,11 +79,28 @@ const generatePackTableHeaders = (): IDataColumn[] => { }, { title: "Last run", - Header: "Last run", + Header: () => { + return ( + <> + Last run + since the last time osquery
started on this host.`} + /> + + ); + }, disableSortBy: true, accessor: "last_run", Cell: (cellProps) => , }, + { + title: "Performance impact", + Header: "Performance impact", + disableSortBy: true, + accessor: "performance", + Cell: (cellProps) => , + }, ]; }; @@ -76,6 +117,12 @@ const enhancePackData = (query_stats: IQueryStats[]): IPackTable[] => { last_executed: query.last_executed, frequency: secondsToHms(query.interval), last_run: humanQueryLastRun(query.last_executed), + performance: [performanceIndicator(query), query.scheduled_query_id], + average_memory: query.average_memory, + denylisted: query.denylisted, + executions: query.executions, + system_time: query.system_time, + user_time: query.user_time, }; }); }; diff --git a/frontend/pages/hosts/HostDetailsPage/_styles.scss b/frontend/pages/hosts/HostDetailsPage/_styles.scss index d064c49b49..67aaa10991 100644 --- a/frontend/pages/hosts/HostDetailsPage/_styles.scss +++ b/frontend/pages/hosts/HostDetailsPage/_styles.scss @@ -460,6 +460,36 @@ display: none; } + .tooltip { + width: 192px; + } + + .data-table__pill--undetermined { + color: $ui-fleet-black-50; + font-style: italic; + font-weight: 400; + padding: 0; + border-radius: 0; + } + + .data-table__pill--denylisted { + font-weight: 400; + padding: 0; + border-radius: 0; + } + + .data-table__pill--minimal { + background-color: $ui-vibrant-blue-10; + } + + .data-table__pill--considerate { + background-color: $ui-vibrant-blue-25; + } + + .data-table__pill--excessive { + background-color: $ui-vibrant-blue-50; + } + .data-table__table { table-layout: fixed; @@ -467,7 +497,7 @@ // Width for all columns except the "Query name" column // Width calculation adjusts for each row's horizontal padding th { - width: calc(160px - 27px * 2); + width: calc(200px - 27px * 2); } // Width for the "Query name" column @@ -482,6 +512,19 @@ text-overflow: ellipsis; } } + + .__react_component_tooltip { + text-align: center; + } + } + + .icon-tooltip { + display: inline; + position: relative; + top: 2px; + margin-left: $pad-small; + font-weight: 400; + text-align: center; } } } diff --git a/frontend/styles/var/colors.scss b/frontend/styles/var/colors.scss index 03870fc092..e27c995731 100644 --- a/frontend/styles/var/colors.scss +++ b/frontend/styles/var/colors.scss @@ -16,6 +16,7 @@ $ui-blue-gray: #dbe3e5; $ui-gray: #e3e3e3; $ui-light-grey: #fafafa; $ui-off-white: #f9fafc; +$ui-vibrant-blue-50: rgba(106, 103, 254, 0.5); $ui-vibrant-blue-25: #d9d9fe; $ui-vibrant-blue-10: #f1f0ff;