Fleet UI: Merge inherited policies into team policies (#18543)
This commit is contained in:
committed by
RachelElysia
parent
270db29328
commit
ba1c783eea
@@ -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 };
|
||||
@@ -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={
|
||||
<>
|
||||
"All teams" policies are checked
|
||||
<br />
|
||||
for this team'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;
|
||||
|
||||
+2
-2
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
+13
-31
@@ -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'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"
|
||||
|
||||
+64
-18
@@ -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,
|
||||
|
||||
+1
-5
@@ -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()}
|
||||
|
||||
+1
-5
@@ -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()}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user