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";