mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
UI for wiping a host (#16874)
# Checklist for submitter add ability in the UI to wipe a host managed by the fleet mdm. This includes: **new wipe host action dropdown option:** ![image](https://github.com/fleetdm/fleet/assets/1153709/a5c01e45-d494-4762-8504-1e1963093809) **new wipe modal to initiate wiping a host:** ![image](https://github.com/fleetdm/fleet/assets/1153709/829c8dfb-a60f-427b-b6b8-2804924c0b71) **wipe and wiping host status tags: ** ![image](https://github.com/fleetdm/fleet/assets/1153709/de947160-7273-409d-bcfd-c219e887bb9d) ![image](https://github.com/fleetdm/fleet/assets/1153709/2a13e79a-2bcd-4aa5-b15f-5bb57348d191) - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. - [x] Manual QA for all new/changed functionality
This commit is contained in:
parent
762cd076d7
commit
5d20ee85fc
1
changes/issue-10489-ui-for-wiping-host
Normal file
1
changes/issue-10489-ui-for-wiping-host
Normal file
@ -0,0 +1 @@
|
||||
- add UI for wiping a host with fleet mdm.
|
@ -106,7 +106,8 @@ type InitialStateType = {
|
||||
isSandboxMode?: boolean;
|
||||
isFreeTier?: boolean;
|
||||
isPremiumTier?: boolean;
|
||||
isMdmEnabledAndConfigured?: boolean;
|
||||
isMacMdmEnabledAndConfigured?: boolean;
|
||||
isWindowsMdmEnabledAndConfigured?: boolean;
|
||||
isGlobalAdmin?: boolean;
|
||||
isGlobalMaintainer?: boolean;
|
||||
isGlobalObserver?: boolean;
|
||||
@ -156,7 +157,8 @@ export const initialState = {
|
||||
isSandboxMode: false,
|
||||
isFreeTier: undefined,
|
||||
isPremiumTier: undefined,
|
||||
isMdmEnabledAndConfigured: undefined,
|
||||
isMacMdmEnabledAndConfigured: undefined,
|
||||
isWindowsMdmEnabledAndConfigured: undefined,
|
||||
isGlobalAdmin: undefined,
|
||||
isGlobalMaintainer: undefined,
|
||||
isGlobalObserver: undefined,
|
||||
@ -212,7 +214,12 @@ const setPermissions = (
|
||||
isSandboxMode: permissions.isSandboxMode(config),
|
||||
isFreeTier: permissions.isFreeTier(config),
|
||||
isPremiumTier: permissions.isPremiumTier(config),
|
||||
isMdmEnabledAndConfigured: permissions.isMdmEnabledAndConfigured(config),
|
||||
isMacMdmEnabledAndConfigured: permissions.isMacMdmEnabledAndConfigured(
|
||||
config
|
||||
),
|
||||
isWindowsMdmEnabledAndConfigured: permissions.isWindowsMdmEnabledAndConfigured(
|
||||
config
|
||||
),
|
||||
isGlobalAdmin: permissions.isGlobalAdmin(user),
|
||||
isGlobalMaintainer: permissions.isGlobalMaintainer(user),
|
||||
isGlobalObserver: permissions.isGlobalObserver(user),
|
||||
@ -365,7 +372,8 @@ const AppProvider = ({ children }: Props): JSX.Element => {
|
||||
isSandboxMode: state.isSandboxMode,
|
||||
isFreeTier: state.isFreeTier,
|
||||
isPremiumTier: state.isPremiumTier,
|
||||
isMdmEnabledAndConfigured: state.isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured: state.isMacMdmEnabledAndConfigured,
|
||||
isWindowsMdmEnabledAndConfigured: state.isWindowsMdmEnabledAndConfigured,
|
||||
isGlobalAdmin: state.isGlobalAdmin,
|
||||
isGlobalMaintainer: state.isGlobalMaintainer,
|
||||
isGlobalObserver: state.isGlobalObserver,
|
||||
|
@ -66,7 +66,15 @@ export enum ActivityType {
|
||||
EditedWindowsUpdates = "edited_windows_updates",
|
||||
LockedHost = "locked_host",
|
||||
UnlockedHost = "unlocked_host",
|
||||
WipedHost = "wiped_host",
|
||||
}
|
||||
|
||||
// This is a subset of ActivityType that are shown only for the host past activities
|
||||
export type IHostPastActivityType =
|
||||
| ActivityType.RanScript
|
||||
| ActivityType.LockedHost
|
||||
| ActivityType.UnlockedHost;
|
||||
|
||||
export interface IActivity {
|
||||
created_at: string;
|
||||
id: number;
|
||||
@ -77,6 +85,11 @@ export interface IActivity {
|
||||
type: ActivityType;
|
||||
details?: IActivityDetails;
|
||||
}
|
||||
|
||||
export type IPastActivity = Omit<IActivity, "type"> & {
|
||||
type: IHostPastActivityType;
|
||||
};
|
||||
|
||||
export interface IActivityDetails {
|
||||
pack_id?: number;
|
||||
pack_name?: string;
|
||||
|
@ -157,8 +157,8 @@ interface IMdmMacOsSetup {
|
||||
bootstrap_package_name: string;
|
||||
}
|
||||
|
||||
export type HostMdmDeviceStatus = "unlocked" | "locked";
|
||||
export type HostMdmPendingAction = "unlock" | "lock" | "";
|
||||
export type HostMdmDeviceStatus = "unlocked" | "locked" | "wiped";
|
||||
export type HostMdmPendingAction = "unlock" | "lock" | "wipe" | "";
|
||||
|
||||
export interface IHostMdmData {
|
||||
encryption_key_available: boolean;
|
||||
|
@ -1165,4 +1165,17 @@ describe("Activity Feed", () => {
|
||||
screen.getByText("deleted multiple queries", { exact: false })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
// test for wipe activity
|
||||
it("renders a 'wiped_host' type activity for a team", () => {
|
||||
const activity = createMockActivity({
|
||||
type: ActivityType.WipedHost,
|
||||
details: {
|
||||
host_display_name: "Foo Host",
|
||||
},
|
||||
});
|
||||
render(<ActivityItem activity={activity} isPremiumTier />);
|
||||
|
||||
expect(screen.getByText("wiped", { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText("Foo Host", { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -755,6 +755,14 @@ const TAGGED_TEMPLATES = {
|
||||
</>
|
||||
);
|
||||
},
|
||||
wipedHost: (activity: IActivity) => {
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
wiped <b>{activity.details?.host_display_name}</b>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const getDetail = (
|
||||
@ -907,6 +915,9 @@ const getDetail = (
|
||||
case ActivityType.UnlockedHost: {
|
||||
return TAGGED_TEMPLATES.unlockedHost(activity);
|
||||
}
|
||||
case ActivityType.WipedHost: {
|
||||
return TAGGED_TEMPLATES.wipedHost(activity);
|
||||
}
|
||||
default: {
|
||||
return TAGGED_TEMPLATES.defaultActivityTemplate(activity);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ describe("Integrations Page", () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
app: { isMdmEnabledAndConfigured: true },
|
||||
app: { isMacMdmEnabledAndConfigured: true },
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -94,7 +94,7 @@ describe("Host Actions Dropdown", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
@ -122,7 +122,7 @@ describe("Host Actions Dropdown", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalMaintainer: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
@ -150,7 +150,7 @@ describe("Host Actions Dropdown", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
currentUser: createMockUser({
|
||||
teams: [createMockTeam({ id: 1, role: "admin" })],
|
||||
}),
|
||||
@ -179,7 +179,7 @@ describe("Host Actions Dropdown", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
currentUser: createMockUser({
|
||||
teams: [createMockTeam({ id: 1, role: "maintainer" })],
|
||||
}),
|
||||
@ -208,7 +208,7 @@ describe("Host Actions Dropdown", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
@ -235,7 +235,7 @@ describe("Host Actions Dropdown", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
@ -267,7 +267,7 @@ describe("Host Actions Dropdown", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
@ -402,7 +402,7 @@ describe("Host Actions Dropdown", () => {
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
@ -431,7 +431,7 @@ describe("Host Actions Dropdown", () => {
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
@ -460,7 +460,7 @@ describe("Host Actions Dropdown", () => {
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
@ -491,7 +491,7 @@ describe("Host Actions Dropdown", () => {
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
@ -520,7 +520,7 @@ describe("Host Actions Dropdown", () => {
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
@ -549,7 +549,7 @@ describe("Host Actions Dropdown", () => {
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
@ -578,7 +578,7 @@ describe("Host Actions Dropdown", () => {
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMdmEnabledAndConfigured: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
@ -601,5 +601,126 @@ describe("Host Actions Dropdown", () => {
|
||||
|
||||
expect(screen.queryByText("Unlock")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not renders when a mac host but does not have Fleet mac mdm enabled and configured", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMacMdmEnabledAndConfigured: false,
|
||||
isWindowsMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = render(
|
||||
<HostActionsDropdown
|
||||
hostTeamId={null}
|
||||
onSelect={noop}
|
||||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="locked"
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Actions"));
|
||||
|
||||
expect(screen.queryByText("Unlock")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Wipe action", () => {
|
||||
it("renders only when the host is unlocked", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = render(
|
||||
<HostActionsDropdown
|
||||
hostTeamId={null}
|
||||
onSelect={noop}
|
||||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Actions"));
|
||||
|
||||
expect(screen.getByText("Wipe")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not renders when a windows host but does not have Fleet windows mdm enabled and configured", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMacMdmEnabledAndConfigured: true,
|
||||
isWindowsMdmEnabledAndConfigured: false,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = render(
|
||||
<HostActionsDropdown
|
||||
hostTeamId={null}
|
||||
onSelect={noop}
|
||||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
mdmName="Fleet"
|
||||
hostPlatform="windows"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Actions"));
|
||||
|
||||
expect(screen.queryByText("Wipe")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not renders when a mac host but does not have Fleet mac mdm enabled and configured", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isMacMdmEnabledAndConfigured: false,
|
||||
isWindowsMdmEnabledAndConfigured: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = render(
|
||||
<HostActionsDropdown
|
||||
hostTeamId={null}
|
||||
onSelect={noop}
|
||||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Actions"));
|
||||
|
||||
expect(screen.queryByText("Wipe")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -38,7 +38,8 @@ const HostActionsDropdown = ({
|
||||
isPremiumTier = false,
|
||||
isGlobalAdmin = false,
|
||||
isGlobalMaintainer = false,
|
||||
isMdmEnabledAndConfigured = false,
|
||||
isMacMdmEnabledAndConfigured = false,
|
||||
isWindowsMdmEnabledAndConfigured = false,
|
||||
isSandboxMode = false,
|
||||
currentUser,
|
||||
} = useContext(AppContext);
|
||||
@ -67,7 +68,8 @@ const HostActionsDropdown = ({
|
||||
hostMdmEnrollmentStatus ?? ""
|
||||
),
|
||||
isFleetMdm: mdmName === "Fleet",
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
isWindowsMdmEnabledAndConfigured,
|
||||
doesStoreEncryptionKey: doesStoreEncryptionKey ?? false,
|
||||
isSandboxMode,
|
||||
hostMdmDeviceStatus,
|
||||
|
@ -44,11 +44,11 @@ const DEFAULT_OPTIONS = [
|
||||
value: "lock",
|
||||
disabled: false,
|
||||
},
|
||||
// {
|
||||
// label: "Wipe",
|
||||
// value: "wipe",
|
||||
// disabled: false,
|
||||
// },
|
||||
{
|
||||
label: "Wipe",
|
||||
value: "wipe",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "Unlock",
|
||||
value: "unlock",
|
||||
@ -74,7 +74,8 @@ interface IHostActionConfigOptions {
|
||||
isHostOnline: boolean;
|
||||
isEnrolledInMdm: boolean;
|
||||
isFleetMdm: boolean;
|
||||
isMdmEnabledAndConfigured: boolean;
|
||||
isMacMdmEnabledAndConfigured: boolean;
|
||||
isWindowsMdmEnabledAndConfigured: boolean;
|
||||
doesStoreEncryptionKey: boolean;
|
||||
isSandboxMode: boolean;
|
||||
hostMdmDeviceStatus: HostMdmDeviceStatusUIState;
|
||||
@ -93,11 +94,11 @@ const canEditMdm = (config: IHostActionConfigOptions) => {
|
||||
isTeamMaintainer,
|
||||
isEnrolledInMdm,
|
||||
isFleetMdm,
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
} = config;
|
||||
return (
|
||||
config.hostPlatform === "darwin" &&
|
||||
isMdmEnabledAndConfigured &&
|
||||
isMacMdmEnabledAndConfigured &&
|
||||
isEnrolledInMdm &&
|
||||
isFleetMdm &&
|
||||
(isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer)
|
||||
@ -107,7 +108,7 @@ const canEditMdm = (config: IHostActionConfigOptions) => {
|
||||
const canLockHost = ({
|
||||
isPremiumTier,
|
||||
hostPlatform,
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
isEnrolledInMdm,
|
||||
isFleetMdm,
|
||||
isGlobalAdmin,
|
||||
@ -120,7 +121,7 @@ const canLockHost = ({
|
||||
const canLockDarwin =
|
||||
hostPlatform === "darwin" &&
|
||||
isFleetMdm &&
|
||||
isMdmEnabledAndConfigured &&
|
||||
isMacMdmEnabledAndConfigured &&
|
||||
isEnrolledInMdm;
|
||||
|
||||
return (
|
||||
@ -143,23 +144,23 @@ const canWipeHost = ({
|
||||
isTeamObserver,
|
||||
isFleetMdm,
|
||||
isEnrolledInMdm,
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
isWindowsMdmEnabledAndConfigured,
|
||||
hostPlatform,
|
||||
hostMdmDeviceStatus,
|
||||
}: IHostActionConfigOptions) => {
|
||||
// TODO: remove when we work on wipe issue.
|
||||
return false;
|
||||
const hostMdmEnabled =
|
||||
(hostPlatform === "darwin" && isMacMdmEnabledAndConfigured) ||
|
||||
(hostPlatform === "windows" && isWindowsMdmEnabledAndConfigured);
|
||||
|
||||
// macOS and Windows hosts have the same conditions and can be wiped if they
|
||||
// are enrolled in MDM and the MDM is enabled.
|
||||
const canWipeMacOrWindows =
|
||||
(hostPlatform === "darwin" || hostPlatform === "windows") &&
|
||||
isFleetMdm &&
|
||||
isMdmEnabledAndConfigured &&
|
||||
isEnrolledInMdm;
|
||||
const canWipeMacOrWindows = hostMdmEnabled && isFleetMdm && isEnrolledInMdm;
|
||||
|
||||
return (
|
||||
isPremiumTier &&
|
||||
(hostPlatform === "linux" || canWipeMacOrWindows) &&
|
||||
hostMdmDeviceStatus === "unlocked" &&
|
||||
(isLinuxLike(hostPlatform) || canWipeMacOrWindows) &&
|
||||
(isGlobalAdmin ||
|
||||
isGlobalMaintainer ||
|
||||
isGlobalObserver ||
|
||||
@ -177,14 +178,14 @@ const canUnlock = ({
|
||||
isTeamMaintainer,
|
||||
isFleetMdm,
|
||||
isEnrolledInMdm,
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
hostPlatform,
|
||||
hostMdmDeviceStatus,
|
||||
}: IHostActionConfigOptions) => {
|
||||
const canLockDarwin =
|
||||
const canUnlockDarwin =
|
||||
hostPlatform === "darwin" &&
|
||||
isFleetMdm &&
|
||||
isMdmEnabledAndConfigured &&
|
||||
isMacMdmEnabledAndConfigured &&
|
||||
isEnrolledInMdm;
|
||||
|
||||
// "unlocking" for a macOS host means that somebody saw the unlock pin, but
|
||||
@ -198,7 +199,7 @@ const canUnlock = ({
|
||||
isPremiumTier &&
|
||||
isValidState &&
|
||||
(isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer) &&
|
||||
(canLockDarwin || hostPlatform === "windows" || isLinuxLike(hostPlatform))
|
||||
(canUnlockDarwin || hostPlatform === "windows" || isLinuxLike(hostPlatform))
|
||||
);
|
||||
};
|
||||
|
||||
@ -265,9 +266,9 @@ const filterOutOptions = (
|
||||
options = options.filter((option) => option.value !== "lock");
|
||||
}
|
||||
|
||||
// if (!canWipeHost(config)) {
|
||||
// options = options.filter((option) => option.value !== "wipe");
|
||||
// }
|
||||
if (!canWipeHost(config)) {
|
||||
options = options.filter((option) => option.value !== "wipe");
|
||||
}
|
||||
|
||||
if (!canUnlock(config)) {
|
||||
options = options.filter((option) => option.value !== "unlock");
|
||||
@ -292,7 +293,12 @@ const setOptionsAsDisabled = (
|
||||
};
|
||||
|
||||
let optionsToDisable: IDropdownOption[] = [];
|
||||
if (!isHostOnline) {
|
||||
if (
|
||||
!isHostOnline ||
|
||||
isDeviceStatusUpdating(hostMdmDeviceStatus) ||
|
||||
hostMdmDeviceStatus === "locked" ||
|
||||
hostMdmDeviceStatus === "wiped"
|
||||
) {
|
||||
optionsToDisable = optionsToDisable.concat(
|
||||
options.filter(
|
||||
(option) => option.value === "query" || option.value === "mdmOff"
|
||||
@ -304,16 +310,6 @@ const setOptionsAsDisabled = (
|
||||
options.filter((option) => option.value === "transfer")
|
||||
);
|
||||
}
|
||||
if (
|
||||
isDeviceStatusUpdating(hostMdmDeviceStatus) ||
|
||||
hostMdmDeviceStatus === "locked"
|
||||
) {
|
||||
optionsToDisable = optionsToDisable.concat(
|
||||
options.filter(
|
||||
(option) => option.value === "query" || option.value === "mdmOff"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
disableOptions(optionsToDisable);
|
||||
return options;
|
||||
|
@ -14,6 +14,7 @@ import { NotificationContext } from "context/notification";
|
||||
|
||||
import activitiesAPI, {
|
||||
IActivitiesResponse,
|
||||
IPastActivitiesResponse,
|
||||
IUpcomingActivitiesResponse,
|
||||
} from "services/entities/activities";
|
||||
import hostAPI from "services/entities/hosts";
|
||||
@ -90,6 +91,7 @@ import {
|
||||
HostMdmDeviceStatusUIState,
|
||||
getHostDeviceStatusUIState,
|
||||
} from "../helpers";
|
||||
import WipeModal from "./modals/WipeModal";
|
||||
|
||||
const baseClass = "host-details";
|
||||
|
||||
@ -164,6 +166,7 @@ const HostDetailsPage = ({
|
||||
);
|
||||
const [showLockHostModal, setShowLockHostModal] = useState(false);
|
||||
const [showUnlockHostModal, setShowUnlockHostModal] = useState(false);
|
||||
const [showWipeModal, setShowWipeModal] = useState(false);
|
||||
const [scriptDetailsId, setScriptDetailsId] = useState("");
|
||||
const [selectedPolicy, setSelectedPolicy] = useState<IHostPolicy | null>(
|
||||
null
|
||||
@ -366,9 +369,9 @@ const HostDetailsPage = ({
|
||||
isError: pastActivitiesIsError,
|
||||
refetch: refetchPastActivities,
|
||||
} = useQuery<
|
||||
IActivitiesResponse,
|
||||
IPastActivitiesResponse,
|
||||
Error,
|
||||
IActivitiesResponse,
|
||||
IPastActivitiesResponse,
|
||||
Array<{
|
||||
scope: string;
|
||||
pageIndex: number;
|
||||
@ -644,6 +647,9 @@ const HostDetailsPage = ({
|
||||
case "unlock":
|
||||
setShowUnlockHostModal(true);
|
||||
break;
|
||||
case "wipe":
|
||||
setShowWipeModal(true);
|
||||
break;
|
||||
default: // do nothing
|
||||
}
|
||||
};
|
||||
@ -976,6 +982,14 @@ const HostDetailsPage = ({
|
||||
onClose={() => setShowUnlockHostModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showWipeModal && (
|
||||
<WipeModal
|
||||
id={host.id}
|
||||
hostName={host.display_name}
|
||||
onSuccess={() => setHostMdmDeviceState("wiping")}
|
||||
onClose={() => setShowWipeModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</MainContent>
|
||||
);
|
||||
|
@ -0,0 +1,78 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import hostAPI from "services/entities/hosts";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
const baseClass = "wipe-modal";
|
||||
|
||||
interface IWipeModalProps {
|
||||
id: number;
|
||||
hostName: string;
|
||||
onSuccess: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const WipeModal = ({ id, hostName, onSuccess, onClose }: IWipeModalProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [lockChecked, setLockChecked] = React.useState(false);
|
||||
const [isWiping, setIsWiping] = React.useState(false);
|
||||
|
||||
const onWipe = async () => {
|
||||
setIsWiping(true);
|
||||
try {
|
||||
await hostAPI.wipeHost(id);
|
||||
onSuccess();
|
||||
renderFlash("success", "Success! Host is wiping.");
|
||||
} catch (error) {
|
||||
const err = error as AxiosError;
|
||||
renderFlash("error", err.message);
|
||||
}
|
||||
onClose();
|
||||
setIsWiping(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal className={baseClass} title="Wipe host" onExit={onClose}>
|
||||
<>
|
||||
<div className={`${baseClass}__modal-content`}>
|
||||
<p>All content will be erased on this host.</p>
|
||||
<div className={`${baseClass}__confirm-message`}>
|
||||
<span>
|
||||
<b>Please check to confirm:</b>
|
||||
</span>
|
||||
<Checkbox
|
||||
wrapperClassName={`${baseClass}__wipe-checkbox`}
|
||||
value={lockChecked}
|
||||
onChange={(value: boolean) => setLockChecked(value)}
|
||||
>
|
||||
I wish to wipe <b>{hostName}</b>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onWipe}
|
||||
variant="alert"
|
||||
className="delete-loading"
|
||||
disabled={!lockChecked}
|
||||
isLoading={isWiping}
|
||||
>
|
||||
Wipe
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WipeModal;
|
@ -0,0 +1,14 @@
|
||||
.wipe-modal {
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__modal-content {
|
||||
display: grid;
|
||||
gap: $pad-large;
|
||||
}
|
||||
|
||||
&__wipe-checkbox {
|
||||
margin-top: $pad-small;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./WipeModal";
|
@ -2,7 +2,10 @@ import React from "react";
|
||||
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
|
||||
|
||||
import { IActivityDetails } from "interfaces/activity";
|
||||
import { IActivitiesResponse } from "services/entities/activities";
|
||||
import {
|
||||
IPastActivitiesResponse,
|
||||
IUpcomingActivitiesResponse,
|
||||
} from "services/entities/activities";
|
||||
|
||||
import Card from "components/Card";
|
||||
import TabsWrapper from "components/TabsWrapper";
|
||||
@ -45,7 +48,7 @@ const UpcomingTooltip = () => {
|
||||
|
||||
interface IActivityProps {
|
||||
activeTab: "past" | "upcoming";
|
||||
activities?: IActivitiesResponse;
|
||||
activities?: IPastActivitiesResponse | IUpcomingActivitiesResponse;
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
upcomingCount: number;
|
||||
@ -93,7 +96,7 @@ const Activity = ({
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<PastActivityFeed
|
||||
activities={activities}
|
||||
activities={activities as IPastActivitiesResponse | undefined}
|
||||
onDetailsClick={onShowDetails}
|
||||
isError={isError}
|
||||
onNextPage={onNextPage}
|
||||
@ -103,7 +106,7 @@ const Activity = ({
|
||||
<TabPanel>
|
||||
<UpcomingTooltip />
|
||||
<UpcomingActivityFeed
|
||||
activities={activities}
|
||||
activities={activities as IUpcomingActivitiesResponse | undefined}
|
||||
onDetailsClick={onShowDetails}
|
||||
isError={isError}
|
||||
onNextPage={onNextPage}
|
||||
|
@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
ActivityType,
|
||||
IHostPastActivityType,
|
||||
IPastActivity,
|
||||
} from "interfaces/activity";
|
||||
|
||||
import { ShowActivityDetailsHandler } from "./Activity";
|
||||
|
||||
import RanScriptActivityItem from "./ActivityItems/RanScriptActivityItem";
|
||||
import LockedHostActivityItem from "./ActivityItems/LockedHostActivityItem";
|
||||
import UnlockedHostActivityItem from "./ActivityItems/UnlockedHostActivityItem";
|
||||
|
||||
/** The component props that all host activity items must adhere to */
|
||||
export interface IHostActivityItemComponentProps {
|
||||
activity: IPastActivity;
|
||||
}
|
||||
|
||||
/** Used for activity items component that need a show details handler */
|
||||
export interface IHostActivityItemComponentPropsWithShowDetails
|
||||
extends IHostActivityItemComponentProps {
|
||||
onShowDetails: ShowActivityDetailsHandler;
|
||||
}
|
||||
|
||||
export const pastActivityComponentMap: Record<
|
||||
IHostPastActivityType,
|
||||
| React.FC<IHostActivityItemComponentProps>
|
||||
| React.FC<IHostActivityItemComponentPropsWithShowDetails>
|
||||
> = {
|
||||
[ActivityType.RanScript]: RanScriptActivityItem,
|
||||
[ActivityType.LockedHost]: LockedHostActivityItem,
|
||||
[ActivityType.UnlockedHost]: UnlockedHostActivityItem,
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
import { IHostActivityItemComponentProps } from "../../ActivityConfig";
|
||||
import HostActivityItem from "../../HostActivityItem";
|
||||
|
||||
const baseClass = "locked-host-activity-item";
|
||||
|
||||
const LockedHostActivityItem = ({
|
||||
activity,
|
||||
}: IHostActivityItemComponentProps) => {
|
||||
return (
|
||||
<HostActivityItem className={baseClass} activity={activity}>
|
||||
<b>{activity.actor_full_name}</b> locked this host.
|
||||
</HostActivityItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default LockedHostActivityItem;
|
@ -0,0 +1 @@
|
||||
export { default } from "./LockedHostActivityItem";
|
@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
|
||||
import { formatScriptNameForActivityItem } from "utilities/helpers";
|
||||
|
||||
import HostActivityItem from "../../HostActivityItem";
|
||||
import { IHostActivityItemComponentPropsWithShowDetails } from "../../ActivityConfig";
|
||||
import ShowDetailsButton from "../../ShowDetailsButton";
|
||||
|
||||
const baseClass = "ran-script-activity-item";
|
||||
|
||||
const RanScriptActivityItem = ({
|
||||
activity,
|
||||
onShowDetails,
|
||||
}: IHostActivityItemComponentPropsWithShowDetails) => {
|
||||
return (
|
||||
<HostActivityItem className={baseClass} activity={activity}>
|
||||
<b>{activity.actor_full_name}</b>
|
||||
<>
|
||||
{" "}
|
||||
ran {formatScriptNameForActivityItem(activity.details?.script_name)} on
|
||||
this host.{" "}
|
||||
<ShowDetailsButton activity={activity} onShowDetails={onShowDetails} />
|
||||
</>
|
||||
</HostActivityItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default RanScriptActivityItem;
|
@ -0,0 +1 @@
|
||||
export { default } from "./RanScriptActivityItem";
|
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
import { IHostActivityItemComponentProps } from "../../ActivityConfig";
|
||||
import HostActivityItem from "../../HostActivityItem";
|
||||
|
||||
const baseClass = "unlocked-host-activity-item";
|
||||
|
||||
const UnlockedHostActivityItem = ({
|
||||
activity,
|
||||
}: IHostActivityItemComponentProps) => {
|
||||
return (
|
||||
<HostActivityItem className={baseClass} activity={activity}>
|
||||
<b>{activity.actor_full_name}</b> unlocked this host.
|
||||
</HostActivityItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnlockedHostActivityItem;
|
@ -0,0 +1 @@
|
||||
export { default } from "./UnlockedHostActivityItem";
|
@ -0,0 +1,94 @@
|
||||
import React from "react";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { formatDistanceToNowStrict } from "date-fns";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { IActivity } from "interfaces/activity";
|
||||
import {
|
||||
addGravatarUrlToResource,
|
||||
internationalTimeFormat,
|
||||
} from "utilities/helpers";
|
||||
import { DEFAULT_GRAVATAR_LINK } from "utilities/constants";
|
||||
|
||||
import Avatar from "components/Avatar";
|
||||
|
||||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
const baseClass = "host-activity-item";
|
||||
|
||||
interface IHostActivityItemProps {
|
||||
activity: IActivity;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper that will render all the common elements of a host activity item.
|
||||
* This includes the avatar, the created at timestamp, and a dash to separate
|
||||
* the activity items. The `children` will be the specific details of the activity
|
||||
* implemented in the component that uses this wrapper.
|
||||
*/
|
||||
const HostActivityItem = ({
|
||||
activity,
|
||||
children,
|
||||
className,
|
||||
}: IHostActivityItemProps) => {
|
||||
const { actor_email } = activity;
|
||||
const { gravatar_url } = actor_email
|
||||
? addGravatarUrlToResource({ email: actor_email })
|
||||
: { gravatar_url: DEFAULT_GRAVATAR_LINK };
|
||||
|
||||
// wrapped just in case the date string does not parse correctly
|
||||
let activityCreatedAt: Date | null = null;
|
||||
try {
|
||||
activityCreatedAt = new Date(activity.created_at);
|
||||
} catch (e) {
|
||||
activityCreatedAt = null;
|
||||
}
|
||||
|
||||
const classNames = classnames(baseClass, className);
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<Avatar
|
||||
className={`${baseClass}__avatar-image`}
|
||||
user={{ gravatar_url }}
|
||||
size="small"
|
||||
hasWhiteBackground
|
||||
/>
|
||||
<div className={`${baseClass}__details-wrapper`}>
|
||||
<div className={"activity-details"}>
|
||||
<span className={`${baseClass}__details-topline`}>
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
<br />
|
||||
<span
|
||||
className={`${baseClass}__details-bottomline`}
|
||||
data-tip
|
||||
data-for={`activity-${activity.id}`}
|
||||
>
|
||||
{activityCreatedAt &&
|
||||
formatDistanceToNowStrict(activityCreatedAt, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
{activityCreatedAt && (
|
||||
<ReactTooltip
|
||||
className="date-tooltip"
|
||||
place="top"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={`activity-${activity.id}`}
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
{internationalTimeFormat(activityCreatedAt)}
|
||||
</ReactTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__dash`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HostActivityItem;
|
@ -1,4 +1,4 @@
|
||||
.past-activity {
|
||||
.host-activity-item {
|
||||
display: grid; // Grid system is used to create variable dashed line lengths
|
||||
grid-template-columns: 16px 16px 1fr;
|
||||
grid-template-rows: 32px max-content;
|
||||
@ -62,4 +62,5 @@
|
||||
padding-bottom: $pad-xxlarge;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./HostActivityItem";
|
@ -1,141 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { formatDistanceToNowStrict } from "date-fns";
|
||||
|
||||
import Avatar from "components/Avatar";
|
||||
import Icon from "components/Icon";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
import { COLORS } from "styles/var/colors";
|
||||
import { DEFAULT_GRAVATAR_LINK } from "utilities/constants";
|
||||
import {
|
||||
addGravatarUrlToResource,
|
||||
formatScriptNameForActivityItem,
|
||||
internationalTimeFormat,
|
||||
} from "utilities/helpers";
|
||||
import { IActivity } from "interfaces/activity";
|
||||
import { ShowActivityDetailsHandler } from "../Activity";
|
||||
|
||||
const baseClass = "past-activity";
|
||||
|
||||
interface IPastActivityProps {
|
||||
activity: IActivity;
|
||||
// TODO: To handle clicks for different activity types, this could be refactored as a reducer that
|
||||
// takes the activity and dispatches the relevant show details action based on the activity type
|
||||
onDetailsClick: ShowActivityDetailsHandler;
|
||||
}
|
||||
|
||||
const RanScriptActivityDetails = ({
|
||||
activity,
|
||||
onDetailsClick,
|
||||
}: Pick<IPastActivityProps, "activity" | "onDetailsClick">) => (
|
||||
<span className={`${baseClass}__details-topline`}>
|
||||
<b>{activity.actor_full_name}</b>
|
||||
<>
|
||||
{" "}
|
||||
ran {formatScriptNameForActivityItem(activity.details?.script_name)} on
|
||||
this host.{" "}
|
||||
<Button
|
||||
className={`${baseClass}__show-query-link`}
|
||||
variant="text-link"
|
||||
onClick={() => onDetailsClick?.(activity)}
|
||||
>
|
||||
Show details{" "}
|
||||
<Icon className={`${baseClass}__show-query-icon`} name="eye" />
|
||||
</Button>
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
|
||||
const LockedHostActivityDetails = ({
|
||||
activity,
|
||||
}: Pick<IPastActivityProps, "activity">) => (
|
||||
<span className={`${baseClass}__details-topline`}>
|
||||
<b>{activity.actor_full_name}</b> locked this host.
|
||||
</span>
|
||||
);
|
||||
|
||||
const UnlockedHostActivityDetails = ({
|
||||
activity,
|
||||
}: Pick<IPastActivityProps, "activity">) => (
|
||||
<span className={`${baseClass}__details-topline`}>
|
||||
<b>{activity.actor_full_name}</b>{" "}
|
||||
{activity.details?.host_platform === "darwin"
|
||||
? "viewed the six-digit unlock PIN for"
|
||||
: "unlocked"}{" "}
|
||||
this host.
|
||||
</span>
|
||||
);
|
||||
|
||||
const PastActivityTopline = ({
|
||||
activity,
|
||||
onDetailsClick,
|
||||
}: IPastActivityProps) => {
|
||||
switch (activity.type) {
|
||||
case "ran_script":
|
||||
return (
|
||||
<RanScriptActivityDetails
|
||||
activity={activity}
|
||||
onDetailsClick={onDetailsClick}
|
||||
/>
|
||||
);
|
||||
case "locked_host":
|
||||
return <LockedHostActivityDetails activity={activity} />;
|
||||
case "unlocked_host":
|
||||
return <UnlockedHostActivityDetails activity={activity} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Combine this with ./UpcomingActivity/UpcomingActivity.tsx and
|
||||
// frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
|
||||
const PastActivity = ({ activity, onDetailsClick }: IPastActivityProps) => {
|
||||
const { actor_email } = activity;
|
||||
const { gravatar_url } = actor_email
|
||||
? addGravatarUrlToResource({ email: actor_email })
|
||||
: { gravatar_url: DEFAULT_GRAVATAR_LINK };
|
||||
const activityCreatedAt = new Date(activity.created_at);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Avatar
|
||||
className={`${baseClass}__avatar-image`}
|
||||
user={{ gravatar_url }}
|
||||
size="small"
|
||||
hasWhiteBackground
|
||||
/>
|
||||
<div className={`${baseClass}__details-wrapper`}>
|
||||
<div className="activity-details">
|
||||
<PastActivityTopline
|
||||
activity={activity}
|
||||
onDetailsClick={onDetailsClick}
|
||||
/>
|
||||
<br />
|
||||
<span
|
||||
className={`${baseClass}__details-bottomline`}
|
||||
data-tip
|
||||
data-for={`activity-${activity.id}`}
|
||||
>
|
||||
{formatDistanceToNowStrict(activityCreatedAt, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
<ReactTooltip
|
||||
className="date-tooltip"
|
||||
place="top"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={`activity-${activity.id}`}
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
{internationalTimeFormat(activityCreatedAt)}
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__dash`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PastActivity;
|
@ -1 +0,0 @@
|
||||
export { default } from "./PastActivity";
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { IActivity } from "interfaces/activity";
|
||||
import { IActivitiesResponse } from "services/entities/activities";
|
||||
import { IPastActivity } from "interfaces/activity";
|
||||
import { IPastActivitiesResponse } from "services/entities/activities";
|
||||
|
||||
// @ts-ignore
|
||||
import FleetIcon from "components/icons/FleetIcon";
|
||||
@ -9,13 +9,14 @@ import Button from "components/buttons/Button";
|
||||
import DataError from "components/DataError";
|
||||
|
||||
import EmptyFeed from "../EmptyFeed/EmptyFeed";
|
||||
import PastActivity from "../PastActivity/PastActivity";
|
||||
import { ShowActivityDetailsHandler } from "../Activity";
|
||||
|
||||
import { pastActivityComponentMap } from "../ActivityConfig";
|
||||
|
||||
const baseClass = "past-activity-feed";
|
||||
|
||||
interface IPastActivityFeedProps {
|
||||
activities?: IActivitiesResponse;
|
||||
activities?: IPastActivitiesResponse;
|
||||
isError?: boolean;
|
||||
onDetailsClick: ShowActivityDetailsHandler;
|
||||
onNextPage: () => void;
|
||||
@ -52,9 +53,16 @@ const PastActivityFeed = ({
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div>
|
||||
{activitiesList.map((activity: IActivity) => (
|
||||
<PastActivity activity={activity} onDetailsClick={onDetailsClick} />
|
||||
))}
|
||||
{activitiesList.map((activity: IPastActivity) => {
|
||||
const ActivityItemComponent = pastActivityComponentMap[activity.type];
|
||||
return (
|
||||
<ActivityItemComponent
|
||||
key={activity.id}
|
||||
activity={activity}
|
||||
onShowDetails={onDetailsClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={`${baseClass}__pagination`}>
|
||||
<Button
|
||||
|
@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
|
||||
import { IActivity } from "interfaces/activity";
|
||||
|
||||
import Icon from "components/Icon";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
import { ShowActivityDetailsHandler } from "../Activity";
|
||||
|
||||
const baseClass = "show-details-button";
|
||||
|
||||
interface IShowDetailsButtonProps {
|
||||
activity: IActivity;
|
||||
onShowDetails: ShowActivityDetailsHandler;
|
||||
}
|
||||
|
||||
const ShowDetailsButton = ({
|
||||
activity,
|
||||
onShowDetails,
|
||||
}: IShowDetailsButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
className={baseClass}
|
||||
variant="text-link"
|
||||
onClick={() => onShowDetails?.(activity)}
|
||||
>
|
||||
Show details{" "}
|
||||
<Icon className={`${baseClass}__show-details-icon`} name="eye" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowDetailsButton;
|
@ -0,0 +1,5 @@
|
||||
.show-details-button {
|
||||
&__show-details-icon {
|
||||
margin-left: $pad-xsmall;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./ShowDetailsButton";
|
@ -12,7 +12,8 @@
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: $ui-error;
|
||||
color: $core-white;
|
||||
background-color: $core-vibrant-red;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,20 @@ export const DEVICE_STATUS_TAGS: DeviceStatusTagConfig = {
|
||||
generateTooltip: (platform) =>
|
||||
"Host will lock when it comes online. If the host is online, it will lock the next time it checks in to Fleet.",
|
||||
},
|
||||
wiped: {
|
||||
title: "WIPED",
|
||||
tagType: "error",
|
||||
generateTooltip: (platform) =>
|
||||
platform === "darwin"
|
||||
? "Host is wiped. To prevent the host from automatically reenrolling to Fleet, first release the host from Apple Business Manager and then delete the host in Fleet."
|
||||
: "Host is wiped.",
|
||||
},
|
||||
wiping: {
|
||||
title: "WIPE PENDING",
|
||||
tagType: "error",
|
||||
generateTooltip: () =>
|
||||
"Host will wipe when it comes online. If the host is online, it will wipe the next time it checks in to Fleet.",
|
||||
},
|
||||
};
|
||||
|
||||
// We exclude "unlocked" as we dont display a tooltip for it.
|
||||
@ -66,4 +80,14 @@ export const REFETCH_TOOLTIP_MESSAGES: Record<
|
||||
You can't fetch data from <br /> a locked host.
|
||||
</>
|
||||
),
|
||||
wiping: (
|
||||
<>
|
||||
You can't fetch data from <br /> a wiping host.
|
||||
</>
|
||||
),
|
||||
wiped: (
|
||||
<>
|
||||
You can't fetch data from <br /> a wiped host.
|
||||
</>
|
||||
),
|
||||
} as const;
|
||||
|
@ -1,5 +1,4 @@
|
||||
/** Helpers used across the host details and my device pages and components. */
|
||||
import { is } from "date-fns/locale";
|
||||
import { HostMdmDeviceStatus, HostMdmPendingAction } from "interfaces/host";
|
||||
import {
|
||||
IHostMdmProfile,
|
||||
@ -39,7 +38,9 @@ export type HostMdmDeviceStatusUIState =
|
||||
| "unlocked"
|
||||
| "locked"
|
||||
| "unlocking"
|
||||
| "locking";
|
||||
| "locking"
|
||||
| "wiped"
|
||||
| "wiping";
|
||||
|
||||
// Exclude the empty string from HostPendingAction as that doesn't represent a
|
||||
// valid device status.
|
||||
@ -51,9 +52,11 @@ const API_TO_UI_DEVICE_STATUS_MAP: Record<
|
||||
locked: "locked",
|
||||
unlock: "unlocking",
|
||||
lock: "locking",
|
||||
wiped: "wiped",
|
||||
wipe: "wiping",
|
||||
};
|
||||
|
||||
const deviceUpdatingStates = ["unlocking", "locking"] as const;
|
||||
const deviceUpdatingStates = ["unlocking", "locking", "wiping"] as const;
|
||||
|
||||
/**
|
||||
* Gets the current UI state for the host device status. This helps us know what
|
||||
@ -74,7 +77,7 @@ export const getHostDeviceStatusUIState = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Helps check if our device status UI state is in an updating state.
|
||||
* Checks if our device status UI state is in an updating state.
|
||||
*/
|
||||
export const isDeviceStatusUpdating = (
|
||||
deviceStatus: HostMdmDeviceStatusUIState
|
||||
|
@ -1,5 +1,5 @@
|
||||
import endpoints from "utilities/endpoints";
|
||||
import { IActivity } from "interfaces/activity";
|
||||
import { IActivity, IPastActivity } from "interfaces/activity";
|
||||
import sendRequest from "services";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
@ -16,6 +16,14 @@ export interface IActivitiesResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export interface IPastActivitiesResponse {
|
||||
activities: IPastActivity[] | null;
|
||||
meta: {
|
||||
has_next_results: boolean;
|
||||
has_previous_results: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IUpcomingActivitiesResponse extends IActivitiesResponse {
|
||||
count: number;
|
||||
}
|
||||
@ -45,7 +53,7 @@ export default {
|
||||
id: number,
|
||||
page = DEFAULT_PAGE,
|
||||
perPage = DEFAULT_PAGE_SIZE
|
||||
): Promise<IActivitiesResponse> => {
|
||||
): Promise<IPastActivitiesResponse> => {
|
||||
const { HOST_PAST_ACTIVITIES } = endpoints;
|
||||
|
||||
const queryParams = {
|
||||
|
@ -397,8 +397,14 @@ export default {
|
||||
const { HOST_LOCK } = endpoints;
|
||||
return sendRequest("POST", HOST_LOCK(id));
|
||||
},
|
||||
|
||||
unlockHost: (id: number): Promise<IUnlockHostResponse> => {
|
||||
const { HOST_UNLOCK } = endpoints;
|
||||
return sendRequest("POST", HOST_UNLOCK(id));
|
||||
},
|
||||
|
||||
wipeHost: (id: number) => {
|
||||
const { HOST_WIPE } = endpoints;
|
||||
return sendRequest("POST", HOST_WIPE(id));
|
||||
},
|
||||
};
|
||||
|
@ -43,6 +43,7 @@ export default {
|
||||
HOSTS_TRANSFER_BY_FILTER: `/${API_VERSION}/fleet/hosts/transfer/filter`,
|
||||
HOST_LOCK: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/lock`,
|
||||
HOST_UNLOCK: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/unlock`,
|
||||
HOST_WIPE: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/wipe`,
|
||||
|
||||
INVITES: `/${API_VERSION}/fleet/invites`,
|
||||
LABELS: `/${API_VERSION}/fleet/labels`,
|
||||
|
@ -13,10 +13,14 @@ export const isPremiumTier = (config: IConfig): boolean => {
|
||||
return config.license.tier === "premium";
|
||||
};
|
||||
|
||||
export const isMdmEnabledAndConfigured = (config: IConfig): boolean => {
|
||||
export const isMacMdmEnabledAndConfigured = (config: IConfig): boolean => {
|
||||
return Boolean(config.mdm.enabled_and_configured);
|
||||
};
|
||||
|
||||
export const isWindowsMdmEnabledAndConfigured = (config: IConfig): boolean => {
|
||||
return Boolean(config.mdm.windows_enabled_and_configured);
|
||||
};
|
||||
|
||||
export const isGlobalAdmin = (user: IUser): boolean => {
|
||||
return user.global_role === "admin";
|
||||
};
|
||||
@ -142,7 +146,8 @@ export default {
|
||||
isSandboxMode,
|
||||
isFreeTier,
|
||||
isPremiumTier,
|
||||
isMdmEnabledAndConfigured,
|
||||
isMacMdmEnabledAndConfigured,
|
||||
isWindowsMdmEnabledAndConfigured,
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isGlobalObserver,
|
||||
|
Loading…
Reference in New Issue
Block a user