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
@@ -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 = (
<span>
Fleet enrollment profile renewal failed for <b>{hostDisplayName}</b>
{displayTime}.
Comment thread
MagnusHJensen marked this conversation as resolved.
</span>
Comment thread
MagnusHJensen marked this conversation as resolved.
);
return (
<div>
<IconStatusMessage
className={`${baseClass}__status-message`}
iconName={GetIconName(result.status)}
message={messageText}
/>
Comment thread
MagnusHJensen marked this conversation as resolved.
<p>
This profile contains a certificate that will expire. If the profile
isn&apos;t renewed before expiration, the host must be re-enrolled. For
assistance, reach out to{" "}
<CustomLink
text="Fleet support"
url="https://fleetdm.com/support"
newTab
/>
.
</p>
</div>
);
};

const FailedEnrollmentProfileModal = ({
command,
onDone,
}: IFailedEnrollmentProfileModalProps) => {
return (
<CommandResultsModal
command={command}
onDone={onDone}
title="Enrollment profile renewal details"
contentBody={failedEnrollmentProfileContentBody}
/>
);
};

export default FailedEnrollmentProfileModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./FailedEnrollmentProfileModal";
export type { IFailedEnrollmentProfileModalProps } from "./FailedEnrollmentProfileModal";
6 changes: 5 additions & 1 deletion frontend/interfaces/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -492,6 +494,8 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record<ActivityType, string> = {
"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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -135,6 +138,10 @@ const ActivityFeed = ({
appStoreDetails,
setAppStoreDetails,
] = useState<IActivityDetails | null>(null);
const [
enrollmentProfileFailedDetails,
setEnrollmentProfileFailedDetails,
] = useState<Omit<IFailedEnrollmentProfileModalProps, "onDone"> | null>(null);

