Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -63,6 +64,7 @@ const PolicyDetailsPage = ({
}: IPolicyDetailsPageProps): JSX.Element => {
const policyId = paramsPolicyId ? parseInt(paramsPolicyId, 10) : null;
const handlePageError = useErrorHandler();
const queryClient = useQueryClient();

const {
currentUser,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -412,8 +439,9 @@ const PolicyDetailsPage = ({
<PolicyAutomations
storedPolicy={storedPolicy}
currentAutomatedPolicies={currentAutomatedPolicies}
onAddAutomation={noop}
isAddingAutomation={false}
canEditPolicy={canEditPolicy}
onAddAutomation={onAddPatchAutomation}
isAddingAutomation={isAddingAutomation}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -34,14 +36,18 @@ interface IAutomationRow {
const PolicyAutomations = ({
storedPolicy,
currentAutomatedPolicies,
canEditPolicy,
onAddAutomation,
isAddingAutomation,
}: IPolicyAutomationsProps): JSX.Element => {
const isPatchPolicy = storedPolicy.type === "patch";
const hasPatchSoftware = !!storedPolicy.patch_software;
const hasSoftwareAutomation = !!storedPolicy.install_software;
const showCtaCard =
isPatchPolicy && hasPatchSoftware && !hasSoftwareAutomation;
isPatchPolicy &&
hasPatchSoftware &&
!hasSoftwareAutomation &&
canEditPolicy;

const automationRows: IAutomationRow[] = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ const PolicyForm = ({
const onAddPatchAutomation = async () => {
if (
!storedPolicy?.patch_software?.software_title_id ||
!storedPolicy?.team_id
storedPolicy?.team_id == null
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixes to work on "Unassigned"

) {
return;
}
Expand Down Expand Up @@ -693,6 +693,7 @@ const PolicyForm = ({
<PolicyAutomations
storedPolicy={storedPolicy}
currentAutomatedPolicies={currentAutomatedPolicies}
canEditPolicy={isEditMode}
onAddAutomation={onAddPatchAutomation}
isAddingAutomation={isAddingAutomation}
/>
Expand Down
Loading