Fleet UI: Merge inherited policies into team policies (#18543)

This commit is contained in:
RachelElysia
2024-05-07 15:44:28 -04:00
committed by RachelElysia
parent 270db29328
commit ba1c783eea
12 changed files with 332 additions and 268 deletions
+84 -1
View File
@@ -11,7 +11,7 @@ const DEFAULT_POLICY_MOCK: IPolicyStats = {
author_id: 1,
author_name: "Test User",
author_email: "test@user.com",
team_id: undefined,
team_id: null,
resolution: "Ensure ClamAV and Freshclam are installed and running.",
platform: "linux" as const,
created_at: "2023-03-24T22:13:59Z",
@@ -29,4 +29,87 @@ const createMockPolicy = (overrides?: Partial<IPolicyStats>): IPolicyStats => {
return { ...DEFAULT_POLICY_MOCK, ...overrides };
};
export const createMockPoliciesResponse = (
overrides?: Partial<IPolicyStats>
) => {
const MOCK_POLICIES_RESPONSE: { policies: IPolicyStats[] } = {
policies: [
{
id: 5,
name: "Gatekeeper enabled",
query: "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
description: "Checks if gatekeeper is enabled on macOS devices",
critical: true,
author_id: 42,
author_name: "John",
author_email: "john@example.com",
team_id: 2,
resolution: "Resolution steps",
platform: "darwin",
created_at: "2021-12-16T14:37:37Z",
updated_at: "2021-12-16T16:39:00Z",
passing_host_count: 2000,
failing_host_count: 300,
host_count_updated_at: "2023-12-20T15:23:57Z",
webhook: "Off",
has_run: true,
next_update_ms: 3600000,
calendar_events_enabled: false,
},
{
id: 29090,
name: "Windows machines with encrypted hard disks",
query: "SELECT 1 FROM bitlocker_info WHERE protection_status = 1;",
description: "Checks if the hard disk is encrypted on Windows devices",
critical: false,
author_id: 43,
author_name: "Alice",
author_email: "alice@example.com",
team_id: 2,
resolution: "Resolution steps",
platform: "windows",
created_at: "2021-12-16T14:37:37Z",
updated_at: "2021-12-16T16:39:00Z",
passing_host_count: 2300,
failing_host_count: 0,
host_count_updated_at: "2023-12-20T15:23:57Z",
webhook: "Off",
has_run: true,
next_update_ms: 3600000,
calendar_events_enabled: false,
},
{
id: 136,
name: "Arbitrary Test Policy (all platforms) (all teams)",
query: "SELECT 1 FROM osquery_info WHERE 1=1;",
description:
"If you're seeing this, mostly likely this is because someone is testing out failing policies in dogfood. You can ignore this.",
critical: true,
author_id: 77,
author_name: "Test Admin",
author_email: "test@admin.com",
team_id: null,
resolution:
'To make it pass, change "1=0" to "1=1". To make it fail, change "1=1" to "1=0".',
platform: "darwin,windows,linux",
created_at: "2022-08-04T19:30:18Z",
updated_at: "2022-08-30T15:08:26Z",
passing_host_count: 10,
failing_host_count: 9,
host_count_updated_at: "2023-12-20T15:23:57Z",
webhook: "Off",
has_run: true,
next_update_ms: 3600000,
calendar_events_enabled: false,
},
],
};
if (overrides) {
MOCK_POLICIES_RESPONSE.policies.push(createMockPolicy(overrides));
}
return MOCK_POLICIES_RESPONSE;
};
export default createMockPolicy;
@@ -0,0 +1,56 @@
// Note: This is a lite solution and does NOT include ability to
// "Select all across pages" nor to disable the header checkbox
import { HeaderProps, Row } from "react-table";
interface GetConditionalSelectHeaderCheckboxProps {
/** react-table header props */
headerProps: React.PropsWithChildren<HeaderProps<any>>;
/** A function defining which rows are selectable */
checkIfRowIsSelectable: (row: Row<any>) => boolean;
}
const getConditionalSelectHeaderCheckboxProps = ({
headerProps,
checkIfRowIsSelectable,
}: GetConditionalSelectHeaderCheckboxProps) => {
// Define if the checkbox should show as checked or indeterminate
const checkIfAllSelectableRowsSelected = (rows: Row<any>[]) =>
rows.filter(checkIfRowIsSelectable).every((row) => row.isSelected);
// Note: This is where we would include disabled logic if we needed it
// Naming matches react-table v7 https://react-table-v7-docs.netlify.app/docs/api/useRowSelect#instance-properties
// getToggleAllPageRowsSelectedProps: Function(props) => props
const checked = checkIfAllSelectableRowsSelected(headerProps.rows);
const indeterminate =
!checked && headerProps.rows.some((row) => row.isSelected);
const onChange = () => {
// If all selectable rows are already selected, deselect all selectable rows on the page
if (checkIfAllSelectableRowsSelected(headerProps.rows)) {
headerProps.rows.forEach((row) => {
headerProps.toggleRowSelected(row.id, false);
});
} else {
// Otherwise select every selectable row on the page
headerProps.page.forEach((row) => {
const rowChecked = checkIfRowIsSelectable(row);
headerProps.toggleRowSelected(row.id, rowChecked);
});
}
};
// Usual checkbox props
const checkboxProps = headerProps.getToggleAllRowsSelectedProps();
// For conditional select, we override value, indeterminate and onChange
return {
...checkboxProps,
value: checked,
indeterminate,
onChange,
// disabled, // Not included
};
};
export default { getConditionalSelectHeaderCheckboxProps };
+2 -3
View File
@@ -36,7 +36,7 @@ export interface IPolicy {
author_email: string;
resolution: string;
platform: SelectedPlatformString;
team_id?: number;
team_id: number | null;
created_at: string;
updated_at: string;
critical: boolean;
@@ -80,7 +80,6 @@ export interface ILoadAllPoliciesResponse {
export interface ILoadTeamPoliciesResponse {
policies: IPolicyStats[];
inherited_policies: IPolicyStats[];
}
export interface IPolicyFormData {
description?: string | number | boolean | undefined;
@@ -89,7 +88,7 @@ export interface IPolicyFormData {
platform?: SelectedPlatformString;
name?: string | number | boolean | undefined;
query?: string | number | boolean | undefined;
team_id?: number;
team_id?: number | null;
id?: number;
calendar_events_enabled?: boolean;
}
@@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from "react";
import { useQuery } from "react-query";
import { InjectedRouter } from "react-router/lib/Router";
import PATHS from "router/paths";
import { noop, isEqual } from "lodash";
import { isEqual } from "lodash";
import { getNextLocationPath } from "utilities/helpers";
@@ -36,7 +36,6 @@ import { ITableQueryData } from "components/TableContainer/TableContainer";
import Button from "components/buttons/Button";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import RevealButton from "components/buttons/RevealButton";
import Spinner from "components/Spinner";
import TeamsDropdown from "components/TeamsDropdown";
import TableDataError from "components/DataError";
@@ -62,10 +61,6 @@ interface IManagePoliciesPageProps {
order_key?: string;
order_direction?: "asc" | "desc";
page?: string;
inherited_table?: "true";
inherited_order_key?: string;
inherited_order_direction?: "asc" | "desc";
inherited_page?: string;
};
search: string;
};
@@ -140,7 +135,6 @@ const ManagePolicyPage = ({
const [showCalendarEventsModal, setShowCalendarEventsModal] = useState(false);
const [teamPolicies, setTeamPolicies] = useState<IPolicyStats[]>();
const [inheritedPolicies, setInheritedPolicies] = useState<IPolicyStats[]>();
// Functions to avoid race conditions
const initialSearchQuery = (() => queryParams.query ?? "")();
@@ -152,40 +146,15 @@ const ManagePolicyPage = ({
DEFAULT_SORT_DIRECTION)();
const initialPage = (() =>
queryParams && queryParams.page ? parseInt(queryParams?.page, 10) : 0)();
const initialShowInheritedTable = (() =>
queryParams && queryParams.inherited_table === "true")();
const initialInheritedSortHeader = (() =>
(queryParams?.inherited_order_key as "name" | "failing_host_count") ??
DEFAULT_SORT_COLUMN)();
const initialInheritedSortDirection = (() =>
(queryParams?.inherited_order_direction as "asc" | "desc") ??
DEFAULT_SORT_DIRECTION)();
const initialInheritedPage = (() =>
queryParams && queryParams.inherited_page
? parseInt(queryParams?.inherited_page, 10)
: 0)();
const showInheritedTable = initialShowInheritedTable;
// Needs update on location change or table state might not match URL
const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
const [page, setPage] = useState(initialPage);
const [inheritedPage, setInheritedPage] = useState(initialInheritedPage);
const [tableQueryData, setTableQueryData] = useState<ITableQueryData>();
const [
inheritedTableQueryData,
setInheritedTableQueryData,
] = useState<ITableQueryData>();
const [sortHeader, setSortHeader] = useState(initialSortHeader);
const [sortDirection, setSortDirection] = useState<
"asc" | "desc" | undefined
>(initialSortDirection);
const [inheritedSortDirection, setInheritedSortDirection] = useState(
initialInheritedSortDirection
);
const [inheritedSortHeader, setInheritedSortHeader] = useState(
initialInheritedSortHeader
);
useEffect(() => {
setLastEditedQueryPlatform(null);
@@ -199,9 +168,6 @@ const ManagePolicyPage = ({
setSearchQuery(initialSearchQuery);
setSortHeader(initialSortHeader);
setSortDirection(initialSortDirection);
setInheritedPage(initialInheritedPage);
setInheritedSortHeader(initialInheritedSortHeader);
setInheritedSortDirection(initialInheritedSortDirection);
}, [location, isRouteOk]);
useEffect(() => {
@@ -260,7 +226,7 @@ const ManagePolicyPage = ({
[
{
scope: "policiesCount",
query: isAnyTeamSelected ? "" : searchQuery, // Search query not used for inherited count
query: isAnyTeamSelected ? "" : searchQuery,
},
],
({ queryKey }) => globalPoliciesAPI.getCount(queryKey[0]),
@@ -291,11 +257,8 @@ const ManagePolicyPage = ({
query: searchQuery,
orderDirection: sortDirection,
orderKey: sortHeader,
inheritedPage: inheritedTableQueryData?.pageIndex,
inheritedPerPage: DEFAULT_PAGE_SIZE,
inheritedOrderDirection: inheritedSortDirection,
inheritedOrderKey: inheritedSortHeader,
teamId: teamIdForApi || 0,
mergeInherited: !!teamIdForApi,
},
],
({ queryKey }) => {
@@ -305,11 +268,38 @@ const ManagePolicyPage = ({
enabled: isRouteOk && isPremiumTier && !!teamIdForApi,
onSuccess: (data) => {
setTeamPolicies(data.policies);
setInheritedPolicies(data.inherited_policies);
},
}
);
const {
data: teamPoliciesCountMergeInherited,
isFetching: isFetchingTeamCountMergeInherited,
refetch: refetchTeamPoliciesCountMergeInherited,
} = useQuery<
IPoliciesCountResponse,
Error,
number,
ITeamPoliciesCountQueryKey[]
>(
[
{
scope: "teamPoliciesCountMergeInherited",
query: searchQuery,
teamId: teamIdForApi || 0, // TODO: Fix number/undefined type
mergeInherited: !!teamIdForApi,
},
],
({ queryKey }) => teamPoliciesAPI.getCount(queryKey[0]),
{
enabled: isRouteOk && !!teamIdForApi,
keepPreviousData: true,
refetchOnWindowFocus: false,
retry: 1,
select: (data) => data.count,
}
);
const {
data: teamPoliciesCount,
isFetching: isFetchingTeamCount,
@@ -325,6 +315,7 @@ const ManagePolicyPage = ({
scope: "teamPoliciesCount",
query: searchQuery,
teamId: teamIdForApi || 0, // TODO: Fix number/undefined type
mergeInherited: false,
},
],
({ queryKey }) => teamPoliciesAPI.getCount(queryKey[0]),
@@ -340,6 +331,9 @@ const ManagePolicyPage = ({
const canAddOrDeletePolicy: boolean =
isGlobalAdmin || isGlobalMaintainer || isTeamMaintainer || isTeamAdmin;
const canManageAutomations: boolean = isGlobalAdmin || isTeamAdmin;
const hasPoliciesToAutomateOrDelete: boolean = teamIdForApi
? !isFetchingTeamCount && !!teamPoliciesCount && teamPoliciesCount > 0
: !!globalPoliciesCount && globalPoliciesCount > 0;
const {
data: config,
@@ -375,6 +369,7 @@ const ManagePolicyPage = ({
const refetchPolicies = (teamId?: number) => {
if (teamId) {
refetchTeamPolicies();
refetchTeamPoliciesCountMergeInherited();
refetchTeamPoliciesCount();
} else {
refetchGlobalPolicies(); // Only call on global policies as this is expensive
@@ -391,72 +386,36 @@ const ManagePolicyPage = ({
);
// TODO: Look into useDebounceCallback with dependencies
// Inherited table uses the same onQueryChange function but routes to different URL params
const onQueryChange = useCallback(
async (newTableQuery: ITableQueryData) => {
if (!isRouteOk || isEqual(newTableQuery, tableQueryData)) {
return;
}
newTableQuery.editingInheritedTable
? setInheritedTableQueryData({ ...newTableQuery })
: setTableQueryData({ ...newTableQuery });
setTableQueryData({ ...newTableQuery });
const {
pageIndex: newPageIndex,
searchQuery: newSearchQuery,
sortDirection: newSortDirection,
sortHeader: newSortHeader,
editingInheritedTable,
} = newTableQuery;
// Rebuild queryParams to dispatch new browser location to react-router
const newQueryParams: { [key: string]: string | number | undefined } = {};
newQueryParams.query = newSearchQuery;
// Updates main policy table URL params
// No change to inherited policy table URL params
if (!editingInheritedTable) {
newQueryParams.order_key = newSortHeader;
newQueryParams.order_direction = newSortDirection;
newQueryParams.page = newPageIndex.toString();
if (showInheritedTable) {
newQueryParams.inherited_order_key = inheritedSortHeader;
newQueryParams.inherited_order_direction = inheritedSortDirection;
newQueryParams.inherited_page = inheritedPage.toString();
}
// Reset page number to 0 for new filters
if (
newSortDirection !== sortDirection ||
newSortHeader !== sortHeader ||
newSearchQuery !== searchQuery
) {
newQueryParams.page = "0";
}
}
newQueryParams.order_key = newSortHeader;
newQueryParams.order_direction = newSortDirection;
newQueryParams.page = newPageIndex.toString();
if (showInheritedTable) {
newQueryParams.inherited_table =
showInheritedTable && showInheritedTable.toString();
}
// Updates inherited policy table URL params
// No change to main policy table URL params
if (showInheritedTable && editingInheritedTable) {
newQueryParams.inherited_order_key = newSortHeader;
newQueryParams.inherited_order_direction = newSortDirection;
newQueryParams.inherited_page = newPageIndex.toString();
newQueryParams.order_key = sortHeader;
newQueryParams.order_direction = sortDirection;
newQueryParams.page = page.toString();
newQueryParams.query = searchQuery;
// Reset page number to 0 for new filters
if (
newSortDirection !== inheritedSortDirection ||
newSortHeader !== inheritedSortHeader
) {
newQueryParams.inherited_page = "0";
}
// Reset page number to 0 for new filters
if (
newSortDirection !== sortDirection ||
newSortHeader !== sortHeader ||
newSearchQuery !== searchQuery
) {
newQueryParams.page = "0";
}
if (isRouteOk && teamIdForApi !== undefined) {
@@ -470,14 +429,7 @@ const ManagePolicyPage = ({
router?.replace(locationPath);
},
[
isRouteOk,
teamIdForApi,
searchQuery,
showInheritedTable,
inheritedSortDirection,
sortDirection,
] // Other dependencies can cause infinite re-renders as URL is source of truth
[isRouteOk, teamIdForApi, searchQuery, sortDirection] // Other dependencies can cause infinite re-renders as URL is source of truth
);
const toggleOtherWorkflowsModal = () =>
@@ -504,19 +456,6 @@ const ManagePolicyPage = ({
}
};
const toggleShowInheritedPolicies = () => {
// URL source of truth
const locationPath = getNextLocationPath({
pathPrefix: PATHS.MANAGE_POLICIES,
queryParams: {
...queryParams,
inherited_table: showInheritedTable ? undefined : "true",
inherited_page: showInheritedTable ? undefined : "0",
},
});
router?.replace(locationPath);
};
const handleUpdateOtherWorkflows = async (requestBody: {
webhook_settings: Pick<IWebhookSettings, "failing_policies_webhook">;
integrations: IZendeskJiraIntegrations;
@@ -649,21 +588,6 @@ const ManagePolicyPage = ({
}
};
const inheritedPoliciesButtonText = (
showPolicies: boolean,
count: number
) => {
return `${showPolicies ? "Hide" : "Show"} ${count} inherited ${
count > 1 ? "policies" : "policy"
}`;
};
const showInheritedPoliciesButton =
isAnyTeamSelected &&
!isFetchingTeamPolicies &&
!teamPoliciesError &&
!!inheritedPolicies?.length; // Returned with team policies
const availablePoliciesForAutomation =
(isAnyTeamSelected ? teamPolicies : globalPolicies) || [];
@@ -731,10 +655,13 @@ const ManagePolicyPage = ({
onAddPolicyClick={onAddPolicyClick}
onDeletePolicyClick={onDeletePolicyClick}
canAddOrDeletePolicy={canAddOrDeletePolicy}
hasPoliciesToDelete={hasPoliciesToAutomateOrDelete}
currentTeam={currentTeamSummary}
currentAutomatedPolicies={currentAutomatedPolicies}
renderPoliciesCount={() =>
!isFetchingTeamCount && renderPoliciesCount(teamPoliciesCount)
(!isFetchingTeamCountMergeInherited &&
renderPoliciesCount(teamPoliciesCountMergeInherited)) ||
null
}
isPremiumTier={isPremiumTier}
isSandboxMode={isSandboxMode}
@@ -753,12 +680,15 @@ const ManagePolicyPage = ({
onAddPolicyClick={onAddPolicyClick}
onDeletePolicyClick={onDeletePolicyClick}
canAddOrDeletePolicy={canAddOrDeletePolicy}
hasPoliciesToDelete={hasPoliciesToAutomateOrDelete}
currentTeam={currentTeamSummary}
currentAutomatedPolicies={currentAutomatedPolicies}
isPremiumTier={isPremiumTier}
isSandboxMode={isSandboxMode}
renderPoliciesCount={() =>
!isFetchingGlobalCount && renderPoliciesCount(globalPoliciesCount)
(!isFetchingGlobalCount &&
renderPoliciesCount(globalPoliciesCount)) ||
null
}
searchQuery={searchQuery}
sortHeader={sortHeader}
@@ -834,17 +764,19 @@ const ManagePolicyPage = ({
</div>
{showCtaButtons && (
<div className={`${baseClass} button-wrap`}>
{canManageAutomations && automationsConfig && (
<div className={`${baseClass}__manage-automations-wrapper`}>
<Dropdown
className={`${baseClass}__manage-automations-dropdown`}
onChange={onSelectAutomationOption}
placeholder="Manage automations"
searchable={false}
options={getAutomationsDropdownOptions()}
/>
</div>
)}
{canManageAutomations &&
automationsConfig &&
hasPoliciesToAutomateOrDelete && (
<div className={`${baseClass}__manage-automations-wrapper`}>
<Dropdown
className={`${baseClass}__manage-automations-dropdown`}
onChange={onSelectAutomationOption}
placeholder="Manage automations"
searchable={false}
options={getAutomationsDropdownOptions()}
/>
</div>
)}
{canAddOrDeletePolicy && (
<div className={`${baseClass}__action-button-container`}>
<Button
@@ -852,7 +784,7 @@ const ManagePolicyPage = ({
className={`${baseClass}__select-policy-button`}
onClick={onAddPolicyClick}
>
Add a policy
Add policy
</Button>
</div>
)}
@@ -867,52 +799,6 @@ const ManagePolicyPage = ({
</p>
</div>
{renderMainTable()}
{showInheritedPoliciesButton && globalPoliciesCount && (
<RevealButton
isShowing={showInheritedTable}
className={baseClass}
hideText={inheritedPoliciesButtonText(
showInheritedTable,
globalPoliciesCount
)}
showText={inheritedPoliciesButtonText(
showInheritedTable,
globalPoliciesCount
)}
caretPosition="before"
tooltipContent={
<>
&quot;All teams&quot; policies are checked
<br />
for this team&apos;s hosts.
</>
}
onClick={toggleShowInheritedPolicies}
/>
)}
{showInheritedPoliciesButton && showInheritedTable && (
<div className={`${baseClass}__inherited-policies-table`}>
{globalPoliciesError && <TableDataError />}
{!globalPoliciesError && (
<PoliciesTable
isLoading={isFetchingTeamPolicies}
policiesList={inheritedPolicies || []}
onDeletePolicyClick={noop}
canAddOrDeletePolicy={canAddOrDeletePolicy}
tableType="inheritedPolicies"
currentTeam={currentTeamSummary}
searchQuery=""
renderPoliciesCount={() =>
renderPoliciesCount(teamPoliciesCount)
}
sortHeader={inheritedSortHeader}
sortDirection={inheritedSortDirection}
page={inheritedPage}
onQueryChange={onQueryChange}
/>
)}
</div>
)}
{config && automationsConfig && showOtherWorkflowsModal && (
<OtherWorkflowsModal
automationsConfig={automationsConfig}
@@ -165,8 +165,8 @@
}
}
.critical-tooltip {
text-align: left;
.critical-tooltip,
.inherited-tooltip {
font-weight: $regular;
}
@@ -184,10 +184,33 @@
display: flex; // required for inline icon
gap: $pad-xsmall;
.tooltip-base {
// Underlines only the name text
&:hover {
text-decoration: none;
.policy-name-text {
text-decoration: underline;
}
}
.critical-badge,
.inherited-badge {
display: inline-flex;
}
.inherited-badge {
display: flex;
padding: 4px;
justify-content: center;
align-items: center;
gap: 4px;
font-weight: $bold;
font-size: $xxx-small;
color: $core-fleet-black;
border-radius: 4px;
background: $ui-vibrant-blue-10;
}
.policy-name-text {
text-overflow: ellipsis;
overflow: hidden;
@@ -21,7 +21,7 @@ describe("Policies table", () => {
searchQuery=""
page={0}
onQueryChange={noop}
renderPoliciesCount={noop}
renderPoliciesCount={() => null}
/>
);
@@ -48,7 +48,7 @@ describe("Policies table", () => {
searchQuery=""
page={0}
onQueryChange={noop}
renderPoliciesCount={noop}
renderPoliciesCount={() => null}
/>
);
@@ -1,6 +1,5 @@
import React, { useContext } from "react";
import { AppContext } from "context/app";
import PATHS from "router/paths";
import { IPolicyStats } from "interfaces/policy";
import { ITeamSummary } from "interfaces/team";
@@ -14,12 +13,6 @@ import { generateTableHeaders, generateDataSet } from "./PoliciesTableConfig";
const baseClass = "policies-table";
const TAGGED_TEMPLATES = {
hostsByTeamRoute: (teamId: number | undefined | null) => {
return `${teamId ? `/?team_id=${teamId}` : ""}`;
},
};
const DEFAULT_SORT_DIRECTION = "asc";
const DEFAULT_SORT_HEADER = "name";
@@ -29,13 +22,12 @@ interface IPoliciesTableProps {
onAddPolicyClick?: () => void;
onDeletePolicyClick: (selectedTableIds: number[]) => void;
canAddOrDeletePolicy?: boolean;
tableType?: "inheritedPolicies";
hasPoliciesToDelete?: boolean;
currentTeam: ITeamSummary | undefined;
currentAutomatedPolicies?: number[];
isPremiumTier?: boolean;
isSandboxMode?: boolean;
// onClientSidePaginationChange?: (pageIndex: number) => void;
renderPoliciesCount: any; // TODO: typing
renderPoliciesCount: () => JSX.Element | null;
onQueryChange: (newTableQuery: ITableQueryData) => void;
searchQuery: string;
sortHeader?: "name" | "failing_host_count";
@@ -49,13 +41,12 @@ const PoliciesTable = ({
onAddPolicyClick,
onDeletePolicyClick,
canAddOrDeletePolicy,
tableType,
hasPoliciesToDelete,
currentTeam,
currentAutomatedPolicies,
isPremiumTier,
isSandboxMode,
onQueryChange,
// onClientSidePaginationChange,
renderPoliciesCount,
searchQuery,
sortHeader,
@@ -64,23 +55,18 @@ const PoliciesTable = ({
}: IPoliciesTableProps): JSX.Element => {
const { config } = useContext(AppContext);
// Inherited table uses the same onQueryChange but require different URL params
const onTableQueryChange = (newTableQuery: ITableQueryData) => {
onQueryChange({
...newTableQuery,
editingInheritedTable: tableType === "inheritedPolicies",
});
};
const emptyState = () => {
const emptyPolicies: IEmptyTableProps = {
graphicName: "empty-policies",
header: <>You don&apos;t have any policies</>,
info: (
<>
Add policies to detect device health issues and trigger automations.
</>
),
header: "You don't have any policies",
info:
"Add policies to detect device health issues and trigger automations.",
};
if (canAddOrDeletePolicy) {
emptyPolicies.primaryButton = (
@@ -96,9 +82,8 @@ const PoliciesTable = ({
if (searchQuery) {
delete emptyPolicies.graphicName;
delete emptyPolicies.primaryButton;
emptyPolicies.header = "No policies match the current search criteria.";
emptyPolicies.info =
"Expecting to see policies? Try again in a few seconds as the system catches up.";
emptyPolicies.header = "No matching policies";
emptyPolicies.info = "No policies match the current filters.";
}
return emptyPolicies;
@@ -106,19 +91,17 @@ const PoliciesTable = ({
const searchable = !(policiesList?.length === 0 && searchQuery === "");
const hasPermissionAndPoliciesToDelete =
canAddOrDeletePolicy && hasPoliciesToDelete;
return (
<div
className={`${baseClass} ${
canAddOrDeletePolicy ? "" : "hide-selection-column"
}`}
>
<div className={baseClass}>
<TableContainer
resultsTitle="policies"
columnConfigs={generateTableHeaders(
{
selectedTeamId: currentTeam?.id,
canAddOrDeletePolicy,
tableType,
hasPermissionAndPoliciesToDelete,
},
policiesList,
isPremiumTier,
@@ -152,7 +135,6 @@ const PoliciesTable = ({
primaryButton: emptyState().primaryButton,
})
}
disableCount={tableType === "inheritedPolicies"}
renderCount={renderPoliciesCount}
onQueryChange={onTableQueryChange}
inputPlaceHolder="Search by name"
@@ -4,7 +4,6 @@
import React from "react";
import {
formatDistanceToNowStrict,
isAfter,
millisecondsToHours,
millisecondsToMinutes,
} from "date-fns";
@@ -13,7 +12,6 @@ import ReactTooltip from "react-tooltip";
import Checkbox from "components/forms/fields/Checkbox";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
import StatusIndicator from "components/StatusIndicator";
import Icon from "components/Icon";
import { IPolicyStats } from "interfaces/policy";
import PATHS from "router/paths";
@@ -21,6 +19,7 @@ import sortUtils from "utilities/sort";
import { PolicyResponse } from "utilities/constants";
import { buildQueryStringFromParams } from "utilities/url";
import { COLORS } from "styles/var/colors";
import configUtils from "components/TableContainer/utilities/config_utils";
import PassingColumnHeader from "../PassingColumnHeader";
interface IGetToggleAllRowsSelectedProps {
@@ -101,15 +100,15 @@ const getTooltip = (osqueryPolicyMs: number): JSX.Element => {
const generateTableHeaders = (
options: {
selectedTeamId?: number | null;
canAddOrDeletePolicy?: boolean;
hasPermissionAndPoliciesToDelete?: boolean;
tableType?: string;
},
policiesList: IPolicyStats[] = [],
isPremiumTier?: boolean,
isSandboxMode?: boolean
): IDataColumn[] => {
const { selectedTeamId, tableType, canAddOrDeletePolicy } = options;
const { selectedTeamId, hasPermissionAndPoliciesToDelete } = options;
const viewingTeamPolicies = selectedTeamId !== -1;
// Figure the time since the host counts were updated.
// First, find first policy item with host_count_updated_at.
const updatedAt =
@@ -146,7 +145,7 @@ const generateTableHeaders = (
{isPremiumTier && cellProps.row.original.critical && (
<>
<span
className="tooltip-base"
className="critical-badge"
data-tip
data-for={`critical-tooltip-${cellProps.row.original.id}`}
>
@@ -175,6 +174,27 @@ const generateTableHeaders = (
</ReactTooltip>
</>
)}
{viewingTeamPolicies && !cellProps.row.original.team_id && (
<>
<span
className="inherited-badge"
data-tip
data-for={`inherited-tooltip-${cellProps.row.original.id}`}
>
Inherited
</span>
<ReactTooltip
className="inherited-tooltip"
place="top"
type="dark"
effect="solid"
id={`inherited-tooltip-${cellProps.row.original.id}`}
backgroundColor={COLORS["tooltip-bg"]}
>
This policy runs on all hosts.
</ReactTooltip>
</>
)}
</>
}
path={PATHS.EDIT_POLICY(cellProps.row.original)}
@@ -282,29 +302,55 @@ const generateTableHeaders = (
sortType: "caseInsensitive",
},
];
if (tableType !== "inheritedPolicies") {
if (!canAddOrDeletePolicy) {
return tableHeaders;
}
console.log(
"hasPermissionAndPoliciesToDelete",
hasPermissionAndPoliciesToDelete
);
if (hasPermissionAndPoliciesToDelete) {
tableHeaders.unshift({
id: "selection",
Header: (cellProps: IHeaderProps) => {
const props = cellProps.getToggleAllRowsSelectedProps();
const checkboxProps = {
value: props.checked,
indeterminate: props.indeterminate,
onChange: () => cellProps.toggleAllRowsSelected(),
Header: (headerProps: any) => {
// When viewing team policies select all checkbox accounts for not selecting inherited policies
const teamCheckboxProps = configUtils.getConditionalSelectHeaderCheckboxProps(
{
headerProps,
checkIfRowIsSelectable: (row) => row.original.team_id !== null,
}
);
// Regular table selection logic
const {
getToggleAllRowsSelectedProps,
toggleAllRowsSelected,
} = headerProps;
const { checked, indeterminate } = getToggleAllRowsSelectedProps();
const regularCheckboxProps = {
value: checked,
indeterminate,
onChange: () => {
toggleAllRowsSelected();
},
};
const checkboxProps = viewingTeamPolicies
? teamCheckboxProps
: regularCheckboxProps;
return <Checkbox {...checkboxProps} />;
},
Cell: (cellProps: ICellProps): JSX.Element => {
const inheritedPolicy = !cellProps.row.original.team_id;
const props = cellProps.row.getToggleRowSelectedProps();
const checkboxProps = {
value: props.checked,
onChange: () => cellProps.row.toggleRowSelected(),
};
// When viewing team policies and a row is an inherited policy, do not render checkbox
if (viewingTeamPolicies && inheritedPolicy) {
return <></>;
}
return <Checkbox {...checkboxProps} />;
},
disableHidden: true,
@@ -26,11 +26,7 @@ const PolicyErrorsTable = ({
canAddOrDeletePolicy,
}: IPolicyErrorsTableProps): JSX.Element => {
return (
<div
className={`${baseClass} ${
canAddOrDeletePolicy ? "" : "hide-selection-column"
}`}
>
<div className={baseClass}>
<TableContainer
resultsTitle={resultsTitle || "policies"}
columnConfigs={generateTableHeaders()}
@@ -26,11 +26,7 @@ const PolicyResultsTable = ({
canAddOrDeletePolicy,
}: IPolicyResultsTableProps): JSX.Element => {
return (
<div
className={`${baseClass} ${
canAddOrDeletePolicy ? "" : "hide-selection-column"
}`}
>
<div className={baseClass}>
<TableContainer
resultsTitle={resultsTitle || "policies"}
columnConfigs={generateTableHeaders()}
+9 -16
View File
@@ -17,14 +17,11 @@ interface IPoliciesApiQueryParams {
orderKey?: string;
orderDirection?: "asc" | "desc";
query?: string;
inheritedPage?: number;
inheritedPerPage?: number;
inheritedOrderKey?: string;
inheritedOrderDirection?: "asc" | "desc";
}
export interface IPoliciesApiParams extends IPoliciesApiQueryParams {
teamId: number;
mergeInherited?: boolean;
}
export interface ITeamPoliciesQueryKey extends IPoliciesApiParams {
@@ -32,13 +29,14 @@ export interface ITeamPoliciesQueryKey extends IPoliciesApiParams {
}
export interface ITeamPoliciesCountQueryKey
extends Pick<IPoliciesApiParams, "query" | "teamId"> {
scope: "teamPoliciesCount";
extends Pick<IPoliciesApiParams, "query" | "teamId" | "mergeInherited"> {
scope: "teamPoliciesCountMergeInherited" | "teamPoliciesCount";
}
interface IPoliciesCountApiParams {
teamId: number;
query?: string;
mergeInherited?: boolean;
}
const ORDER_KEY = "name";
@@ -118,7 +116,6 @@ export default {
load: (team_id: number, id: number) => {
const { TEAMS } = endpoints;
const path = `${TEAMS}/${team_id}/policies/${id}`;
return sendRequest("GET", path);
},
loadAll: (team_id?: number): Promise<ILoadTeamPoliciesResponse> => {
@@ -137,10 +134,7 @@ export default {
orderKey = ORDER_KEY,
orderDirection: orderDir = ORDER_DIRECTION,
query,
inheritedPage,
inheritedPerPage,
inheritedOrderKey = ORDER_KEY,
inheritedOrderDirection: inheritedOrderDir = ORDER_DIRECTION,
mergeInherited,
}: IPoliciesApiParams): Promise<ILoadTeamPoliciesResponse> => {
const { TEAMS } = endpoints;
@@ -150,10 +144,7 @@ export default {
orderKey,
orderDirection: orderDir,
query,
inheritedPage,
inheritedPerPage,
inheritedOrderKey,
inheritedOrderDirection: inheritedOrderDir,
mergeInherited,
};
const snakeCaseParams = convertParamsToSnakeCase(queryParams);
@@ -168,14 +159,16 @@ export default {
getCount: async ({
query,
teamId,
mergeInherited = false,
}: Pick<
IPoliciesCountApiParams,
"query" | "teamId"
"query" | "teamId" | "mergeInherited"
>): Promise<IPoliciesCountResponse> => {
const { TEAM_POLICIES } = endpoints;
const path = `${TEAM_POLICIES(teamId)}/count`;
const queryParams = {
query,
mergeInherited,
};
const snakeCaseParams = convertParamsToSnakeCase(queryParams);
const queryString = buildQueryStringFromParams(snakeCaseParams);
@@ -4,6 +4,8 @@
* Also please check the README for how to use the mock service :)
*/
import { createMockPoliciesResponse } from "__mocks__/policyMock";
const count = {
targets_count: 1,
targets_online: 0,
@@ -10590,6 +10592,7 @@ const globalQuery5 = { query: globalQueries.queries[5] };
const globalQuery6 = { query: globalQueries.queries[6] };
const teamQuery1 = { query: teamQueries.queries[0] };
const teamQuery2 = { query: teamQueries.queries[1] };
const teamPolicy1 = createMockPoliciesResponse();
const aiAutofillPolicy = {
description:
@@ -10614,4 +10617,5 @@ export default {
teamQuery1,
teamQuery2,
aiAutofillPolicy,
teamPolicy1,
};