UI – 14415 frontend - host details (#15437)

## Addresses the first major part of #15011 (item 2) – Host Details >
Queries tab

<img width="1274" alt="Screenshot 2023-12-04 at 1 09 31 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/47075ebb-eb98-48f5-82ab-af4022932376">
<img width="678" alt="Screenshot 2023-12-04 at 1 09 57 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/db48ca6d-e73b-4a90-b782-f9ee265927f1">

- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
Jacob Shandling
2023-12-05 13:58:02 -08:00
committed by GitHub
co-authored by Jacob Shandling
parent 5677d734de
commit bb56e288e5
23 changed files with 716 additions and 286 deletions
@@ -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 = ({
<ReactTooltip
place="top"
effect="solid"
backgroundColor="#3e4771"
backgroundColor={COLORS["tooltip-bg"]}
id={tooltipId}
>
{emptyCellTooltipText}
@@ -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;
}
+3
View File
@@ -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;
@@ -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 <Spinner />;
}
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 = ({
)}
</TabPanel>
<TabPanel>
<ScheduleCard
isChromeOSHost={host?.platform === "chrome"}
<QueriesCard
hostId={host.id}
router={router}
isChromeOSHost={host.platform === "chrome"}
schedule={schedule}
isLoading={isLoadingHost}
queryReportsDisabled={
config?.server_settings?.query_reports_disabled
}
/>
{canViewPacks && (
<PacksCard packsState={packsState} isLoading={isLoadingHost} />
@@ -292,4 +292,11 @@
align-items: center;
}
}
.empty-table {
&__container {
margin: 0 0 $pad-xxlarge 0;
min-height: initial;
}
}
}
@@ -340,10 +340,10 @@ const HostSummary = ({
: titleData.display_name || DEFAULT_EMPTY_CELL_VALUE}
</h1>
<p className="last-fetched">
<div className="last-fetched">
{"Last fetched"} {lastFetched}
&nbsp;
</p>
</div>
{renderRefetch()}
</div>
</div>
@@ -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 (
<EmptyTable
header="Scheduled queries are not supported for this host"
info={
<>
<span>Interested in collecting data from your Chromebooks? </span>
<CustomLink
url="https://www.fleetdm.com/contact"
text="Let us know"
newTab
/>
</>
}
/>
);
}
return (
<EmptyTable
header="No queries are scheduled to run on this host"
info={
<>
Expecting to see queries? Try selecting <b>Refetch</b> 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 (
<div className="section section--host-queries">
<p className="section__header">Queries</p>
{!schedule || !schedule.length || isChromeOSHost ? (
renderEmptyQueriesTab()
) : (
<div>
<TableContainer
columns={columnConfigs}
data={tableData}
onQueryChange={() => 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 }}
/>
</div>
)}
</div>
);
};
export default HostQueries;
@@ -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<IQueryStats> {
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) => (
<TextCell value={cellProps.cell.value} />
),
},
{
Header: () => {
return (
<TooltipWrapper
tipContent={
<>
This is the performance <br />
impact on this host.
</>
}
>
Performance impact
</TooltipWrapper>
);
},
disableSortBy: true,
accessor: "performance",
Cell: (cellProps: IPillCellProps) => {
const baseClass = "performance-cell";
return (
<span className={baseClass}>
<PillCell
value={cellProps.cell.value}
customIdPrefix="query-perf-pill"
hostDetails
/>
{!queryReportsDisabled &&
cellProps.row.original.should_link_to_hqr && (
<Icon
name="chevron-right"
className={`${baseClass}__link-icon`}
color="core-fleet-blue"
/>
)}
</span>
);
},
},
];
// 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) => (
<ReportUpdatedCell {...cellProps.row.original} />
),
});
}
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 };
@@ -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(
<ReportUpdatedCell
interval={1000}
discard_data
automations_enabled
should_link_to_hqr={false}
/>
);
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(
<ReportUpdatedCell
interval={1000}
discard_data={false}
automations_enabled={false}
should_link_to_hqr
/>
);
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(
<ReportUpdatedCell
interval={1000}
discard_data={false}
automations_enabled={false}
should_link_to_hqr
last_fetched="2023-11-29T15:20:02Z"
/>
);
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(
<ReportUpdatedCell
interval={0}
discard_data={false}
automations_enabled={false}
should_link_to_hqr
last_fetched="2023-11-29T15:20:02Z"
/>
);
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();
});
});
@@ -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 (
<TextCell
greyed
classes={`${baseClass}__value`}
emptyCellTooltipText={
<>
Results from this query are not reported in Fleet.
<br />
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 (
<TextCell
value="Never"
formatter={(val) => (
<>
<span data-tip data-for={tipId}>
{val}
</span>
<ReactTooltip
id={tipId}
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
place="top"
>
This query has not run on this host.
</ReactTooltip>
</>
)}
greyed
classes={`${baseClass}__value`}
/>
);
}
}
// render with link to cached results (link handled by clickable parent row)
return (
<>
<TextCell
// last_fetched will be truthy at this point
value={{ timeString: last_fetched ?? "" }}
formatter={HumanTimeDiffWithFleetLaunchCutoff}
classes={`${baseClass}__value`}
/>
</>
);
};
return (
<span className={baseClass}>
{renderCellValue()}
{should_link_to_hqr && (
// actual link functionality handled by clickable parent row
<span
className={`${baseClass}__link`}
title="link to host query report"
>
<span className={`${baseClass}__link-text`}>View report</span>
<Icon
name="chevron-right"
className={`${baseClass}__link-icon`}
color="core-fleet-blue"
/>
</span>
)}
</span>
);
};
export default ReportUpdatedCell;
@@ -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;
}
}
@@ -0,0 +1 @@
export { default } from "./ReportUpdatedCell";
@@ -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;
}
}
}
}
}
}
}
@@ -0,0 +1 @@
export { default } from "./HostQueries";
@@ -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 (
<EmptyTable
header="Scheduled queries are not supported for this host"
info={
<>
<span>Interested in collecting data from your Chromebooks? </span>
<CustomLink
url="https://www.fleetdm.com/contact"
text="Let us know"
newTab
/>
</>
}
/>
);
}
return (
<EmptyTable
header="No queries are scheduled for this host"
info="Expecting to see queries? Try selecting Refetch to ask this host
to report new vitals."
/>
);
};
return (
<div className="section section--schedule">
<p className="section__header">Schedule</p>
{!schedule || !schedule.length || isChromeOSHost ? (
renderEmptyScheduleTab()
) : (
<div className={`${wrapperClassName}`}>
<TableContainer
columns={tableHeaders}
data={generateDataSet(schedule)}
isLoading={isLoading}
onQueryChange={() => null}
resultsTitle={"queries"}
defaultSortHeader={"scheduled_query_name"}
defaultSortDirection={"asc"}
showMarkAllPages={false}
isAllPagesSelected={false}
emptyComponent={() => <></>}
disablePagination
disableCount
/>
</div>
)}
</div>
);
};
export default Schedule;
@@ -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<IQueryStats> {
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) => (
<TextCell value={cellProps.cell.value} />
),
},
{
title: "Frequency",
Header: "Frequency",
disableSortBy: true,
accessor: "frequency",
Cell: (cellProps: ICellProps) => (
<TextCell value={cellProps.cell.value} />
),
},
{
Header: () => {
return (
<TooltipWrapper
tipContent={
<>
This is the performance <br />
impact on this host.
</>
}
>
Performance impact
</TooltipWrapper>
);
},
disableSortBy: true,
accessor: "performance",
Cell: (cellProps: IPillCellProps) => (
<PillCell
value={cellProps.cell.value}
customIdPrefix="query-perf-pill"
hostDetails
/>
),
},
];
};
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 };
@@ -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;
}
}
}
}
}
@@ -1 +0,0 @@
export { default } from "./Schedule";
@@ -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={() => (
<EmptySoftwareTable
isSoftwareDisabled={!isSoftwareEnabled}
@@ -650,11 +648,8 @@ const ManageSoftwarePage = ({
pageSize={DEFAULT_PAGE_SIZE}
showMarkAllPages={false}
isAllPagesSelected={false}
resetPageIndex={resetPageIndex}
disableNextPage={isLastPage}
searchable={searchable}
inputPlaceHolder="Search by name or vulnerabilities (CVEs)"
onQueryChange={onQueryChange}
additionalQueries={filterVuln ? "vulnerable" : ""} // additionalQueries serves as a trigger
// for the useDeepEffect hook to fire onQueryChange for events happeing outside of
// the TableContainer
@@ -663,7 +658,7 @@ const ManageSoftwarePage = ({
renderCount={renderSoftwareCount}
renderFooter={renderTableFooter}
disableMultiRowSelect
onSelectSingleRow={handleRowSelect}
{...{ resetPageIndex, searchable, onQueryChange, onSelectSingleRow }}
/>
);
};
@@ -709,15 +704,17 @@ const ManageSoftwarePage = ({
{showManageAutomationsModal && (
<ManageAutomationsModal
onCancel={toggleManageAutomationsModal}
onCreateWebhookSubmit={onCreateWebhookSubmit}
togglePreviewPayloadModal={togglePreviewPayloadModal}
togglePreviewTicketModal={togglePreviewTicketModal}
showPreviewPayloadModal={showPreviewPayloadModal}
showPreviewTicketModal={showPreviewTicketModal}
softwareVulnerabilityAutomationEnabled={isAnyVulnAutomationEnabled}
softwareVulnerabilityWebhookEnabled={isVulnWebhookEnabled}
currentDestinationUrl={vulnWebhookSettings?.destination_url || ""}
recentVulnerabilityMaxAge={recentVulnerabilityMaxAge}
{...{
onCreateWebhookSubmit,
togglePreviewPayloadModal,
togglePreviewTicketModal,
showPreviewPayloadModal,
showPreviewTicketModal,
recentVulnerabilityMaxAge,
}}
/>
)}
</div>
+3 -1
View File
@@ -182,7 +182,9 @@ const routes = (
<Route path=":host_id" component={HostDetailsPage}>
<Route path="scripts" component={HostDetailsPage} />
<Route path="software" component={HostDetailsPage} />
<Route path="schedule" component={HostDetailsPage} />
<Route path="queries" component={HostDetailsPage} />
{/* legacy route */}
<Redirect from="schedule" to="queries" />
<Route path="policies" component={HostDetailsPage} />
</Route>
</Route>
+4 -2
View File
@@ -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}`;
},
+1 -11
View File
@@ -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 {
+29
View File
@@ -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%;
}