From 49222896103ac4bf6d00a3349cc262b0107859ff Mon Sep 17 00:00:00 2001 From: Andrew Mellor Date: Fri, 10 Jul 2026 10:07:30 +0100 Subject: [PATCH] 48917 Show a deleted state instead of a generic error for stale MDM command (#49012) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Related issue:** Resolves #48917 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results ## Summary by CodeRabbit ## Summary by CodeRabbit * **Bug Fixes** * The MDM command details modal now shows **“This command has been deleted.”** instead of a generic error when a command result is removed after the host is wiped and re-enrolled. * The modal now uses additional stored activity context (like host display name and request type) to render more accurate, host-specific details for deleted commands. * **Tests** * Updated and added coverage to confirm the deleted-message UI and that the generic error text no longer appears. --- ...8917-command-details-modal-reenrolled-host | 1 + .../cards/ActivityFeed/ActivityFeed.tsx | 43 ++++++++++++++++++- .../CommandDetailsModal.tests.tsx | 24 ++++++++++- .../CommandDetailsModal.tsx | 35 +++++++++++++-- 4 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 changes/48917-command-details-modal-reenrolled-host diff --git a/changes/48917-command-details-modal-reenrolled-host b/changes/48917-command-details-modal-reenrolled-host new file mode 100644 index 0000000000..58838e0734 --- /dev/null +++ b/changes/48917-command-details-modal-reenrolled-host @@ -0,0 +1 @@ +- Fixed the MDM command details modal showing a generic error, instead of a clear message, for a command sent to a host that was later wiped and re-enrolled. diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index 82e6abbc26..39201100f1 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -160,6 +160,8 @@ const ActivityFeed = ({ host_uuid?: string; command_uuid: string; actor_full_name?: string; + host_display_name?: string; + request_type?: string; } | null>(null); const [searchQuery, setSearchQuery] = useState(""); @@ -337,6 +339,8 @@ const ActivityFeed = ({ command_uuid: details.command_uuid, host_uuid: details?.host_uuid, actor_full_name, + host_display_name: details?.host_display_name, + request_type: details?.request_type, }); break; } @@ -513,15 +517,52 @@ const ActivityFeed = ({ { + const isDeleted = result.status === "Deleted"; const isPending = getIconName(result.status) === "pending-outline"; const cmdDisplayName = getMdmCommandDisplayName( - result.request_type + isDeleted + ? mdmCommandActivityDetails.request_type + : result.request_type ); const timeAgoText = result.updated_at ? ` (${timeAgo(new Date(result.updated_at), { addSuffix: true, })})` : ""; + + if (isDeleted) { + // no result -- likely the host was wiped and re-enrolled since. + // Use the activity's own details, captured at click-time, + // instead of the (empty) fetched result. Both fields are + // optional on that captured state, so guard against a leading + // "ran ..." with no actor and a bare "on ." with no hostname. + const { + actor_full_name: actorText, + host_display_name: hostText, + } = mdmCommandActivityDetails; + return ( + <> + + {actorText && {actorText}} + {actorText ? " ran " : "Ran "} + {formatMdmCommandNameForActivityItem( + mdmCommandActivityDetails.request_type + )} + {" on "} + {hostText ? {hostText} : "this host"} + {"."} + + } + /> +
This command has been deleted.
+ + ); + } + return ( { it("returns error for Apple Error status", () => { @@ -67,3 +74,18 @@ describe("getVerbForCommandStatus", () => { expect(getVerbForCommandStatus("unknown")).toEqual("sent"); }); }); + +describe("ModalContent", () => { + it("renders normally, not as an error, when the API returns a 200 with no results (e.g. host re-enrolled since the command was sent)", () => { + render( + + ); + + expect( + screen.getByText("This command has been deleted.") + ).toBeInTheDocument(); + expect( + screen.queryByText(/something's gone wrong/i) + ).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx index 81510e516c..eb4f6566c5 100644 --- a/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx +++ b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx @@ -33,6 +33,10 @@ export const getIconName = (status: string): IconNames => { case "Pending": case "NotNow": return "pending-outline"; + // sentinel used when the command results API returns a 200 with no + // results (e.g. the host it was sent to was wiped and re-enrolled since) + case "Deleted": + return "info-outline"; default: break; } @@ -113,6 +117,9 @@ const getStatusMessage = (result: ICommandResult): React.ReactNode => { ); + case "Deleted": + return This command has been deleted.; + default: // FIXME: update for other platforms and design appropriate default handling for unknown // statuses; for now, just fallback to status string @@ -128,7 +135,7 @@ const defaultModalContentBody = (baseclass: string, result: ICommandResult) => ( /> ); -const ModalContent = ({ +export const ModalContent = ({ data, isLoading, error, @@ -148,9 +155,29 @@ const ModalContent = ({ } if (!data?.results?.[0]) { - // this should not happen, but just in case - console.error("No results found in MDM command results data"); - return ; + // a 200 with no results means the command no longer has anything to show -- + // most commonly because the host it was sent to was wiped and re-enrolled + // since. Render the modal normally (via the caller's contentBody, same as a + // real result) rather than as an error, since nothing actually went wrong. + // The "Deleted" sentinel status lets the caller render its own copy for + // this case using the activity's own details, since there's no real + // result to pull hostname/request_type from. + const deletedCommandResult: ICommandResult = { + host_uuid: "", + command_uuid: "", + status: "Deleted", + updated_at: "", + request_type: "", + hostname: "", + payload: "", + result: "", + name: null, + }; + return ( +
+ {contentBody(baseClass, deletedCommandResult)} +
+ ); } if (data.results.length > 1) {