UI – Add and update performance impact features to uitilize metrics that include live query runs (#15642)

Merging during freeze with approval from all stakeholders, including verbal approval from @sharon-fdm 

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
Jacob Shandling 2023-12-14 11:49:56 -08:00 committed by GitHub
parent b011418b71
commit 1fa5004428
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 219 additions and 111 deletions

View File

@ -0,0 +1 @@
* Add UI features to incorporate new live query stats

View File

@ -2,13 +2,15 @@ import React from "react";
import { screen } from "@testing-library/react";
import { renderWithSetup } from "test/test-utils";
import PillCell from "./PillCell";
import PerformanceImpactCell from "./PerformanceImpactCell";
const PERFORMANCE_IMPACT = { indicator: "Minimal", id: 3 };
describe("Pill cell", () => {
describe("Query performance cell", () => {
it("renders pill text and tooltip on hover", async () => {
const { user } = renderWithSetup(<PillCell value={PERFORMANCE_IMPACT} />);
const { user } = renderWithSetup(
<PerformanceImpactCell value={PERFORMANCE_IMPACT} />
);
await user.hover(screen.getByText("Minimal"));

View File

@ -5,21 +5,27 @@ import { uniqueId } from "lodash";
import ReactTooltip from "react-tooltip";
import { COLORS } from "styles/var/colors";
interface IPillCellProps {
value: { indicator: string; id: number };
interface IPerformanceImpactCellValue {
indicator: string;
id?: number;
}
interface IPerformanceImpactCellProps {
value: IPerformanceImpactCellValue;
isHostSpecific?: boolean;
customIdPrefix?: string;
hostDetails?: boolean;
}
const generateClassTag = (rawValue: string): string => {
return rawValue.replace(" ", "-").toLowerCase();
};
const PillCell = ({
const baseClass = "performance-impact-cell";
const PerformanceImpactCell = ({
value,
isHostSpecific = false,
customIdPrefix,
hostDetails,
}: IPillCellProps): JSX.Element => {
}: IPerformanceImpactCellProps): JSX.Element => {
const { indicator, id } = value;
const pillClassName = classnames(
"data-table__pill",
@ -27,43 +33,34 @@ const PillCell = ({
"tooltip"
);
const disable = () => {
switch (indicator) {
case "Minimal":
return false;
case "Considerable":
return false;
case "Excessive":
return false;
case "Undetermined":
return false;
default:
return true;
}
};
const disableTooltip = ![
"Minimal",
"Considerable",
"Excessive",
"Undetermined",
].includes(indicator);
const tooltipText = () => {
switch (indicator) {
case "Minimal":
return (
<>
Running this query very <br />
frequently has little to no <br /> impact on your devices <br />
performance.
Running this query very frequently has little to no <br /> impact on
your device&apos;s performance.
</>
);
case "Considerable":
return (
<>
Running this query <br /> frequently can have a <br /> noticeable
impact on your <br /> devices performance.
Running this query frequently can have a noticeable <br />
impact on your device&apos;s performance.
</>
);
case "Excessive":
return (
<>
Running this query, even <br /> infrequently, can have a <br />
significant impact on your <br /> devices performance.
Running this query, even infrequently, can have a <br />
significant impact on your device&apos;s performance.
</>
);
case "Denylisted":
@ -76,8 +73,9 @@ const PillCell = ({
case "Undetermined":
return (
<>
To see performance impact, this query must have run with{" "}
<b>automations</b> on {hostDetails ? "this" : "at least one"} host.
Performance impact will be available when{" "}
{isHostSpecific ? "the" : "this"} <br />
query runs{isHostSpecific && " on this host"}.
</>
);
default:
@ -87,11 +85,11 @@ const PillCell = ({
const tooltipId = uniqueId();
return (
<>
<span className={`${baseClass}`}>
<span
data-tip
data-for={`${customIdPrefix || "pill"}__${id?.toString() || tooltipId}`}
data-tip-disable={disable()}
data-tip-disable={disableTooltip}
>
<span className={pillClassName}>{indicator}</span>
</span>
@ -110,8 +108,8 @@ const PillCell = ({
{tooltipText()}
</span>
</ReactTooltip>
</>
</span>
);
};
export default PillCell;
export default PerformanceImpactCell;

View File

@ -1,3 +1,9 @@
.performance-impact-cell {
.__react_component_tooltip {
@include tooltip-text;
}
}
.data-table__pill {
color: $core-fleet-black;
font-weight: $bold;

View File

@ -0,0 +1 @@
export { default } from "./PerformanceImpactCell";

View File

@ -1 +0,0 @@
export { default } from "./PillCell";

View File

@ -16,20 +16,6 @@
}
&__tip-text {
width: max-content;
max-width: 360px;
padding: 6px;
color: $core-white;
background-color: $tooltip-bg;
font-weight: $regular;
font-size: $xx-small;
border-radius: 4px;
box-sizing: border-box;
z-index: 99; // not more than the site nav
line-height: 1.375;
white-space: initial;
p {
margin: 0;
}
@include tooltip-text;
}
}

View File

@ -3,16 +3,19 @@ import React from "react";
import FleetAce from "components/FleetAce";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell";
const baseClass = "show-query-modal";
interface IShowQueryModalProps {
onCancel: () => void;
query?: string;
impact?: string;
}
const ShowQueryModal = ({
query,
impact,
onCancel,
}: IShowQueryModalProps): JSX.Element => {
return (
@ -30,6 +33,12 @@ const ShowQueryModal = ({
wrapEnabled
readOnly
/>
{impact && (
<div className={`${baseClass}__performance-impact`}>
Performance impact:{" "}
<PerformanceImpactCell value={{ indicator: impact }} />
</div>
)}
<div className="modal-cta-wrap">
<Button onClick={onCancel} variant="brand">
Done

View File

@ -1,4 +1,8 @@
.show-query-modal {
display: flex;
flex-direction: column;
gap: $pad-large;
.yaml-ace {
min-height: 0;
}
@ -6,4 +10,24 @@
.yaml-ace__label {
height: 0;
}
&__performance-impact {
display: flex;
align-items: center;
gap: $pad-small;
.data-table__pill {
font-size: $xxx-small;
font-weight: $xbold;
padding: 6px 12px;
.__react_component_tooltip {
@include tooltip-text;
}
}
}
.modal-cta-wrap {
margin-top: initial;
}
}

View File

@ -4,14 +4,17 @@
import React from "react";
import { find } from "lodash";
import { performanceIndicator, secondsToDhms } from "utilities/helpers";
import {
getPerformanceImpactDescription,
secondsToDhms,
} from "utilities/helpers";
import { IScheduledQuery } from "interfaces/scheduled_query";
import { IDropdownOption } from "interfaces/dropdownOption";
import Checkbox from "components/forms/fields/Checkbox";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import TooltipWrapper from "components/TooltipWrapper";
@ -45,7 +48,7 @@ interface ICellProps extends IRowProps {
};
}
interface IPillCellProps extends IRowProps {
interface IPerformanceImpactCellProps extends IRowProps {
cell: {
value: { indicator: string; id: number };
};
@ -64,7 +67,7 @@ interface IDataColumn {
accessor?: string;
Cell:
| ((props: ICellProps) => JSX.Element)
| ((props: IPillCellProps) => JSX.Element)
| ((props: IPerformanceImpactCellProps) => JSX.Element)
| ((props: IDropdownCellProps) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
@ -170,8 +173,8 @@ const generateTableHeaders = (
},
disableSortBy: true,
accessor: "performance",
Cell: (cellProps: IPillCellProps) => (
<PillCell value={cellProps.cell.value} />
Cell: (cellProps: IPerformanceImpactCellProps) => (
<PerformanceImpactCell value={cellProps.cell.value} />
),
},
{
@ -291,7 +294,7 @@ const enhancePackQueriesData = (
query_name: query.query_name,
actions: generateActionDropdownOptions(),
performance: [
performanceIndicator(scheduledQueryPerformance),
getPerformanceImpactDescription(scheduledQueryPerformance),
query.query_id,
],
stats: query.stats,

View File

@ -1,5 +1,6 @@
import { IPolicy } from "./policy";
import { IQuery } from "./query";
import { IScheduledQueryStats } from "./scheduled_query_stats";
import { ITeamSummary } from "./team";
import { UserRole } from "./user";
@ -104,4 +105,5 @@ export interface IActivityDetails {
script_name?: string;
deadline_days?: number;
grace_period_days?: number;
stats?: IScheduledQueryStats;
}

View File

@ -7,6 +7,7 @@ import activitiesAPI, {
} from "services/entities/activities";
import { ActivityType, IActivityDetails } from "interfaces/activity";
import { getPerformanceImpactDescription } from "utilities/helpers";
import ShowQueryModal from "components/modals/ShowQueryModal";
import DataError from "components/DataError";
@ -35,6 +36,7 @@ const ActivityFeed = ({
const [showShowQueryModal, setShowShowQueryModal] = useState(false);
const [showScriptDetailsModal, setShowScriptDetailsModal] = useState(false);
const queryShown = useRef("");
const queryImpact = useRef<string | undefined>(undefined);
const scriptExecutionId = useRef("");
const {
@ -82,6 +84,9 @@ const ActivityFeed = ({
switch (activityType) {
case ActivityType.LiveQuery:
queryShown.current = details.query_sql ?? "";
queryImpact.current = details.stats
? getPerformanceImpactDescription(details.stats)
: undefined;
setShowShowQueryModal(true);
break;
case ActivityType.RanScript:
@ -169,6 +174,7 @@ const ActivityFeed = ({
{showShowQueryModal && (
<ShowQueryModal
query={queryShown.current}
impact={queryImpact.current}
onCancel={() => setShowShowQueryModal(false)}
/>
)}

View File

@ -5,6 +5,8 @@ import createMockActivity from "__mocks__/activityMock";
import createMockQuery from "__mocks__/queryMock";
import { createMockTeamSummary } from "__mocks__/teamMock";
import { ActivityType } from "interfaces/activity";
import { createCustomRenderer } from "test/test-utils";
import createMockConfig from "__mocks__/configMock";
import ActivityItem from ".";
@ -92,12 +94,57 @@ describe("Activity Feed", () => {
});
render(<ActivityItem activity={activity} isPremiumTier />);
expect(
screen.getByText("ran the query as a live query .")
).toBeInTheDocument();
expect(screen.getByText(/ran the/)).toBeInTheDocument();
expect(screen.getByText("Test Query")).toBeInTheDocument();
expect(screen.getByText("Show query")).toBeInTheDocument();
});
it("renders a live_query type activity for a saved live query with targets and performance impact", () => {
const activity = createMockActivity({
type: ActivityType.LiveQuery,
details: {
query_name: "Test Query",
query_sql: "SELECT * FROM users",
targets_count: 10,
stats: {
system_time_p50: 0,
system_time_p95: 50.4923,
total_executions: 345,
},
},
});
render(<ActivityItem activity={activity} isPremiumTier />);
expect(screen.getByText(/ran the/)).toBeInTheDocument();
expect(screen.getByText("Test Query")).toBeInTheDocument();
expect(
screen.getByText(/with excessive performance impact on 10 hosts\./)
).toBeInTheDocument();
expect(screen.getByText("Show query")).toBeInTheDocument();
});
it("renders a live_query type activity for a saved live query with targets and no performance impact", () => {
const activity = createMockActivity({
type: ActivityType.LiveQuery,
details: {
query_name: "Test Query",
query_sql: "SELECT * FROM users",
targets_count: 10,
stats: {
system_time_p50: 0,
system_time_p95: 0,
total_executions: 0,
},
},
});
render(<ActivityItem activity={activity} isPremiumTier />);
expect(screen.getByText(/ran the/)).toBeInTheDocument();
expect(screen.getByText("Test Query")).toBeInTheDocument();
expect(screen.queryByText(/Undetermined/)).toBeNull();
expect(screen.getByText("Show query")).toBeInTheDocument();
});
it("renders an applied_spec_pack type activity", () => {
const activity = createMockActivity({

View File

@ -5,6 +5,7 @@ import { formatDistanceToNowStrict } from "date-fns";
import { ActivityType, IActivity, IActivityDetails } from "interfaces/activity";
import {
addGravatarUrlToResource,
getPerformanceImpactDescription,
internationalTimeFormat,
} from "utilities/helpers";
import { DEFAULT_GRAVATAR_LINK } from "utilities/constants";
@ -89,27 +90,40 @@ const TAGGED_TEMPLATES = {
activity: IActivity,
onDetailsClick?: (type: ActivityType, details: IActivityDetails) => void
) => {
const count = activity.details?.targets_count;
const queryName = activity.details?.query_name;
const querySql = activity.details?.query_sql;
const {
targets_count: count,
query_name: queryName,
query_sql: querySql,
stats,
} = activity.details || {};
const savedQueryName = queryName ? (
const impactDescription = stats
? getPerformanceImpactDescription(stats)
: undefined;
const queryNameCopy = queryName ? (
<>
the <b>{queryName}</b> query as
the <b>{queryName}</b> query
</>
) : (
<>a live query</>
);
const impactCopy =
impactDescription && impactDescription !== "Undetermined" ? (
<>with {impactDescription.toLowerCase()} performance impact</>
) : (
<></>
);
const hostCount =
const hostCountCopy =
count !== undefined
? ` on ${count} ${count === 1 ? "host" : "hosts"}`
: "";
return (
<>
<span>
ran {savedQueryName} a live query {hostCount}.
<span className={`${baseClass}__details-content`}>
ran {queryNameCopy} {impactCopy} {hostCountCopy}.
</span>
{querySql && (
<>
@ -119,6 +133,7 @@ const TAGGED_TEMPLATES = {
onClick={() =>
onDetailsClick?.(ActivityType.LiveQuery, {
query_sql: querySql,
stats,
})
}
>

View File

@ -40,15 +40,15 @@
overflow-wrap: anywhere;
}
&__details-content {
margin-right: $pad-xsmall;
}
&__details-bottomline {
font-size: $xx-small;
color: $ui-fleet-black-25;
}
&__show-query-link {
margin-left: $pad-xsmall;
}
&__show-query-icon {
margin-left: $pad-xsmall;
}

View File

@ -4,12 +4,12 @@ import { uniqueId } from "lodash";
import { IQueryStats } from "interfaces/query_stats";
import {
humanQueryLastRun,
performanceIndicator,
getPerformanceImpactDescription,
secondsToHms,
} from "utilities/helpers";
import TextCell from "components/TableContainer/DataTable/TextCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell";
import TooltipWrapper from "components/TooltipWrapper";
interface IHeaderProps {
@ -31,7 +31,7 @@ interface ICellProps extends IRowProps {
};
}
interface IPillCellProps extends IRowProps {
interface IPerformanceImpactCell extends IRowProps {
cell: {
value: { indicator: string; id: number };
};
@ -43,7 +43,7 @@ interface IDataColumn {
accessor: string;
Cell:
| ((props: ICellProps) => JSX.Element)
| ((props: IPillCellProps) => JSX.Element);
| ((props: IPerformanceImpactCell) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
}
@ -116,8 +116,8 @@ const generatePackTableHeaders = (): IDataColumn[] => {
},
disableSortBy: true,
accessor: "performance",
Cell: (cellProps: IPillCellProps) => (
<PillCell
Cell: (cellProps: IPerformanceImpactCell) => (
<PerformanceImpactCell
value={cellProps.cell.value}
customIdPrefix="query-perf-pill"
/>
@ -139,7 +139,7 @@ const enhancePackData = (query_stats: IQueryStats[]): IPackTable[] => {
frequency: secondsToHms(query.interval),
last_run: humanQueryLastRun(query.last_executed),
performance: {
indicator: performanceIndicator(scheduledQueryPerformance),
indicator: getPerformanceImpactDescription(scheduledQueryPerformance),
id: query.scheduled_query_id || parseInt(uniqueId(), 10),
},
};

View File

@ -1,10 +1,10 @@
import React from "react";
import { IQueryStats } from "interfaces/query_stats";
import { performanceIndicator } from "utilities/helpers";
import { getPerformanceImpactDescription } from "utilities/helpers";
import TextCell from "components/TableContainer/DataTable/TextCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell";
import TooltipWrapper from "components/TooltipWrapper";
import ReportUpdatedCell from "pages/hosts/details/cards/Queries/ReportUpdatedCell";
import Icon from "components/Icon";
@ -32,7 +32,7 @@ interface ICellProps extends IRowProps {
};
}
interface IPillCellProps extends IRowProps {
interface IPerformanceImpactCell extends IRowProps {
cell: {
value: {
indicator: string;
@ -47,7 +47,7 @@ interface IDataColumn {
accessor: string;
Cell:
| ((props: ICellProps) => JSX.Element)
| ((props: IPillCellProps) => JSX.Element);
| ((props: IPerformanceImpactCell) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
}
@ -84,14 +84,14 @@ const generateColumnConfigs = (
},
disableSortBy: true,
accessor: "performance",
Cell: (cellProps: IPillCellProps) => {
Cell: (cellProps: IPerformanceImpactCell) => {
const baseClass = "performance-cell";
return (
<span className={baseClass}>
<PillCell
<PerformanceImpactCell
value={cellProps.cell.value}
customIdPrefix="query-perf-pill"
hostDetails
isHostSpecific
/>
{!queryReportsDisabled &&
cellProps.row.original.should_link_to_hqr && (
@ -145,7 +145,7 @@ const enhanceScheduleData = (
query_name,
id: scheduled_query_id,
performance: {
indicator: performanceIndicator(scheduledQueryPerformance),
indicator: getPerformanceImpactDescription(scheduledQueryPerformance),
id: scheduled_query_id,
},
last_fetched,

View File

@ -13,7 +13,7 @@ import { AppContext } from "context/app";
import { QueryContext } from "context/query";
import { TableContext } from "context/table";
import { NotificationContext } from "context/notification";
import { performanceIndicator } from "utilities/helpers";
import { getPerformanceImpactDescription } from "utilities/helpers";
import { SupportedPlatform } from "interfaces/platform";
import { API_ALL_TEAMS_ID } from "interfaces/team";
import {
@ -67,7 +67,7 @@ const getPlatforms = (queryString: string): SupportedPlatform[] => {
const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => {
return {
...q,
performance: performanceIndicator(
performance: getPerformanceImpactDescription(
pick(q.stats, ["user_time_p50", "system_time_p50", "total_executions"])
),
platforms: getPlatforms(q.query),

View File

@ -7,8 +7,6 @@ import { IQuery } from "interfaces/query";
import { IEmptyTableProps } from "interfaces/empty_table";
import { ITableQueryData } from "components/TableContainer/TableContainer";
import PATHS from "router/paths";
import { isEmpty } from "lodash";
import { getNextLocationPath } from "utilities/helpers";
import Button from "components/buttons/Button";
import TableContainer from "components/TableContainer";
@ -16,7 +14,7 @@ import CustomLink from "components/CustomLink";
import EmptyTable from "components/EmptyTable";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import generateTableHeaders from "./QueriesTableConfig";
import generateColumnConfigs from "./QueriesTableConfig";
const baseClass = "queries-table";
@ -278,22 +276,22 @@ const QueriesTable = ({
);
};
const tableHeaders = useMemo(
const columnConfigs = useMemo(
() =>
currentUser &&
generateTableHeaders({ currentUser, isInherited, currentTeamId }),
generateColumnConfigs({ currentUser, isInherited, currentTeamId }),
[currentUser, isInherited, currentTeamId]
);
const searchable =
!(queriesList?.length === 0 && searchQuery === "") && !isInherited;
return tableHeaders && !isLoading ? (
return columnConfigs && !isLoading ? (
<div className={`${baseClass}`}>
<TableContainer
disableCount={isInherited}
resultsTitle="queries"
columnConfigs={tableHeaders}
columnConfigs={columnConfigs}
data={queriesList}
filters={{ name: isInherited ? "" : searchQuery }}
isLoading={isLoading}

View File

@ -21,7 +21,7 @@ import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import PlatformCell from "components/TableContainer/DataTable/PlatformCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell";
import TooltipWrapper from "components/TooltipWrapper";
import { COLORS } from "styles/var/colors";
import QueryAutomationsStatusIndicator from "../QueryAutomationsStatusIndicator";
@ -196,14 +196,7 @@ const generateTableHeaders = ({
Header: () => {
return (
<div>
<TooltipWrapper
tipContent={
<>
This is the average performance impact across <br />
all hosts where this query was scheduled.
</>
}
>
<TooltipWrapper tipContent="The average performance impact across all hosts.">
Performance impact
</TooltipWrapper>
</div>
@ -212,7 +205,7 @@ const generateTableHeaders = ({
disableSortBy: true,
accessor: "performance",
Cell: (cellProps: IStringCellProps) => (
<PillCell
<PerformanceImpactCell
value={{
indicator: cellProps.cell.value,
id: cellProps.row.original.id,

View File

@ -186,3 +186,21 @@ $max-width: 2560px;
box-shadow: 0px 3px 0px rgba(226, 228, 234, 0.4);
}
}
@mixin tooltip-text {
width: max-content;
max-width: 360px;
padding: 6px;
color: $core-white;
background-color: $tooltip-bg;
font-weight: $regular;
font-size: $xx-small;
border-radius: 4px;
box-sizing: border-box;
z-index: 99; // not more than the site nav
line-height: 1.375;
white-space: initial;
p {
margin: 0;
}
}

View File

@ -642,9 +642,9 @@ export const readableDate = (date: string) => {
}).format(dateString);
};
export const performanceIndicator = (
export const getPerformanceImpactDescription = (
scheduledQueryStats: IScheduledQueryStats
): string => {
) => {
if (
!scheduledQueryStats.total_executions ||
scheduledQueryStats.total_executions === 0 ||