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;