diff --git a/.vscode/typescriptreact.code-snippets b/.vscode/typescriptreact.code-snippets
index 4bba69a4ac..6cde7b7e8d 100644
--- a/.vscode/typescriptreact.code-snippets
+++ b/.vscode/typescriptreact.code-snippets
@@ -5,15 +5,24 @@
"body": [
"import React from \"react\";",
"",
+ "const baseClass = \"${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}\";",
+ "",
"interface I${TM_FILENAME_BASE}Props {}",
"",
"const $TM_FILENAME_BASE = ({}: I${TM_FILENAME_BASE}Props) => {",
- " return <>>;",
+ "\treturn
;",
"};",
"",
"export default $TM_FILENAME_BASE;",
"",
],
"description": "Creates a React stateless component with the typescrip interface setup"
+ },
+ "Fleet - baseClass classname": {
+ "scope": "typescriptreact,javascriptreact",
+ "prefix": "bc",
+ "body": [
+ "`\\${baseClass}__$0`"
+ ]
}
}
diff --git a/assets/images/icon-action-close-16x15@2x.png b/assets/images/icon-action-close-16x15@2x.png
deleted file mode 100644
index e574603fc5..0000000000
Binary files a/assets/images/icon-action-close-16x15@2x.png and /dev/null differ
diff --git a/assets/images/icon-action-delete-14x14@2x.png b/assets/images/icon-action-delete-14x14@2x.png
deleted file mode 100644
index 8e88170918..0000000000
Binary files a/assets/images/icon-action-delete-14x14@2x.png and /dev/null differ
diff --git a/assets/images/icon-action-query-16x16@2x.png b/assets/images/icon-action-query-16x16@2x.png
deleted file mode 100644
index a32779cfb9..0000000000
Binary files a/assets/images/icon-action-query-16x16@2x.png and /dev/null differ
diff --git a/cypress/integration/all/app/hosts.spec.ts b/cypress/integration/all/app/hosts.spec.ts
index dde78e407d..9011b8fbac 100644
--- a/cypress/integration/all/app/hosts.spec.ts
+++ b/cypress/integration/all/app/hosts.spec.ts
@@ -161,27 +161,6 @@ describe("Hosts flow", () => {
cy.getAttached(".button--text-link").first().click();
});
});
- it("runs query on an existing host", () => {
- cy.getAttached(".host-details__action-button-container").within(() => {
- cy.getAttached('img[alt="Query host icon"]').click();
- });
-
- cy.getAttached(".select-query-modal__modal").within(() => {
- cy.getAttached(".modal-query-button").eq(2).click();
- });
-
- cy.getAttached(".query-form__button-wrap--new-query").within(() => {
- cy.findByText(/run query/i)
- .should("exist")
- .click();
- });
- cy.getAttached(".query-page__wrapper").within(() => {
- cy.getAttached(".data-table").within(() => {
- cy.findByText(hostname).should("exist");
- });
- cy.findByText(/run/i).click();
- });
- });
it("renders and searches the host's users", () => {
cy.getAttached(".section--users").within(() => {
cy.getAttached("tbody>tr").should("have.length.greaterThan", 0);
@@ -278,9 +257,5 @@ describe("Hosts flow", () => {
});
}
);
- it("deletes an existing host", () => {
- hostDetailsPage.allowsDeleteHost();
- hostDetailsPage.verifiesDeletedHost(hostname);
- });
});
});
diff --git a/cypress/integration/free/admin.spec.ts b/cypress/integration/free/admin.spec.ts
index d82493359a..e7acc5083e 100644
--- a/cypress/integration/free/admin.spec.ts
+++ b/cypress/integration/free/admin.spec.ts
@@ -104,24 +104,12 @@ describe(
hostDetailsPage.verifiesTeamsisDisabled();
hostDetailsPage.hidesButton("Transfer");
});
- it("allows admin to delete the host", () => {
- hostDetailsPage.allowsDeleteHost();
- });
- it("allows admin to custom query the host", () => {
- hostDetailsPage.allowsCustomQueryHost();
- });
});
describe("Manage software page", () => {
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
manageSoftwarePage.visitManageSoftwarePage();
});
- // it(`displays "Vulnerabilities" column`, () => {
- // cy.getAttached("thead").within(() => {
- // cy.findByText(/vulnerabilities/i).should("exist");
- // cy.findByText(/probability of exploit/i).should("not.exist");
- // });
- // });
it("allows admin to click 'Manage automations' button", () => {
manageSoftwarePage.allowsManageAutomations();
});
diff --git a/cypress/integration/free/maintainer.spec.ts b/cypress/integration/free/maintainer.spec.ts
index 8a2866ea47..c2e25f8ff1 100644
--- a/cypress/integration/free/maintainer.spec.ts
+++ b/cypress/integration/free/maintainer.spec.ts
@@ -99,12 +99,6 @@ describe(
it("allows maintainer to create an operating system policy", () => {
hostDetailsPage.allowsCreateOsPolicy();
});
- it("allows maintainer to custom query the host", () => {
- hostDetailsPage.allowsCustomQueryHost();
- });
- it("allows maintainer to delete the host", () => {
- hostDetailsPage.allowsDeleteHost();
- });
});
describe("Manage software page", () => {
beforeEach(() => manageSoftwarePage.visitManageSoftwarePage());
diff --git a/cypress/integration/pages/hostDetailsPage.ts b/cypress/integration/pages/hostDetailsPage.ts
index 1c193c5676..5b240f4c67 100644
--- a/cypress/integration/pages/hostDetailsPage.ts
+++ b/cypress/integration/pages/hostDetailsPage.ts
@@ -18,68 +18,6 @@ const hostDetailsPage = {
cy.contains("button", text).should("not.exist");
},
- allowsDeleteHost: () => {
- cy.findByRole("button", { name: /delete/i }).click();
- cy.getAttached(".modal__modal_container").within(() => {
- cy.findByRole("button", { name: /delete/i }).should("be.enabled");
- });
- },
-
- verifiesDeletedHost: (hostname: string) => {
- cy.getAttached(".modal__modal_container")
- .within(() => {
- cy.findByRole("button", { name: /delete/i }).click();
- })
- .then(() => {
- cy.findByText(/add your devices to fleet/i).should("exist");
- cy.findByText(/add hosts/i).should("exist");
- cy.findByText(/about this host/i).should("not.exist");
- cy.findByText(hostname).should("not.exist");
- });
- },
-
- allowsTransferHost: (create?: boolean) => {
- cy.findByRole("button", { name: /transfer/i }).click();
- if (create) {
- cy.findByText(/create a team/i).should("exist");
- } else {
- cy.findByText(/create a team/i).should("not.exist");
- }
- cy.getAttached(".Select-control").click();
- cy.getAttached(".Select-menu").within(() => {
- cy.findByText(/no team/i).should("exist");
- cy.findByText(/oranges/i).should("exist");
- cy.findByText(/apples/i).click();
- });
- cy.getAttached(".transfer-host-modal .modal-cta-wrap")
- .contains("button", /transfer/i)
- .should("be.enabled");
- },
-
- verifiesTransferredHost: () => {
- cy.getAttached(".transfer-host-modal .modal-cta-wrap")
- .contains("button", /transfer/i)
- .click();
- cy.findByText(/transferred to apples/i).should("exist");
- cy.findByText(/team/i).next().contains("Apples");
- },
-
- allowsCustomQueryHost: () => {
- cy.findByRole("button", { name: /query/i }).click();
- cy.findByRole("button", { name: /create custom query/i }).should(
- "be.enabled"
- );
- cy.getAttached(".modal__ex").within(() => {
- cy.findByRole("button").click();
- });
- },
-
- hidesCustomQueryHost: () => {
- cy.findByRole("button", { name: /query/i }).click();
- cy.contains("button", /create custom query/i).should("not.exist");
- cy.getAttached(".modal__ex").click();
- },
-
allowsCreateOsPolicy: () => {
cy.getAttached(".info-flex").within(() => {
cy.findByText(/ubuntu/i).should("exist");
diff --git a/cypress/integration/premium/admin.spec.ts b/cypress/integration/premium/admin.spec.ts
index 58503cc2ca..5b9e371f23 100644
--- a/cypress/integration/premium/admin.spec.ts
+++ b/cypress/integration/premium/admin.spec.ts
@@ -386,19 +386,9 @@ describe("Premium tier - Global Admin user", () => {
});
describe("Host details page", () => {
beforeEach(() => hostDetailsPage.visitsHostDetailsPage(1));
- it("allows global admin to transfer host to an existing team", () => {
- hostDetailsPage.allowsTransferHost("andCreate");
- hostDetailsPage.verifiesTransferredHost();
- });
it("allows global admin to create an operating system policy", () => {
hostDetailsPage.allowsCreateOsPolicy();
});
- it("allows global admin to custom query a host", () => {
- hostDetailsPage.allowsCustomQueryHost();
- });
- it("allows global admin to delete a host", () => {
- hostDetailsPage.allowsDeleteHost();
- });
});
describe("Manage software page", () => {
beforeEach(() => {
diff --git a/cypress/integration/premium/maintainer.spec.ts b/cypress/integration/premium/maintainer.spec.ts
index 27f665f253..76f8b82a7d 100644
--- a/cypress/integration/premium/maintainer.spec.ts
+++ b/cypress/integration/premium/maintainer.spec.ts
@@ -82,19 +82,9 @@ describe("Premium tier - Maintainer user", () => {
beforeEach(() => {
hostDetailsPage.visitsHostDetailsPage(1);
});
- it("allows global maintainer to transfer host to an existing team", () => {
- hostDetailsPage.allowsTransferHost();
- hostDetailsPage.verifiesTransferredHost();
- });
it("allows global maintainer to create an operating system policy", () => {
hostDetailsPage.allowsCreateOsPolicy();
});
- it("allows global maintainer to custom query a host", () => {
- hostDetailsPage.allowsCustomQueryHost();
- });
- it("allows global maintainer to delete a host", () => {
- hostDetailsPage.allowsDeleteHost();
- });
});
describe("Manage software page", () => {
beforeEach(() => manageSoftwarePage.visitManageSoftwarePage());
diff --git a/cypress/integration/premium/observer.spec.ts b/cypress/integration/premium/observer.spec.ts
index 786608c07d..c255501bef 100644
--- a/cypress/integration/premium/observer.spec.ts
+++ b/cypress/integration/premium/observer.spec.ts
@@ -85,9 +85,6 @@ describe("Premium tier - Observer user", () => {
beforeEach(() => hostDetailsPage.visitsHostDetailsPage(1));
it("should render elements according to role-based access controls", () => {
hostDetailsPage.verifiesTeam("Apples");
- hostDetailsPage.hidesButton("Transfer");
- hostDetailsPage.hidesButton("Delete");
- hostDetailsPage.hidesCustomQueryHost();
hostDetailsPage.hidesCreateOSPolicy();
});
});
diff --git a/cypress/integration/premium/team_admin.spec.ts b/cypress/integration/premium/team_admin.spec.ts
index 4a6ebcff87..e4861e146f 100644
--- a/cypress/integration/premium/team_admin.spec.ts
+++ b/cypress/integration/premium/team_admin.spec.ts
@@ -87,12 +87,6 @@ describe("Premium tier - Team Admin user", () => {
it("allows team admin to create an operating system policy", () => {
hostDetailsPage.allowsCreateOsPolicy();
});
- it("allows team admin to query host, delete host but not transfer host", () => {
- hostDetailsPage.allowsCustomQueryHost();
- hostDetailsPage.allowsDeleteHost();
- hostDetailsPage.verifiesDeletedHost;
- hostDetailsPage.hidesButton("Transfer");
- });
});
describe("Manage software page", () => {
beforeEach(() => manageSoftwarePage.visitManageSoftwarePage());
diff --git a/frontend/__mocks__/hostMock.ts b/frontend/__mocks__/hostMock.ts
index 0e11c777b1..af722c18c4 100644
--- a/frontend/__mocks__/hostMock.ts
+++ b/frontend/__mocks__/hostMock.ts
@@ -33,6 +33,7 @@ const DEFAULT_HOST_MOCK: IHost = {
hardware_serial: "",
computer_name: "9b20fc72a247",
mdm: {
+ encryption_key_available: false,
enrollment_status: "Off",
server_url: "https://www.example.com/1",
},
diff --git a/frontend/__mocks__/macAdminsMock.ts b/frontend/__mocks__/macAdminsMock.ts
index 53550acf88..ee8178121b 100644
--- a/frontend/__mocks__/macAdminsMock.ts
+++ b/frontend/__mocks__/macAdminsMock.ts
@@ -3,6 +3,7 @@ import { IMacadminsResponse } from "interfaces/host";
const DEFAULT_MAC_ADMINS_MOCK: IMacadminsResponse = {
macadmins: {
mobile_device_management: {
+ encryption_key_available: false,
enrollment_status: "On (manual)",
server_url: "https://kandji.com/2",
name: "Kandji",
diff --git a/frontend/__mocks__/mdmMock.ts b/frontend/__mocks__/mdmMock.ts
index 81ee177a39..028895f7ce 100644
--- a/frontend/__mocks__/mdmMock.ts
+++ b/frontend/__mocks__/mdmMock.ts
@@ -15,6 +15,7 @@ export const createMockMdmSolution = (
};
const DEFAULT_HOST_MDM_DATA: IHostMdmData = {
+ encryption_key_available: false,
enrollment_status: "On (automatic)",
server_url: "http://mdmsolution.com",
name: "MDM Solution",
diff --git a/frontend/components/EnrollSecrets/EnrollSecretTable/EnrollSecretRow/EnrollSecretRow.tsx b/frontend/components/EnrollSecrets/EnrollSecretTable/EnrollSecretRow/EnrollSecretRow.tsx
index bb89325b13..a8a9346826 100644
--- a/frontend/components/EnrollSecrets/EnrollSecretTable/EnrollSecretRow/EnrollSecretRow.tsx
+++ b/frontend/components/EnrollSecrets/EnrollSecretTable/EnrollSecretRow/EnrollSecretRow.tsx
@@ -98,6 +98,7 @@ const EnrollSecretRow = ({
key={uniqueId()}
data-testid="osquery-secret"
>
+ {/* TODO: replace with InputFieldHiddenContent component */}
{
+ const [copyMessage, setCopyMessage] = useState("");
+ const [showSecret, setShowSecret] = useState(false);
+
+ const classNames = classnames(baseClass, className);
+
+ const onCopySecret = (evt: React.MouseEvent) => {
+ evt.preventDefault();
+
+ stringToClipboard(value)
+ .then(() => setCopyMessage("Copied!"))
+ .catch(() => setCopyMessage("Copy failed"));
+
+ // Clear message after 1 second
+ setTimeout(() => setCopyMessage(""), 1000);
+
+ return false;
+ };
+
+ const onToggleSecret = (evt: React.MouseEvent) => {
+ evt.preventDefault();
+
+ setShowSecret(!showSecret);
+ return false;
+ };
+
+ const renderLabel = () => {
+ return (
+
+
+ {copyMessage && (
+ {`${copyMessage} `}
+ )}
+
+
+
+
+ );
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default InputFieldHiddenContent;
diff --git a/frontend/components/forms/fields/InputFieldHiddenContent/_styles.scss b/frontend/components/forms/fields/InputFieldHiddenContent/_styles.scss
new file mode 100644
index 0000000000..c7ee1c90a9
--- /dev/null
+++ b/frontend/components/forms/fields/InputFieldHiddenContent/_styles.scss
@@ -0,0 +1,63 @@
+.input-field-hidden-content {
+ &__secret {
+ display: flex;
+ align-items: center;
+ margin-bottom: $pad-medium;
+ }
+
+ .form-field {
+ margin-bottom: 0;
+ }
+
+ &__secret-input {
+ .form-field__label {
+ position: relative;
+ font-size: $x-small;
+ font-weight: $bold;
+ margin-bottom: 0;
+ // TODO: figure out width when pulling out to common component
+ height: 0;
+ min-height: 0;
+ }
+
+ .input-field {
+ &--disabled {
+ letter-spacing: 0;
+ }
+
+ &--password {
+ letter-spacing: 4px;
+ }
+ }
+ }
+
+ &__copy-message {
+ position: absolute;
+ right: 65px;
+ background-color: $ui-light-grey;
+ border: solid 1px #e2e4ea;
+ border-radius: 10px;
+ padding: 2px 6px;
+ }
+
+ .buttons {
+ display: flex;
+ align-items: center;
+ position: absolute;
+ right: 16px;
+ top: 12px;
+ height: 16px;
+
+ span {
+ font-weight: $regular;
+ }
+ }
+
+ &__show-secret-icon,
+ &__copy-secret-icon,
+ &__edit-secret-icon,
+ &__delete-secret-icon {
+ padding: 0 $pad-small;
+ margin-left: $pad-xsmall;
+ }
+}
diff --git a/frontend/components/forms/fields/InputFieldHiddenContent/index.ts b/frontend/components/forms/fields/InputFieldHiddenContent/index.ts
new file mode 100644
index 0000000000..cd67ef8dc5
--- /dev/null
+++ b/frontend/components/forms/fields/InputFieldHiddenContent/index.ts
@@ -0,0 +1 @@
+export { default } from "./InputFieldHiddenContent";
diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts
index 845e3bc19e..bbaabd7e4e 100644
--- a/frontend/interfaces/activity.ts
+++ b/frontend/interfaces/activity.ts
@@ -32,6 +32,7 @@ export enum ActivityType {
MdmEnrolled = "mdm_enrolled",
MdmUnenrolled = "mdm_unenrolled",
EditedMacosMinVersion = "edited_macos_min_version",
+ ReadHostDiskEncryptionKey = "read_host_disk_encryption_key",
}
export interface IActivity {
created_at: string;
diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts
index dc190cc12d..a83882e469 100644
--- a/frontend/interfaces/host.ts
+++ b/frontend/interfaces/host.ts
@@ -87,6 +87,7 @@ export interface IMunkiData {
}
export interface IHostMdmData {
+ encryption_key_available: boolean;
enrollment_status: MdmEnrollmentStatus | null;
server_url: string;
id?: number;
@@ -149,6 +150,14 @@ export interface IDeviceUserResponse {
global_config: IDeviceGlobalConfig;
}
+export interface IHostEncrpytionKeyResponse {
+ host_id: number;
+ encryption_key: {
+ updated_at: string;
+ key: string;
+ };
+}
+
export interface IHost {
created_at: string;
updated_at: string;
diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
index c9de1dffe5..8eafd963bf 100644
--- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
+++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
@@ -200,6 +200,16 @@ const TAGGED_TEMPLATES = {
);
},
+ readHostDiskEncryptionKey: (activity: IActivity) => {
+ return (
+ <>
+ {" "}
+ viewed the disk encryption key for {activity.details?.host_display_name}
+ .
+ >
+ );
+ },
+
defaultActivityTemplate: (activity: IActivity) => {
const entityName = find(activity.details, (_, key) =>
key.includes("_name")
@@ -280,6 +290,9 @@ const getDetail = (
case ActivityType.EditedMacosMinVersion: {
return TAGGED_TEMPLATES.editedMacosMinVersion(activity);
}
+ case ActivityType.ReadHostDiskEncryptionKey: {
+ return TAGGED_TEMPLATES.readHostDiskEncryptionKey(activity);
+ }
default: {
return TAGGED_TEMPLATES.defaultActivityTemplate(activity);
}
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx
new file mode 100644
index 0000000000..005a24012d
--- /dev/null
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx
@@ -0,0 +1,67 @@
+import React, { useContext } from "react";
+
+import { MdmEnrollmentStatus } from "interfaces/mdm";
+import permissionUtils from "utilities/permissions";
+import { AppContext } from "context/app";
+
+// @ts-ignore
+import Dropdown from "components/forms/fields/Dropdown";
+import { generateHostActionOptions } from "./helpers";
+
+const baseClass = "host-actions-dropdown";
+
+interface IHostActionsDropdownProps {
+ onSelect: (value: string) => void;
+ teamId: number | null;
+ hostStatus: string;
+ hostMdmEnrollemntStatus: MdmEnrollmentStatus | null;
+ doesStoreEncryptionKey?: boolean;
+}
+
+const HostActionsDropdown = ({
+ onSelect,
+ teamId,
+ hostStatus,
+ hostMdmEnrollemntStatus,
+ doesStoreEncryptionKey,
+}: IHostActionsDropdownProps) => {
+ const {
+ currentUser,
+ isPremiumTier = false,
+ isGlobalAdmin = false,
+ isGlobalMaintainer = false,
+ } = useContext(AppContext);
+
+ const options = generateHostActionOptions({
+ isPremiumTier,
+ isGlobalAdmin,
+ isGlobalMaintainer,
+ isTeamAdmin: permissionUtils.isTeamAdmin(currentUser, teamId ?? null),
+ isTeamMaintainer: permissionUtils.isTeamMaintainer(
+ currentUser,
+ teamId ?? null
+ ),
+ isHostOnline: hostStatus === "online",
+ isEnrolledInMdm: ["On (automatic)", "On (manual)"].includes(
+ hostMdmEnrollemntStatus ?? ""
+ ),
+ doesStoreEncryptionKey: doesStoreEncryptionKey ?? false,
+ });
+
+ // No options to render. Exit early
+ if (options.length === 0) return null;
+
+ return (
+
+
+
+ );
+};
+
+export default HostActionsDropdown;
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/_styles.scss
new file mode 100644
index 0000000000..b7d1fbf802
--- /dev/null
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/_styles.scss
@@ -0,0 +1,3 @@
+.host-actions-dropdown {
+ width: 204px
+}
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.ts b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.ts
new file mode 100644
index 0000000000..5ebcdd0f23
--- /dev/null
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.ts
@@ -0,0 +1,132 @@
+import { IDropdownOption } from "interfaces/dropdownOption";
+import { cloneDeep } from "lodash";
+
+const DEFAULT_OPTIONS: IDropdownOption[] = [
+ {
+ label: "Transfer",
+ value: "transfer",
+ disabled: false,
+ },
+ {
+ label: "Query",
+ value: "query",
+ disabled: false,
+ },
+ {
+ label: "Show disk encryption key",
+ value: "diskEncryption",
+ disabled: false,
+ },
+ {
+ label: "Turn off MDM",
+ value: "mdmOff",
+ disabled: false,
+ },
+ {
+ label: "Delete",
+ disabled: false,
+ value: "delete",
+ },
+];
+
+// eslint-disable-next-line import/prefer-default-export
+interface IHostActionConfigOptions {
+ isPremiumTier: boolean;
+ isGlobalAdmin: boolean;
+ isGlobalMaintainer: boolean;
+ isTeamAdmin: boolean;
+ isTeamMaintainer: boolean;
+ isHostOnline: boolean;
+ isEnrolledInMdm: boolean;
+ doesStoreEncryptionKey: boolean;
+}
+
+const canTransferTeam = (config: IHostActionConfigOptions) => {
+ const { isPremiumTier, isGlobalAdmin, isGlobalMaintainer } = config;
+ return isPremiumTier && (isGlobalAdmin || isGlobalMaintainer);
+};
+
+const canEditMdm = (config: IHostActionConfigOptions) => {
+ const {
+ isGlobalAdmin,
+ isGlobalMaintainer,
+ isTeamAdmin,
+ isTeamMaintainer,
+ isEnrolledInMdm,
+ } = config;
+ return (
+ isEnrolledInMdm &&
+ (isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer)
+ );
+};
+
+const canDeleteHost = (config: IHostActionConfigOptions) => {
+ const {
+ isGlobalAdmin,
+ isGlobalMaintainer,
+ isTeamAdmin,
+ isTeamMaintainer,
+ } = config;
+ return isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer;
+};
+
+const canShowDiskEncryption = (config: IHostActionConfigOptions) => {
+ const { isPremiumTier, doesStoreEncryptionKey } = config;
+ return isPremiumTier && doesStoreEncryptionKey;
+};
+
+const filterOutOptions = (
+ options: IDropdownOption[],
+ config: IHostActionConfigOptions
+) => {
+ if (!canTransferTeam(config)) {
+ options = options.filter((option) => option.value !== "transfer");
+ }
+
+ if (!canShowDiskEncryption(config)) {
+ options = options.filter((option) => option.value !== "diskEncryption");
+ }
+
+ if (!canEditMdm(config)) {
+ options = options.filter((option) => option.value !== "mdmOff");
+ }
+
+ if (!canDeleteHost(config)) {
+ options = options.filter((option) => option.value !== "delete");
+ }
+
+ return options;
+};
+
+const setOptionsAsDisabled = (
+ options: IDropdownOption[],
+ isHostOnline: boolean
+) => {
+ if (!isHostOnline) {
+ const disableOptions = options.filter(
+ (option) => option.value === "query" || option.value === "mdmOff"
+ );
+ disableOptions.forEach((option) => {
+ option.disabled = true;
+ });
+ }
+
+ return options;
+};
+
+/**
+ * Generates the host actions options depending on the configuration. There are
+ * many variations of the options that are shown/not shown or disabled/enabled
+ * which are all controlled by the configurations options argument.
+ */
+// eslint-disable-next-line import/prefer-default-export
+export const generateHostActionOptions = (config: IHostActionConfigOptions) => {
+ // deep clone to always start with a fresh copy of the default options.
+ let options = cloneDeep(DEFAULT_OPTIONS);
+ options = filterOutOptions(options, config);
+
+ if (options.length === 0) return options;
+
+ options = setOptionsAsDisabled(options, config.isHostOnline);
+ return options;
+};
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/index.ts b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/index.ts
new file mode 100644
index 0000000000..9c9746dfa3
--- /dev/null
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/index.ts
@@ -0,0 +1 @@
+export { default } from "./HostActionsDropdown";
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
index 7ba96cf64a..ff193a7095 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
@@ -28,12 +28,8 @@ import { IQuery, IFleetQueriesResponse } from "interfaces/query";
import { IQueryStats } from "interfaces/query_stats";
import { ISoftware } from "interfaces/software";
import { ITeam } from "interfaces/team";
-import { IUser } from "interfaces/user";
-import permissionUtils from "utilities/permissions";
-import ReactTooltip from "react-tooltip";
import Spinner from "components/Spinner";
-import Button from "components/buttons/Button";
import TabsWrapper from "components/TabsWrapper";
import MainContent from "components/MainContent";
import InfoBanner from "components/InfoBanner";
@@ -63,10 +59,9 @@ import TransferHostModal from "../../components/TransferHostModal";
import DeleteHostModal from "../../components/DeleteHostModal";
import parseOsVersion from "./modals/OSPolicyModal/helpers";
-import DeleteIcon from "../../../../../assets/images/icon-action-delete-14x14@2x.png";
-import QueryIcon from "../../../../../assets/images/icon-action-query-16x16@2x.png";
-import TransferIcon from "../../../../../assets/images/icon-action-transfer-16x16@2x.png";
-import CloseIcon from "../../../../../assets/images/icon-action-close-16x15@2x.png";
+
+import DiskEncryptionKeyModal from "./modals/DiskEncryptionKeyModal";
+import HostActionDropdown from "./HostActionsDropdown/HostActionsDropdown";
const baseClass = "host-details";
@@ -111,11 +106,9 @@ const HostDetailsPage = ({
const hostIdFromURL = parseInt(host_id, 10);
const {
config,
- currentUser,
- isGlobalAdmin,
- isPremiumTier,
+ isGlobalAdmin = false,
+ isPremiumTier = false,
isOnlyObserver,
- isGlobalMaintainer,
filteredHostsPath,
} = useContext(AppContext);
const {
@@ -128,17 +121,6 @@ const HostDetailsPage = ({
} = useContext(PolicyContext);
const { renderFlash } = useContext(NotificationContext);
const handlePageError = useErrorHandler();
- const canTransferTeam =
- isPremiumTier && (isGlobalAdmin || isGlobalMaintainer);
-
- const canDeleteHost = (user: IUser, host: IHost) => {
- return (
- isGlobalAdmin ||
- isGlobalMaintainer ||
- permissionUtils.isTeamAdmin(user, host.team_id) ||
- permissionUtils.isTeamMaintainer(user, host.team_id)
- );
- };
const [showDeleteHostModal, setShowDeleteHostModal] = useState(false);
const [showTransferHostModal, setShowTransferHostModal] = useState(false);
@@ -146,6 +128,7 @@ const HostDetailsPage = ({
const [showPolicyDetailsModal, setPolicyDetailsModal] = useState(false);
const [showOSPolicyModal, setShowOSPolicyModal] = useState(false);
const [showUnenrollMdmModal, setShowUnenrollMdmModal] = useState(false);
+ const [showDiskEncryptionModal, setShowDiskEncryptionModal] = useState(false);
const [selectedPolicy, setSelectedPolicy] = useState(
null
);
@@ -333,19 +316,6 @@ const HostDetailsPage = ({
}
);
- const canEditMdm = (() => {
- const userHasPermission =
- !!currentUser &&
- !!host &&
- (isGlobalAdmin ||
- isGlobalMaintainer ||
- permissionUtils.isTeamMaintainerOrTeamAdmin(currentUser, host.team_id));
- const hostEnrolled = ["On (automatic)", "On (manual)"].includes(
- host?.mdm.enrollment_status ?? ""
- );
- return userHasPermission && hostEnrolled;
- })();
-
const featuresConfig = host?.team_id
? teams?.find((t) => t.id === host.team_id)?.features
: config?.features;
@@ -360,7 +330,7 @@ const HostDetailsPage = ({
}) || []
);
});
- }, [usersSearchString]);
+ }, [usersSearchString, host?.users]);
const titleData = normalizeEmptyValues(
pick(host, [
@@ -536,72 +506,40 @@ const HostDetailsPage = ({
[]
);
+ const onSelectHostAction = (action: string) => {
+ switch (action) {
+ case "transfer":
+ setShowTransferHostModal(true);
+ break;
+ case "query":
+ setShowQueryHostModal(true);
+ break;
+ case "diskEncryption":
+ setShowDiskEncryptionModal(true);
+ break;
+ case "mdmOff":
+ toggleUnenrollMdmModal();
+ break;
+ case "delete":
+ setShowDeleteHostModal(true);
+ break;
+ default:
+ }
+ };
+
const renderActionButtons = () => {
- const isOnline = host?.status === "online";
+ if (!host) {
+ return null;
+ }
+
return (
-
- {canTransferTeam && (
-
- )}
-
-
-
-
-
- You can’t query
an offline host.
-
-
- {canEditMdm && !hideEditMdm && (
-
- )}
- {currentUser && host && canDeleteHost(currentUser, host) && (
-
- )}
-
+
);
};
@@ -808,6 +746,12 @@ const HostDetailsPage = ({
}}
/>
)}
+ {showDiskEncryptionModal && host && (
+ setShowDiskEncryptionModal(false)}
+ />
+ )}
);
diff --git a/frontend/pages/hosts/details/HostDetailsPage/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/_styles.scss
index 4e1831ecf9..7e936b4a92 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/_styles.scss
+++ b/frontend/pages/hosts/details/HostDetailsPage/_styles.scss
@@ -348,12 +348,6 @@
margin: $pad-xxlarge 0 0;
}
- &__action-button-container {
- display: flex;
- align-items: center;
- gap: $pad-large;
- }
-
&__device_mapping {
.device_mapping--tooltip {
flex-direction: column;
diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx
new file mode 100644
index 0000000000..508fe27c4b
--- /dev/null
+++ b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx
@@ -0,0 +1,64 @@
+import React from "react";
+import { useQuery } from "react-query";
+
+import { IHostEncrpytionKeyResponse } from "interfaces/host";
+import hostAPI from "services/entities/hosts";
+
+import Modal from "components/Modal";
+import CustomLink from "components/CustomLink";
+import Button from "components/buttons/Button";
+import InputFieldHiddenContent from "components/forms/fields/InputFieldHiddenContent";
+import DataError from "components/DataError";
+
+const baseClass = "disk-encryption-key-modal";
+
+interface IDiskEncryptionKeyModal {
+ hostId: number;
+ onCancel: () => void;
+}
+
+const DiskEncryptionKeyModal = ({
+ hostId,
+ onCancel,
+}: IDiskEncryptionKeyModal) => {
+ const { data: encrpytionKey, error: encryptionKeyError } = useQuery<
+ IHostEncrpytionKeyResponse,
+ unknown,
+ string
+ >("hostEncrpytionKey", () => hostAPI.getEncryptionKey(hostId), {
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ refetchOnWindowFocus: false,
+ retry: false,
+ select: (data) => data.encryption_key.key,
+ });
+
+ return (
+
+ {encryptionKeyError ? (
+
+ ) : (
+ <>
+
+
+ The disk encryption key refers to the FileVault recovery key for
+ macOS.
+
+
+ Use this key to log in to the host if you forgot the password.{" "}
+
+
+
+
+
+ >
+ )}
+
+ );
+};
+
+export default DiskEncryptionKeyModal;
diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/_styles.scss
new file mode 100644
index 0000000000..6072b2bdc5
--- /dev/null
+++ b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/_styles.scss
@@ -0,0 +1,3 @@
+.disk-encryption-key-modal {
+ width: 500px;
+}
diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/index.ts b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/index.ts
new file mode 100644
index 0000000000..aa4d95a872
--- /dev/null
+++ b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./DiskEncryptionKeyModal";
diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/UnenrollMdmModal/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/modals/UnenrollMdmModal/_styles.scss
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx
index dd07c0623a..d0fc99c864 100644
--- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx
+++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx
@@ -28,7 +28,7 @@ interface IHostSummaryProps {
onRefetchHost: (
evt: React.MouseEvent
) => void;
- renderActionButtons: () => JSX.Element;
+ renderActionButtons: () => JSX.Element | null;
deviceUser?: boolean;
}
@@ -58,7 +58,7 @@ const HostSummary = ({