Fleet UI: Allow users with edit access to add automation from policy details page (#45239)
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useQuery, useQueryClient } from "react-query";
|
||||
import { InjectedRouter, Params } from "react-router/lib/Router";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import { noop } from "lodash";
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { PolicyContext } from "context/policy";
|
||||
import { IPolicy, IStoredPolicyResponse } from "interfaces/policy";
|
||||
import { ILabelPolicy } from "interfaces/label";
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "interfaces/team";
|
||||
import { PLATFORM_DISPLAY_NAMES, Platform } from "interfaces/platform";
|
||||
import policiesAPI from "services/entities/policies";
|
||||
import teamPoliciesAPI from "services/entities/team_policies";
|
||||
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
|
||||
import { addGravatarUrlToResource } from "utilities/helpers";
|
||||
import { DOCUMENT_TITLE_SUFFIX } from "utilities/constants";
|
||||
@@ -63,6 +64,7 @@ const PolicyDetailsPage = ({
|
||||
}: IPolicyDetailsPageProps): JSX.Element => {
|
||||
const policyId = paramsPolicyId ? parseInt(paramsPolicyId, 10) : null;
|
||||
const handlePageError = useErrorHandler();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
currentUser,
|
||||
@@ -112,7 +114,10 @@ const PolicyDetailsPage = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const [showQueryModal, setShowQueryModal] = useState(false);
|
||||
const [isAddingAutomation, setIsAddingAutomation] = useState(false);
|
||||
|
||||
if (policyId === null || isNaN(policyId)) {
|
||||
router.push(PATHS.MANAGE_POLICIES);
|
||||
@@ -211,6 +216,28 @@ const PolicyDetailsPage = ({
|
||||
|
||||
const disabledLiveQuery = config?.server_settings.live_query_disabled;
|
||||
|
||||
const onAddPatchAutomation = async () => {
|
||||
if (
|
||||
!storedPolicy?.patch_software?.software_title_id ||
|
||||
storedPolicy?.team_id == null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setIsAddingAutomation(true);
|
||||
try {
|
||||
await teamPoliciesAPI.update(policyId as number, {
|
||||
team_id: storedPolicy.team_id,
|
||||
software_title_id: storedPolicy.patch_software.software_title_id,
|
||||
});
|
||||
queryClient.invalidateQueries(["policy", policyId]);
|
||||
renderFlash("success", "Automation added.");
|
||||
} catch {
|
||||
renderFlash("error", "Couldn't set automation. Please try again.");
|
||||
} finally {
|
||||
setIsAddingAutomation(false);
|
||||
}
|
||||
};
|
||||
|
||||
const backToPoliciesPath = getPathWithQueryParams(PATHS.MANAGE_POLICIES, {
|
||||
fleet_id: teamIdForApi,
|
||||
});
|
||||
@@ -412,8 +439,9 @@ const PolicyDetailsPage = ({
|
||||
<PolicyAutomations
|
||||
storedPolicy={storedPolicy}
|
||||
currentAutomatedPolicies={currentAutomatedPolicies}
|
||||
onAddAutomation={noop}
|
||||
isAddingAutomation={false}
|
||||
canEditPolicy={canEditPolicy}
|
||||
onAddAutomation={onAddPatchAutomation}
|
||||
isAddingAutomation={isAddingAutomation}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { AppContext, initialState } from "context/app";
|
||||
import { IPolicy } from "interfaces/policy";
|
||||
import createMockConfig from "__mocks__/configMock";
|
||||
import PolicyAutomations from "./PolicyAutomations";
|
||||
|
||||
// Stub SoftwareIcon to avoid asset resolution in tests
|
||||
jest.mock("pages/SoftwarePage/components/icons/SoftwareIcon", () => {
|
||||
return () => <span data-testid="software-icon" />;
|
||||
});
|
||||
|
||||
const createMockPatchPolicy = (overrides?: Partial<IPolicy>): IPolicy => ({
|
||||
id: 10,
|
||||
name: "macOS - Zoom up to date",
|
||||
query: "SELECT 1;",
|
||||
description: "Checks Zoom is up to date",
|
||||
author_id: 1,
|
||||
author_name: "Admin",
|
||||
author_email: "admin@example.com",
|
||||
resolution: "Install the latest version from self-service.",
|
||||
platform: "darwin",
|
||||
team_id: 1,
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
critical: false,
|
||||
calendar_events_enabled: false,
|
||||
conditional_access_enabled: false,
|
||||
type: "patch",
|
||||
patch_software: {
|
||||
name: "Zoom",
|
||||
display_name: "Zoom",
|
||||
software_title_id: 42,
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
// Wrap with AppContext so GitOpsModeTooltipWrapper's useGitOpsMode hook works
|
||||
const renderWithAppContext = (ui: React.ReactElement) => {
|
||||
return render(
|
||||
<AppContext.Provider
|
||||
value={{ ...initialState, config: createMockConfig() }}
|
||||
>
|
||||
{ui}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onAddAutomation: jest.fn(),
|
||||
currentAutomatedPolicies: [] as number[],
|
||||
};
|
||||
|
||||
describe("PolicyAutomations", () => {
|
||||
describe("CTA card (patch policy with patch_software, no install_software)", () => {
|
||||
it("shows the CTA card and Add automation button when canEditPolicy is true", () => {
|
||||
renderWithAppContext(
|
||||
<PolicyAutomations
|
||||
storedPolicy={createMockPatchPolicy()}
|
||||
canEditPolicy
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Automatically patch Zoom/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: /Add automation/ })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onAddAutomation when the button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onAddAutomation = jest.fn();
|
||||
renderWithAppContext(
|
||||
<PolicyAutomations
|
||||
storedPolicy={createMockPatchPolicy()}
|
||||
currentAutomatedPolicies={[]}
|
||||
canEditPolicy
|
||||
onAddAutomation={onAddAutomation}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /Add automation/ }));
|
||||
expect(onAddAutomation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does NOT show the CTA card when canEditPolicy is false", () => {
|
||||
renderWithAppContext(
|
||||
<PolicyAutomations
|
||||
storedPolicy={createMockPatchPolicy()}
|
||||
canEditPolicy={false}
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByText(/Automatically patch Zoom/)
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /Add automation/ })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows 'Adding...' text when isAddingAutomation is true", () => {
|
||||
renderWithAppContext(
|
||||
<PolicyAutomations
|
||||
storedPolicy={createMockPatchPolicy()}
|
||||
canEditPolicy
|
||||
{...defaultProps}
|
||||
isAddingAutomation
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Adding...")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /Add automation/ })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("CTA card is hidden when conditions are not met", () => {
|
||||
it("hides the CTA card for a dynamic (non-patch) policy", () => {
|
||||
renderWithAppContext(
|
||||
<PolicyAutomations
|
||||
storedPolicy={createMockPatchPolicy({ type: "dynamic" })}
|
||||
canEditPolicy
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/Automatically patch/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides the CTA card when patch_software is not set", () => {
|
||||
renderWithAppContext(
|
||||
<PolicyAutomations
|
||||
storedPolicy={createMockPatchPolicy({ patch_software: undefined })}
|
||||
canEditPolicy
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/Automatically patch/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the CTA card for a no-team policy (team_id === 0)", () => {
|
||||
renderWithAppContext(
|
||||
<PolicyAutomations
|
||||
storedPolicy={createMockPatchPolicy({ team_id: 0 })}
|
||||
canEditPolicy
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Automatically patch Zoom/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: /Add automation/ })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides the CTA card when install_software is already set", () => {
|
||||
renderWithAppContext(
|
||||
<PolicyAutomations
|
||||
storedPolicy={createMockPatchPolicy({
|
||||
install_software: {
|
||||
name: "Zoom",
|
||||
software_title_id: 42,
|
||||
},
|
||||
})}
|
||||
canEditPolicy
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/Automatically patch/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,8 +17,10 @@ const baseClass = "policy-automations";
|
||||
interface IPolicyAutomationsProps {
|
||||
storedPolicy: IPolicy;
|
||||
currentAutomatedPolicies: number[];
|
||||
/** Some users only have access to read-only view */
|
||||
canEditPolicy: boolean;
|
||||
onAddAutomation: () => void;
|
||||
isAddingAutomation: boolean;
|
||||
isAddingAutomation?: boolean;
|
||||
}
|
||||
|
||||
interface IAutomationRow {
|
||||
@@ -34,6 +36,7 @@ interface IAutomationRow {
|
||||
const PolicyAutomations = ({
|
||||
storedPolicy,
|
||||
currentAutomatedPolicies,
|
||||
canEditPolicy,
|
||||
onAddAutomation,
|
||||
isAddingAutomation,
|
||||
}: IPolicyAutomationsProps): JSX.Element => {
|
||||
@@ -41,7 +44,10 @@ const PolicyAutomations = ({
|
||||
const hasPatchSoftware = !!storedPolicy.patch_software;
|
||||
const hasSoftwareAutomation = !!storedPolicy.install_software;
|
||||
const showCtaCard =
|
||||
isPatchPolicy && hasPatchSoftware && !hasSoftwareAutomation;
|
||||
isPatchPolicy &&
|
||||
hasPatchSoftware &&
|
||||
!hasSoftwareAutomation &&
|
||||
canEditPolicy;
|
||||
|
||||
const automationRows: IAutomationRow[] = [];
|
||||
|
||||
|
||||
@@ -378,7 +378,7 @@ const PolicyForm = ({
|
||||
const onAddPatchAutomation = async () => {
|
||||
if (
|
||||
!storedPolicy?.patch_software?.software_title_id ||
|
||||
!storedPolicy?.team_id
|
||||
storedPolicy?.team_id == null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -693,6 +693,7 @@ const PolicyForm = ({
|
||||
<PolicyAutomations
|
||||
storedPolicy={storedPolicy}
|
||||
currentAutomatedPolicies={currentAutomatedPolicies}
|
||||
canEditPolicy={isEditMode}
|
||||
onAddAutomation={onAddPatchAutomation}
|
||||
isAddingAutomation={isAddingAutomation}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user