Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
25b52c7
Add ran_custom_mdm_command activity when a custom MDM command is run
andymFleet Jun 17, 2026
fe2abf8
add change file
andymFleet Jun 17, 2026
023f6ce
Skip activity for hosts where MDM command enqueue partially fails
andymFleet Jun 17, 2026
2b01096
Log activity errors instead of returning them after successful MDM co…
andymFleet Jun 17, 2026
9541478
Assert activity user in MDM command tests
andymFleet Jun 17, 2026
460655b
Enqueue MDM commands using resolved host UUIDs, not raw request input
andymFleet Jun 18, 2026
8ed91ed
Use commandPlatform for activity platform field
andymFleet Jun 18, 2026
24d577c
Merge branch '45640-apple-windows-mdm-command-activities' into 45641-…
andymFleet Jun 18, 2026
fb1f40e
Add ran_custom_mdm_command activity feed UI
andymFleet Jun 19, 2026
250bc09
Fix empty bold element when actor_full_name is missing in MDM command…
andymFleet Jun 19, 2026
eb93746
Guard against missing command_uuid and host_uuid in MDM command activ…
andymFleet Jun 19, 2026
88601dc
Fix getMdmCommandDisplayName dropping empty last segment for trailing…
andymFleet Jun 19, 2026
48db4ce
Use accurate verb based on MDM command status icon in activity modal
andymFleet Jun 19, 2026
42f27e2
Add additional tests for custom MDM command activity
andymFleet Jun 19, 2026
ecca0a9
Merge branch 'main' into 45641-mdm-command-activity-feed-ui
andymFleet Jun 19, 2026
dd452f1
Add boundary tests for Windows MDM status code ranges in GetIconName
andymFleet Jun 19, 2026
de1124b
Merge branch 'main' into 45641-mdm-command-activity-feed-ui
andymFleet Jun 19, 2026
76f3bef
Merge branch 'main' into 45641-mdm-command-activity-feed-ui
andymFleet Jun 19, 2026
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
6 changes: 5 additions & 1 deletion frontend/interfaces/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export enum ActivityType {
EnabledWindowsMdmMigration = "enabled_windows_mdm_migration",
DisabledWindowsMdmMigration = "disabled_windows_mdm_migration",
RanScript = "ran_script",
RanCustomMdmCommand = "ran_custom_mdm_command",
RanScriptBatch = "ran_script_batch",
ScheduledScriptBatch = "scheduled_script_batch",
CanceledScriptBatch = "canceled_script_batch",
Expand Down Expand Up @@ -215,7 +216,8 @@ export type IHostPastActivityType =
| ActivityType.CreatedManagedLocalAccount
| ActivityType.RotatedManagedLocalAccountPassword
| ActivityType.FailedToRotateManagedLocalAccountPassword
| ActivityType.FailedEnrollmentProfileRenewal;
| ActivityType.FailedEnrollmentProfileRenewal
| ActivityType.RanCustomMdmCommand;

/** This is a subset of ActivityType that are shown only for the host upcoming activities */
export type IHostUpcomingActivityType =
Expand Down Expand Up @@ -297,6 +299,7 @@ export interface IActivityDetails {
query_ids?: number[];
query_name?: string;
query_sql?: string;
request_type?: string;
role?: UserRole;
script_execution_id?: string;
script_name?: string;
Expand Down Expand Up @@ -469,6 +472,7 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record<ActivityType, string> = {
locked_host: "Locked host",
mdm_enrolled: "MDM turned on",
mdm_unenrolled: "MDM turned off",
ran_custom_mdm_command: "Ran custom MDM command",
ran_script: "Ran script",
ran_script_batch: "Bulk ran script",
scheduled_script_batch: "Scheduled script batch",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useMemo, useRef, useState } from "react";
import { formatDistanceToNow } from "date-fns";
import { useQuery } from "react-query";
import { isEmpty } from "lodash";
import { InjectedRouter } from "react-router";
Expand All @@ -21,7 +22,11 @@ import {
} from "interfaces/activity";
import { PerformanceImpactIndicator } from "interfaces/schedulable_query";

import { getPerformanceImpactDescription } from "utilities/helpers";
import {
formatMdmCommandNameForActivityItem,
getMdmCommandDisplayName,
getPerformanceImpactDescription,
} from "utilities/helpers";

import ShowQueryModal from "components/modals/ShowQueryModal";
import DataError from "components/DataError";
Expand All @@ -41,6 +46,11 @@ import { getDisplayedSoftwareName } from "pages/SoftwarePage/helpers";
import FailedEnrollmentProfileModal, {
IFailedEnrollmentProfileModalProps,
} from "components/modals/FailedEnrollmentProfileModal";
import MdmCommandDetailsModal, {
GetIconName,
getVerbForCommandStatus,
} from "pages/hosts/components/CommandDetailsModal";
import IconStatusMessage from "components/IconStatusMessage";

import GlobalActivityItem from "./GlobalActivityItem";
import ActivityAutomationDetailsModal from "./components/ActivityAutomationDetailsModal";
Expand Down Expand Up @@ -146,6 +156,11 @@ const ActivityFeed = ({
enrollmentProfileFailedDetails,
setEnrollmentProfileFailedDetails,
] = useState<Omit<IFailedEnrollmentProfileModalProps, "onDone"> | null>(null);
const [mdmCommandActivityDetails, setMdmCommandActivityDetails] = useState<{
host_uuid?: string;
command_uuid: string;
actor_full_name?: string;
} | null>(null);
Comment thread
andymFleet marked this conversation as resolved.

const [searchQuery, setSearchQuery] = useState("");
const [createdAtDirection, setCreatedAtDirection] = useState("desc");
Expand Down Expand Up @@ -314,6 +329,17 @@ const ActivityFeed = ({
},
});
break;
case ActivityType.RanCustomMdmCommand: {
if (!details?.command_uuid) {
break;
}
setMdmCommandActivityDetails({
command_uuid: details.command_uuid,
host_uuid: details?.host_uuid,
actor_full_name,
});
break;
}
default:
break;
}
Expand Down Expand Up @@ -483,6 +509,58 @@ const ActivityFeed = ({
onDone={() => setEnrollmentProfileFailedDetails(null)}
/>
)}
{!!mdmCommandActivityDetails && (
<MdmCommandDetailsModal
command={mdmCommandActivityDetails}
contentBody={(cls, result) => {
const isPending = GetIconName(result.status) === "pending-outline";
const cmdDisplayName = getMdmCommandDisplayName(
result.request_type
);
const timeAgo = result.updated_at
? ` (${formatDistanceToNow(new Date(result.updated_at), {
addSuffix: true,
})})`
: "";
return (
<IconStatusMessage
className={`${cls}__status-message`}
iconName={GetIconName(result.status)}
message={
isPending ? (
<span>
{cmdDisplayName ? (
<>
{"The "}
<b>{cmdDisplayName}</b>
{" custom MDM command"}
</>
) : (
"A custom MDM command"
)}
{" is pending on "}
<b>{result.hostname}</b>
{`${timeAgo}.`}
</span>
) : (
<span>
{mdmCommandActivityDetails.actor_full_name && (
<b>{mdmCommandActivityDetails.actor_full_name}</b>
)}
{` ${getVerbForCommandStatus(result.status)} `}
{formatMdmCommandNameForActivityItem(result.request_type)}
{" on "}
<b>{result.hostname}</b>
{"."}
</span>
)
}
/>
);
}}
onDone={() => setMdmCommandActivityDetails(null)}
/>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2181,4 +2181,29 @@ describe("Activity Feed", () => {
screen.getByText(/installed all the software in self-service/i)
).toBeInTheDocument();
});

it("renders a ran_custom_mdm_command activity with a command name", () => {
const activity = createMockActivity({
type: ActivityType.RanCustomMdmCommand,
details: {
request_type: "./Device/Vendor/MSFT/DMClient/Provider/DEMO/EntDMID",
host_display_name: "Huck's MacBook Pro",
},
});
render(<GlobalActivityItem activity={activity} isPremiumTier />);

expect(screen.getByText(".../EntDMID")).toBeInTheDocument();
expect(screen.getByText("Huck's MacBook Pro")).toBeInTheDocument();
});

it("renders a ran_custom_mdm_command activity without a command name", () => {
const activity = createMockActivity({
type: ActivityType.RanCustomMdmCommand,
details: { host_display_name: "Huck's MacBook Pro" },
});
render(<GlobalActivityItem activity={activity} isPremiumTier />);

expect(screen.getByText(/a custom MDM command/i)).toBeInTheDocument();
expect(screen.getByText("Huck's MacBook Pro")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
SCRIPT_PACKAGE_SOURCES,
} from "interfaces/software";
import {
formatMdmCommandNameForActivityItem,
formatScriptNameForActivityItem,
getPerformanceImpactDescription,
} from "utilities/helpers";
Expand All @@ -31,6 +32,7 @@ import { API_NO_TEAM_ID } from "interfaces/team";
const baseClass = "global-activity-item";

const ACTIVITIES_WITH_DETAILS = new Set([
ActivityType.RanCustomMdmCommand,
ActivityType.RanScript,
ActivityType.AddedSoftware,
ActivityType.EditedSoftware,
Expand Down Expand Up @@ -1094,6 +1096,16 @@ const TAGGED_TEMPLATES = {
</>
);
},
ranCustomMdmCommand: (activity: IActivity) => {
const { request_type, host_display_name } = activity.details || {};
return (
<>
{" "}
ran {formatMdmCommandNameForActivityItem(request_type)} on{" "}
<b>{host_display_name}</b>.
</>
);
},
ranScript: (activity: IActivity) => {
const { script_name, host_display_name, from_setup_experience } =
activity.details || {};
Expand Down Expand Up @@ -2390,6 +2402,9 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => {
case ActivityType.DisabledWindowsMdmMigration: {
return TAGGED_TEMPLATES.disabledWindowsMdmMigration();
}
case ActivityType.RanCustomMdmCommand: {
return TAGGED_TEMPLATES.ranCustomMdmCommand(activity);
}
case ActivityType.RanScript: {
return TAGGED_TEMPLATES.ranScript(activity);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { GetIconName, getVerbForCommandStatus } from "./CommandDetailsModal";

describe("GetIconName", () => {
it("returns error for Apple Error status", () => {
expect(GetIconName("Error")).toEqual("error");
});

it("returns error for Apple CommandFormatError status", () => {
expect(GetIconName("CommandFormatError")).toEqual("error");
});

it("returns success for Apple Acknowledged status", () => {
expect(GetIconName("Acknowledged")).toEqual("success");
});

it("returns pending-outline for Apple Pending status", () => {
expect(GetIconName("Pending")).toEqual("pending-outline");
});

it("returns pending-outline for Apple NotNow status", () => {
expect(GetIconName("NotNow")).toEqual("pending-outline");
});

it("returns success for Windows 200 status", () => {
expect(GetIconName("200")).toEqual("success");
});

it("returns error for Windows 400 status", () => {
expect(GetIconName("400")).toEqual("error");
});

it("returns error for Windows 500 status", () => {
expect(GetIconName("500")).toEqual("error");
});

it("returns pending-outline for Windows 101 status", () => {
expect(GetIconName("101")).toEqual("pending-outline");
});

it("returns pending-outline for Windows 199 status (upper pending boundary)", () => {
expect(GetIconName("199")).toEqual("pending-outline");
});

it("returns success for Windows 399 status (upper success boundary)", () => {
expect(GetIconName("399")).toEqual("success");
});

it("returns warning for an unknown status", () => {
expect(GetIconName("unknown")).toEqual("warning");
});
});

describe("getVerbForCommandStatus", () => {
it("returns 'ran' for a successful status", () => {
expect(getVerbForCommandStatus("Acknowledged")).toEqual("ran");
});

it("returns 'failed to run' for an error status", () => {
expect(getVerbForCommandStatus("Error")).toEqual("failed to run");
});

it("returns 'sent' for a pending status", () => {
expect(getVerbForCommandStatus("Pending")).toEqual("sent");
});

it("returns 'sent' for an unknown status", () => {
expect(getVerbForCommandStatus("unknown")).toEqual("sent");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,41 @@ import Button from "components/buttons/Button";
const baseClass = "command-details-modal";

export const GetIconName = (status: string): IconNames => {
// Apple MDM status strings
switch (status) {
case "Error":
return "error";
case "CommandFormatError":
return "error";
case "Acknowledged":
return "success";
case "Pending":
return "pending-outline";
case "NotNow":
return "pending-outline";
default:
break;
}
// Windows OMA-DM status codes (numeric strings): 101 = pending, 200-399 = ran, 400+ = failed
const code = parseInt(status, 10);
if (!Number.isNaN(code)) {
if (code >= 400) return "error";
if (code >= 200) return "success";
return "pending-outline";
}
return "warning";
};

export const getVerbForCommandStatus = (status: string): string => {
const icon = GetIconName(status);
switch (icon) {
case "error":
return "failed to run";
case "success":
return "ran";
case "pending-outline":
return "sent";
default:
// FIXME: update for other platforms and design appropriate default handling for unknown
// statuses; for now, just return warning icon to indicate unknown state
return "warning";
// unknown status
return "sent";
}
};
Comment thread
Copilot marked this conversation as resolved.
Comment thread
andymFleet marked this conversation as resolved.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { default } from "./CommandDetailsModal";
export { GetIconName } from "./CommandDetailsModal";
export { GetIconName, getVerbForCommandStatus } from "./CommandDetailsModal";
Loading
Loading