From bb56e288e59f5cb36cc52dd74cf3372aa02d124a Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:58:02 -0800 Subject: [PATCH] =?UTF-8?q?UI=20=E2=80=93=2014415=20frontend=20-=20host=20?= =?UTF-8?q?details=20(#15437)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses the first major part of #15011 (item 2) – Host Details > Queries tab Screenshot 2023-12-04 at 1 09 31 PM Screenshot 2023-12-04 at 1 09 57 PM - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- .../DataTable/TextCell/TextCell.tsx | 3 +- .../components/ViewAllHostsLink/_styles.scss | 6 +- frontend/interfaces/query_stats.ts | 3 + .../HostDetailsPage/HostDetailsPage.tsx | 115 +++++++++++- frontend/pages/hosts/details/_styles.scss | 7 + .../details/cards/HostSummary/HostSummary.tsx | 4 +- .../details/cards/Queries/HostQueries.tsx | 118 +++++++++++++ .../cards/Queries/HostQueriesTableConfig.tsx | 166 ++++++++++++++++++ .../ReportUpdatedCell.tests.tsx | 72 ++++++++ .../ReportUpdatedCell/ReportUpdatedCell.tsx | 111 ++++++++++++ .../Queries/ReportUpdatedCell/_styles.scss | 17 ++ .../cards/Queries/ReportUpdatedCell/index.ts | 1 + .../hosts/details/cards/Queries/_styles.scss | 60 +++++++ .../hosts/details/cards/Queries/index.ts | 1 + .../hosts/details/cards/Schedule/Schedule.tsx | 80 --------- .../cards/Schedule/ScheduleTableConfig.tsx | 130 -------------- .../hosts/details/cards/Schedule/_styles.scss | 29 --- .../hosts/details/cards/Schedule/index.ts | 1 - .../ManageSoftwarePage/ManageSoftwarePage.tsx | 27 ++- frontend/router/index.tsx | 4 +- frontend/router/paths.ts | 6 +- frontend/styles/global/_global.scss | 12 +- frontend/styles/var/mixins.scss | 29 +++ 23 files changed, 716 insertions(+), 286 deletions(-) create mode 100644 frontend/pages/hosts/details/cards/Queries/HostQueries.tsx create mode 100644 frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx create mode 100644 frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx create mode 100644 frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx create mode 100644 frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/_styles.scss create mode 100644 frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/index.ts create mode 100644 frontend/pages/hosts/details/cards/Queries/_styles.scss create mode 100644 frontend/pages/hosts/details/cards/Queries/index.ts delete mode 100644 frontend/pages/hosts/details/cards/Schedule/Schedule.tsx delete mode 100644 frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx delete mode 100644 frontend/pages/hosts/details/cards/Schedule/_styles.scss delete mode 100644 frontend/pages/hosts/details/cards/Schedule/index.ts diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx index eabf2da046..d086ef5e39 100644 --- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx +++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx @@ -1,6 +1,7 @@ import { uniqueId } from "lodash"; import React from "react"; import ReactTooltip from "react-tooltip"; +import { COLORS } from "styles/var/colors"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; interface ITextCellProps { @@ -38,7 +39,7 @@ const TextCell = ({ {emptyCellTooltipText} diff --git a/frontend/components/ViewAllHostsLink/_styles.scss b/frontend/components/ViewAllHostsLink/_styles.scss index 19e6685d31..3e4d049865 100644 --- a/frontend/components/ViewAllHostsLink/_styles.scss +++ b/frontend/components/ViewAllHostsLink/_styles.scss @@ -1,7 +1,3 @@ .view-all-hosts-link { - display: inline-flex; - align-items: center; - padding: $pad-small $pad-xxsmall; // larger clickable area - gap: $pad-xsmall; - white-space: nowrap; + @include table-link; } diff --git a/frontend/interfaces/query_stats.ts b/frontend/interfaces/query_stats.ts index b6906c10de..edc319fa61 100644 --- a/frontend/interfaces/query_stats.ts +++ b/frontend/interfaces/query_stats.ts @@ -27,6 +27,9 @@ export interface IQueryStats { scheduled_query_name: string; scheduled_query_id: number; query_name: string; + discard_data: boolean; + last_fetched: string | null; // timestamp + automations_enabled: boolean; description: string; pack_name: string; pack_id: number; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index e2c7af8ac4..b5aa3f6561 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -62,7 +62,7 @@ import ScriptsCard from "../cards/Scripts"; import SoftwareCard from "../cards/Software"; import UsersCard from "../cards/Users"; import PoliciesCard from "../cards/Policies"; -import ScheduleCard from "../cards/Schedule"; +import QueriesCard from "../cards/Queries"; import PacksCard from "../cards/Packs"; import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal"; import UnenrollMdmModal from "./modals/UnenrollMdmModal"; @@ -238,6 +238,96 @@ const HostDetailsPage = ({ mdm?.enrollment_status !== null && refetchMdm(); }; + // TODO - remove dummy schedule + const dummySchedule: IQueryStats[] = [ + { + scheduled_query_name: "cached query 1 - Never reported", + query_name: "query 1", + description: "should render 'Never' ", + discard_data: false, + last_fetched: null, + automations_enabled: false, + interval: 1000, + scheduled_query_id: 1, + + pack_id: 1, + pack_name: "Team: 💻 Workstations", + average_memory: 435814, + denylisted: false, + executions: 5, + last_executed: "2023-11-29T15:20:02Z", + output_size: 1204, + system_time: 9, + user_time: 3, + wall_time: 0, + }, + { + scheduled_query_name: "cached query 2 - stored results", + description: "should render with row clickable to its report", + discard_data: false, + query_name: "query 2", + last_fetched: "2023-11-29T15:20:02Z", + automations_enabled: false, + interval: 1000, + scheduled_query_id: 2, + + pack_id: 1, + pack_name: "Team: 💻 Workstations", + average_memory: 435814, + denylisted: false, + executions: 5, + last_executed: "2023-11-29T15:20:02Z", + output_size: 1204, + system_time: 9, + user_time: 3, + wall_time: 0, + }, + { + scheduled_query_name: + "cached query 3 - sending results to a log destination, not storing in Fleet", + description: "should render '---', not link to report", + interval: 1000, + discard_data: true, + automations_enabled: true, + query_name: "query 3", + last_fetched: null, + scheduled_query_id: 3, + + pack_id: 1, + pack_name: "Team: 💻 Workstations", + average_memory: 435814, + denylisted: false, + executions: 5, + last_executed: "2023-11-29T15:20:02Z", + output_size: 1204, + system_time: 9, + user_time: 3, + wall_time: 0, + }, + { + scheduled_query_name: + "cached query 4 - stored results, but no current interval", + description: "should render with row clickable to its report", + discard_data: false, + query_name: "query 4", + last_fetched: "2023-11-29T15:20:02Z", + automations_enabled: false, + interval: 0, + scheduled_query_id: 4, + + pack_id: 1, + pack_name: "Team: 💻 Workstations", + average_memory: 435814, + denylisted: false, + executions: 5, + last_executed: "2023-11-29T15:20:02Z", + output_size: 1204, + system_time: 9, + user_time: 3, + wall_time: 0, + }, + ]; + const { isLoading: isLoadingHost, data: host, @@ -300,6 +390,8 @@ const HostDetailsPage = ({ } setHostSoftware(returnedHost.software || []); setUsersState(returnedHost.users || []); + // TODO – remove dummy data + setSchedule(dummySchedule); if (returnedHost.pack_stats) { const packStatsByType = returnedHost.pack_stats.reduce( ( @@ -318,7 +410,8 @@ const HostDetailsPage = ({ }, { packs: [], schedule: [] } ); - setSchedule(packStatsByType.schedule); + // TODO - restore real data + // setSchedule(packStatsByType.schedule); setPacksState(packStatsByType.packs); } }, @@ -583,7 +676,7 @@ const HostDetailsPage = ({ ); }; - if (isLoadingHost) { + if (!host || isLoadingHost) { return ; } const failingPoliciesCount = host?.issues.failing_policies_count || 0; @@ -605,9 +698,9 @@ const HostDetailsPage = ({ pathname: PATHS.HOST_SOFTWARE(hostIdFromURL), }, { - name: "Schedule", - title: "schedule", - pathname: PATHS.HOST_SCHEDULE(hostIdFromURL), + name: "Queries", + title: "queries", + pathname: PATHS.HOST_QUERIES(hostIdFromURL), }, { name: ( @@ -768,10 +861,14 @@ const HostDetailsPage = ({ )} - {canViewPacks && ( diff --git a/frontend/pages/hosts/details/_styles.scss b/frontend/pages/hosts/details/_styles.scss index 919d3a8c51..87b0fc45be 100644 --- a/frontend/pages/hosts/details/_styles.scss +++ b/frontend/pages/hosts/details/_styles.scss @@ -292,4 +292,11 @@ align-items: center; } } + + .empty-table { + &__container { + margin: 0 0 $pad-xxlarge 0; + min-height: initial; + } + } } diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index 6c3cb957ca..b60930eccd 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -340,10 +340,10 @@ const HostSummary = ({ : titleData.display_name || DEFAULT_EMPTY_CELL_VALUE} -

+

{"Last fetched"} {lastFetched}   -

+
{renderRefetch()} diff --git a/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx b/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx new file mode 100644 index 0000000000..18362d3937 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx @@ -0,0 +1,118 @@ +import React, { useCallback, useMemo } from "react"; + +import { IQueryStats } from "interfaces/query_stats"; +import TableContainer from "components/TableContainer"; +import EmptyTable from "components/EmptyTable"; +import CustomLink from "components/CustomLink"; +import PATHS from "router/paths"; +import { InjectedRouter } from "react-router"; +import { Row } from "react-table"; + +import { + generateColumnConfigs, + generateDataSet, +} from "./HostQueriesTableConfig"; + +const baseClass = "host-queries"; + +interface IHostQueriesProps { + hostId: number; + schedule?: IQueryStats[]; + isChromeOSHost: boolean; + queryReportsDisabled?: boolean; + router: InjectedRouter; +} + +interface IHostQueriesRowProps extends Row { + original: { + id?: number; + should_link_to_hqr?: boolean; + }; +} +const HostQueries = ({ + hostId, + schedule, + isChromeOSHost, + queryReportsDisabled, + router, +}: IHostQueriesProps): JSX.Element => { + const renderEmptyQueriesTab = () => { + if (isChromeOSHost) { + return ( + + Interested in collecting data from your Chromebooks? + + + } + /> + ); + } + 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; + + if (!hostId || !queryId || !should_link_to_hqr || queryReportsDisabled) { + return; + } + router.push(`${PATHS.HOST_QUERY_REPORT(hostId, queryId)}`); + }, + [hostId, queryReportsDisabled, router] + ); + + const tableData = useMemo(() => generateDataSet(schedule ?? []), [schedule]); + + const columnConfigs = useMemo( + () => generateColumnConfigs(queryReportsDisabled), + [queryReportsDisabled] + ); + + return ( +
+

Queries

+ {!schedule || !schedule.length || isChromeOSHost ? ( + renderEmptyQueriesTab() + ) : ( +
+ null} + resultsTitle="queries" + defaultSortHeader="scheduled_query_name" + defaultSortDirection="asc" + showMarkAllPages={false} + isAllPagesSelected={false} + emptyComponent={() => <>} + disablePagination + disableCount + disableMultiRowSelect + isLoading={false} // loading state handled at parent level + {...{ onSelectSingleRow }} + /> +
+ )} +
+ ); +}; + +export default HostQueries; diff --git a/frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx b/frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx new file mode 100644 index 0000000000..6630ed0897 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Queries/HostQueriesTableConfig.tsx @@ -0,0 +1,166 @@ +import React from "react"; + +import { IQueryStats } from "interfaces/query_stats"; +import { performanceIndicator } from "utilities/helpers"; + +import TextCell from "components/TableContainer/DataTable/TextCell"; +import PillCell from "components/TableContainer/DataTable/PillCell"; +import TooltipWrapper from "components/TooltipWrapper"; +import ReportUpdatedCell from "pages/hosts/details/cards/Queries/ReportUpdatedCell"; +import Icon from "components/Icon"; + +interface IHostQueriesTableData extends Partial { + performance: { indicator: string; id: number }; + should_link_to_hqr: boolean; +} +interface IHeaderProps { + column: { + title: string; + isSortedDesc: boolean; + }; +} + +interface IRowProps { + row: { + original: IHostQueriesTableData; + }; +} + +interface ICellProps extends IRowProps { + cell: { + value: string | number | boolean; + }; +} + +interface IPillCellProps extends IRowProps { + cell: { + value: { + indicator: string; + id: number; + }; + }; +} + +interface IDataColumn { + title?: string; + Header: ((props: IHeaderProps) => JSX.Element) | string; + accessor: string; + Cell: + | ((props: ICellProps) => JSX.Element) + | ((props: IPillCellProps) => JSX.Element); + disableHidden?: boolean; + disableSortBy?: boolean; +} + +// NOTE: cellProps come from react-table +// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties +const generateColumnConfigs = ( + queryReportsDisabled?: boolean +): IDataColumn[] => { + const cols: IDataColumn[] = [ + { + title: "Query", + Header: "Query", + disableSortBy: true, + accessor: "query_name", + Cell: (cellProps: ICellProps) => ( + + ), + }, + { + Header: () => { + return ( + + This is the performance
+ impact on this host. + + } + > + Performance impact +
+ ); + }, + disableSortBy: true, + accessor: "performance", + Cell: (cellProps: IPillCellProps) => { + const baseClass = "performance-cell"; + return ( + + + {!queryReportsDisabled && + cellProps.row.original.should_link_to_hqr && ( + + )} + + ); + }, + }, + ]; + + // include the Report updated column if query reports are globally enabled + if (!queryReportsDisabled) { + cols.push({ + Header: "Report updated", + disableSortBy: true, + accessor: "last_fetched", // tbd - may change + Cell: (cellProps: ICellProps) => ( + + ), + }); + } + return cols; +}; + +const enhanceScheduleData = ( + query_stats: IQueryStats[] +): IHostQueriesTableData[] => { + return Object.values(query_stats).map((query) => { + const { + user_time, + system_time, + executions, + query_name, + scheduled_query_id, + last_fetched, + interval, + discard_data, + automations_enabled, + } = query; + const scheduledQueryPerformance = { + user_time_p50: user_time, + system_time_p50: system_time, + total_executions: executions, + }; + return { + query_name, + id: scheduled_query_id, + performance: { + indicator: performanceIndicator(scheduledQueryPerformance), + id: scheduled_query_id, + }, + last_fetched, + interval, + discard_data, + automations_enabled, + should_link_to_hqr: !!last_fetched || (!!interval && !discard_data), + }; + }); +}; + +const generateDataSet = ( + query_stats: IQueryStats[] +): IHostQueriesTableData[] => { + return query_stats ? enhanceScheduleData(query_stats) : []; +}; + +export { generateColumnConfigs, generateDataSet }; diff --git a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx new file mode 100644 index 0000000000..e9865c7328 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tests.tsx @@ -0,0 +1,72 @@ +import React from "react"; + +import { render, screen } from "@testing-library/react"; + +import ReportUpdatedCell from "./ReportUpdatedCell"; + +describe("ReportUpdatedCell component", () => { + it("Renders '---' with tooltip and no link when run on an interval with discard data and automations enabled", () => { + render( + + ); + + expect(screen.getByText(/---/)).toBeInTheDocument(); + expect(screen.getByText(/Results from this query/)).toBeInTheDocument(); + expect(screen.queryByText(/View report/)).toBeNull(); + }); + + it("Renders 'Never with tooltip and link to report when run on an interval with discard data off and no last_fetched time", () => { + render( + + ); + + expect(screen.getByText(/Never/)).toBeInTheDocument(); + expect(screen.getByText(/This query has not run/)).toBeInTheDocument(); + expect(screen.getByText(/View report/)).toBeInTheDocument(); + }); + + it("Renders a last-updated timestamp with tooltip and link to report when a last_fetched date is present", () => { + render( + + ); + + expect( + screen.getByText(/\d\d\/\d\d\/\d\d\d\d, \d{1,2}:\d{1,2}:\d{1,2}( AM|PM)?/) + ).toBeInTheDocument(); + expect(screen.getByText(/\d+ days ago/)).toBeInTheDocument(); + expect(screen.getByText(/View report/)).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", () => { + render( + + ); + + expect( + screen.getByText(/\d\d\/\d\d\/\d\d\d\d, \d{1,2}:\d{1,2}:\d{1,2}( AM|PM)?/) + ).toBeInTheDocument(); + expect(screen.getByText(/\d+ days ago/)).toBeInTheDocument(); + expect(screen.getByText(/View report/)).toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx new file mode 100644 index 0000000000..8bdec1eab0 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/ReportUpdatedCell.tsx @@ -0,0 +1,111 @@ +import React from "react"; + +import { HumanTimeDiffWithFleetLaunchCutoff } from "components/HumanTimeDiffWithDateTip"; +import { uniqueId } from "lodash"; +import ReactTooltip from "react-tooltip"; +import { COLORS } from "styles/var/colors"; +import Icon from "components/Icon"; +import TextCell from "components/TableContainer/DataTable/TextCell"; + +const baseClass = "report-updated-cell"; + +interface IReportUpdatedCell { + last_fetched?: string | null; + interval?: number; + discard_data?: boolean; + automations_enabled?: boolean; + should_link_to_hqr?: boolean; +} + +const ReportUpdatedCell = ({ + last_fetched, + interval, + discard_data, + automations_enabled, + should_link_to_hqr, +}: IReportUpdatedCell) => { + const renderCellValue = () => { + // if this query doesn't have an interval, it either has a stored report from previous runs + // and will link to that report, or won't be included in this data in the first place. + if (interval) { + if (discard_data && automations_enabled) { + // this is also the only case where the row is NOT clickable with a link to the host's HQR + // query runs, sends results to a logging dest, doesn't cache + return ( + + Results from this query are not reported in Fleet. +
+ Data is being sent to your log destination. + + } + /> + ); + } + + // Query is scheduled to run on host, but hasn't yet + if (!last_fetched) { + const tipId = uniqueId(); + return ( + ( + <> + + {val} + + + This query has not run on this host. + + + )} + greyed + classes={`${baseClass}__value`} + /> + ); + } + } + + // render with link to cached results (link handled by clickable parent row) + return ( + <> + + + ); + }; + + return ( + + {renderCellValue()} + {should_link_to_hqr && ( + // actual link functionality handled by clickable parent row + + View report + + + )} + + ); +}; + +export default ReportUpdatedCell; diff --git a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/_styles.scss b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/_styles.scss new file mode 100644 index 0000000000..ef4296bb5c --- /dev/null +++ b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/_styles.scss @@ -0,0 +1,17 @@ +.report-updated-cell { + @include cell-with-link; + + &__value { + min-width: initial; + } + + &__link { + @include table-link; + } + + &__link-text { + // hover state of parent tr sets opacity to 1 + opacity: 0; + transition: opacity 250ms; + } +} diff --git a/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/index.ts b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/index.ts new file mode 100644 index 0000000000..1fd58ec49c --- /dev/null +++ b/frontend/pages/hosts/details/cards/Queries/ReportUpdatedCell/index.ts @@ -0,0 +1 @@ +export { default } from "./ReportUpdatedCell"; diff --git a/frontend/pages/hosts/details/cards/Queries/_styles.scss b/frontend/pages/hosts/details/cards/Queries/_styles.scss new file mode 100644 index 0000000000..bffcdac122 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Queries/_styles.scss @@ -0,0 +1,60 @@ +.section--host-queries { + margin-top: $pad-medium; + .section__header { + margin-bottom: $pad-medium; + } + .table-container__header { + display: none; + } + .data-table-block { + .data-table__table { + thead { + .query_name__header { + width: $col-lg; + } + .last_fetched__header { + display: table-cell; + } + @media (max-width: $break-md) { + .last_fetched__header { + display: none; + width: 0; + } + } + } + tbody { + tr { + .query_name__cell { + width: $col-lg; + } + .last_fetched__cell { + display: table-cell; + } + .performance-cell { + @include cell-with-link; + &__link-icon { + display: none; + width: 0; + } + } + &:hover { + .report-updated-cell__link-text { + opacity: 1; + } + } + @media (max-width: $break-md) { + .last_fetched__cell { + display: none; + width: 0; + } + .performance-cell__link-icon { + display: inline-flex; + align-self: center; + width: initial; + } + } + } + } + } + } +} diff --git a/frontend/pages/hosts/details/cards/Queries/index.ts b/frontend/pages/hosts/details/cards/Queries/index.ts new file mode 100644 index 0000000000..b6536c4c04 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Queries/index.ts @@ -0,0 +1 @@ +export { default } from "./HostQueries"; diff --git a/frontend/pages/hosts/details/cards/Schedule/Schedule.tsx b/frontend/pages/hosts/details/cards/Schedule/Schedule.tsx deleted file mode 100644 index 1c37493510..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/Schedule.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from "react"; - -import { IQueryStats } from "interfaces/query_stats"; -import TableContainer from "components/TableContainer"; -import EmptyTable from "components/EmptyTable"; -import CustomLink from "components/CustomLink"; - -import { generateTableHeaders, generateDataSet } from "./ScheduleTableConfig"; - -const baseClass = "schedule"; - -interface IScheduleProps { - schedule?: IQueryStats[]; - isChromeOSHost: boolean; - isLoading: boolean; -} - -const Schedule = ({ - schedule, - isChromeOSHost, - isLoading, -}: IScheduleProps): JSX.Element => { - const wrapperClassName = `${baseClass}__pack-table`; - const tableHeaders = generateTableHeaders(); - - const renderEmptyScheduleTab = () => { - if (isChromeOSHost) { - return ( - - Interested in collecting data from your Chromebooks? - - - } - /> - ); - } - return ( - - ); - }; - - return ( -
-

