From 2e70ad2955b46b663b29d2e13184be073f4da25c Mon Sep 17 00:00:00 2001 From: Nico <32375741+nulmete@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:06:12 -0300 Subject: [PATCH] Surface queries in host details (#37646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Related issue:** Resolves #27322 [Figma](https://www.figma.com/design/v7WjL5zQuFIZerWYaSwy8o/-27322-Surface-custom-host-vitals?node-id=5636-4950&t=LuE3Kp09a5sj24Tt-0) ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually (WIP) ## Screenshots ### Host details Screenshot 2025-12-26 at 2 14
48 PM - `Queries` tab removed. - Shows `Queries` card. #### Queries Card - Added client-side pagination. - Added `Add query` button (screenshots below are with `Admin` role). Screenshot 2025-12-26 at 2 15 07 PM Screenshot 2025-12-26 at 2 15 00 PM - As an `Observer`, `Add query` is not displayed Screenshot 2025-12-26 at 2 27
25 PM - As a `Maintainer`, `Add query` is displayed Screenshot 2025-12-26 at 2 31
16 PM ### New query page If the user navigates from `Host details`, `host_id` search parameter is added to the URL and the back button displays `Back to host details`. Screenshot 2025-12-26 at 2 15 32 PM ### Host Queries (/hosts/:hostId/queries/:queryId) `Performance impact` added above the table. Screenshot 2025-12-26 at 2 16 00 PM Screenshot 2025-12-26 at 2 16 05 PM --- changes/27322-surface-queries-in-host-details | 2 + .../PerformanceImpactCell.tsx | 58 ++----- .../modals/ShowQueryModal/ShowQueryModal.tsx | 3 +- frontend/context/app.tsx | 9 + frontend/interfaces/schedulable_query.ts | 20 ++- .../cards/ActivityFeed/ActivityFeed.tsx | 3 +- .../HostDetailsPage/HostDetailsPage.tsx | 102 +++++------ .../details/HostDetailsPage/_styles.scss | 4 +- .../HQRTable/HQRTable.tests.tsx | 4 + .../HostQueryReport/HQRTable/HQRTable.tsx | 59 ++++++- .../HostQueryReport/HQRTable/_styles.scss | 12 +- .../HostQueryReport/HostQueryReport.tsx | 7 +- .../cards/Packs/PackTable/PackTableConfig.tsx | 157 ----------------- .../pages/hosts/details/cards/Packs/Packs.tsx | 84 --------- .../hosts/details/cards/Packs/_styles.scss | 108 ------------ .../pages/hosts/details/cards/Packs/index.ts | 1 - .../details/cards/Queries/HostQueries.tsx | 164 ++++++++---------- .../cards/Queries/HostQueriesTableConfig.tsx | 90 ++-------- .../ReportUpdatedCell.tests.tsx | 4 +- .../ReportUpdatedCell/ReportUpdatedCell.tsx | 4 +- .../hosts/details/cards/Queries/_styles.scss | 36 ++-- .../pages/hosts/details/cards/User/User.tsx | 1 - .../QueryDetailsPage/QueryDetailsPage.tsx | 21 ++- frontend/pages/queries/edit/EditQueryPage.tsx | 47 +++-- .../EditQueryForm/EditQueryForm.tsx | 31 +++- .../SaveAsNewQueryModal.tsx | 3 + ...thAnyMaintainerAdminObserverPlusRoutes.tsx | 21 +-- frontend/router/index.tsx | 3 - frontend/utilities/helpers.tsx | 62 ++++++- 29 files changed, 423 insertions(+), 697 deletions(-) create mode 100644 changes/27322-surface-queries-in-host-details delete mode 100644 frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx delete mode 100644 frontend/pages/hosts/details/cards/Packs/Packs.tsx delete mode 100644 frontend/pages/hosts/details/cards/Packs/_styles.scss delete mode 100644 frontend/pages/hosts/details/cards/Packs/index.ts diff --git a/changes/27322-surface-queries-in-host-details b/changes/27322-surface-queries-in-host-details new file mode 100644 index 0000000000..8cfaaef2bd --- /dev/null +++ b/changes/27322-surface-queries-in-host-details @@ -0,0 +1,2 @@ +- Remove Queries tab from Host Details page. +- Surface Queries within the Details tab. \ No newline at end of file diff --git a/frontend/components/TableContainer/DataTable/PerformanceImpactCell/PerformanceImpactCell.tsx b/frontend/components/TableContainer/DataTable/PerformanceImpactCell/PerformanceImpactCell.tsx index edc10e31d0..d659efb693 100644 --- a/frontend/components/TableContainer/DataTable/PerformanceImpactCell/PerformanceImpactCell.tsx +++ b/frontend/components/TableContainer/DataTable/PerformanceImpactCell/PerformanceImpactCell.tsx @@ -5,6 +5,12 @@ import { uniqueId } from "lodash"; import ReactTooltip from "react-tooltip"; import { COLORS } from "styles/var/colors"; +import { getPerformanceImpactIndicatorTooltip } from "utilities/helpers"; +import { + isPerformanceImpactIndicator, + PerformanceImpactIndicatorValue, +} from "interfaces/schedulable_query"; + interface IPerformanceImpactCellValue { indicator: string; id?: number; @@ -40,50 +46,12 @@ const PerformanceImpactCell = ({ "Undetermined", ].includes(indicator); - const tooltipText = () => { - switch (indicator) { - case "Minimal": - return ( - <> - Running this query very frequently has little to no
impact on - your device's performance. - - ); - case "Considerable": - 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. - - ); - case "Undetermined": - return ( - <> - Performance impact will be available when{" "} - {isHostSpecific ? "the" : "this"}
- query runs{isHostSpecific && " on this host"}. - - ); - default: - return null; - } - }; const tooltipId = uniqueId(); + const indicatorValue = isPerformanceImpactIndicator(indicator) + ? indicator + : PerformanceImpactIndicatorValue.UNDETERMINED; + return ( - {indicator} + {indicatorValue} - {tooltipText()} + {getPerformanceImpactIndicatorTooltip(indicatorValue, isHostSpecific)} diff --git a/frontend/components/modals/ShowQueryModal/ShowQueryModal.tsx b/frontend/components/modals/ShowQueryModal/ShowQueryModal.tsx index 64fe85f92d..7c2d5337cc 100644 --- a/frontend/components/modals/ShowQueryModal/ShowQueryModal.tsx +++ b/frontend/components/modals/ShowQueryModal/ShowQueryModal.tsx @@ -4,13 +4,14 @@ import SQLEditor from "components/SQLEditor"; import Modal from "components/Modal"; import Button from "components/buttons/Button"; import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell"; +import { PerformanceImpactIndicator } from "interfaces/schedulable_query"; const baseClass = "show-query-modal"; interface IShowQueryModalProps { onCancel: () => void; query?: string; - impact?: string; + impact?: PerformanceImpactIndicator; } const ShowQueryModal = ({ diff --git a/frontend/context/app.tsx b/frontend/context/app.tsx index 70ef7542d0..ef39e93c3c 100644 --- a/frontend/context/app.tsx +++ b/frontend/context/app.tsx @@ -167,6 +167,7 @@ type InitialStateType = { isOnlyObserver?: boolean; isObserverPlus?: boolean; isNoAccess?: boolean; + isAnyMaintainerAdminObserverPlus?: boolean; isAndroidEnterpriseDeleted: boolean; isAppleBmExpired: boolean; isApplePnsExpired: boolean; @@ -237,6 +238,7 @@ export const initialState = { isOnlyObserver: undefined, isObserverPlus: undefined, isNoAccess: undefined, + isAnyMaintainerAdminObserverPlus: undefined, filteredHostsPath: undefined, filteredSoftwarePath: undefined, filteredQueriesPath: undefined, @@ -522,6 +524,13 @@ const AppProvider = ({ children }: Props): JSX.Element => { isOnlyObserver: state.isOnlyObserver, isObserverPlus: state.isObserverPlus, isNoAccess: state.isNoAccess, + isAnyMaintainerAdminObserverPlus: + state.isGlobalAdmin || + state.isGlobalMaintainer || + state.isAnyTeamAdmin || + state.isAnyTeamMaintainer || + state.isObserverPlus || + state.isAnyTeamObserverPlus, setAvailableTeams: ( user: IUser | null, availableTeams: ITeamSummary[] diff --git a/frontend/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index 6dc8bb6ae9..78b61ad1da 100644 --- a/frontend/interfaces/schedulable_query.ts +++ b/frontend/interfaces/schedulable_query.ts @@ -37,7 +37,7 @@ export interface ISchedulableQuery { } export interface IEnhancedQuery extends ISchedulableQuery { - performance: string; + performance: PerformanceImpactIndicator; targetedPlatforms: QueryablePlatform[]; } export interface ISchedulableQueryStats { @@ -48,6 +48,24 @@ export interface ISchedulableQueryStats { total_executions?: number; } +export const PerformanceImpactIndicatorValue = { + MINIMAL: "Minimal", + CONSIDERABLE: "Considerable", + EXCESSIVE: "Excessive", + UNDETERMINED: "Undetermined", + DENYLISTED: "Denylisted", +} as const; + +export type PerformanceImpactIndicator = typeof PerformanceImpactIndicatorValue[keyof typeof PerformanceImpactIndicatorValue]; + +export const isPerformanceImpactIndicator = ( + value: unknown +): value is PerformanceImpactIndicator => { + return Object.values(PerformanceImpactIndicatorValue).includes( + value as PerformanceImpactIndicator + ); +}; + // legacy export default PropTypes.shape({ user_time_p50: PropTypes.number, diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index cfac95b79c..3fcd637a9c 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -16,6 +16,7 @@ import { SCRIPT_PACKAGE_SOURCES, } from "interfaces/software"; import { ActivityType, IActivityDetails } from "interfaces/activity"; +import { PerformanceImpactIndicator } from "interfaces/schedulable_query"; import { getPerformanceImpactDescription } from "utilities/helpers"; @@ -141,7 +142,7 @@ const ActivityFeed = ({ const [typeFilter, setTypeFilter] = useState([""]); const queryShown = useRef(""); - const queryImpact = useRef(undefined); + const queryImpact = useRef(undefined); const scriptExecutionId = useRef(""); const { startDate, endDate } = useMemo(() => generateDateFilter(dateFilter), [ diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 6c712decc5..59b71f16d1 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -56,6 +56,7 @@ import { HOST_OSQUERY_DATA, DEFAULT_USE_QUERY_OPTIONS, } from "utilities/constants"; +import { getPathWithQueryParams } from "utilities/url"; import { isAppleDevice, @@ -107,7 +108,6 @@ import SoftwareLibraryCard from "../cards/HostSoftwareLibrary"; import LocalUserAccountsCard from "../cards/LocalUserAccounts"; import PoliciesCard from "../cards/Policies"; import QueriesCard from "../cards/Queries"; -import PacksCard from "../cards/Packs"; import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal"; import CertificatesCard from "../cards/Certificates"; @@ -141,7 +141,7 @@ const baseClass = "host-details"; const defaultCardClass = `${baseClass}__card`; const fullWidthCardClass = `${baseClass}__card--full-width`; -const tripleHeightCardClass = `${baseClass}__card--triple-height`; +const doubleHeightCardClass = `${baseClass}__card--double-height`; export const REFETCH_HOST_DETAILS_POLLING_INTERVAL = 2000; // 2 seconds const BYOD_SW_INSTALL_LEARN_MORE_LINK = @@ -197,12 +197,12 @@ const HostDetailsPage = ({ currentUser, isGlobalAdmin = false, isGlobalMaintainer, - isGlobalObserver, isTeamMaintainerOrTeamAdmin, isPremiumTier = false, isOnlyObserver, filteredHostsPath, currentTeam, + isAnyMaintainerAdminObserverPlus, } = useContext(AppContext); const { renderFlash } = useContext(NotificationContext); @@ -259,7 +259,6 @@ const HostDetailsPage = ({ const [refetchStartTime, setRefetchStartTime] = useState(null); const [showRefetchSpinner, setShowRefetchSpinner] = useState(false); const [schedule, setSchedule] = useState(); - const [packsState, setPackState] = useState(); const [usersState, setUsersState] = useState<{ username: string }[]>([]); const [usersSearchString, setUsersSearchString] = useState(""); const [ @@ -949,6 +948,15 @@ const HostDetailsPage = ({ setSelectedCertificate(certificate); }; + const onClickAddQuery = () => { + router.push( + getPathWithQueryParams(PATHS.NEW_QUERY, { + team_id: currentTeam?.id, + host_id: hostIdFromURL, + }) + ); + }; + const renderActionsDropdown = () => { if (!host) { return null; @@ -1028,11 +1036,6 @@ const HostDetailsPage = ({ title: "software", pathname: PATHS.HOST_SOFTWARE(hostIdFromURL), }, - { - name: "Queries", - title: "queries", - pathname: PATHS.HOST_QUERIES(hostIdFromURL), - }, { name: "Policies", title: "policies", @@ -1086,23 +1089,6 @@ const HostDetailsPage = ({ host?.team_id ); - /* Context team id might be different that host's team id -Observer plus must be checked against host's team id */ - const isGlobalOrHostsTeamObserverPlus = - currentUser && host?.team_id - ? permissions.isObserverPlus(currentUser, host.team_id) - : false; - - const isHostsTeamObserver = - currentUser && host?.team_id - ? permissions.isTeamObserver(currentUser, host.team_id) - : false; - - const canViewPacks = - !isGlobalObserver && - !isGlobalOrHostsTeamObserverPlus && - !isHostsTeamObserver; - const bootstrapPackageData = { status: host?.mdm.macos_setup?.bootstrap_package_status, details: host?.mdm.macos_setup?.details, @@ -1317,11 +1303,39 @@ Observer plus must be checked against host's team id */ host.platform )} /> + + + | React.KeyboardEvent + ) => { + e.preventDefault(); + setShowUpdateEndUserModal(true); + }} + /> {showActivityCard && ( )} - - | React.KeyboardEvent - ) => { - e.preventDefault(); - setShowUpdateEndUserModal(true); - }} - /> {showAgentOptionsCard && ( - - - {canViewPacks && ( - - )} - { it("Renders results normally when they are present", () => { const testData: IHQRTable[] = [ { + queryId: 1, queryName: "testQuery0", queryDescription: "testDescription0", hostName: "testHost0", @@ -50,6 +51,7 @@ describe("HQRTable component", () => { it("Renders the 'collecting results' empty state when results have never been collected.", () => { const testData: IHQRTable[] = [ { + queryId: 1, queryName: "testQuery0", queryDescription: "testDescription0", hostName: "testHost0", @@ -71,6 +73,7 @@ describe("HQRTable component", () => { it("Renders the 'report clipped' empty state when reporting for this query has been paused and there are no existing results.", () => { const testData: IHQRTable[] = [ { + queryId: 1, queryName: "testQuery0", queryDescription: "testDescription0", hostName: "testHost0", @@ -92,6 +95,7 @@ describe("HQRTable component", () => { it("Renders the 'nothing to report' empty state when the query has run and there are no results.", () => { const testData: IHQRTable[] = [ { + queryId: 1, queryName: "testQuery0", queryDescription: "testDescription0", hostName: "testHost0", diff --git a/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx b/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx index 9cdd4ac30a..619dd03348 100644 --- a/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx +++ b/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx @@ -12,13 +12,57 @@ import { import FileSaver from "file-saver"; import Spinner from "components/Spinner"; import { HumanTimeDiffWithFleetLaunchCutoff } from "components/HumanTimeDiffWithDateTip"; +import TooltipWrapper from "components/TooltipWrapper"; +import { + getPerformanceImpactDescription, + getPerformanceImpactIndicatorTooltip, +} from "utilities/helpers"; +import { ISchedulableQueryStats } from "interfaces/schedulable_query"; import generateColumnConfigs from "./HQRTableConfig"; const baseClass = "hqr-table"; +const DEFAULT_CSV_TITLE = "Host-Specific Query Report"; + +type PerformanceImpactProps = { + queryStats?: ISchedulableQueryStats; + queryId: number; +}; + +const PerformanceImpact = ({ queryStats, queryId }: PerformanceImpactProps) => { + const { total_executions = 0, user_time_p50 = 0, system_time_p50 = 0 } = + queryStats || {}; + + const scheduledQueryPerformance = { + user_time_p50: + total_executions > 0 ? Number(user_time_p50) / total_executions : 0, + system_time_p50: + total_executions > 0 ? Number(system_time_p50) / total_executions : 0, + total_executions, + }; + + const performanceImpact = { + indicator: getPerformanceImpactDescription(scheduledQueryPerformance), + id: queryId, + }; + + return ( + + + Performance impact: {performanceImpact.indicator} + + + ); +}; export interface IHQRTable { + queryId: number; queryName?: string; queryDescription?: string; + queryStats?: ISchedulableQueryStats; hostName?: string; rows: Record[]; reportClipped?: boolean; @@ -27,11 +71,11 @@ export interface IHQRTable { isLoading: boolean; } -const DEFAULT_CSV_TITLE = "Host-Specific Query Report"; - const HQRTable = ({ + queryId, queryName, queryDescription, + queryStats, hostName, rows, reportClipped, @@ -85,8 +129,6 @@ const HQRTable = ({ }, [onShowQuery, filteredResults, queryName, hostName, columnConfigs]); const renderEmptyState = useCallback(() => { - // rows.length === 0 - if (reportClipped) { return ( (
-

{queryName}

-

{queryDescription}

+
+

{queryName}

+

{queryDescription}

+
+
), - [queryDescription, queryName] + [queryDescription, queryName, queryStats, queryId] ); if (isLoading) { diff --git a/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss b/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss index 707bb799f1..eb65d0a30b 100644 --- a/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss +++ b/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss @@ -1,6 +1,10 @@ .hqr-table { @include vertical-card-layout; + .performance-impact { + font-size: $x-small; + } + .last-fetched { font-weight: initial; @include grey-text; @@ -13,7 +17,8 @@ &__query-info { display: flex; - flex-direction: column; + align-items: start; + justify-content: space-between; gap: $pad-xsmall; h2 { @@ -26,6 +31,11 @@ font-weight: $regular; margin: 0; } + + @media (max-width: $break-sm) { + flex-direction: column; + gap: $pad-medium; + } } .data-table { diff --git a/frontend/pages/hosts/details/HostQueryReport/HostQueryReport.tsx b/frontend/pages/hosts/details/HostQueryReport/HostQueryReport.tsx index f0c329483d..7f61c6983f 100644 --- a/frontend/pages/hosts/details/HostQueryReport/HostQueryReport.tsx +++ b/frontend/pages/hosts/details/HostQueryReport/HostQueryReport.tsx @@ -92,6 +92,7 @@ const HostQueryReport = ({ description: queryDescription, query: querySQL, discard_data: queryDiscardData, + stats, } = queryResponse || {}; // previous reroute can be done before API call, not this one, hence 2 @@ -118,7 +119,7 @@ const HostQueryReport = ({
@@ -131,7 +132,7 @@ const HostQueryReport = ({ iconStroke > <> - View full query report + View data for all hosts @@ -148,8 +149,10 @@ const HostQueryReport = ({ <> JSX.Element) | string; - accessor: string; - Cell: - | ((props: ICellProps) => JSX.Element) - | ((props: IPerformanceImpactCell) => JSX.Element); - disableHidden?: boolean; - disableSortBy?: boolean; -} - -interface IPackTable extends Partial { - frequency: string; - last_run: string; - performance: { indicator: string; id: number }; -} - -// NOTE: cellProps come from react-table -// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties -const generatePackTableHeaders = (): IDataColumn[] => { - return [ - { - title: "Query", - Header: "Query", - disableSortBy: true, - accessor: "query_name", - Cell: (cellProps: ICellProps) => ( - - ), - }, - { - title: "Frequency", - Header: "Frequency", - disableSortBy: true, - accessor: "frequency", - Cell: (cellProps: ICellProps) => ( - - ), - }, - { - Header: () => { - return ( - - The last time the query ran -
- since the last time osquery
- started on this host. - - } - > - Last run -
- ); - }, - disableSortBy: true, - accessor: "last_run", - Cell: (cellProps: ICellProps) => ( - - ), - }, - { - Header: () => { - return ( - - This is the performance
- impact on this host. - - } - > - Performance impact -
- ); - }, - disableSortBy: true, - accessor: "performance", - Cell: (cellProps: IPerformanceImpactCell) => ( - - ), - }, - ]; -}; - -const enhancePackData = (query_stats: IQueryStats[]): IPackTable[] => { - return Object.values(query_stats).map((query) => { - const scheduledQueryPerformance = { - user_time_p50: query.user_time, - system_time_p50: query.system_time, - total_executions: query.executions, - }; - return { - query_name: query.query_name, - last_executed: query.last_executed, - frequency: secondsToHms(query.interval), - last_run: humanQueryLastRun(query.last_executed), - performance: { - indicator: getPerformanceImpactDescription(scheduledQueryPerformance), - id: query.scheduled_query_id || parseInt(uniqueId(), 10), - }, - }; - }); -}; - -const generatePackDataSet = (query_stats: IQueryStats[]): IPackTable[] => { - if (!query_stats) { - return query_stats; - } - - return [...enhancePackData(query_stats)]; -}; - -export { generatePackTableHeaders, generatePackDataSet }; diff --git a/frontend/pages/hosts/details/cards/Packs/Packs.tsx b/frontend/pages/hosts/details/cards/Packs/Packs.tsx deleted file mode 100644 index 2d4cd23c00..0000000000 --- a/frontend/pages/hosts/details/cards/Packs/Packs.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from "react"; - -import { IPackStats } from "interfaces/host"; -import TableContainer from "components/TableContainer"; -import Card from "components/Card"; -import CardHeader from "components/CardHeader"; - -import { - Accordion, - AccordionItem, - AccordionItemHeading, - AccordionItemButton, - AccordionItemPanel, -} from "react-accessible-accordion"; - -import { - generatePackTableHeaders, - generatePackDataSet, -} from "./PackTable/PackTableConfig"; - -const baseClass = "schedule-card"; - -interface IPacksProps { - packsState?: IPackStats[]; - isLoading: boolean; -} - -const Packs = ({ packsState, isLoading }: IPacksProps): JSX.Element => { - const packs = packsState; - const wrapperClassName = `${baseClass}__pack-table`; - const tableHeaders = generatePackTableHeaders(); - - let packsAccordion; - if (packs) { - packsAccordion = packs.map((pack) => { - return ( - - - {pack.pack_name} - - - {pack.query_stats.length === 0 ? ( -
There are no schedule queries for this pack.
- ) : ( - <> - {!!pack.query_stats.length && ( -
- null} - resultsTitle="queries" - defaultSortHeader="scheduled_query_name" - defaultSortDirection="asc" - showMarkAllPages={false} - isAllPagesSelected={false} - emptyComponent={() => <>} - disablePagination - disableCount - /> -
- )} - - )} -
-
- ); - }); - } - - return !packs || !packs.length ? ( - <> - ) : ( - - - - {packsAccordion} - - - ); -}; - -export default Packs; diff --git a/frontend/pages/hosts/details/cards/Packs/_styles.scss b/frontend/pages/hosts/details/cards/Packs/_styles.scss deleted file mode 100644 index f5d0326ecf..0000000000 --- a/frontend/pages/hosts/details/cards/Packs/_styles.scss +++ /dev/null @@ -1,108 +0,0 @@ -.card--packs { - .table-container__header { - display: none; - } - - .data-table-block { - .data-table__table { - thead { - .query_name__header { - width: $col-lg; - } - .frequency__header { - width: $col-md; - } - .last_run__header { - display: none; - width: 0; - } - @media (min-width: $break-md) { - .last_run__header { - display: table-cell; - } - } - } - tbody { - .query_name__cell { - width: $col-lg; - } - .frequency__cell { - width: $col-md; - } - .last_run__cell { - display: none; - width: 0; - } - @media (min-width: $break-md) { - .last_run__cell { - display: table-cell; - } - } - } - } - } - - .accordion { - border-radius: 2px; - - .accordion__item + .accordion__item { - border-top: 1px solid rgba(0, 0, 0, 0.1); - } - - &__button { - background-color: #fff; - color: $core-fleet-black; - cursor: pointer; - text-align: left; - font-size: $x-small; - font-weight: $bold; - border: none; - padding: 17px 12px; - - &:hover { - background-color: $ui-fleet-black-10; - } - - &:after { - display: block; - content: url("../assets/images/icon-chevron-purple-9x6@2x.png"); - text-align: center; - top: 50%; - float: right; - width: 32px; - height: 32px; - border-radius: 4px; - transform: scale(0.5) translate(40%, -40%); - } - - &[aria-expanded="true"]::after, - &[aria-selected="true"]::after { - background-color: $core-vibrant-blue; - content: url("../assets/images/icon-accordion-collapse-16x16@2x.png"); - } - } - - [hidden] { - display: none; - } - - &__panel { - padding: 0; - animation: fadein 0.35s ease-in; - } - - /* -------------------------------------------------- */ - /* ---------------- Animation part ------------------ */ - /* -------------------------------------------------- */ - - @keyframes fadein { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } - } - } -} diff --git a/frontend/pages/hosts/details/cards/Packs/index.ts b/frontend/pages/hosts/details/cards/Packs/index.ts deleted file mode 100644 index 5ce4f55582..0000000000 --- a/frontend/pages/hosts/details/cards/Packs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./Packs"; diff --git a/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx b/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx index 640efea06a..dff268938c 100644 --- a/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx +++ b/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx @@ -1,12 +1,14 @@ import React, { useCallback, useMemo } from "react"; -import { isAndroid } from "interfaces/platform"; +import { isAndroid, HostPlatform } from "interfaces/platform"; import { IQueryStats } from "interfaces/query_stats"; import { SUPPORT_LINK } from "utilities/constants"; import TableContainer from "components/TableContainer"; -import EmptyTable from "components/EmptyTable"; +import Card from "components/Card"; +import Button from "components/buttons/Button"; import CustomLink from "components/CustomLink"; import CardHeader from "components/CardHeader"; +import Icon from "components/Icon"; import PATHS from "router/paths"; import { InjectedRouter } from "react-router"; import { Row } from "react-table"; @@ -17,13 +19,16 @@ import { } from "./HostQueriesTableConfig"; const baseClass = "host-queries-card"; +const PAGE_SIZE = 4; interface IHostQueriesProps { hostId: number; schedule?: IQueryStats[]; - hostPlatform: string; + hostPlatform: HostPlatform; queryReportsDisabled?: boolean; router: InjectedRouter; + canAddQuery?: boolean; + onClickAddQuery: () => void; } interface IHostQueriesRowProps extends Row { @@ -34,74 +39,49 @@ interface IHostQueriesRowProps extends Row { }; } +type EmptyHostQueriesProps = { + hostPlatform: HostPlatform; +}; + +const EmptyHostQueries = ({ hostPlatform }: EmptyHostQueriesProps) => { + const platformActions: Record = { + chrome: "collecting data from your Chromebooks", + ios: "querying iPhones", + ipados: "querying iPads", + android: "querying Android hosts", + }; + + const action = platformActions[hostPlatform]; + + if (action) { + return ( +
+

Queries not supported for this host

+

+ Interested in {action}?{" "} + +

+
+ ); + } + + return ( +
+

No queries

+

Add a query to view custom vitals.

+
+ ); +}; + const HostQueries = ({ hostId, schedule, hostPlatform, queryReportsDisabled, router, + canAddQuery, + onClickAddQuery, }: IHostQueriesProps): JSX.Element => { - const renderEmptyQueriesTab = () => { - if (hostPlatform === "chrome") { - return ( - - Interested in collecting data from your Chromebooks? - - - } - /> - ); - } - - if (hostPlatform === "ios" || hostPlatform === "ipados") { - return ( - - Interested in querying{" "} - {hostPlatform === "ios" ? "iPhones" : "iPads"}?{" "} - - - } - /> - ); - } - - if (isAndroid(hostPlatform)) { - return ( - - Interested in querying Android hosts?{" "} - - - } - /> - ); - } - - return ( - - Expecting to see queries? Try selecting Refetch to ask this - host to report fresh vitals. - - } - /> - ); - }; - const onSelectSingleRow = useCallback( (row: IHostQueriesRowProps) => { const { id: queryId, should_link_to_hqr } = row.original; @@ -127,38 +107,48 @@ const HostQueries = ({ !schedule.length || hostPlatform === "chrome" || hostPlatform === "ios" || - hostPlatform === "ipados" + hostPlatform === "ipados" || + isAndroid(hostPlatform) ) { - return renderEmptyQueriesTab(); + return ; } return ( -
- null} - resultsTitle="queries" - defaultSortHeader="query_name" - defaultSortDirection="asc" - showMarkAllPages={false} - isAllPagesSelected={false} - emptyComponent={() => <>} - disablePagination - disableCount - disableMultiRowSelect={!queryReportsDisabled} // Removes hover/click state if reports are disabled - isLoading={false} // loading state handled at parent level - onSelectSingleRow={onSelectSingleRow} - /> -
+ null} + resultsTitle="queries" + defaultSortHeader="query_name" + defaultSortDirection="asc" + showMarkAllPages={false} + isAllPagesSelected={false} + emptyComponent={() => <>} + disablePagination={tableData.length <= PAGE_SIZE} + pageSize={PAGE_SIZE} + isClientSidePagination + disableCount + disableMultiRowSelect={!queryReportsDisabled} // Removes hover/click state if reports are disabled + isLoading={false} // loading state handled at parent level + onSelectSingleRow={onSelectSingleRow} + /> ); }; return ( -
- + +
+ + {canAddQuery && ( + + )} +
+ {renderHostQueries()} -
+ ); }; diff --git a/frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx b/frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx index b7eee6ddf7..65d1b6d830 100644 --- a/frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx @@ -1,18 +1,13 @@ import React from "react"; import { IQueryStats } from "interfaces/query_stats"; -import { getPerformanceImpactDescription } from "utilities/helpers"; import TooltipTruncatedTextCell from "components/TableContainer/DataTable/TooltipTruncatedTextCell"; -import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell"; +import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; import TooltipWrapper from "components/TooltipWrapper"; import ReportUpdatedCell from "pages/hosts/details/cards/Queries/ReportUpdatedCell"; -import Icon from "components/Icon"; -import { Link } from "react-router"; -import PATHS from "router/paths"; interface IHostQueriesTableData extends Partial { - performance: { indicator: string; id: number }; should_link_to_hqr: boolean; id: number; } @@ -64,69 +59,34 @@ const generateColumnConfigs = ( ): IDataColumn[] => { const cols: IDataColumn[] = [ { - title: "Query", - Header: "Query", - disableSortBy: true, accessor: "query_name", Cell: (cellProps: ICellProps) => ( ), + Header: (cellProps) => ( + + ), sortType: "caseInsensitive", }, - { - Header: () => { - return ( - - This is the performance
- impact on this host. - - } - > - Performance impact -
- ); - }, - disableSortBy: true, - accessor: "performance", - Cell: (cellProps: IPerformanceImpactCell) => { - const baseClass = "performance-cell"; - const queryId = cellProps.row.original.id; - return ( - - - {!queryReportsDisabled && - cellProps.row.original.should_link_to_hqr && - hostId && - queryId && ( - // parent row has same onClick functionality but link here is required for keyboard accessibility - - - - )} - - ); - }, - }, ]; // include the Report updated column if query reports are globally enabled if (!queryReportsDisabled) { cols.push({ - Header: "Report updated", + Header: () => { + return ( + + Each query is updated based on an
+ individually set interval. + + } + > + Last updated +
+ ); + }, disableSortBy: true, accessor: "last_fetched", // tbd - may change Cell: (cellProps: ICellProps) => { @@ -148,9 +108,6 @@ const enhanceScheduleData = ( ): IHostQueriesTableData[] => { return Object.values(query_stats).map((query) => { const { - user_time, - system_time, - executions, query_name, scheduled_query_id, last_fetched, @@ -158,20 +115,9 @@ const enhanceScheduleData = ( discard_data, automations_enabled, } = query; - // getPerformanceImpactDescription takes aggregate p50 values - // getPerformanceImpactDescription takes aggregate p50 values so we need to divide by total executions in order to show average performance per query execution - const scheduledQueryPerformance = { - user_time_p50: executions > 0 ? user_time / executions : 0, - system_time_p50: executions > 0 ? system_time / executions : 0, - total_executions: executions, - }; return { query_name, id: scheduled_query_id, - performance: { - indicator: getPerformanceImpactDescription(scheduledQueryPerformance), - id: scheduled_query_id, - }, last_fetched, interval, discard_data, diff --git a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx index 15434ed31b..b75c905bb4 100644 --- a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx +++ b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx @@ -60,7 +60,7 @@ describe("ReportUpdatedCell component", () => { expect(screen.getByText(HUMAN_READABLE_DATETIME_REGEX)).toBeInTheDocument(); expect(screen.getByText(/\d+.+ago/)).toBeInTheDocument(); - expect(screen.getByText(/View report/)).toBeInTheDocument(); + expect(screen.getByText(/View data/)).toBeInTheDocument(); }); it("Renders a last-updated timestamp with tooltip and link to report when a last_fetched date is present but not currently running an interval", () => { const tenDaysAgo = new Date(); @@ -79,6 +79,6 @@ describe("ReportUpdatedCell component", () => { expect(screen.getByText(HUMAN_READABLE_DATETIME_REGEX)).toBeInTheDocument(); expect(screen.getByText(/\d+.+ago/)).toBeInTheDocument(); - expect(screen.getByText(/View report/)).toBeInTheDocument(); + expect(screen.getByText(/View data/)).toBeInTheDocument(); }); }); diff --git a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx index 1db7f44f24..24d1d9a583 100644 --- a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx +++ b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx @@ -115,7 +115,7 @@ const ReportUpdatedCell = ({ ); }; - const onClick = (): void => { + const onClick = () => { hostId && queryId && browserHistory.push(PATHS.HOST_QUERY_REPORT(hostId, queryId)); @@ -132,7 +132,7 @@ const ReportUpdatedCell = ({ onClick={onClick} size="small" > - View report + View data )} diff --git a/frontend/pages/hosts/details/cards/Queries/_styles.scss b/frontend/pages/hosts/details/cards/Queries/_styles.scss index c983c74a80..c750d91381 100644 --- a/frontend/pages/hosts/details/cards/Queries/_styles.scss +++ b/frontend/pages/hosts/details/cards/Queries/_styles.scss @@ -1,6 +1,16 @@ .host-queries-card { @include vertical-page-tab-panel-layout; + // prevent layout shift if last page of paginated table + // doesn't fill all the vertical space due to fewer rows. + min-height: 305px; + + &__header { + display: flex; + align-items: baseline; + justify-content: space-between; + } + .table-container__header { display: none; } @@ -8,17 +18,11 @@ .data-table__table { thead { .query_name__header { - min-width: $col-lg; + min-width: $col-sm; } .last_fetched__header { display: table-cell; } - @media (max-width: $break-md) { - .last_fetched__header { - display: none; - width: 0; - } - } } tbody { tr { @@ -27,7 +31,7 @@ } .query_name__cell { - min-width: $col-lg; + min-width: $col-sm; } .last_fetched__cell { .report-updated-cell { @@ -49,19 +53,23 @@ opacity: 1; } } - @media (max-width: $break-md) { - .last_fetched__cell { + + @media (min-width: $break-md) and (max-width: 1300px) { + .report-updated-cell__view-report--text { display: none; width: 0; } - .performance-cell__link-icon { - display: inline-flex; - align-self: center; - width: initial; + + td { + max-width: 140px; } } } } } } + + .empty-header { + font-weight: $bold; + } } diff --git a/frontend/pages/hosts/details/cards/User/User.tsx b/frontend/pages/hosts/details/cards/User/User.tsx index 6567fd17d6..7d662065fb 100644 --- a/frontend/pages/hosts/details/cards/User/User.tsx +++ b/frontend/pages/hosts/details/cards/User/User.tsx @@ -1,6 +1,5 @@ import React from "react"; import classnames from "classnames"; -import { noop } from "lodash"; import { IHostEndUser } from "interfaces/host"; diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx index 02f40aaa6f..360fcc32fc 100644 --- a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx +++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx @@ -250,19 +250,23 @@ const QueryDetailsPage = ({ isTeamMaintainerOrTeamAdmin; // Function instead of constant eliminates race condition with filteredQueriesPath - const backToQueriesPath = () => { - return ( - filteredQueriesPath || - getPathWithQueryParams(PATHS.MANAGE_QUERIES, { - team_id: currentTeamId, - }) - ); + const backPath = () => { + if (filteredQueriesPath) return filteredQueriesPath; + + if (hostId) return getPathWithQueryParams(PATHS.HOST_DETAILS(hostId)); + + return getPathWithQueryParams(PATHS.MANAGE_QUERIES, { + team_id: currentTeamId, + }); }; return ( <>
- +
{!isLoading && !isApiError && ( <> @@ -329,6 +333,7 @@ const QueryDetailsPage = ({ router.push( getPathWithQueryParams(PATHS.EDIT_QUERY(queryId), { team_id: currentTeamId, + host_id: hostId, }) ); }} diff --git a/frontend/pages/queries/edit/EditQueryPage.tsx b/frontend/pages/queries/edit/EditQueryPage.tsx index 0fe0dc39da..1a1ffdbc6c 100644 --- a/frontend/pages/queries/edit/EditQueryPage.tsx +++ b/frontend/pages/queries/edit/EditQueryPage.tsx @@ -176,6 +176,7 @@ const EditQueryPage = ({ router.push( getPathWithQueryParams(location.pathname, { team_id: storedQuery?.team_id?.toString(), + host_id: hostId, }) ); } @@ -266,6 +267,7 @@ const EditQueryPage = ({ router.push( getPathWithQueryParams(PATHS.QUERY_DETAILS(query.id), { team_id: query.team_id, + host_id: hostId, }) ); renderFlash("success", "Query created!"); @@ -370,15 +372,36 @@ const EditQueryPage = ({ // Function instead of constant eliminates race condition // Returns to queries details page, manage queries page with filters, or default manage queries page - const backToQueriesPath = () => - queryId - ? getPathWithQueryParams(PATHS.QUERY_DETAILS(queryId), { - team_id: currentTeamId, - }) - : filteredQueriesPath || - getPathWithQueryParams(PATHS.MANAGE_QUERIES, { - team_id: currentTeamId, - }); + const backPath = () => { + if (queryId) { + return getPathWithQueryParams(PATHS.QUERY_DETAILS(queryId), { + team_id: currentTeamId, + host_id: hostId, + }); + } + + if (hostId) { + return getPathWithQueryParams(PATHS.HOST_DETAILS(hostId)); + } + + if (filteredQueriesPath) return filteredQueriesPath; + + return getPathWithQueryParams(PATHS.MANAGE_QUERIES, { + team_id: currentTeamId, + }); + }; + + const backButtonText = () => { + if (queryId) { + return "Back to report"; + } + + if (hostId) { + return "Back to host details"; + } + + return "Back to queries"; + }; const showSidebar = isSidebarOpen && @@ -394,10 +417,7 @@ const EditQueryPage = ({ <>
- +
{ + if (isFreeTier) return null; + + if (currentTeamName) { + if (isEditing) { + return ( +

+ Editing query for {currentTeamName} team. +

+ ); + } + return ( +

+ Creating a new query for {currentTeamName} team. +

+ ); + } + + if (isEditing) { + return

Editing global query.

; + } + return

Creating a new global query.

; + }; + // Observers and observer+ of existing query const renderNonEditableForm = (
@@ -572,6 +599,7 @@ const EditQueryForm = ({ {renderAuthor()}
+ {renderQueryTeam()}
{renderName()} - {savedQueryMode && renderAuthor()}
+ {renderQueryTeam(true)} {renderDescription()} )} diff --git a/frontend/pages/queries/edit/components/SaveAsNewQueryModal/SaveAsNewQueryModal.tsx b/frontend/pages/queries/edit/components/SaveAsNewQueryModal/SaveAsNewQueryModal.tsx index 42dcea9803..612feb1fb2 100644 --- a/frontend/pages/queries/edit/components/SaveAsNewQueryModal/SaveAsNewQueryModal.tsx +++ b/frontend/pages/queries/edit/components/SaveAsNewQueryModal/SaveAsNewQueryModal.tsx @@ -35,6 +35,7 @@ interface ISaveAsNewQueryModal { router: InjectedRouter; location: Location; initialQueryData: ICreateQueryRequestBody; + hostId?: number; onExit: () => void; } @@ -62,6 +63,7 @@ const SaveAsNewQueryModal = ({ router, location, initialQueryData, + hostId, onExit, }: ISaveAsNewQueryModal) => { const { renderFlash } = useContext(NotificationContext); @@ -161,6 +163,7 @@ const SaveAsNewQueryModal = ({ router.push( getPathWithQueryParams(PATHS.QUERY_DETAILS(newQuery.id), { team_id: newQuery.team_id, + host_id: hostId, }) ); } catch (createError: unknown) { diff --git a/frontend/router/components/AuthAnyMaintainerAdminObserverPlusRoutes/AuthAnyMaintainerAdminObserverPlusRoutes.tsx b/frontend/router/components/AuthAnyMaintainerAdminObserverPlusRoutes/AuthAnyMaintainerAdminObserverPlusRoutes.tsx index 2e2986dcd1..547b113863 100644 --- a/frontend/router/components/AuthAnyMaintainerAdminObserverPlusRoutes/AuthAnyMaintainerAdminObserverPlusRoutes.tsx +++ b/frontend/router/components/AuthAnyMaintainerAdminObserverPlusRoutes/AuthAnyMaintainerAdminObserverPlusRoutes.tsx @@ -13,28 +13,15 @@ const AuthAnyMaintainerAdminObserverPlusRoutes = ({ children, }: IAuthAnyMaintainerAdminObserverPlusRoutesProps) => { const handlePageError = useErrorHandler(); - const { - currentUser, - isGlobalAdmin, - isGlobalMaintainer, - isAnyTeamAdmin, - isAnyTeamMaintainer, - isAnyTeamObserverPlus, - isObserverPlus, - } = useContext(AppContext); + const { currentUser, isAnyMaintainerAdminObserverPlus } = useContext( + AppContext + ); if (!currentUser) { return null; } - if ( - !isGlobalAdmin && - !isGlobalMaintainer && - !isAnyTeamAdmin && - !isAnyTeamMaintainer && - !isObserverPlus && - !isAnyTeamObserverPlus - ) { + if (!isAnyMaintainerAdminObserverPlus) { handlePageError({ status: 403 }); return null; } diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index c9cbc56b8c..aa9846c345 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -277,9 +277,6 @@ const routes = ( - - - diff --git a/frontend/utilities/helpers.tsx b/frontend/utilities/helpers.tsx index 40f5808867..855b89aab5 100644 --- a/frontend/utilities/helpers.tsx +++ b/frontend/utilities/helpers.tsx @@ -25,6 +25,11 @@ import { QueryParams, buildQueryStringFromParams } from "utilities/url"; import { IHost } from "interfaces/host"; import { ILabel } from "interfaces/label"; import { IPack } from "interfaces/pack"; +import type { PerformanceImpactIndicator } from "interfaces/schedulable_query"; +import { + PerformanceImpactIndicatorValue, + ISchedulableQueryStats, +} from "interfaces/schedulable_query"; import { IScheduledQuery, IPackQueryFormData, @@ -49,7 +54,6 @@ import { PLATFORM_LABEL_DISPLAY_TYPES, isPlatformLabelNameFromAPI, } from "utilities/constants"; -import { ISchedulableQueryStats } from "interfaces/schedulable_query"; import { IDropdownOption } from "interfaces/dropdownOption"; import CustomLink from "components/CustomLink"; @@ -658,13 +662,13 @@ export const readableDate = (date: string) => { export const getPerformanceImpactDescription = ( scheduledQueryStats: ISchedulableQueryStats -) => { +): PerformanceImpactIndicator => { if ( !scheduledQueryStats.total_executions || scheduledQueryStats.total_executions === 0 || scheduledQueryStats.total_executions === null ) { - return "Undetermined"; + return PerformanceImpactIndicatorValue.UNDETERMINED; } if ( @@ -675,13 +679,59 @@ export const getPerformanceImpactDescription = ( scheduledQueryStats.user_time_p50 + scheduledQueryStats.system_time_p50; if (indicator < 2000) { - return "Minimal"; + return PerformanceImpactIndicatorValue.MINIMAL; } if (indicator < 4000) { - return "Considerable"; + return PerformanceImpactIndicatorValue.CONSIDERABLE; } } - return "Excessive"; + return PerformanceImpactIndicatorValue.EXCESSIVE; +}; + +export const getPerformanceImpactIndicatorTooltip = ( + indicator: PerformanceImpactIndicator, + isHostSpecific = false +) => { + switch (indicator) { + case PerformanceImpactIndicatorValue.MINIMAL: + return ( + <> + Running this query very frequently has little to no
impact on + your device's performance. + + ); + case PerformanceImpactIndicatorValue.CONSIDERABLE: + return ( + <> + Running this query frequently can have a noticeable
+ impact on your device's performance. + + ); + case PerformanceImpactIndicatorValue.EXCESSIVE: + return ( + <> + Running this query, even infrequently, can have a
+ significant impact on your device's performance. + + ); + case PerformanceImpactIndicatorValue.DENYLISTED: + return ( + <> + This query has been
stopped from running
because of + excessive
resource consumption. + + ); + case PerformanceImpactIndicatorValue.UNDETERMINED: + return ( + <> + Performance impact will be available when{" "} + {isHostSpecific ? "the" : "this"}
+ query runs{isHostSpecific && " on this host"}. + + ); + default: + return null; + } }; export const secondsToDhms = (s: number): string => {