Fleet UI: Improve host policy empty modal (#43805)

This commit is contained in:
RachelElysia 2026-04-21 08:09:37 -05:00 committed by GitHub
parent e006ad948e
commit 4da30fc321
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 176 additions and 9 deletions

View file

@ -0,0 +1 @@
* Fleet UI: Improve host policy empty state

View file

@ -850,6 +850,7 @@ const DeviceUserPage = ({
<PolicyDetailsModal
onCancel={onCancelPolicyDetailsModal}
policy={selectedPolicy}
isDeviceUser
onResolveLater={
globalConfig?.features?.enable_conditional_access &&
globalConfig.features?.enable_conditional_access_bypass &&

View file

@ -0,0 +1,146 @@
import React from "react";
import { screen } from "@testing-library/react";
import { noop } from "lodash";
import { renderWithSetup } from "test/test-utils";
import { IHostPolicy } from "interfaces/policy";
import PolicyDetailsModal from "./PolicyDetailsModal";
const createMockPolicy = (
overrides: Partial<IHostPolicy> = {}
): IHostPolicy => ({
id: 1,
name: "Test policy",
query: "SELECT 1;",
description: "",
author_id: 1,
author_name: "Admin",
author_email: "admin@fleet.co",
resolution: "",
platform: "",
team_id: null,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
critical: false,
calendar_events_enabled: false,
conditional_access_enabled: false,
type: "policy",
response: "fail",
...overrides,
});
describe("PolicyDetailsModal", () => {
it("renders policy name, description, and resolution", () => {
renderWithSetup(
<PolicyDetailsModal
onCancel={noop}
policy={createMockPolicy({
name: "Disk encryption enabled",
description: "Checks that FileVault is enabled.",
resolution: "Enable FileVault in System Settings.",
})}
/>
);
expect(screen.getByText("Disk encryption enabled")).toBeInTheDocument();
expect(
screen.getByText("Checks that FileVault is enabled.")
).toBeInTheDocument();
expect(screen.getByText("Resolve:")).toBeInTheDocument();
expect(
screen.getByText("Enable FileVault in System Settings.")
).toBeInTheDocument();
expect(screen.queryByText(/missing description/)).not.toBeInTheDocument();
});
it("renders only description or resolution without showing empty state", () => {
const { unmount } = renderWithSetup(
<PolicyDetailsModal
onCancel={noop}
policy={createMockPolicy({ description: "Some description" })}
/>
);
expect(screen.getByText("Some description")).toBeInTheDocument();
expect(screen.queryByText("Resolve:")).not.toBeInTheDocument();
expect(screen.queryByText(/missing description/)).not.toBeInTheDocument();
unmount();
renderWithSetup(
<PolicyDetailsModal
onCancel={noop}
policy={createMockPolicy({ resolution: "Some resolution" })}
/>
);
expect(screen.getByText("Resolve:")).toBeInTheDocument();
expect(screen.getByText("Some resolution")).toBeInTheDocument();
expect(screen.queryByText(/missing description/)).not.toBeInTheDocument();
});
it("renders empty state when no description or resolution", () => {
renderWithSetup(
<PolicyDetailsModal onCancel={noop} policy={createMockPolicy()} />
);
expect(
screen.getByText(
"This policy is missing description and resolution instructions."
)
).toBeInTheDocument();
expect(screen.queryByText("Resolve:")).not.toBeInTheDocument();
expect(
screen.queryByText(/Please contact your IT admin/)
).not.toBeInTheDocument();
});
it("renders device user empty state with IT admin message", () => {
renderWithSetup(
<PolicyDetailsModal
onCancel={noop}
policy={createMockPolicy()}
isDeviceUser
/>
);
expect(
screen.getByText(
/missing description and resolution instructions.*Please contact your IT admin/
)
).toBeInTheDocument();
});
it("renders 'Resolve later' button only for failing conditional access policies", () => {
const { unmount } = renderWithSetup(
<PolicyDetailsModal
onCancel={noop}
policy={createMockPolicy({
conditional_access_enabled: true,
response: "fail",
description: "Must comply",
})}
onResolveLater={jest.fn()}
/>
);
expect(screen.getByText("Resolve later")).toBeInTheDocument();
unmount();
renderWithSetup(
<PolicyDetailsModal
onCancel={noop}
policy={createMockPolicy({
conditional_access_enabled: true,
response: "pass",
description: "All good",
})}
onResolveLater={jest.fn()}
/>
);
expect(screen.queryByText("Resolve later")).not.toBeInTheDocument();
});
});

View file

@ -9,6 +9,7 @@ interface IPolicyDetailsProps {
onCancel: () => void;
policy: IHostPolicy | null;
onResolveLater?: () => void;
isDeviceUser?: boolean;
}
const baseClass = "policy-details-modal";
@ -17,7 +18,11 @@ const PolicyDetailsModal = ({
onCancel,
policy,
onResolveLater,
isDeviceUser = false,
}: IPolicyDetailsProps): JSX.Element => {
const hasNoDescriptionOrResolution =
!policy?.description && !policy?.resolution;
return (
<Modal
title={`${policy?.name || "Policy name"}`}
@ -26,14 +31,27 @@ const PolicyDetailsModal = ({
className={baseClass}
>
<div className={`${baseClass}__body`}>
<span className={`${baseClass}__description`}>
{policy?.description}
</span>
{policy?.resolution && (
<div className={`${baseClass}__resolution`}>
<span className={`${baseClass}__resolution-header`}>Resolve:</span>
{policy?.resolution && <ClickableUrls text={policy?.resolution} />}
</div>
{hasNoDescriptionOrResolution ? (
<span className={`${baseClass}__empty`}>
This policy is missing description and resolution instructions.
{isDeviceUser ? " Please contact your IT admin." : ""}
</span>
) : (
<>
{policy?.description && (
<span className={`${baseClass}__description`}>
{policy.description}
</span>
)}
{policy?.resolution && (
<div className={`${baseClass}__resolution`}>
<span className={`${baseClass}__resolution-header`}>
Resolve:
</span>
<ClickableUrls text={policy.resolution} />
</div>
)}
</>
)}
<div className="modal-cta-wrap">
<Button onClick={onCancel}>Close</Button>

View file

@ -1,9 +1,10 @@
.policy-details-modal {
&__body {
@include vertical-modal-layout;
white-space: pre-wrap;
}
&__resolution {
margin-top: $pad-small;
overflow: hidden;
}