const [searchQuery, setSearchQuery] = useState("");
const [createdAtDirection, setCreatedAtDirection] = useState("desc");
Expand Down Expand Up @@ -285,6 +292,13 @@ const ActivityFeed = ({
)
);
break;
case ActivityType.FailedEnrollmentProfileRenewal:
setEnrollmentProfileFailedDetails({
command: {
command_uuid: details?.command_uuid || "",
Comment thread
MagnusHJensen marked this conversation as resolved.
},
});
break;
default:
break;
}
Expand Down Expand Up @@ -440,6 +454,12 @@ const ActivityFeed = ({
onCancel={() => setAppStoreDetails(null)}
/>
)}
{enrollmentProfileFailedDetails && (
<FailedEnrollmentProfileModal
command={enrollmentProfileFailedDetails.command}
onDone={() => setEnrollmentProfileFailedDetails(null)}
/>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const ACTIVITIES_WITH_DETAILS = new Set([
ActivityType.InstalledAppStoreApp,
ActivityType.RanScriptBatch,
ActivityType.CanceledScriptBatch,
ActivityType.FailedEnrollmentProfileRenewal,
]);

const getProfilesPlatformDisplayName = (
Expand Down Expand Up @@ -1939,6 +1940,14 @@ const TAGGED_TEMPLATES = {
</>
);
},
failedEnrollmentRenewalProfile: (activity: IActivity) => {
return (
<>
enrollment profile renewal failed for{" "}
<b>{activity.details?.host_display_name}</b>.
</>
);
},
};

const getDetail = (activity: IActivity, isPremiumTier: boolean) => {
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";
Expand Down Expand Up @@ -101,14 +101,24 @@ const getStatusMessage = (result: ICommandResult): React.ReactNode => {
}
};

const defaultModalContentBody = (baseclass: string, result: ICommandResult) => (
<IconStatusMessage
className={`${baseclass}__status-message`}
iconName={GetIconName(result.status)}
message={getStatusMessage(result)}
/>
);

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 <Spinner />;
Expand Down Expand Up @@ -136,11 +146,7 @@ const ModalContent = ({

return (
<div className={`${baseClass}__modal-content`}>
<IconStatusMessage
className={`${baseClass}__status-message`}
iconName={getIconName(result.status)}
message={getStatusMessage(result)}
/>
{contentBody(baseClass, result)}
{!!result.payload && (
<InputField
type="textarea"
Expand All @@ -167,13 +173,24 @@ const ModalContent = ({
);
};

type ICommandResultsModalCommand = {
host_uuid?: string;
command_uuid: string;
};

interface ICommandResultsModalProps {
command: ICommand;
command: ICommandResultsModalCommand;
// contentBody if provided will be used to render content above the request and response payloads.
// if not defined, a default contentBody will be used to display a status message and icon based on profile status
contentBody?: (baseClass: string, result: ICommandResult) => React.ReactNode;
Comment thread
MagnusHJensen marked this conversation as resolved.
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<
Expand All @@ -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,
Expand All @@ -205,13 +233,13 @@ const CommandResultsModal = ({
);

return (
<Modal
className={baseClass}
width="large"
title="MDM command details"
onExit={onDone}
>
<ModalContent data={data} isLoading={isLoading} error={error} />
<Modal className={baseClass} width="large" title={title} onExit={onDone}>
<ModalContent
data={data}
isLoading={isLoading}
error={error}
contentBody={contentBody}
/>
<ModalFooter primaryButtons={<Button onClick={onDone}>Close</Button>} />
</Modal>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default } from "./CommandDetailsModal";
export { GetIconName } from "./CommandDetailsModal";
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -286,6 +289,10 @@ const HostDetailsPage = ({
const [mdmCommandDetails, setMdmCommandDetails] = useState<ICommand | null>(
null
);
const [
enrollmentProfileFailedDetails,
setEnrollmentProfileFailedDetails,
] = useState<Omit<IFailedEnrollmentProfileModalProps, "onDone"> | null>(null);

const [refetchStartTime, setRefetchStartTime] = useState<number | null>(null);
const [showRefetchSpinner, setShowRefetchSpinner] = useState(false);
Expand Down Expand Up @@ -870,6 +877,13 @@ const HostDetailsPage = ({
detail: details?.detail || "",
});
break;
case ActivityType.FailedEnrollmentProfileRenewal:
setEnrollmentProfileFailedDetails({
command: {
command_uuid: details?.command_uuid || "",
},
});
break;
Comment thread
MagnusHJensen marked this conversation as resolved.
default: // do nothing
}
},
Expand Down Expand Up @@ -1715,6 +1729,12 @@ const HostDetailsPage = ({
onDone={onCancelMdmCommandDetailsModal}
/>
)}
{enrollmentProfileFailedDetails && (
<FailedEnrollmentProfileModal
command={enrollmentProfileFailedDetails.command}
onDone={() => setEnrollmentProfileFailedDetails(null)}
/>
)}
{showLockHostModal && (
<LockModal
id={host.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import ClearedPasscodeActivityItem from "./ActivityItems/ClearedPasscodeActivity
import FailedWipeActivityItem from "./ActivityItems/FailedWipeActivityItem";
import ViewedManagedLocalAccountActivityItem from "./ActivityItems/ViewedManagedLocalAccountActivityItem/ViewedManagedLocalAccountActivityItem";
import CreatedManagedLocalAccountActivityItem from "./ActivityItems/CreatedManagedLocalAccountActivityItem/CreatedManagedLocalAccountActivityItem";
import FailedEnrollmentProfileRenewalActivityItem from "./ActivityItems/FailedEnrollmentProfileRenewalActivityItem";

/** The component props that all host activity items must adhere to */
export interface IHostActivityItemComponentProps {
Expand Down Expand Up @@ -78,6 +79,7 @@ export const pastActivityComponentMap: Record<
[ActivityType.ClearedPasscode]: ClearedPasscodeActivityItem,
[ActivityType.ViewedManagedLocalAccount]: ViewedManagedLocalAccountActivityItem,
[ActivityType.CreatedManagedLocalAccount]: CreatedManagedLocalAccountActivityItem,
[ActivityType.FailedEnrollmentProfileRenewal]: FailedEnrollmentProfileRenewalActivityItem,
};

export const upcomingActivityComponentMap: Record<
Expand Down
Loading
Loading