Schedule

- {!schedule || !schedule.length || isChromeOSHost ? ( - renderEmptyScheduleTab() - ) : ( -
- null} - resultsTitle={"queries"} - defaultSortHeader={"scheduled_query_name"} - defaultSortDirection={"asc"} - showMarkAllPages={false} - isAllPagesSelected={false} - emptyComponent={() => <>} - disablePagination - disableCount - /> -
- )} -
- ); -}; - -export default Schedule; diff --git a/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx b/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx deleted file mode 100644 index 1bada23c35..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from "react"; - -import { IQueryStats } from "interfaces/query_stats"; -import { performanceIndicator, secondsToDhms } from "utilities/helpers"; - -import TextCell from "components/TableContainer/DataTable/TextCell"; -import PillCell from "components/TableContainer/DataTable/PillCell"; -import TooltipWrapper from "components/TooltipWrapper"; - -interface IHeaderProps { - column: { - title: string; - isSortedDesc: boolean; - }; -} - -interface IRowProps { - row: { - original: IQueryStats; - }; -} - -interface ICellProps extends IRowProps { - cell: { - value: string | number | boolean; - }; -} - -interface IPillCellProps extends IRowProps { - cell: { - value: { - indicator: string; - id: number; - }; - }; -} - -interface IDataColumn { - title?: string; - Header: ((props: IHeaderProps) => JSX.Element) | string; - accessor: string; - Cell: - | ((props: ICellProps) => JSX.Element) - | ((props: IPillCellProps) => JSX.Element); - disableHidden?: boolean; - disableSortBy?: boolean; -} - -interface IScheduleTable extends Partial { - frequency: 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 generateTableHeaders = (): 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 ( - - This is the performance
- impact on this host. - - } - > - Performance impact -
- ); - }, - disableSortBy: true, - accessor: "performance", - Cell: (cellProps: IPillCellProps) => ( - - ), - }, - ]; -}; - -const enhanceScheduleData = (query_stats: IQueryStats[]): IScheduleTable[] => { - 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, - frequency: secondsToDhms(query.interval), - performance: { - indicator: performanceIndicator(scheduledQueryPerformance), - id: query.scheduled_query_id, - }, - }; - }); -}; - -const generateDataSet = (query_stats: IQueryStats[]): IScheduleTable[] => { - if (!query_stats) { - return query_stats; - } - - return [...enhanceScheduleData(query_stats)]; -}; - -export { generateTableHeaders, generateDataSet }; diff --git a/frontend/pages/hosts/details/cards/Schedule/_styles.scss b/frontend/pages/hosts/details/cards/Schedule/_styles.scss deleted file mode 100644 index 4841674726..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/_styles.scss +++ /dev/null @@ -1,29 +0,0 @@ -.section--schedule { - margin-top: $pad-medium; - .section__header { - margin-bottom: $pad-medium; - } - .table-container__header { - display: none; - } - .data-table-block { - .data-table__table { - thead { - .query_name__header { - width: $col-lg; - } - .frequency__header { - width: $col-md; - } - } - tbody { - .query_name__cell { - width: $col-lg; - } - .frequency__cell { - width: $col-md; - } - } - } - } -} diff --git a/frontend/pages/hosts/details/cards/Schedule/index.ts b/frontend/pages/hosts/details/cards/Schedule/index.ts deleted file mode 100644 index 39250f5640..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./Schedule"; diff --git a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx index ee01fbf49f..714547380b 100644 --- a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx +++ b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx @@ -11,7 +11,6 @@ import { useQuery } from "react-query"; import { InjectedRouter } from "react-router/lib/Router"; import { RouteProps } from "react-router/lib/Route"; import { isEmpty, isEqual } from "lodash"; -// import { useDebouncedCallback } from "use-debounce"; import { AppContext } from "context/app"; import { NotificationContext } from "context/notification"; @@ -48,7 +47,6 @@ import TableDataError from "components/DataError"; import Dropdown from "components/forms/fields/Dropdown"; import LastUpdatedText from "components/LastUpdatedText"; import MainContent from "components/MainContent"; -import Spinner from "components/Spinner"; import TableContainer from "components/TableContainer"; import { ITableQueryData } from "components/TableContainer/TableContainer"; import TeamsDropdown from "components/TeamsDropdown"; @@ -91,7 +89,7 @@ interface ISoftwareAutomations { }; } -interface IRowProps extends Row { +interface ISoftwareRowProps extends Row { original: { id?: number; }; @@ -593,7 +591,7 @@ const ManageSoftwarePage = ({ ), [isPremiumTier, isSandboxMode, router, currentTeamId] ); - const handleRowSelect = (row: IRowProps) => { + const onSelectSingleRow = (row: ISoftwareRowProps) => { const hostsBySoftwareParams = { software_id: row.original.id, team_id: currentTeamId, @@ -631,7 +629,7 @@ const ManageSoftwarePage = ({ !globalConfig || (!softwareConfig && !softwareConfigError) } - resultsTitle={"software items"} + resultsTitle="software items" emptyComponent={() => ( ); }; @@ -709,15 +704,17 @@ const ManageSoftwarePage = ({ {showManageAutomationsModal && ( )} diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index 645c3616d2..ae73ef01c2 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -182,7 +182,9 @@ const routes = ( - + + {/* legacy route */} + diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index 6babce6bba..2d9ed40a49 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -92,12 +92,14 @@ export default { HOST_SOFTWARE: (id: number): string => { return `${URL_PREFIX}/hosts/${id}/software`; }, - HOST_SCHEDULE: (id: number): string => { - return `${URL_PREFIX}/hosts/${id}/schedule`; + HOST_QUERIES: (id: number): string => { + return `${URL_PREFIX}/hosts/${id}/queries`; }, HOST_POLICIES: (id: number): string => { return `${URL_PREFIX}/hosts/${id}/policies`; }, + HOST_QUERY_REPORT: (hostId: number, queryId: number): string => + `${URL_PREFIX}/hosts/${hostId}/queries/${queryId}`, DEVICE_USER_DETAILS: (deviceAuthToken: any): string => { return `${URL_PREFIX}/device/${deviceAuthToken}`; }, diff --git a/frontend/styles/global/_global.scss b/frontend/styles/global/_global.scss index 17714c51a3..414b11949b 100644 --- a/frontend/styles/global/_global.scss +++ b/frontend/styles/global/_global.scss @@ -53,17 +53,7 @@ h1 { } a { - color: $core-vibrant-blue; - font-weight: $bold; - font-size: $x-small; - text-decoration: none; - - &:focus-visible { - outline-color: #d9d9fe; - outline-offset: 3px; - outline-style: solid; - outline-width: 2px; - } + @include link; } .__react_component_tooltip { diff --git a/frontend/styles/var/mixins.scss b/frontend/styles/var/mixins.scss index f8fd2d6fe9..355c10af82 100644 --- a/frontend/styles/var/mixins.scss +++ b/frontend/styles/var/mixins.scss @@ -121,3 +121,32 @@ $max-width: 2560px; font-size: $xx-small; color: $ui-fleet-black-75; } + +@mixin link { + color: $core-vibrant-blue; + font-weight: $bold; + font-size: $x-small; + text-decoration: none; + &:focus-visible { + outline-color: #d9d9fe; + outline-offset: 3px; + outline-style: solid; + outline-width: 2px; + } +} + +@mixin table-link { + display: inline-flex; + align-items: center; + padding: $pad-small $pad-xxsmall; // larger clickable area + gap: $pad-small; + white-space: nowrap; + @include link; +} + +@mixin cell-with-link { + display: inline-flex; + align-items: center; + justify-content: space-between; + width: 100%; +}