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 = ({