Indicate that a policy's data is not yet accurate (#5031)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Indicate if a policy has not completed an initial run
|
||||
@@ -332,6 +332,8 @@ describe("Policies flow (seeded)", () => {
|
||||
cy.visit("/policies/manage");
|
||||
});
|
||||
it("links to manage host page filtered by policy", () => {
|
||||
// Move internal clock forward 2 hours so that policies report host results
|
||||
cy.clock(Date.now() + 1000 * 60 * 120);
|
||||
cy.getAttached(".failing_host_count__cell")
|
||||
.first()
|
||||
.within(() => {
|
||||
|
||||
@@ -11,7 +11,7 @@ const TextCell = ({
|
||||
value,
|
||||
formatter = (val) => val, // identity function if no formatter is provided
|
||||
greyed,
|
||||
classes: className = "w250",
|
||||
classes = "w250",
|
||||
}: ITextCellProps): JSX.Element => {
|
||||
let val = value;
|
||||
|
||||
@@ -20,7 +20,7 @@ const TextCell = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`text-cell ${className} ${greyed || ""}`}>
|
||||
<span className={`text-cell ${classes} ${greyed || ""}`}>
|
||||
{formatter(val)}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface IPolicyStats extends IPolicy {
|
||||
passing_host_count: number;
|
||||
failing_host_count: number;
|
||||
webhook: string;
|
||||
has_run: boolean;
|
||||
osquery_policy_ms: number;
|
||||
}
|
||||
|
||||
// Used on the host details page and other places where the status of individual hosts are displayed
|
||||
|
||||
@@ -7,7 +7,6 @@ import { AppContext } from "context/app";
|
||||
import { PolicyContext } from "context/policy";
|
||||
import { TableContext } from "context/table";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { inMilliseconds, secondsToHms } from "fleet/helpers";
|
||||
import { IPolicyStats, ILoadAllPoliciesResponse } from "interfaces/policy";
|
||||
import { IWebhookFailingPolicies } from "interfaces/webhook";
|
||||
import { IConfig } from "interfaces/config";
|
||||
@@ -20,7 +19,6 @@ import usersAPI, { IGetMeResponse } from "services/entities/users";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
import Spinner from "components/Spinner";
|
||||
import TeamsDropdown from "components/TeamsDropdown";
|
||||
import TableDataError from "components/TableDataError";
|
||||
@@ -43,16 +41,12 @@ interface IManagePoliciesPageProps {
|
||||
|
||||
const baseClass = "manage-policies-page";
|
||||
|
||||
const DOCS_LINK =
|
||||
"https://fleetdm.com/docs/deploying/configuration#osquery-policy-update-interval";
|
||||
|
||||
const ManagePolicyPage = ({
|
||||
router,
|
||||
location,
|
||||
}: IManagePoliciesPageProps): JSX.Element => {
|
||||
const {
|
||||
availableTeams,
|
||||
config,
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isOnGlobalTeam,
|
||||
@@ -75,7 +69,6 @@ const ManagePolicyPage = ({
|
||||
const {
|
||||
setLastEditedQueryName,
|
||||
setLastEditedQueryDescription,
|
||||
setLastEditedQueryBody,
|
||||
setLastEditedQueryResolution,
|
||||
setLastEditedQueryPlatform,
|
||||
} = useContext(PolicyContext);
|
||||
@@ -282,17 +275,8 @@ const ManagePolicyPage = ({
|
||||
}`;
|
||||
};
|
||||
|
||||
const policyUpdateInterval =
|
||||
secondsToHms(
|
||||
inMilliseconds(config?.update_interval.osquery_policy || 0) / 1000
|
||||
) || "osquery policy update interval";
|
||||
|
||||
const showTeamDescription = isPremiumTier && !!teamId;
|
||||
|
||||
const showInfoBanner =
|
||||
(teamId && !teamPoliciesError && !!teamPolicies?.length) ||
|
||||
(!teamId && !globalPoliciesError && !!globalPolicies?.length);
|
||||
|
||||
const showInheritedPoliciesButton =
|
||||
!!teamId &&
|
||||
!isLoadingTeamPolicies &&
|
||||
@@ -395,23 +379,6 @@ const ManagePolicyPage = ({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{!!policyUpdateInterval && showInfoBanner && (
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p>
|
||||
Your policies are checked every{" "}
|
||||
<b>{policyUpdateInterval.trim()}</b>.{" "}
|
||||
{isGlobalAdmin && (
|
||||
<span>
|
||||
Check out the Fleet documentation on{" "}
|
||||
<a href={DOCS_LINK} target="_blank" rel="noreferrer">
|
||||
<b>how to edit this frequency</b>
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</InfoBanner>
|
||||
)}
|
||||
<div>
|
||||
{!!teamId && teamPoliciesError && <TableDataError />}
|
||||
{!!teamId &&
|
||||
|
||||
+9
-2
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { AppContext } from "context/app";
|
||||
import { noop } from "lodash";
|
||||
import paths from "router/paths";
|
||||
|
||||
@@ -42,6 +43,8 @@ const PoliciesListWrapper = ({
|
||||
}: IPoliciesListWrapperProps): JSX.Element => {
|
||||
const { MANAGE_HOSTS } = paths;
|
||||
|
||||
const { config } = useContext(AppContext);
|
||||
|
||||
const NoPolicies = () => {
|
||||
return (
|
||||
<div
|
||||
@@ -105,7 +108,11 @@ const PoliciesListWrapper = ({
|
||||
canAddOrRemovePolicy,
|
||||
tableType,
|
||||
})}
|
||||
data={generateDataSet(policiesList, currentAutomatedPolicies)}
|
||||
data={generateDataSet(
|
||||
policiesList,
|
||||
currentAutomatedPolicies,
|
||||
config?.update_interval.osquery_policy
|
||||
)}
|
||||
isLoading={isLoading}
|
||||
defaultSortHeader={"name"}
|
||||
defaultSortDirection={"asc"}
|
||||
|
||||
+129
-31
@@ -2,7 +2,8 @@
|
||||
// disable this rule as it was throwing an error in Header and Cell component
|
||||
// definitions for the selection row for some reason when we dont really need it.
|
||||
import React from "react";
|
||||
|
||||
import { millisecondsToHours, millisecondsToMinutes, isAfter } from "date-fns";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
// @ts-ignore
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
|
||||
@@ -66,6 +67,29 @@ interface IDataColumn {
|
||||
sortType?: string;
|
||||
}
|
||||
|
||||
const getPolicyRefreshTime = (ms: number): string => {
|
||||
const seconds = ms / 1000;
|
||||
if (seconds < 60) {
|
||||
return `${seconds} seconds`;
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
const minutes = millisecondsToMinutes(ms);
|
||||
return `${minutes} minute${minutes > 1 ? "s" : ""}`;
|
||||
}
|
||||
const hours = millisecondsToHours(ms);
|
||||
return `${hours} hour${hours > 1 ? "s" : ""}`;
|
||||
};
|
||||
|
||||
const getTooltip = (osqueryPolicyMs: number): JSX.Element => {
|
||||
return (
|
||||
<span className={`tooltip__tooltip-text`}>
|
||||
Fleet is collecting policy results. Try again
|
||||
<br />
|
||||
in about {getPolicyRefreshTime(osqueryPolicyMs)} as the system catches up.
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// NOTE: cellProps come from react-table
|
||||
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||
const generateTableHeaders = (options: {
|
||||
@@ -102,6 +126,7 @@ const generateTableHeaders = (options: {
|
||||
<LinkCell
|
||||
value={cellProps.cell.value}
|
||||
path={PATHS.EDIT_POLICY(cellProps.row.original)}
|
||||
classes="" // Override default
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -115,19 +140,46 @@ const generateTableHeaders = (options: {
|
||||
),
|
||||
disableSortBy: true,
|
||||
accessor: "passing_host_count",
|
||||
Cell: (cellProps: ICellProps): JSX.Element => (
|
||||
<LinkCell
|
||||
value={`${cellProps.cell.value} hosts`}
|
||||
path={
|
||||
PATHS.MANAGE_HOSTS +
|
||||
TAGGED_TEMPLATES.hostsByPolicyRoute(
|
||||
cellProps.row.original.id,
|
||||
PolicyResponse.PASSING,
|
||||
selectedTeamId
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
Cell: (cellProps: ICellProps): JSX.Element => {
|
||||
if (cellProps.row.original.has_run) {
|
||||
return (
|
||||
<LinkCell
|
||||
value={`${cellProps.cell.value} host${
|
||||
cellProps.cell.value.toString() === "1" ? "" : "s"
|
||||
}`}
|
||||
path={
|
||||
PATHS.MANAGE_HOSTS +
|
||||
TAGGED_TEMPLATES.hostsByPolicyRoute(
|
||||
cellProps.row.original.id,
|
||||
PolicyResponse.PASSING,
|
||||
selectedTeamId
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className="text-cell text-muted has-not-run"
|
||||
data-tip
|
||||
data-for={`passing_${cellProps.row.original.id.toString()}`}
|
||||
>
|
||||
---
|
||||
</span>
|
||||
<ReactTooltip
|
||||
place="bottom"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
backgroundColor="#3e4771"
|
||||
id={`passing_${cellProps.row.original.id.toString()}`}
|
||||
data-html
|
||||
>
|
||||
{getTooltip(cellProps.row.original.osquery_policy_ms)}
|
||||
</ReactTooltip>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "No",
|
||||
@@ -139,19 +191,46 @@ const generateTableHeaders = (options: {
|
||||
),
|
||||
disableSortBy: true,
|
||||
accessor: "failing_host_count",
|
||||
Cell: (cellProps: ICellProps): JSX.Element => (
|
||||
<LinkCell
|
||||
value={`${cellProps.cell.value} hosts`}
|
||||
path={
|
||||
PATHS.MANAGE_HOSTS +
|
||||
TAGGED_TEMPLATES.hostsByPolicyRoute(
|
||||
cellProps.row.original.id,
|
||||
PolicyResponse.FAILING,
|
||||
selectedTeamId
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
Cell: (cellProps: ICellProps): JSX.Element => {
|
||||
if (cellProps.row.original.has_run) {
|
||||
return (
|
||||
<LinkCell
|
||||
value={`${cellProps.cell.value} host${
|
||||
cellProps.cell.value.toString() === "1" ? "" : "s"
|
||||
}`}
|
||||
path={
|
||||
PATHS.MANAGE_HOSTS +
|
||||
TAGGED_TEMPLATES.hostsByPolicyRoute(
|
||||
cellProps.row.original.id,
|
||||
PolicyResponse.FAILING,
|
||||
selectedTeamId
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className="text-cell text-muted has-not-run"
|
||||
data-tip
|
||||
data-for={`failing_${cellProps.row.original.id.toString()}`}
|
||||
>
|
||||
---
|
||||
</span>
|
||||
<ReactTooltip
|
||||
place="bottom"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
backgroundColor="#3e4771"
|
||||
id={`failing_${cellProps.row.original.id.toString()}`}
|
||||
data-html
|
||||
>
|
||||
{getTooltip(cellProps.row.original.osquery_policy_ms)}
|
||||
</ReactTooltip>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -200,17 +279,36 @@ const generateTableHeaders = (options: {
|
||||
|
||||
const generateDataSet = (
|
||||
policiesList: IPolicyStats[] = [],
|
||||
currentAutomatedPolicies?: number[]
|
||||
currentAutomatedPolicies?: number[],
|
||||
osquery_policy?: number
|
||||
): IPolicyStats[] => {
|
||||
policiesList = policiesList.sort((a, b) =>
|
||||
sortUtils.caseInsensitiveAsc(a.name, b.name)
|
||||
);
|
||||
let policiesLastRun: Date;
|
||||
let osqueryPolicyMs: number;
|
||||
|
||||
policiesList.forEach((policy) => {
|
||||
policy.webhook =
|
||||
currentAutomatedPolicies && currentAutomatedPolicies.includes(policy.id)
|
||||
if (osquery_policy) {
|
||||
osqueryPolicyMs = osquery_policy / 1000000;
|
||||
// Convert from nanosecond to milliseconds
|
||||
policiesLastRun = new Date(Date.now() - osqueryPolicyMs);
|
||||
}
|
||||
|
||||
policiesList.forEach((policyItem) => {
|
||||
policyItem.webhook =
|
||||
currentAutomatedPolicies &&
|
||||
currentAutomatedPolicies.includes(policyItem.id)
|
||||
? "On"
|
||||
: "Off";
|
||||
|
||||
// Define policy has_run based on updated_at compared againist last time policies ran as
|
||||
// defined by osquery_policy.
|
||||
policyItem.has_run = isAfter(
|
||||
policiesLastRun,
|
||||
new Date(policyItem.updated_at)
|
||||
);
|
||||
// Include osquery policy in item for reference in tooltip
|
||||
policyItem.osquery_policy_ms = osqueryPolicyMs;
|
||||
});
|
||||
|
||||
return policiesList;
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.has-not-run {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.no-policies {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user