diff --git a/frontend/components/modals/FailedEnrollmentProfileModal/FailedEnrollmentProfileModal.tsx b/frontend/components/modals/FailedEnrollmentProfileModal/FailedEnrollmentProfileModal.tsx new file mode 100644 index 0000000000..e149b9c0cb --- /dev/null +++ b/frontend/components/modals/FailedEnrollmentProfileModal/FailedEnrollmentProfileModal.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { ICommandResult } from "interfaces/command"; +import CommandResultsModal, { + GetIconName, +} from "pages/hosts/components/CommandDetailsModal"; +import { formatDistanceToNow } from "date-fns"; +import IconStatusMessage from "components/IconStatusMessage"; +import CustomLink from "components/CustomLink"; + +export interface IFailedEnrollmentProfileModalProps { + command: { command_uuid: string }; + onDone: () => void; +} + +const failedEnrollmentProfileContentBody = ( + baseClass: string, + result: ICommandResult +) => { + const displayTime = result.updated_at + ? ` (${formatDistanceToNow(new Date(result.updated_at), { + includeSeconds: true, + addSuffix: true, + })})` + : null; + const hostDisplayName = result.hostname || "this host"; + const messageText = ( + + Fleet enrollment profile renewal failed for {hostDisplayName} + {displayTime}. + + ); + return ( +
+ +

+ This profile contains a certificate that will expire. If the profile + isn't renewed before expiration, the host must be re-enrolled. For + assistance, reach out to{" "} + + . +

+
+ ); +}; + +const FailedEnrollmentProfileModal = ({ + command, + onDone, +}: IFailedEnrollmentProfileModalProps) => { + return ( + + ); +}; + +export default FailedEnrollmentProfileModal; diff --git a/frontend/components/modals/FailedEnrollmentProfileModal/index.ts b/frontend/components/modals/FailedEnrollmentProfileModal/index.ts new file mode 100644 index 0000000000..8842908d8e --- /dev/null +++ b/frontend/components/modals/FailedEnrollmentProfileModal/index.ts @@ -0,0 +1,2 @@ +export { default } from "./FailedEnrollmentProfileModal"; +export type { IFailedEnrollmentProfileModalProps } from "./FailedEnrollmentProfileModal"; diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index 0a8ccdeadd..db2e98681d 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -169,6 +169,7 @@ export enum ActivityType { DisabledManagedLocalAccount = "disabled_managed_local_account", ViewedManagedLocalAccount = "read_managed_local_account", CreatedManagedLocalAccount = "created_managed_local_account", + FailedEnrollmentProfileRenewal = "failed_enrollment_profile_renewal", CreatedLabel = "created_label", EditedLabel = "edited_label", DeletedLabel = "deleted_label", @@ -197,7 +198,8 @@ export type IHostPastActivityType = | ActivityType.ResentCertificate | ActivityType.ClearedPasscode | ActivityType.ViewedManagedLocalAccount - | ActivityType.CreatedManagedLocalAccount; + | ActivityType.CreatedManagedLocalAccount + | ActivityType.FailedEnrollmentProfileRenewal; /** This is a subset of ActivityType that are shown only for the host upcoming activities */ export type IHostUpcomingActivityType = @@ -492,6 +494,8 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record = { "Turned off managed local account", [ActivityType.ViewedManagedLocalAccount]: "Viewed managed account", [ActivityType.CreatedManagedLocalAccount]: "Created managed account", + [ActivityType.FailedEnrollmentProfileRenewal]: + "Enrollment profile renewal failed", [ActivityType.CreatedLabel]: "Created label", [ActivityType.EditedLabel]: "Edited label", [ActivityType.DeletedLabel]: "Deleted label", diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index 1436d54078..33448992a2 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -34,6 +34,9 @@ import SoftwareUninstallDetailsModal, { } from "components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal"; import { IShowActivityDetailsData } from "components/ActivityItem/ActivityItem"; import { getDisplayedSoftwareName } from "pages/SoftwarePage/helpers"; +import FailedEnrollmentProfileModal, { + IFailedEnrollmentProfileModalProps, +} from "components/modals/FailedEnrollmentProfileModal"; import GlobalActivityItem from "./GlobalActivityItem"; import ActivityAutomationDetailsModal from "./components/ActivityAutomationDetailsModal"; @@ -135,6 +138,10 @@ const ActivityFeed = ({ appStoreDetails, setAppStoreDetails, ] = useState(null); + const [ + enrollmentProfileFailedDetails, + setEnrollmentProfileFailedDetails, + ] = useState | null>(null); const [searchQuery, setSearchQuery] = useState(""); const [createdAtDirection, setCreatedAtDirection] = useState("desc"); @@ -285,6 +292,13 @@ const ActivityFeed = ({ ) ); break; + case ActivityType.FailedEnrollmentProfileRenewal: + setEnrollmentProfileFailedDetails({ + command: { + command_uuid: details?.command_uuid || "", + }, + }); + break; default: break; } @@ -440,6 +454,12 @@ const ActivityFeed = ({ onCancel={() => setAppStoreDetails(null)} /> )} + {enrollmentProfileFailedDetails && ( + setEnrollmentProfileFailedDetails(null)} + /> + )} ); }; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index 84d3760ce2..b152e5ad26 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -40,6 +40,7 @@ const ACTIVITIES_WITH_DETAILS = new Set([ ActivityType.InstalledAppStoreApp, ActivityType.RanScriptBatch, ActivityType.CanceledScriptBatch, + ActivityType.FailedEnrollmentProfileRenewal, ]); const getProfilesPlatformDisplayName = ( @@ -1939,6 +1940,14 @@ const TAGGED_TEMPLATES = { ); }, + failedEnrollmentRenewalProfile: (activity: IActivity) => { + return ( + <> + enrollment profile renewal failed for{" "} + {activity.details?.host_display_name}. + + ); + }, }; const getDetail = (activity: IActivity, isPremiumTier: boolean) => { @@ -2376,6 +2385,9 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => { case ActivityType.ClearedPasscode: { return TAGGED_TEMPLATES.clearedPasscode(activity); } + case ActivityType.FailedEnrollmentProfileRenewal: { + return TAGGED_TEMPLATES.failedEnrollmentRenewalProfile(activity); + } default: { return TAGGED_TEMPLATES.defaultActivityTemplate(activity); } diff --git a/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx index 762bf9ef0e..f3f536283f 100644 --- a/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx +++ b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx @@ -4,7 +4,7 @@ import { formatDistanceToNow } from "date-fns"; import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; -import { ICommand, ICommandResult } from "interfaces/command"; +import { ICommandResult } from "interfaces/command"; import commandApi, { IGetCommandResultsResponse, @@ -22,7 +22,7 @@ import Button from "components/buttons/Button"; const baseClass = "command-details-modal"; -const getIconName = (status: string): IconNames => { +export const GetIconName = (status: string): IconNames => { switch (status) { case "Error": return "error"; @@ -101,14 +101,24 @@ const getStatusMessage = (result: ICommandResult): React.ReactNode => { } }; +const defaultModalContentBody = (baseclass: string, result: ICommandResult) => ( + +); + const ModalContent = ({ data, isLoading, error, + contentBody = defaultModalContentBody, }: { data: IGetCommandResultsResponse | undefined; isLoading: boolean; error: Error | null; + contentBody?: (baseClass: string, result: ICommandResult) => React.ReactNode; }) => { if (isLoading) { return ; @@ -136,11 +146,7 @@ const ModalContent = ({ return (
- + {contentBody(baseClass, result)} {!!result.payload && ( React.ReactNode; + title?: string; onDone: () => void; } const CommandResultsModal = ({ command: { host_uuid: host_identifier, command_uuid }, + contentBody, + title = "MDM command details", onDone, }: ICommandResultsModalProps) => { const { data, isLoading, error } = useQuery< @@ -182,21 +199,32 @@ const CommandResultsModal = ({ IGetCommandResultsResponse, IGetHostCommandResultsQueryKey[] >( - [{ scope: "command_results", host_identifier, command_uuid }], - ({ queryKey }) => - commandApi.getHostCommandResults(queryKey[0]).then((resp) => { - if (!resp?.results) { - // this should not happen, but just in case return the response as is - return resp; - } - return { - results: resp.results.map?.((r) => ({ - ...r, - payload: atob(r.payload), - result: atob(r.result), - })), - }; - }), + [ + { + scope: "command_results", + host_identifier: host_identifier ?? "", + command_uuid, + }, + ], + async ({ queryKey }) => { + const resp = + queryKey[0].host_identifier === "" + ? // if host_identifier is not provided, use the getCommandResults endpoint which does not require host_identifier + await commandApi.getCommandResults(queryKey[0].command_uuid) + : await commandApi.getHostCommandResults(queryKey[0]); + + if (!resp?.results) { + // this should not happen, but just in case return the response as is + return resp; + } + return { + results: resp.results.map?.((r) => ({ + ...r, + payload: atob(r.payload), + result: atob(r.result), + })), + }; + }, { ...DEFAULT_USE_QUERY_OPTIONS, keepPreviousData: true, @@ -205,13 +233,13 @@ const CommandResultsModal = ({ ); return ( - - + + Close} /> ); diff --git a/frontend/pages/hosts/components/CommandDetailsModal/index.ts b/frontend/pages/hosts/components/CommandDetailsModal/index.ts index 5ec586288f..31e35d5a58 100644 --- a/frontend/pages/hosts/components/CommandDetailsModal/index.ts +++ b/frontend/pages/hosts/components/CommandDetailsModal/index.ts @@ -1 +1,2 @@ export { default } from "./CommandDetailsModal"; +export { GetIconName } from "./CommandDetailsModal"; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index c0b07efbf8..42dd035887 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -95,6 +95,9 @@ import CertificateInstallDetailsModal, { import { getDisplayedSoftwareName } from "pages/SoftwarePage/helpers"; import CommandResultsModal from "pages/hosts/components/CommandDetailsModal"; +import FailedEnrollmentProfileModal, { + IFailedEnrollmentProfileModalProps, +} from "components/modals/FailedEnrollmentProfileModal"; import HostSummaryCard from "../cards/HostSummary"; import VitalsCard from "../cards/Vitals"; @@ -286,6 +289,10 @@ const HostDetailsPage = ({ const [mdmCommandDetails, setMdmCommandDetails] = useState( null ); + const [ + enrollmentProfileFailedDetails, + setEnrollmentProfileFailedDetails, + ] = useState | null>(null); const [refetchStartTime, setRefetchStartTime] = useState(null); const [showRefetchSpinner, setShowRefetchSpinner] = useState(false); @@ -870,6 +877,13 @@ const HostDetailsPage = ({ detail: details?.detail || "", }); break; + case ActivityType.FailedEnrollmentProfileRenewal: + setEnrollmentProfileFailedDetails({ + command: { + command_uuid: details?.command_uuid || "", + }, + }); + break; default: // do nothing } }, @@ -1715,6 +1729,12 @@ const HostDetailsPage = ({ onDone={onCancelMdmCommandDetailsModal} /> )} + {enrollmentProfileFailedDetails && ( + setEnrollmentProfileFailedDetails(null)} + /> + )} {showLockHostModal && ( { + return ( + + + Fleet enrollment profile renewal failed for this host.{" "} + + + ); +}; + +export default FailedEnrollmentProfileRenewalActivityItem; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/FailedEnrollmentProfileRenewalActivityItem/index.ts b/frontend/pages/hosts/details/cards/Activity/ActivityItems/FailedEnrollmentProfileRenewalActivityItem/index.ts new file mode 100644 index 0000000000..06e1a2536a --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/FailedEnrollmentProfileRenewalActivityItem/index.ts @@ -0,0 +1 @@ +export { default } from "./FailedEnrollmentProfileRenewalActivityItem";