Add ability to target labels on policies (#27599)
For #27276 # Details This PR adds the ability to select labels when saving or editing a query in the UI, so that the query will only target hosts with those labels. It follows the API design from https://github.com/fleetdm/fleet/pull/27196, utilizing the labels_include_any and labels_exclude_any fields. The expectation is that when creating or updating a query, labels_include_any and labels_exclude_any are arrays of label names, and when fetching a single query, they are arrays of objects with a name and an id key. Other updates in this PR: * Removed colons from various headings on the Save Policy Modal and Edit Policy form * Updated the "Delete label" text * Removed "Policy runs on all hosts with these platforms." subheading underneath the platform selector * TargetLabelSelector component now has `suppressTitle` flag to turn off the "Target" title.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Allow adding labels when saving or editing polices in the UI
|
||||
@@ -76,7 +76,7 @@ export const PlatformSelector = ({
|
||||
|
||||
return (
|
||||
<div className={`${parentClass}__${baseClass} ${baseClass} form-field`}>
|
||||
<span className={labelClasses}>Target:</span>
|
||||
<span className={labelClasses}>Target</span>
|
||||
<span className={`${baseClass}__checkboxes`}>
|
||||
<Checkbox
|
||||
value={checkDarwin}
|
||||
@@ -112,7 +112,6 @@ export const PlatformSelector = ({
|
||||
</Checkbox>
|
||||
</span>
|
||||
<div className="form-field__help-text">
|
||||
Policy runs on all hosts with these platforms.
|
||||
{renderInstallSoftwareHelpText()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -112,4 +112,75 @@ describe("TargetLabelSelector component", () => {
|
||||
expect(screen.getByRole("checkbox", { name: "label 1" })).toBeChecked();
|
||||
expect(screen.getByRole("checkbox", { name: "label 2" })).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("sets the title to Target by default", () => {
|
||||
const TITLE = "Target";
|
||||
render(
|
||||
<TargetLabelSelector
|
||||
selectedTargetType="Custom"
|
||||
selectedCustomTarget="labelIncludeAny"
|
||||
customTargetOptions={[
|
||||
{ value: "labelIncludeAny", label: "Include any" },
|
||||
]}
|
||||
selectedLabels={{}}
|
||||
labels={[
|
||||
{ id: 1, name: "label 1", label_type: "regular" },
|
||||
{ id: 2, name: "label 2", label_type: "regular" },
|
||||
]}
|
||||
onSelectCustomTarget={noop}
|
||||
onSelectLabel={noop}
|
||||
onSelectTargetType={noop}
|
||||
title={TITLE}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(TITLE)).toBeVisible();
|
||||
});
|
||||
|
||||
it("allows a custom title to be passed in", () => {
|
||||
const TITLE = "Choose a target";
|
||||
render(
|
||||
<TargetLabelSelector
|
||||
selectedTargetType="Custom"
|
||||
selectedCustomTarget="labelIncludeAny"
|
||||
customTargetOptions={[
|
||||
{ value: "labelIncludeAny", label: "Include any" },
|
||||
]}
|
||||
selectedLabels={{}}
|
||||
labels={[
|
||||
{ id: 1, name: "label 1", label_type: "regular" },
|
||||
{ id: 2, name: "label 2", label_type: "regular" },
|
||||
]}
|
||||
onSelectCustomTarget={noop}
|
||||
onSelectLabel={noop}
|
||||
onSelectTargetType={noop}
|
||||
title={TITLE}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(TITLE)).toBeVisible();
|
||||
});
|
||||
|
||||
it("suppresses the title when suppressTitle is true", () => {
|
||||
render(
|
||||
<TargetLabelSelector
|
||||
selectedTargetType="Custom"
|
||||
selectedCustomTarget="labelIncludeAny"
|
||||
customTargetOptions={[
|
||||
{ value: "labelIncludeAny", label: "Include any" },
|
||||
]}
|
||||
selectedLabels={{}}
|
||||
labels={[
|
||||
{ id: 1, name: "label 1", label_type: "regular" },
|
||||
{ id: 2, name: "label 2", label_type: "regular" },
|
||||
]}
|
||||
onSelectCustomTarget={noop}
|
||||
onSelectLabel={noop}
|
||||
onSelectTargetType={noop}
|
||||
suppressTitle
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Target")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,16 +42,18 @@ interface ITargetChooserProps {
|
||||
selectedTarget: string;
|
||||
onSelect: (val: string) => void;
|
||||
disableOptions?: boolean;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
const TargetChooser = ({
|
||||
selectedTarget,
|
||||
onSelect,
|
||||
disableOptions = false,
|
||||
title,
|
||||
}: ITargetChooserProps) => {
|
||||
return (
|
||||
<div className="form-field">
|
||||
<div className="form-field__label">Target</div>
|
||||
{title && <div className="form-field__label">{title}</div>}
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label="All hosts"
|
||||
@@ -178,6 +180,8 @@ interface ITargetLabelSelectorProps {
|
||||
onSelectCustomTarget?: (val: string) => void;
|
||||
onSelectLabel: ({ name, value }: { name: string; value: boolean }) => void;
|
||||
disableOptions?: boolean;
|
||||
title?: string;
|
||||
suppressTitle?: boolean;
|
||||
}
|
||||
|
||||
const TargetLabelSelector = ({
|
||||
@@ -195,6 +199,8 @@ const TargetLabelSelector = ({
|
||||
onSelectCustomTarget,
|
||||
onSelectLabel,
|
||||
disableOptions = false,
|
||||
title = "Target",
|
||||
suppressTitle = false,
|
||||
}: ITargetLabelSelectorProps) => {
|
||||
const classNames = classnames(baseClass, className, "form");
|
||||
|
||||
@@ -204,6 +210,7 @@ const TargetLabelSelector = ({
|
||||
selectedTarget={selectedTargetType}
|
||||
onSelect={onSelectTargetType}
|
||||
disableOptions={disableOptions}
|
||||
title={suppressTitle ? null : title}
|
||||
/>
|
||||
{selectedTargetType === "Custom" && (
|
||||
<LabelChooser
|
||||
|
||||
@@ -10,6 +10,7 @@ import { find } from "lodash";
|
||||
import { osqueryTables } from "utilities/osquery_tables";
|
||||
import { IOsQueryTable, DEFAULT_OSQUERY_TABLE } from "interfaces/osquery_table";
|
||||
import { CommaSeparatedPlatformString } from "interfaces/platform";
|
||||
import { ILabelPolicy } from "interfaces/label";
|
||||
|
||||
enum ACTIONS {
|
||||
SET_LAST_EDITED_QUERY_INFO = "SET_LAST_EDITED_QUERY_INFO",
|
||||
@@ -26,6 +27,8 @@ interface ISetLastEditedQueryInfo {
|
||||
lastEditedQueryResolution?: string;
|
||||
lastEditedQueryCritical?: boolean;
|
||||
lastEditedQueryPlatform?: CommaSeparatedPlatformString | null;
|
||||
lastEditedQueryLabelsIncludeAny?: ILabelPolicy[];
|
||||
lastEditedQueryLabelsExcludeAny?: ILabelPolicy[];
|
||||
defaultPolicy?: boolean;
|
||||
}
|
||||
|
||||
@@ -56,6 +59,8 @@ type InitialStateType = {
|
||||
lastEditedQueryResolution: string;
|
||||
lastEditedQueryCritical: boolean;
|
||||
lastEditedQueryPlatform: CommaSeparatedPlatformString | null;
|
||||
lastEditedQueryLabelsIncludeAny: ILabelPolicy[];
|
||||
lastEditedQueryLabelsExcludeAny: ILabelPolicy[];
|
||||
defaultPolicy: boolean;
|
||||
setLastEditedQueryId: (value: number | null) => void;
|
||||
setLastEditedQueryName: (value: string) => void;
|
||||
@@ -66,6 +71,8 @@ type InitialStateType = {
|
||||
setLastEditedQueryPlatform: (
|
||||
value: CommaSeparatedPlatformString | null
|
||||
) => void;
|
||||
setLastEditedQueryLabelsIncludeAny: (value: ILabelPolicy[]) => void;
|
||||
setLastEditedQueryLabelsExcludeAny: (value: ILabelPolicy[]) => void;
|
||||
setDefaultPolicy: (value: boolean) => void;
|
||||
policyTeamId: number;
|
||||
setPolicyTeamId: (id: number) => void;
|
||||
@@ -87,6 +94,8 @@ const initialState = {
|
||||
lastEditedQueryResolution: "",
|
||||
lastEditedQueryCritical: false,
|
||||
lastEditedQueryPlatform: null,
|
||||
lastEditedQueryLabelsIncludeAny: [],
|
||||
lastEditedQueryLabelsExcludeAny: [],
|
||||
defaultPolicy: false,
|
||||
setLastEditedQueryId: () => null,
|
||||
setLastEditedQueryName: () => null,
|
||||
@@ -95,6 +104,8 @@ const initialState = {
|
||||
setLastEditedQueryResolution: () => null,
|
||||
setLastEditedQueryCritical: () => null,
|
||||
setLastEditedQueryPlatform: () => null,
|
||||
setLastEditedQueryLabelsIncludeAny: () => null,
|
||||
setLastEditedQueryLabelsExcludeAny: () => null,
|
||||
setDefaultPolicy: () => null,
|
||||
policyTeamId: 0,
|
||||
setPolicyTeamId: () => null,
|
||||
@@ -147,6 +158,14 @@ const reducer = (state: InitialStateType, action: IAction) => {
|
||||
typeof action.lastEditedQueryPlatform === "undefined"
|
||||
? state.lastEditedQueryPlatform
|
||||
: action.lastEditedQueryPlatform,
|
||||
lastEditedQueryLabelsIncludeAny:
|
||||
typeof action.lastEditedQueryLabelsIncludeAny === "undefined"
|
||||
? state.lastEditedQueryLabelsIncludeAny
|
||||
: action.lastEditedQueryLabelsIncludeAny,
|
||||
lastEditedQueryLabelsExcludeAny:
|
||||
typeof action.lastEditedQueryLabelsExcludeAny === "undefined"
|
||||
? state.lastEditedQueryLabelsExcludeAny
|
||||
: action.lastEditedQueryLabelsExcludeAny,
|
||||
defaultPolicy:
|
||||
typeof action.defaultPolicy === "undefined"
|
||||
? state.defaultPolicy
|
||||
@@ -228,6 +247,24 @@ const PolicyProvider = ({ children }: Props): JSX.Element => {
|
||||
},
|
||||
[]
|
||||
);
|
||||
const setLastEditedQueryLabelsIncludeAny = useCallback(
|
||||
(lastEditedQueryLabelsIncludeAny: ILabelPolicy[]) => {
|
||||
dispatch({
|
||||
type: ACTIONS.SET_LAST_EDITED_QUERY_INFO,
|
||||
lastEditedQueryLabelsIncludeAny,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
const setLastEditedQueryLabelsExcludeAny = useCallback(
|
||||
(lastEditedQueryLabelsExcludeAny: ILabelPolicy[]) => {
|
||||
dispatch({
|
||||
type: ACTIONS.SET_LAST_EDITED_QUERY_INFO,
|
||||
lastEditedQueryLabelsExcludeAny,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
const setDefaultPolicy = useCallback((defaultPolicy: boolean) => {
|
||||
dispatch({
|
||||
type: ACTIONS.SET_LAST_EDITED_QUERY_INFO,
|
||||
@@ -248,6 +285,8 @@ const PolicyProvider = ({ children }: Props): JSX.Element => {
|
||||
lastEditedQueryResolution: state.lastEditedQueryResolution,
|
||||
lastEditedQueryCritical: state.lastEditedQueryCritical,
|
||||
lastEditedQueryPlatform: state.lastEditedQueryPlatform,
|
||||
lastEditedQueryLabelsIncludeAny: state.lastEditedQueryLabelsIncludeAny,
|
||||
lastEditedQueryLabelsExcludeAny: state.lastEditedQueryLabelsExcludeAny,
|
||||
defaultPolicy: state.defaultPolicy,
|
||||
setLastEditedQueryId,
|
||||
setLastEditedQueryName,
|
||||
@@ -256,6 +295,8 @@ const PolicyProvider = ({ children }: Props): JSX.Element => {
|
||||
setLastEditedQueryResolution,
|
||||
setLastEditedQueryCritical,
|
||||
setLastEditedQueryPlatform,
|
||||
setLastEditedQueryLabelsIncludeAny,
|
||||
setLastEditedQueryLabelsExcludeAny,
|
||||
setDefaultPolicy,
|
||||
policyTeamId: state.policyTeamId,
|
||||
setPolicyTeamId,
|
||||
@@ -270,6 +311,8 @@ const PolicyProvider = ({ children }: Props): JSX.Element => {
|
||||
setLastEditedQueryId,
|
||||
setLastEditedQueryName,
|
||||
setLastEditedQueryPlatform,
|
||||
setLastEditedQueryLabelsIncludeAny,
|
||||
setLastEditedQueryLabelsExcludeAny,
|
||||
setLastEditedQueryResolution,
|
||||
setPolicyTeamId,
|
||||
setSelectedOsqueryTable,
|
||||
@@ -280,6 +323,8 @@ const PolicyProvider = ({ children }: Props): JSX.Element => {
|
||||
state.lastEditedQueryId,
|
||||
state.lastEditedQueryName,
|
||||
state.lastEditedQueryPlatform,
|
||||
state.lastEditedQueryLabelsIncludeAny,
|
||||
state.lastEditedQueryLabelsExcludeAny,
|
||||
state.lastEditedQueryResolution,
|
||||
state.policyTeamId,
|
||||
state.selectedOsqueryTable,
|
||||
|
||||
@@ -34,6 +34,11 @@ export interface ILabelQuery {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ILabelPolicy {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ILabel extends ILabelSummary {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { CommaSeparatedPlatformString } from "interfaces/platform";
|
||||
import { IScript } from "./script";
|
||||
import { ILabelPolicy } from "./label";
|
||||
|
||||
// Legacy PropTypes used on host interface
|
||||
export default PropTypes.shape({
|
||||
@@ -44,6 +45,8 @@ export interface IPolicy {
|
||||
calendar_events_enabled: boolean;
|
||||
install_software?: IPolicySoftwareToInstall;
|
||||
run_script?: Pick<IScript, "id" | "name">;
|
||||
labels_include_any?: ILabelPolicy[];
|
||||
labels_exclude_any?: ILabelPolicy[];
|
||||
}
|
||||
export interface IPolicySoftwareToInstall {
|
||||
name: string;
|
||||
@@ -108,6 +111,8 @@ export interface IPolicyFormData {
|
||||
software_title_id?: number | null;
|
||||
// null for PATCH to unset - note asymmetry with GET/LIST - see IPolicy.run_script
|
||||
script_id?: number | null;
|
||||
labels_include_any?: string[];
|
||||
labels_exclude_any?: string[];
|
||||
}
|
||||
|
||||
export interface IPolicyNew {
|
||||
|
||||
+17
-11
@@ -1,4 +1,6 @@
|
||||
import React from "react";
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
@@ -16,6 +18,7 @@ const DeleteLabelModal = ({
|
||||
onCancel,
|
||||
isUpdatingLabel,
|
||||
}: IDeleteLabelModalProps): JSX.Element => {
|
||||
const { isPremiumTier } = useContext(AppContext);
|
||||
return (
|
||||
<Modal
|
||||
title="Delete label"
|
||||
@@ -24,16 +27,19 @@ const DeleteLabelModal = ({
|
||||
className={baseClass}
|
||||
>
|
||||
<>
|
||||
<p>
|
||||
If a configuration profile uses this label as a custom target, the
|
||||
profile will break. After deleting the label, remove broken profiles
|
||||
and upload new profiles in their place.
|
||||
</p>
|
||||
<p>
|
||||
If software uses this label as a custom target, the label will not be
|
||||
able to be deleted. Please remove the label from the software target
|
||||
first before deleting.
|
||||
</p>
|
||||
<p>Are you sure you wish to delete this label?</p>
|
||||
{isPremiumTier && (
|
||||
<ul>
|
||||
<li>
|
||||
Configuration profiles that target this label will not be applied
|
||||
to new hosts.
|
||||
</li>
|
||||
<li>
|
||||
Queries and policies that target this label will continue to run,
|
||||
but may target different hosts.
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
|
||||
@@ -75,6 +75,8 @@ const PolicyPage = ({
|
||||
setLastEditedQueryResolution,
|
||||
setLastEditedQueryCritical,
|
||||
setLastEditedQueryPlatform,
|
||||
setLastEditedQueryLabelsIncludeAny,
|
||||
setLastEditedQueryLabelsExcludeAny,
|
||||
setPolicyTeamId,
|
||||
} = useContext(PolicyContext);
|
||||
|
||||
@@ -173,6 +175,12 @@ const PolicyPage = ({
|
||||
setLastEditedQueryResolution(returnedQuery.resolution);
|
||||
setLastEditedQueryCritical(returnedQuery.critical);
|
||||
setLastEditedQueryPlatform(returnedQuery.platform);
|
||||
setLastEditedQueryLabelsIncludeAny(
|
||||
returnedQuery.labels_include_any || []
|
||||
);
|
||||
setLastEditedQueryLabelsExcludeAny(
|
||||
returnedQuery.labels_exclude_any || []
|
||||
);
|
||||
// TODO(sarah): What happens if the team id in the policy response doesn't match the
|
||||
// url param? In theory, the backend should ensure this doesn't happen.
|
||||
setPolicyTeamId(
|
||||
|
||||
@@ -1,219 +1,449 @@
|
||||
import React from "react";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { createCustomRenderer } from "test/test-utils";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import mockServer from "test/mock-server";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import createMockPolicy from "__mocks__/policyMock";
|
||||
import createMockUser from "__mocks__/userMock";
|
||||
import createMockConfig from "__mocks__/configMock";
|
||||
|
||||
import { ILabelSummary } from "interfaces/label";
|
||||
import PolicyProvider from "context/policy";
|
||||
import PolicyForm from "./PolicyForm";
|
||||
|
||||
const baseUrl = (path: string) => {
|
||||
return `/api/latest/fleet${path}`;
|
||||
};
|
||||
|
||||
const mockPolicy = createMockPolicy();
|
||||
|
||||
const mockLabels: ILabelSummary[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Fun",
|
||||
description: "Computers that like to have a good time",
|
||||
label_type: "regular",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Fresh",
|
||||
description: "Laptops with dirty mouths",
|
||||
label_type: "regular",
|
||||
},
|
||||
];
|
||||
|
||||
const labelSummariesHandler = http.get(baseUrl("/labels/summary"), () => {
|
||||
return HttpResponse.json({
|
||||
labels: mockLabels,
|
||||
});
|
||||
});
|
||||
|
||||
describe("PolicyForm - component", () => {
|
||||
it("disables save button for missing policy name", async () => {
|
||||
const defaultProps = {
|
||||
policyIdForEdit: mockPolicy.id,
|
||||
showOpenSchemaActionText: false,
|
||||
storedPolicy: createMockPolicy({ name: "Foo" }),
|
||||
isStoredPolicyLoading: false,
|
||||
isTeamObserver: false,
|
||||
isUpdatingPolicy: false,
|
||||
onCreatePolicy: jest.fn(),
|
||||
onOsqueryTableSelect: jest.fn(),
|
||||
goToSelectTargets: jest.fn(),
|
||||
onUpdate: jest.fn(),
|
||||
onOpenSchemaSidebar: jest.fn(),
|
||||
renderLiveQueryWarning: jest.fn(),
|
||||
backendValidators: {},
|
||||
onClickAutofillDescription: jest.fn(),
|
||||
onClickAutofillResolution: jest.fn(),
|
||||
isFetchingAutofillDescription: false,
|
||||
isFetchingAutofillResolution: false,
|
||||
resetAiAutofillData: jest.fn(),
|
||||
};
|
||||
|
||||
it("should not show the target selector in the free tier", async () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
policy: {
|
||||
policyTeamId: undefined,
|
||||
lastEditedQueryId: mockPolicy.id,
|
||||
lastEditedQueryName: "", // missing policy name
|
||||
lastEditedQueryDescription: mockPolicy.description,
|
||||
lastEditedQueryBody: mockPolicy.query,
|
||||
lastEditedQueryResolution: mockPolicy.resolution,
|
||||
lastEditedQueryCritical: mockPolicy.critical,
|
||||
lastEditedQueryPlatform: mockPolicy.platform,
|
||||
defaultPolicy: false,
|
||||
setLastEditedQueryName: jest.fn(),
|
||||
setLastEditedQueryDescription: jest.fn(),
|
||||
setLastEditedQueryBody: jest.fn(),
|
||||
setLastEditedQueryResolution: jest.fn(),
|
||||
setLastEditedQueryCritical: jest.fn(),
|
||||
setLastEditedQueryPlatform: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
isGlobalObserver: false,
|
||||
isGlobalAdmin: true,
|
||||
isGlobalMaintainer: false,
|
||||
isOnGlobalTeam: true,
|
||||
isPremiumTier: true,
|
||||
isSandboxMode: false,
|
||||
config: createMockConfig(),
|
||||
isPremiumTier: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<PolicyForm
|
||||
policyIdForEdit={mockPolicy.id}
|
||||
showOpenSchemaActionText={false}
|
||||
storedPolicy={createMockPolicy({ name: "" })}
|
||||
isStoredPolicyLoading={false}
|
||||
isTeamObserver={false}
|
||||
isUpdatingPolicy={false}
|
||||
onCreatePolicy={jest.fn()}
|
||||
onOsqueryTableSelect={jest.fn()}
|
||||
goToSelectTargets={jest.fn()}
|
||||
onUpdate={jest.fn()}
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
onClickAutofillDescription={jest.fn()}
|
||||
onClickAutofillResolution={jest.fn()}
|
||||
isFetchingAutofillDescription={false}
|
||||
isFetchingAutofillResolution={false}
|
||||
resetAiAutofillData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
render(<PolicyForm {...defaultProps} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
|
||||
// Wait for any queries (that should not be happening) to finish.
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Check that the target selector is not present.
|
||||
expect(screen.queryByText("All hosts")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables save and run button with tooltip for missing policy platforms", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
policy: {
|
||||
policyTeamId: undefined,
|
||||
lastEditedQueryId: mockPolicy.id,
|
||||
lastEditedQueryName: mockPolicy.name,
|
||||
lastEditedQueryDescription: mockPolicy.description,
|
||||
lastEditedQueryBody: mockPolicy.query,
|
||||
lastEditedQueryResolution: mockPolicy.resolution,
|
||||
lastEditedQueryCritical: mockPolicy.critical,
|
||||
lastEditedQueryPlatform: undefined, // missing policy platforms
|
||||
defaultPolicy: false,
|
||||
setLastEditedQueryName: jest.fn(),
|
||||
setLastEditedQueryDescription: jest.fn(),
|
||||
setLastEditedQueryBody: jest.fn(),
|
||||
setLastEditedQueryResolution: jest.fn(),
|
||||
setLastEditedQueryCritical: jest.fn(),
|
||||
setLastEditedQueryPlatform: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
isGlobalObserver: false,
|
||||
isGlobalAdmin: true,
|
||||
isGlobalMaintainer: false,
|
||||
isOnGlobalTeam: true,
|
||||
isPremiumTier: true,
|
||||
isSandboxMode: false,
|
||||
config: createMockConfig(),
|
||||
},
|
||||
},
|
||||
describe("in premium tier", () => {
|
||||
beforeEach(() => {
|
||||
mockServer.use(labelSummariesHandler);
|
||||
});
|
||||
|
||||
const { container, user } = render(
|
||||
<PolicyForm
|
||||
policyIdForEdit={mockPolicy.id}
|
||||
showOpenSchemaActionText={false}
|
||||
storedPolicy={createMockPolicy({ platform: undefined })}
|
||||
isStoredPolicyLoading={false}
|
||||
isTeamObserver={false}
|
||||
isUpdatingPolicy={false}
|
||||
onCreatePolicy={jest.fn()}
|
||||
onOsqueryTableSelect={jest.fn()}
|
||||
goToSelectTargets={jest.fn()}
|
||||
onUpdate={jest.fn()}
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
onClickAutofillDescription={jest.fn()}
|
||||
onClickAutofillResolution={jest.fn()}
|
||||
isFetchingAutofillDescription={false}
|
||||
isFetchingAutofillResolution={false}
|
||||
resetAiAutofillData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Run" })).toBeDisabled();
|
||||
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
user.hover(screen.getByRole("button", { name: "Save" }));
|
||||
it("disables save button for missing policy name", async () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
policy: {
|
||||
policyTeamId: undefined,
|
||||
lastEditedQueryId: mockPolicy.id,
|
||||
lastEditedQueryName: "", // missing policy name
|
||||
lastEditedQueryDescription: mockPolicy.description,
|
||||
lastEditedQueryBody: mockPolicy.query,
|
||||
lastEditedQueryResolution: mockPolicy.resolution,
|
||||
lastEditedQueryCritical: mockPolicy.critical,
|
||||
lastEditedQueryPlatform: mockPolicy.platform,
|
||||
lastEditedQueryLabelsIncludeAny: [],
|
||||
lastEditedQueryLabelsExcludeAny: [],
|
||||
defaultPolicy: false,
|
||||
setLastEditedQueryName: jest.fn(),
|
||||
setLastEditedQueryDescription: jest.fn(),
|
||||
setLastEditedQueryBody: jest.fn(),
|
||||
setLastEditedQueryResolution: jest.fn(),
|
||||
setLastEditedQueryCritical: jest.fn(),
|
||||
setLastEditedQueryPlatform: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
isGlobalObserver: false,
|
||||
isGlobalAdmin: true,
|
||||
isGlobalMaintainer: false,
|
||||
isOnGlobalTeam: true,
|
||||
isPremiumTier: true,
|
||||
isSandboxMode: false,
|
||||
config: createMockConfig(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.querySelector("#save-policy-button")).toHaveTextContent(
|
||||
/to save or run the policy/i
|
||||
render(
|
||||
<PolicyForm
|
||||
policyIdForEdit={mockPolicy.id}
|
||||
showOpenSchemaActionText={false}
|
||||
storedPolicy={createMockPolicy({ name: "" })}
|
||||
isStoredPolicyLoading={false}
|
||||
isTeamObserver={false}
|
||||
isUpdatingPolicy={false}
|
||||
onCreatePolicy={jest.fn()}
|
||||
onOsqueryTableSelect={jest.fn()}
|
||||
goToSelectTargets={jest.fn()}
|
||||
onUpdate={jest.fn()}
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
onClickAutofillDescription={jest.fn()}
|
||||
onClickAutofillResolution={jest.fn()}
|
||||
isFetchingAutofillDescription={false}
|
||||
isFetchingAutofillResolution={false}
|
||||
resetAiAutofillData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("disables run button with tooltip when live queries are globally disabled", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
policy: {
|
||||
policyTeamId: undefined,
|
||||
lastEditedQueryId: mockPolicy.id,
|
||||
lastEditedQueryName: mockPolicy.name,
|
||||
lastEditedQueryDescription: mockPolicy.description,
|
||||
lastEditedQueryBody: mockPolicy.query,
|
||||
lastEditedQueryResolution: mockPolicy.resolution,
|
||||
lastEditedQueryCritical: mockPolicy.critical,
|
||||
lastEditedQueryPlatform: undefined, // missing policy platforms
|
||||
defaultPolicy: false,
|
||||
setLastEditedQueryName: jest.fn(),
|
||||
setLastEditedQueryDescription: jest.fn(),
|
||||
setLastEditedQueryBody: jest.fn(),
|
||||
setLastEditedQueryResolution: jest.fn(),
|
||||
setLastEditedQueryCritical: jest.fn(),
|
||||
setLastEditedQueryPlatform: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
isGlobalObserver: false,
|
||||
isGlobalAdmin: true,
|
||||
isGlobalMaintainer: false,
|
||||
isOnGlobalTeam: true,
|
||||
isPremiumTier: true,
|
||||
isSandboxMode: false,
|
||||
config: createMockConfig({
|
||||
server_settings: {
|
||||
...createMockConfig().server_settings,
|
||||
live_query_disabled: true, // Live query disabled
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
|
||||
});
|
||||
|
||||
const { user } = render(
|
||||
<PolicyForm
|
||||
policyIdForEdit={mockPolicy.id}
|
||||
showOpenSchemaActionText={false}
|
||||
storedPolicy={createMockPolicy()}
|
||||
isStoredPolicyLoading={false}
|
||||
isTeamObserver={false}
|
||||
isUpdatingPolicy={false}
|
||||
onCreatePolicy={jest.fn()}
|
||||
onOsqueryTableSelect={jest.fn()}
|
||||
goToSelectTargets={jest.fn()}
|
||||
onUpdate={jest.fn()}
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
onClickAutofillDescription={jest.fn()}
|
||||
onClickAutofillResolution={jest.fn()}
|
||||
isFetchingAutofillDescription={false}
|
||||
isFetchingAutofillResolution={false}
|
||||
resetAiAutofillData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Run" })).toBeDisabled();
|
||||
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
user.hover(screen.getByRole("button", { name: "Run" }));
|
||||
it("disables save and run button with tooltip for missing policy platforms", async () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
policy: {
|
||||
policyTeamId: undefined,
|
||||
lastEditedQueryId: mockPolicy.id,
|
||||
lastEditedQueryName: mockPolicy.name,
|
||||
lastEditedQueryDescription: mockPolicy.description,
|
||||
lastEditedQueryBody: mockPolicy.query,
|
||||
lastEditedQueryResolution: mockPolicy.resolution,
|
||||
lastEditedQueryCritical: mockPolicy.critical,
|
||||
lastEditedQueryPlatform: undefined, // missing policy platforms
|
||||
lastEditedQueryLabelsIncludeAny: [],
|
||||
lastEditedQueryLabelsExcludeAny: [],
|
||||
defaultPolicy: false,
|
||||
setLastEditedQueryName: jest.fn(),
|
||||
setLastEditedQueryDescription: jest.fn(),
|
||||
setLastEditedQueryBody: jest.fn(),
|
||||
setLastEditedQueryResolution: jest.fn(),
|
||||
setLastEditedQueryCritical: jest.fn(),
|
||||
setLastEditedQueryPlatform: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
isGlobalObserver: false,
|
||||
isGlobalAdmin: true,
|
||||
isGlobalMaintainer: false,
|
||||
isOnGlobalTeam: true,
|
||||
isPremiumTier: true,
|
||||
isSandboxMode: false,
|
||||
config: createMockConfig(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(/live queries are disabled/i)
|
||||
).toBeInTheDocument();
|
||||
const { container, user } = render(
|
||||
<PolicyForm
|
||||
policyIdForEdit={mockPolicy.id}
|
||||
showOpenSchemaActionText={false}
|
||||
storedPolicy={createMockPolicy({ platform: undefined })}
|
||||
isStoredPolicyLoading={false}
|
||||
isTeamObserver={false}
|
||||
isUpdatingPolicy={false}
|
||||
onCreatePolicy={jest.fn()}
|
||||
onOsqueryTableSelect={jest.fn()}
|
||||
goToSelectTargets={jest.fn()}
|
||||
onUpdate={jest.fn()}
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
onClickAutofillDescription={jest.fn()}
|
||||
onClickAutofillResolution={jest.fn()}
|
||||
isFetchingAutofillDescription={false}
|
||||
isFetchingAutofillResolution={false}
|
||||
resetAiAutofillData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Run" })).toBeDisabled();
|
||||
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
user.hover(screen.getByRole("button", { name: "Save" }));
|
||||
});
|
||||
|
||||
expect(
|
||||
container.querySelector("#save-policy-button")
|
||||
).toHaveTextContent(/to save or run the policy/i);
|
||||
});
|
||||
});
|
||||
|
||||
it("disables run button with tooltip when live queries are globally disabled", async () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
policy: {
|
||||
policyTeamId: undefined,
|
||||
lastEditedQueryId: mockPolicy.id,
|
||||
lastEditedQueryName: mockPolicy.name,
|
||||
lastEditedQueryDescription: mockPolicy.description,
|
||||
lastEditedQueryBody: mockPolicy.query,
|
||||
lastEditedQueryResolution: mockPolicy.resolution,
|
||||
lastEditedQueryCritical: mockPolicy.critical,
|
||||
lastEditedQueryPlatform: undefined, // missing policy platforms
|
||||
lastEditedQueryLabelsIncludeAny: [],
|
||||
lastEditedQueryLabelsExcludeAny: [],
|
||||
defaultPolicy: false,
|
||||
setLastEditedQueryName: jest.fn(),
|
||||
setLastEditedQueryDescription: jest.fn(),
|
||||
setLastEditedQueryBody: jest.fn(),
|
||||
setLastEditedQueryResolution: jest.fn(),
|
||||
setLastEditedQueryCritical: jest.fn(),
|
||||
setLastEditedQueryPlatform: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
isGlobalObserver: false,
|
||||
isGlobalAdmin: true,
|
||||
isGlobalMaintainer: false,
|
||||
isOnGlobalTeam: true,
|
||||
isPremiumTier: true,
|
||||
isSandboxMode: false,
|
||||
config: createMockConfig({
|
||||
server_settings: {
|
||||
...createMockConfig().server_settings,
|
||||
live_query_disabled: true, // Live query disabled
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = render(
|
||||
<PolicyForm
|
||||
policyIdForEdit={mockPolicy.id}
|
||||
showOpenSchemaActionText={false}
|
||||
storedPolicy={createMockPolicy()}
|
||||
isStoredPolicyLoading={false}
|
||||
isTeamObserver={false}
|
||||
isUpdatingPolicy={false}
|
||||
onCreatePolicy={jest.fn()}
|
||||
onOsqueryTableSelect={jest.fn()}
|
||||
goToSelectTargets={jest.fn()}
|
||||
onUpdate={jest.fn()}
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
onClickAutofillDescription={jest.fn()}
|
||||
onClickAutofillResolution={jest.fn()}
|
||||
isFetchingAutofillDescription={false}
|
||||
isFetchingAutofillResolution={false}
|
||||
resetAiAutofillData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Run" })).toBeDisabled();
|
||||
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
user.hover(screen.getByRole("button", { name: "Run" }));
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(/live queries are disabled/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("target selector", () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
isGlobalObserver: false,
|
||||
isGlobalAdmin: true,
|
||||
isGlobalMaintainer: false,
|
||||
isOnGlobalTeam: true,
|
||||
isPremiumTier: true,
|
||||
isSandboxMode: false,
|
||||
config: createMockConfig(),
|
||||
},
|
||||
policy: {
|
||||
policyTeamId: undefined,
|
||||
lastEditedQueryId: mockPolicy.id,
|
||||
lastEditedQueryName: "sumthin sumthin",
|
||||
lastEditedQueryDescription: mockPolicy.description,
|
||||
lastEditedQueryBody: mockPolicy.query,
|
||||
lastEditedQueryResolution: mockPolicy.resolution,
|
||||
lastEditedQueryCritical: mockPolicy.critical,
|
||||
lastEditedQueryPlatform: "linux",
|
||||
lastEditedQueryLabelsIncludeAny: [],
|
||||
lastEditedQueryLabelsExcludeAny: [],
|
||||
setLastEditedQueryName: jest.fn(),
|
||||
setLastEditedQueryDescription: jest.fn(),
|
||||
setLastEditedQueryBody: jest.fn(),
|
||||
setLastEditedQueryResolution: jest.fn(),
|
||||
setLastEditedQueryCritical: jest.fn(),
|
||||
setLastEditedQueryPlatform: jest.fn(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("should show the target selector in All hosts target mode when the query has no labels", async () => {
|
||||
render(<PolicyForm {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("All hosts")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Custom")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("All hosts")).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable the save button in Custom target mode when no labels are selected, and enable it once labels are selected", async () => {
|
||||
render(<PolicyForm {...defaultProps} />);
|
||||
let allHosts;
|
||||
let custom;
|
||||
await waitFor(() => {
|
||||
allHosts = screen.getByLabelText("All hosts");
|
||||
custom = screen.getByLabelText("Custom");
|
||||
expect(allHosts).toBeInTheDocument();
|
||||
expect(custom).toBeInTheDocument();
|
||||
});
|
||||
custom && (await userEvent.click(custom));
|
||||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
expect(saveButton).toBeDisabled();
|
||||
|
||||
const funButton = screen.getByLabelText("Fun");
|
||||
expect(funButton).not.toBeChecked();
|
||||
await userEvent.click(funButton);
|
||||
await waitFor(() => {
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should send labels when saving a new query in Custom target mode (include any)", async () => {
|
||||
const onUpdate = jest.fn();
|
||||
const props = { ...defaultProps, onUpdate };
|
||||
render(<PolicyForm {...props} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("All hosts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Set a label.
|
||||
await userEvent.click(screen.getByLabelText("Custom"));
|
||||
await userEvent.click(screen.getByLabelText("Fun"));
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
expect(saveButton).toBeEnabled();
|
||||
await userEvent.click(saveButton);
|
||||
|
||||
expect(onUpdate.mock.calls[0][0].labels_include_any).toEqual(["Fun"]);
|
||||
expect(onUpdate.mock.calls[0][0].labels_exclude_any).toEqual([]);
|
||||
});
|
||||
|
||||
it("should send labels when saving a new query in Custom target mode (exclude any)", async () => {
|
||||
const onUpdate = jest.fn();
|
||||
const props = { ...defaultProps, onUpdate };
|
||||
render(<PolicyForm {...props} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("All hosts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Set a label.
|
||||
await userEvent.click(screen.getByLabelText("Custom"));
|
||||
await userEvent.click(screen.getByLabelText("Fun"));
|
||||
|
||||
// Click "Include any" to open the dropdown.
|
||||
const includeAnyOption = screen.getByRole("option", {
|
||||
name: "Include any",
|
||||
});
|
||||
await userEvent.click(includeAnyOption);
|
||||
|
||||
// Click "Exclude any" to select it.
|
||||
let excludeAnyOption: unknown;
|
||||
await waitFor(() => {
|
||||
excludeAnyOption = screen.getByRole("option", {
|
||||
name: "Exclude any",
|
||||
});
|
||||
});
|
||||
await userEvent.click(excludeAnyOption as Element);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
expect(saveButton).toBeEnabled();
|
||||
await userEvent.click(saveButton);
|
||||
|
||||
expect(onUpdate.mock.calls[0][0].labels_exclude_any).toEqual(["Fun"]);
|
||||
expect(onUpdate.mock.calls[0][0].labels_include_any).toEqual([]);
|
||||
});
|
||||
|
||||
it("should clear labels when saving a new query in All hosts target mode", async () => {
|
||||
const onUpdate = jest.fn();
|
||||
const props = { ...defaultProps, onUpdate };
|
||||
render(<PolicyForm {...props} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("All hosts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Set a label.
|
||||
await userEvent.click(screen.getByLabelText("Custom"));
|
||||
await userEvent.click(screen.getByLabelText("Fun"));
|
||||
|
||||
await userEvent.click(screen.getByLabelText("All hosts"));
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
expect(saveButton).toBeEnabled();
|
||||
await userEvent.click(saveButton);
|
||||
|
||||
expect(onUpdate.mock.calls[0][0].labels_include_any).toEqual([]);
|
||||
expect(onUpdate.mock.calls[0][0].labels_exclude_any).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Consider testing save button is disabled for a sql error
|
||||
// Trickiness is in modifying react-ace using react-testing library
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */
|
||||
/* eslint-disable jsx-a11y/interactive-supports-focus */
|
||||
import React, { useState, useContext, useEffect, KeyboardEvent } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import { IAceEditor } from "react-ace/lib/types";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
@@ -13,12 +15,16 @@ import { AppContext } from "context/app";
|
||||
import { PolicyContext } from "context/policy";
|
||||
import usePlatformCompatibility from "hooks/usePlatformCompatibility";
|
||||
import usePlatformSelector from "hooks/usePlatformSelector";
|
||||
import CUSTOM_TARGET_OPTIONS from "pages/policies/helpers";
|
||||
|
||||
import { IPolicy, IPolicyFormData } from "interfaces/policy";
|
||||
import { CommaSeparatedPlatformString } from "interfaces/platform";
|
||||
import { DEFAULT_POLICIES } from "pages/policies/constants";
|
||||
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
import {
|
||||
DEFAULT_USE_QUERY_OPTIONS,
|
||||
LEARN_MORE_ABOUT_BASE_LINK,
|
||||
} from "utilities/constants";
|
||||
|
||||
import Avatar from "components/Avatar";
|
||||
import SQLEditor from "components/SQLEditor";
|
||||
@@ -33,6 +39,12 @@ import Icon from "components/Icon/Icon";
|
||||
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import TargetLabelSelector from "components/TargetLabelSelector";
|
||||
|
||||
import labelsAPI, {
|
||||
getCustomLabels,
|
||||
ILabelsSummaryResponse,
|
||||
} from "services/entities/labels";
|
||||
|
||||
import SaveNewPolicyModal from "../SaveNewPolicyModal";
|
||||
|
||||
@@ -100,6 +112,12 @@ const PolicyForm = ({
|
||||
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
||||
const [isEditingResolution, setIsEditingResolution] = useState(false);
|
||||
|
||||
const [selectedTargetType, setSelectedTargetType] = useState("All hosts");
|
||||
const [selectedCustomTarget, setSelectedCustomTarget] = useState(
|
||||
"labelsIncludeAny"
|
||||
);
|
||||
const [selectedLabels, setSelectedLabels] = useState({});
|
||||
|
||||
// Note: The PolicyContext values should always be used for any mutable policy data such as query name
|
||||
// The storedPolicy prop should only be used to access immutable metadata such as author id
|
||||
const {
|
||||
@@ -110,6 +128,8 @@ const PolicyForm = ({
|
||||
lastEditedQueryResolution,
|
||||
lastEditedQueryCritical,
|
||||
lastEditedQueryPlatform,
|
||||
lastEditedQueryLabelsIncludeAny,
|
||||
lastEditedQueryLabelsExcludeAny,
|
||||
defaultPolicy,
|
||||
setLastEditedQueryName,
|
||||
setLastEditedQueryDescription,
|
||||
@@ -119,6 +139,19 @@ const PolicyForm = ({
|
||||
setLastEditedQueryPlatform,
|
||||
} = useContext(PolicyContext);
|
||||
|
||||
const onSelectLabel = ({
|
||||
name: labelName,
|
||||
value,
|
||||
}: {
|
||||
name: string;
|
||||
value: boolean;
|
||||
}) => {
|
||||
setSelectedLabels({
|
||||
...selectedLabels,
|
||||
[labelName]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
currentUser,
|
||||
currentTeam,
|
||||
@@ -132,6 +165,20 @@ const PolicyForm = ({
|
||||
config,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const {
|
||||
data: { labels } = { labels: [] },
|
||||
isFetching: isFetchingLabels,
|
||||
} = useQuery<ILabelsSummaryResponse, Error>(
|
||||
["custom_labels"],
|
||||
() => labelsAPI.summary(),
|
||||
{
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
enabled: isPremiumTier,
|
||||
staleTime: 10000,
|
||||
select: (res) => ({ labels: getCustomLabels(res.labels) }),
|
||||
}
|
||||
);
|
||||
|
||||
const disabledLiveQuery = config?.server_settings.live_query_disabled;
|
||||
const aiFeaturesDisabled =
|
||||
config?.server_settings.ai_features_disabled || false;
|
||||
@@ -178,6 +225,30 @@ const PolicyForm = ({
|
||||
!policyIdForEdit &&
|
||||
DEFAULT_POLICIES.find((p) => p.name === lastEditedQueryName);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTargetType(
|
||||
!lastEditedQueryLabelsIncludeAny.length &&
|
||||
!lastEditedQueryLabelsExcludeAny.length
|
||||
? "All hosts"
|
||||
: "Custom"
|
||||
);
|
||||
setSelectedCustomTarget(
|
||||
lastEditedQueryLabelsExcludeAny.length
|
||||
? "labelsExcludeAny"
|
||||
: "labelsIncludeAny"
|
||||
);
|
||||
setSelectedLabels(
|
||||
lastEditedQueryLabelsIncludeAny
|
||||
.concat(lastEditedQueryLabelsExcludeAny)
|
||||
.reduce((acc, label) => {
|
||||
return {
|
||||
...acc,
|
||||
[label.name]: true,
|
||||
};
|
||||
}, {}) || {}
|
||||
);
|
||||
}, [lastEditedQueryLabelsIncludeAny, lastEditedQueryLabelsExcludeAny]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewTemplatePolicy) {
|
||||
setCompatiblePlatforms(lastEditedQueryBody);
|
||||
@@ -276,6 +347,20 @@ const PolicyForm = ({
|
||||
query: lastEditedQueryBody,
|
||||
resolution: lastEditedQueryResolution,
|
||||
platform: newPlatformString,
|
||||
labels_include_any:
|
||||
selectedTargetType === "Custom" &&
|
||||
selectedCustomTarget === "labelsIncludeAny"
|
||||
? Object.entries(selectedLabels)
|
||||
.filter(([, selected]) => selected)
|
||||
.map(([labelName]) => labelName)
|
||||
: [],
|
||||
labels_exclude_any:
|
||||
selectedTargetType === "Custom" &&
|
||||
selectedCustomTarget === "labelsExcludeAny"
|
||||
? Object.entries(selectedLabels)
|
||||
.filter(([, selected]) => selected)
|
||||
.map(([labelName]) => labelName)
|
||||
: [],
|
||||
};
|
||||
if (isPremiumTier) {
|
||||
payload.critical = lastEditedQueryCritical;
|
||||
@@ -473,7 +558,7 @@ const PolicyForm = ({
|
||||
if (isEditMode) {
|
||||
return (
|
||||
<div className={`form-field ${baseClass}__policy-resolve`}>
|
||||
<div className="form-field__label">Resolve:</div>
|
||||
<div className="form-field__label">Resolve</div>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={16}
|
||||
@@ -547,7 +632,7 @@ const PolicyForm = ({
|
||||
</p>
|
||||
}
|
||||
>
|
||||
Critical:
|
||||
Critical
|
||||
</TooltipWrapper>
|
||||
</Checkbox>
|
||||
</div>
|
||||
@@ -617,6 +702,10 @@ const PolicyForm = ({
|
||||
const disableSaveFormErrors =
|
||||
(isEditMode && !isAnyPlatformSelected) ||
|
||||
(lastEditedQueryName === "" && !!lastEditedQueryId) ||
|
||||
(selectedTargetType === "Custom" &&
|
||||
!Object.entries(selectedLabels).some(([, value]) => {
|
||||
return value;
|
||||
})) ||
|
||||
!!size(errors);
|
||||
|
||||
return (
|
||||
@@ -646,6 +735,26 @@ const PolicyForm = ({
|
||||
/>
|
||||
{renderPlatformCompatibility()}
|
||||
{isEditMode && platformSelector.render()}
|
||||
{isEditMode && isPremiumTier && (
|
||||
<TargetLabelSelector
|
||||
selectedTargetType={selectedTargetType}
|
||||
selectedCustomTarget={selectedCustomTarget}
|
||||
customTargetOptions={CUSTOM_TARGET_OPTIONS}
|
||||
onSelectCustomTarget={setSelectedCustomTarget}
|
||||
selectedLabels={selectedLabels}
|
||||
className={`${baseClass}__target`}
|
||||
onSelectTargetType={setSelectedTargetType}
|
||||
onSelectLabel={onSelectLabel}
|
||||
labels={labels || []}
|
||||
customHelpText={
|
||||
<span className="form-field__help-text">
|
||||
Policy will target hosts on selected platforms that{" "}
|
||||
<b>have any</b> of these labels:
|
||||
</span>
|
||||
}
|
||||
suppressTitle
|
||||
/>
|
||||
)}
|
||||
{isEditMode && isPremiumTier && renderCriticalPolicy()}
|
||||
{renderLiveQueryWarning()}
|
||||
<div className="button-wrap">
|
||||
@@ -741,6 +850,7 @@ const PolicyForm = ({
|
||||
isFetchingAutofillResolution={isFetchingAutofillResolution}
|
||||
onClickAutofillDescription={onClickAutofillDescription}
|
||||
onClickAutofillResolution={onClickAutofillResolution}
|
||||
labels={labels}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
import React from "react";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { createCustomRenderer } from "test/test-utils";
|
||||
import createMockQuery from "__mocks__/queryMock";
|
||||
import createMockUser from "__mocks__/userMock";
|
||||
import createMockConfig from "__mocks__/configMock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import mockServer from "test/mock-server";
|
||||
|
||||
import { ILabelSummary } from "interfaces/label";
|
||||
import PolicyProvider from "context/policy";
|
||||
import SaveNewPolicyModal from "./SaveNewPolicyModal";
|
||||
|
||||
const baseUrl = (path: string) => {
|
||||
return `/api/latest/fleet${path}`;
|
||||
};
|
||||
|
||||
const mockLabels: ILabelSummary[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Fun",
|
||||
description: "Computers that like to have a good time",
|
||||
label_type: "regular",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Fresh",
|
||||
description: "Laptops with dirty mouths",
|
||||
label_type: "regular",
|
||||
},
|
||||
];
|
||||
|
||||
const labelSummariesHandler = http.get(baseUrl("/labels/summary"), () => {
|
||||
return HttpResponse.json({
|
||||
labels: mockLabels,
|
||||
});
|
||||
});
|
||||
|
||||
describe("SaveNewPolicyModal", () => {
|
||||
const defaultProps = {
|
||||
baseClass: "",
|
||||
queryValue: "",
|
||||
onCreatePolicy: jest.fn(),
|
||||
setIsSaveNewPolicyModalOpen: jest.fn(),
|
||||
backendValidators: {},
|
||||
platformSelector: {
|
||||
setSelectedPlatforms: jest.fn(),
|
||||
getSelectedPlatforms: () => {
|
||||
return [];
|
||||
},
|
||||
isAnyPlatformSelected: true,
|
||||
render: () => <div />,
|
||||
disabled: false,
|
||||
},
|
||||
isUpdatingPolicy: false,
|
||||
isFetchingAutofillDescription: false,
|
||||
isFetchingAutofillResolution: false,
|
||||
onClickAutofillDescription: jest.fn(),
|
||||
onClickAutofillResolution: jest.fn(),
|
||||
labels: mockLabels,
|
||||
};
|
||||
|
||||
it("should not show the target selector in the free tier", async () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
config: createMockConfig(),
|
||||
isPremiumTier: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<SaveNewPolicyModal {...defaultProps} />);
|
||||
|
||||
// Wait for any queries (that should not be happening) to finish.
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Check that the target selector is not present.
|
||||
expect(screen.queryByText("All hosts")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("in premium tier", () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
isGlobalObserver: false,
|
||||
isGlobalAdmin: true,
|
||||
isGlobalMaintainer: false,
|
||||
isOnGlobalTeam: true,
|
||||
isPremiumTier: true,
|
||||
isSandboxMode: false,
|
||||
config: createMockConfig(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockServer.use(labelSummariesHandler);
|
||||
});
|
||||
|
||||
it("should show the target selector in All hosts target mode when the policy has no labels", async () => {
|
||||
render(<SaveNewPolicyModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("All hosts")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Custom")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("All hosts")).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable the save button in Custom target mode when no labels are selected, and enable it once labels are selected", async () => {
|
||||
render(<SaveNewPolicyModal {...defaultProps} />);
|
||||
let allHosts;
|
||||
let custom;
|
||||
await waitFor(() => {
|
||||
allHosts = screen.getByLabelText("All hosts");
|
||||
custom = screen.getByLabelText("Custom");
|
||||
expect(allHosts).toBeInTheDocument();
|
||||
expect(custom).toBeInTheDocument();
|
||||
});
|
||||
custom && (await userEvent.click(custom));
|
||||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
expect(saveButton).toBeDisabled();
|
||||
|
||||
const funButton = screen.getByLabelText("Fun");
|
||||
expect(funButton).not.toBeChecked();
|
||||
await userEvent.click(funButton);
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should send labels when saving a new policy in Custom target mode (include any)", async () => {
|
||||
const onCreatePolicy = jest.fn();
|
||||
const props = { ...defaultProps, onCreatePolicy };
|
||||
render(
|
||||
<PolicyProvider>
|
||||
<SaveNewPolicyModal {...props} />
|
||||
</PolicyProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("All hosts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Set a name.
|
||||
const nameInput = screen.getByLabelText("Name");
|
||||
await userEvent.type(nameInput, "A Brand New Policy!");
|
||||
|
||||
// Set a label.
|
||||
await userEvent.click(screen.getByLabelText("Custom"));
|
||||
await userEvent.click(screen.getByLabelText("Fun"));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
expect(onCreatePolicy.mock.calls[0][0].labels_include_any).toEqual([
|
||||
"Fun",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should send labels when saving a new policy in Custom target mode (exclude any)", async () => {
|
||||
const onCreatePolicy = jest.fn();
|
||||
const props = { ...defaultProps, onCreatePolicy };
|
||||
render(
|
||||
<PolicyProvider>
|
||||
<SaveNewPolicyModal {...props} />
|
||||
</PolicyProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("All hosts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Set a name.
|
||||
const nameInput = screen.getByLabelText("Name");
|
||||
await userEvent.type(nameInput, "A Brand New Policy!");
|
||||
|
||||
// Set a label.
|
||||
await userEvent.click(screen.getByLabelText("Custom"));
|
||||
await userEvent.click(screen.getByLabelText("Fun"));
|
||||
|
||||
// Click "Include any" to open the dropdown.
|
||||
const includeAnyOption = screen.getByRole("option", {
|
||||
name: "Include any",
|
||||
});
|
||||
await userEvent.click(includeAnyOption);
|
||||
|
||||
// Click "Exclude any" to select it.
|
||||
let excludeAnyOption: unknown;
|
||||
await waitFor(() => {
|
||||
excludeAnyOption = screen.getByRole("option", { name: "Exclude any" });
|
||||
});
|
||||
await userEvent.click(excludeAnyOption as Element);
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
expect(onCreatePolicy.mock.calls[0][0].labels_exclude_any).toEqual([
|
||||
"Fun",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should clear labels when saving a new policy in All hosts target mode", async () => {
|
||||
const onCreatePolicy = jest.fn();
|
||||
const props = { ...defaultProps, onCreatePolicy };
|
||||
render(
|
||||
<PolicyProvider>
|
||||
<SaveNewPolicyModal {...props} />
|
||||
</PolicyProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("All hosts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Set a name.
|
||||
await userEvent.type(
|
||||
screen.getByLabelText("Name"),
|
||||
"A Brand New Policy!"
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
expect(onCreatePolicy.mock.calls[0][0].labels_include_any).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+67
-2
@@ -2,9 +2,12 @@ import React, { useState, useContext, useEffect, useCallback } from "react";
|
||||
import { size } from "lodash";
|
||||
import classNames from "classnames";
|
||||
|
||||
import CUSTOM_TARGET_OPTIONS from "pages/policies/helpers";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { PolicyContext } from "context/policy";
|
||||
import { IPlatformSelector } from "hooks/usePlatformSelector";
|
||||
import { ILabelSummary } from "interfaces/label";
|
||||
import { IPolicyFormData } from "interfaces/policy";
|
||||
import { CommaSeparatedPlatformString } from "interfaces/platform";
|
||||
import useDeepEffect from "hooks/useDeepEffect";
|
||||
@@ -15,6 +18,7 @@ import Checkbox from "components/forms/fields/Checkbox";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
import TargetLabelSelector from "components/TargetLabelSelector";
|
||||
import Icon from "components/Icon";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
@@ -32,6 +36,7 @@ export interface ISaveNewPolicyModalProps {
|
||||
isFetchingAutofillResolution: boolean;
|
||||
onClickAutofillDescription: () => Promise<void>;
|
||||
onClickAutofillResolution: () => Promise<void>;
|
||||
labels: ILabelSummary[];
|
||||
}
|
||||
|
||||
const validatePolicyName = (name: string) => {
|
||||
@@ -58,6 +63,7 @@ const SaveNewPolicyModal = ({
|
||||
isFetchingAutofillResolution,
|
||||
onClickAutofillDescription,
|
||||
onClickAutofillResolution,
|
||||
labels,
|
||||
}: ISaveNewPolicyModalProps): JSX.Element => {
|
||||
const { isPremiumTier } = useContext(AppContext);
|
||||
const {
|
||||
@@ -77,9 +83,34 @@ const SaveNewPolicyModal = ({
|
||||
backendValidators
|
||||
);
|
||||
|
||||
const [selectedTargetType, setSelectedTargetType] = useState("All hosts");
|
||||
const [selectedCustomTarget, setSelectedCustomTarget] = useState(
|
||||
"labelsIncludeAny"
|
||||
);
|
||||
const [selectedLabels, setSelectedLabels] = useState({});
|
||||
|
||||
const onSelectLabel = ({
|
||||
name: labelName,
|
||||
value,
|
||||
}: {
|
||||
name: string;
|
||||
value: boolean;
|
||||
}) => {
|
||||
setSelectedLabels({
|
||||
...selectedLabels,
|
||||
[labelName]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const disableForm =
|
||||
isFetchingAutofillDescription || isFetchingAutofillResolution;
|
||||
const disableSave = !platformSelector.isAnyPlatformSelected || disableForm;
|
||||
const disableSave =
|
||||
!platformSelector.isAnyPlatformSelected ||
|
||||
disableForm ||
|
||||
(selectedTargetType === "Custom" &&
|
||||
!Object.entries(selectedLabels).some(([, value]) => {
|
||||
return value;
|
||||
}));
|
||||
|
||||
useDeepEffect(() => {
|
||||
if (lastEditedQueryName) {
|
||||
@@ -115,6 +146,20 @@ const SaveNewPolicyModal = ({
|
||||
resolution: lastEditedQueryResolution,
|
||||
platform: newPlatformString,
|
||||
critical: lastEditedQueryCritical,
|
||||
labels_include_any:
|
||||
selectedTargetType === "Custom" &&
|
||||
selectedCustomTarget === "labelsIncludeAny"
|
||||
? Object.entries(selectedLabels)
|
||||
.filter(([, selected]) => selected)
|
||||
.map(([labelName]) => labelName)
|
||||
: [],
|
||||
labels_exclude_any:
|
||||
selectedTargetType === "Custom" &&
|
||||
selectedCustomTarget === "labelsExcludeAny"
|
||||
? Object.entries(selectedLabels)
|
||||
.filter(([, selected]) => selected)
|
||||
.map(([labelName]) => labelName)
|
||||
: [],
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -235,6 +280,26 @@ const SaveNewPolicyModal = ({
|
||||
disabled={disableForm}
|
||||
/>
|
||||
{platformSelector.render()}
|
||||
{isPremiumTier && (
|
||||
<TargetLabelSelector
|
||||
selectedTargetType={selectedTargetType}
|
||||
selectedCustomTarget={selectedCustomTarget}
|
||||
customTargetOptions={CUSTOM_TARGET_OPTIONS}
|
||||
onSelectCustomTarget={setSelectedCustomTarget}
|
||||
selectedLabels={selectedLabels}
|
||||
className={`${baseClass}__target`}
|
||||
onSelectTargetType={setSelectedTargetType}
|
||||
onSelectLabel={onSelectLabel}
|
||||
labels={labels || []}
|
||||
customHelpText={
|
||||
<span className="form-field__help-text">
|
||||
Policy will target hosts on selected platforms that{" "}
|
||||
<b>have any</b> of these labels:
|
||||
</span>
|
||||
}
|
||||
suppressTitle
|
||||
/>
|
||||
)}
|
||||
{isPremiumTier && (
|
||||
<div className="critical-checkbox-wrapper">
|
||||
<Checkbox
|
||||
@@ -272,7 +337,7 @@ const SaveNewPolicyModal = ({
|
||||
className="save-policy-loading"
|
||||
isLoading={isUpdatingPolicy}
|
||||
>
|
||||
Save policy
|
||||
Save
|
||||
</Button>
|
||||
<ReactTooltip
|
||||
className={`${baseClass}__button--modal-save-tooltip`}
|
||||
|
||||
@@ -153,6 +153,8 @@ const QueryEditor = ({
|
||||
query: formData.query,
|
||||
resolution: formData.resolution,
|
||||
platform: formData.platform,
|
||||
labels_include_any: formData.labels_include_any,
|
||||
labels_exclude_any: formData.labels_exclude_any,
|
||||
};
|
||||
if (isPremiumTier) {
|
||||
payload.critical = formData.critical;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
|
||||
import { IDropdownOption } from "interfaces/dropdownOption";
|
||||
|
||||
const CUSTOM_TARGET_OPTIONS: IDropdownOption[] = [
|
||||
{
|
||||
value: "labelsIncludeAny",
|
||||
label: "Include any",
|
||||
helpText: (
|
||||
<>
|
||||
Policy will target hosts on selected platforms that <b>have any</b> of
|
||||
these labels:
|
||||
</>
|
||||
),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
value: "labelsExcludeAny",
|
||||
label: "Exclude any",
|
||||
helpText: (
|
||||
<>
|
||||
Policy will target hosts on selected platforms that{" "}
|
||||
<b>don’t have any</b> of these labels:
|
||||
</>
|
||||
),
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
export default CUSTOM_TARGET_OPTIONS;
|
||||
@@ -65,6 +65,8 @@ export default {
|
||||
platform,
|
||||
critical,
|
||||
software_title_id,
|
||||
labels_include_any,
|
||||
labels_exclude_any,
|
||||
// note absence of automations-related fields, which are only set by the UI via update
|
||||
} = data;
|
||||
const { TEAMS } = endpoints;
|
||||
@@ -78,6 +80,8 @@ export default {
|
||||
platform,
|
||||
critical,
|
||||
software_title_id,
|
||||
labels_include_any,
|
||||
labels_exclude_any,
|
||||
});
|
||||
},
|
||||
update: (id: number, data: IPolicyFormData) => {
|
||||
@@ -93,6 +97,8 @@ export default {
|
||||
calendar_events_enabled,
|
||||
software_title_id,
|
||||
script_id,
|
||||
labels_include_any,
|
||||
labels_exclude_any,
|
||||
} = data;
|
||||
const { TEAMS } = endpoints;
|
||||
const path = `${TEAMS}/${team_id}/policies/${id}`;
|
||||
@@ -107,6 +113,8 @@ export default {
|
||||
calendar_events_enabled,
|
||||
software_title_id,
|
||||
script_id,
|
||||
labels_include_any,
|
||||
labels_exclude_any,
|
||||
});
|
||||
},
|
||||
destroy: (teamId: number | undefined, ids: number[]) => {
|
||||
|
||||
Reference in New Issue
Block a user