Indicate that a policy's data is not yet accurate (#5031)

This commit is contained in:
Luke Heath
2022-04-11 15:21:34 -05:00
committed by GitHub
parent ab85963cbd
commit d75cf11cec
8 changed files with 149 additions and 68 deletions
@@ -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>
);
+2
View File
@@ -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 &&
@@ -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"}
@@ -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;