From 156302adb4a9570fb49bb61695ffe5d6d6708c0b Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Tue, 7 Jul 2026 14:35:07 -0400 Subject: [PATCH] Fix policy automation activities missing from host activity feed Closes #48616 Adds missing policy automation activities to host's details page. --- frontend/interfaces/activity.ts | 10 ++- .../details/cards/Activity/ActivityConfig.tsx | 9 ++ .../PolicyAutomationActivityItem.tests.tsx | 83 +++++++++++++++++++ .../PolicyAutomationActivityItem.tsx | 65 +++++++++++++++ .../PolicyAutomationActivityItem/index.ts | 1 + 5 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/PolicyAutomationActivityItem.tests.tsx create mode 100644 frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/PolicyAutomationActivityItem.tsx create mode 100644 frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/index.ts diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index b2c6b55d31..bee2119427 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -228,7 +228,15 @@ export type IHostPastActivityType = | ActivityType.RotatedManagedLocalAccountPassword | ActivityType.FailedToRotateManagedLocalAccountPassword | ActivityType.FailedEnrollmentProfileRenewal - | ActivityType.RanCustomMdmCommand; + | ActivityType.RanCustomMdmCommand + | ActivityType.RanAutomationWebhook + | ActivityType.RanAutomationTicket + | ActivityType.RanAutomationCalendarEvent + | ActivityType.RanAutomationConditionalAccess + | ActivityType.FailedAutomationWebhook + | ActivityType.FailedAutomationTicket + | ActivityType.FailedAutomationCalendarEvent + | ActivityType.FailedAutomationConditionalAccess; /** This is a subset of ActivityType that are shown only for the host upcoming activities */ export type IHostUpcomingActivityType = diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx index d8464fcb98..54ed94e69c 100644 --- a/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx +++ b/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx @@ -36,6 +36,7 @@ import FailedToRotateManagedLocalAccountPasswordActivityItem from "./ActivityIte import FailedEnrollmentProfileRenewalActivityItem from "./ActivityItems/FailedEnrollmentProfileRenewalActivityItem"; import MdmUnenrolledActivityItem from "./ActivityItems/MdmUnenrolledActivityItem"; import RanCustomMdmCommandActivityItem from "./ActivityItems/RanCustomMdmCommandActivityItem"; +import PolicyAutomationActivityItem from "./ActivityItems/PolicyAutomationActivityItem"; /** The component props that all host activity items must adhere to */ export interface IHostActivityItemComponentProps { @@ -92,6 +93,14 @@ export const pastActivityComponentMap: Record< [ActivityType.FailedEnrollmentProfileRenewal]: FailedEnrollmentProfileRenewalActivityItem, [ActivityType.MdmUnenrolled]: MdmUnenrolledActivityItem, [ActivityType.RanCustomMdmCommand]: RanCustomMdmCommandActivityItem, + [ActivityType.RanAutomationWebhook]: PolicyAutomationActivityItem, + [ActivityType.RanAutomationTicket]: PolicyAutomationActivityItem, + [ActivityType.RanAutomationCalendarEvent]: PolicyAutomationActivityItem, + [ActivityType.RanAutomationConditionalAccess]: PolicyAutomationActivityItem, + [ActivityType.FailedAutomationWebhook]: PolicyAutomationActivityItem, + [ActivityType.FailedAutomationTicket]: PolicyAutomationActivityItem, + [ActivityType.FailedAutomationCalendarEvent]: PolicyAutomationActivityItem, + [ActivityType.FailedAutomationConditionalAccess]: PolicyAutomationActivityItem, }; export const upcomingActivityComponentMap: Record< diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/PolicyAutomationActivityItem.tests.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/PolicyAutomationActivityItem.tests.tsx new file mode 100644 index 0000000000..af9ec1395e --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/PolicyAutomationActivityItem.tests.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { createMockHostPastActivity } from "__mocks__/activityMock"; + +import { ActivityType, IHostPastActivityType } from "interfaces/activity"; + +import PolicyAutomationActivityItem from "./PolicyAutomationActivityItem"; + +const renderItem = (type: IHostPastActivityType) => + render( + + ); + +describe("PolicyAutomationActivityItem", () => { + const cases: Array<[IHostPastActivityType, RegExp]> = [ + [ActivityType.RanAutomationWebhook, /sent a webhook because this host/i], + [ActivityType.RanAutomationTicket, /created a ticket because this host/i], + [ + ActivityType.RanAutomationCalendarEvent, + /created a calendar event because this host/i, + ], + [ + ActivityType.RanAutomationConditionalAccess, + /blocked single sign-on because this host/i, + ], + [ + ActivityType.FailedAutomationWebhook, + /failed to send a webhook after this host/i, + ], + [ + ActivityType.FailedAutomationTicket, + /failed to create a ticket after this host/i, + ], + [ + ActivityType.FailedAutomationCalendarEvent, + /failed to create a calendar event after this host/i, + ], + [ + ActivityType.FailedAutomationConditionalAccess, + /failed to block single sign-on after this host/i, + ], + ]; + + it.each(cases)("renders copy for %s", (type, expected) => { + renderItem(type); + expect(screen.getByText("Fleet")).toBeVisible(); + expect(screen.getByText(expected)).toBeVisible(); + }); + + it("does not render the cancel or show-details icons", () => { + renderItem(ActivityType.RanAutomationWebhook); + expect(screen.queryByTestId("close-icon")).not.toBeInTheDocument(); + expect(screen.queryByTestId("info-outline-icon")).not.toBeInTheDocument(); + }); + + // These activities are always Fleet-initiated in practice; this documents the + // defensive actor branch that keeps the bold name in sync with the avatar. + it("renders the actor name when the activity is not Fleet-initiated", () => { + render( + + ); + expect(screen.getByText("Admin User")).toBeVisible(); + expect(screen.queryByText("Fleet")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/PolicyAutomationActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/PolicyAutomationActivityItem.tsx new file mode 100644 index 0000000000..3a7d1bc8c9 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/PolicyAutomationActivityItem.tsx @@ -0,0 +1,65 @@ +import React from "react"; + +import { ActivityType, IHostPastActivityType } from "interfaces/activity"; +import ActivityItem from "components/ActivityItem"; + +import { IHostActivityItemComponentProps } from "../../ActivityConfig"; + +const baseClass = "policy-automation-activity-item"; + +type PolicyAutomationActivityType = + | ActivityType.RanAutomationWebhook + | ActivityType.RanAutomationTicket + | ActivityType.RanAutomationCalendarEvent + | ActivityType.RanAutomationConditionalAccess + | ActivityType.FailedAutomationWebhook + | ActivityType.FailedAutomationTicket + | ActivityType.FailedAutomationCalendarEvent + | ActivityType.FailedAutomationConditionalAccess; + +const AUTOMATION_COPY: Record = { + [ActivityType.RanAutomationWebhook]: + "sent a webhook because this host failed a policy.", + [ActivityType.RanAutomationTicket]: + "created a ticket because this host failed a policy.", + [ActivityType.RanAutomationCalendarEvent]: + "created a calendar event because this host failed a policy.", + [ActivityType.RanAutomationConditionalAccess]: + "blocked single sign-on because this host failed a policy.", + [ActivityType.FailedAutomationWebhook]: + "failed to send a webhook after this host failed a policy.", + [ActivityType.FailedAutomationTicket]: + "failed to create a ticket after this host failed a policy.", + [ActivityType.FailedAutomationCalendarEvent]: + "failed to create a calendar event after this host failed a policy.", + [ActivityType.FailedAutomationConditionalAccess]: + "failed to block single sign-on after this host failed a policy.", +}; + +const isPolicyAutomationActivityType = ( + type: IHostPastActivityType +): type is PolicyAutomationActivityType => type in AUTOMATION_COPY; + +const PolicyAutomationActivityItem = ({ + activity, + isSoloActivity, +}: IHostActivityItemComponentProps) => { + if (!isPolicyAutomationActivityType(activity.type)) { + return null; + } + + return ( + + {activity.fleet_initiated ? "Fleet" : activity.actor_full_name}{" "} + {AUTOMATION_COPY[activity.type]} + + ); +}; + +export default PolicyAutomationActivityItem; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/index.ts b/frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/index.ts new file mode 100644 index 0000000000..29bbb35589 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/PolicyAutomationActivityItem/index.ts @@ -0,0 +1 @@ +export { default } from "./PolicyAutomationActivityItem";