From 12ab93d9e0c4c5ec9d44141a2392562a7d95ada4 Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Mon, 6 Oct 2025 16:45:53 +0100 Subject: [PATCH] Update "Setting up your device" page for MacOS Setup Experience (#33770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Related issue:** Resolves #33173 # Details This PR updates the "Setting up your device" page which appears in Linux and Windows (and as of https://github.com/fleetdm/fleet/issues/30117, MacOS) setup experiences. Front-end updates: * Lots of renaming of things that were software-specific to now more generically refer to "setup step" * Removed the "My Device" heading * Moved the info button inside the table header * Added status of setup script run to the table * Updated the empty state to not refer specifically to software * Added optional `setup_only` query param to the `/device` page which, if set, will always show the "setting up your device" page even if all setup is complete. Normally as soon as setup finishes, the front-end redirects to the regular My Device page. In the case of MacOS setup experience, we don't want this to happen as we expect to either 1) keep the setup experience up indefinitely if we're blocking device setup on software install failure, or 2) close the setup dialog on successful completion. This query param is also handy for testing. * Added new "Configuration complete" state to be shown when all setup steps are finished (successfully or not). This is only applicable on MacOS, since other platforms will redirect to the My Device page when finished. This PR also includes one small backend change to the `/device/{token}/setup_experience/status` API endpoint, to have it return a `scripts` array alongside the existing `software` array. This endpoint is not documented publicly. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [X] Added/updated automated tests Updated existing DeviceUserPage tests that check the SettingUpYourDevice content, and added new tests for the new scripts content and the new query param. - [X] QA'd all new/changed functionality manually Screenshot 2025-10-02 at 7 20 28 PM (note that as of now we'd only have at most one script, showing multiple here to demonstrate the different states) Screenshot 2025-10-02 at 7 22 01 PM image --------- Co-authored-by: Ian Littman --- changes/33173-update-setting-up-your-device | 1 + ee/server/service/devices.go | 5 ++ frontend/__mocks__/deviceUserMock.ts | 35 +++++----- .../SetupScriptProcessCell.tsx | 21 ++++++ .../SetupScriptProcessCell/_styles.scss | 10 +++ .../DataTable/SetupScriptProcessCell/index.ts | 1 + .../SetupScriptStatusCell.tsx | 43 +++++++++++++ .../SetupScriptStatusCell/_styles.scss | 11 ++++ .../DataTable/SetupScriptStatusCell/index.ts | 1 + .../SetupSoftwareProcessCell/_styles.scss | 4 ++ .../SetupSoftwareStatusCell.tsx | 6 +- frontend/interfaces/setup.ts | 19 ++++++ frontend/interfaces/software.ts | 15 ----- .../DeviceUserPage/DeviceUserPage.tests.tsx | 64 ++++++++++++++++--- .../details/DeviceUserPage/DeviceUserPage.tsx | 47 +++++++++----- .../SettingUpYourDevice.tsx | 42 +++++++----- .../SetupSoftwareStatusTable.tsx | 44 ------------- .../SetupSoftwareStatusTableConfig.tsx | 36 ----------- .../SetupSoftwareStatusTable/index.ts | 1 - .../SetupStatusTable/SetupStatusTable.tsx | 54 ++++++++++++++++ .../SetupStatusTableConfig.tsx | 48 ++++++++++++++ .../SetupStatusTable/index.ts | 1 + .../SettingUpYourDevice/_styles.scss | 10 ++- .../hosts/details/DeviceUserPage/helpers.ts | 6 +- frontend/services/entities/device_user.ts | 17 ++--- frontend/test/handlers/device-handler.ts | 6 +- frontend/utilities/endpoints.ts | 2 +- server/fleet/setup_experience.go | 2 + 28 files changed, 383 insertions(+), 169 deletions(-) create mode 100644 changes/33173-update-setting-up-your-device create mode 100644 frontend/components/TableContainer/DataTable/SetupScriptProcessCell/SetupScriptProcessCell.tsx create mode 100644 frontend/components/TableContainer/DataTable/SetupScriptProcessCell/_styles.scss create mode 100644 frontend/components/TableContainer/DataTable/SetupScriptProcessCell/index.ts create mode 100644 frontend/components/TableContainer/DataTable/SetupScriptStatusCell/SetupScriptStatusCell.tsx create mode 100644 frontend/components/TableContainer/DataTable/SetupScriptStatusCell/_styles.scss create mode 100644 frontend/components/TableContainer/DataTable/SetupScriptStatusCell/index.ts create mode 100644 frontend/interfaces/setup.ts delete mode 100644 frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/SetupSoftwareStatusTable.tsx delete mode 100644 frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/SetupSoftwareStatusTableConfig.tsx delete mode 100644 frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/index.ts create mode 100644 frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTable.tsx create mode 100644 frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTableConfig.tsx create mode 100644 frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/index.ts diff --git a/changes/33173-update-setting-up-your-device b/changes/33173-update-setting-up-your-device new file mode 100644 index 0000000000..9d0b4fbdeb --- /dev/null +++ b/changes/33173-update-setting-up-your-device @@ -0,0 +1 @@ +- Updated the "Setting up your device" page to show status of setup script run. diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index 3667d7f90e..097e63588d 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -286,10 +286,14 @@ func (svc *Service) getHostSetupExperienceStatus(ctx context.Context, host *flee } var software []*fleet.SetupExperienceStatusResult + var scripts []*fleet.SetupExperienceStatusResult for _, result := range results { if result.IsForSoftware() { software = append(software, result) } + if result.IsForScript() { + scripts = append(scripts, result) + } } // Continue with next step in setup experience. @@ -299,5 +303,6 @@ func (svc *Service) getHostSetupExperienceStatus(ctx context.Context, host *flee return &fleet.DeviceSetupExperienceStatusPayload{ Software: software, + Scripts: scripts, }, nil } diff --git a/frontend/__mocks__/deviceUserMock.ts b/frontend/__mocks__/deviceUserMock.ts index 3e3601c6bf..384c89b70f 100644 --- a/frontend/__mocks__/deviceUserMock.ts +++ b/frontend/__mocks__/deviceUserMock.ts @@ -1,8 +1,9 @@ import { IDeviceUser } from "interfaces/host"; -import { IDeviceSoftware, ISetupSoftwareStatus } from "interfaces/software"; +import { IDeviceSoftware } from "interfaces/software"; +import { ISetupStep } from "interfaces/setup"; import { IGetDeviceSoftwareResponse, - IGetSetupSoftwareStatusesResponse, + IGetSetupExperienceStatusesResponse, } from "services/entities/device_user"; import { createMockHostSoftwarePackage } from "./hostMock"; @@ -53,32 +54,36 @@ export const createMockDeviceSoftwareResponse = ( }; }; -const DEFAULT_SETUP_SOFTWARE_STATUS_MOCK: ISetupSoftwareStatus = { +const DEFAULT_SETUP_STEP_STATUS_MOCK: ISetupStep = { name: "Slack", status: "pending", + type: "", }; -export const createMockSetupSoftwareStatus = ( - overrides?: Partial -): ISetupSoftwareStatus => { - return { ...DEFAULT_SETUP_SOFTWARE_STATUS_MOCK, ...overrides }; +export const createMockSetupStepStatus = ( + overrides?: Partial +): ISetupStep => { + return { ...DEFAULT_SETUP_STEP_STATUS_MOCK, ...overrides }; }; -const DEFAULT_SETUP_SOFTWARE_STATUSES_RESPONSE_MOCK: IGetSetupSoftwareStatusesResponse = { +const DEFAULT_SETUP_SOFTWARE_STATUSES_RESPONSE_MOCK: IGetSetupExperienceStatusesResponse = { setup_experience_results: { software: [ - createMockSetupSoftwareStatus({ name: "1Password", status: "pending" }), - createMockSetupSoftwareStatus({ name: "Chrome", status: "failure" }), - createMockSetupSoftwareStatus({ name: "Firefox", status: "cancelled" }), - createMockSetupSoftwareStatus({ name: "Slack", status: "success" }), - createMockSetupSoftwareStatus({ name: "Zoom", status: "running" }), + createMockSetupStepStatus({ name: "1Password", status: "pending" }), + createMockSetupStepStatus({ name: "Chrome", status: "failure" }), + createMockSetupStepStatus({ name: "Firefox", status: "cancelled" }), + createMockSetupStepStatus({ name: "Slack", status: "success" }), + createMockSetupStepStatus({ name: "Zoom", status: "running" }), + ], + scripts: [ + createMockSetupStepStatus({ name: "test.sh", status: "running" }), ], }, }; export const createMockSetupSoftwareStatusesResponse = ( - overrides?: Partial -): IGetSetupSoftwareStatusesResponse => { + overrides?: Partial +): IGetSetupExperienceStatusesResponse => { return { ...DEFAULT_SETUP_SOFTWARE_STATUSES_RESPONSE_MOCK, ...overrides, diff --git a/frontend/components/TableContainer/DataTable/SetupScriptProcessCell/SetupScriptProcessCell.tsx b/frontend/components/TableContainer/DataTable/SetupScriptProcessCell/SetupScriptProcessCell.tsx new file mode 100644 index 0000000000..d6e05139cd --- /dev/null +++ b/frontend/components/TableContainer/DataTable/SetupScriptProcessCell/SetupScriptProcessCell.tsx @@ -0,0 +1,21 @@ +import Graphic from "components/Graphic/Graphic"; +import React from "react"; + +const baseClass = "setup-script-process-cell"; + +interface ISetupScriptProcessCell { + name: string; +} + +const SetupScriptProcessCell = ({ name }: ISetupScriptProcessCell) => { + return ( + + +
+ Run {name || "Unknown script"} +
+
+ ); +}; + +export default SetupScriptProcessCell; diff --git a/frontend/components/TableContainer/DataTable/SetupScriptProcessCell/_styles.scss b/frontend/components/TableContainer/DataTable/SetupScriptProcessCell/_styles.scss new file mode 100644 index 0000000000..371e5a2931 --- /dev/null +++ b/frontend/components/TableContainer/DataTable/SetupScriptProcessCell/_styles.scss @@ -0,0 +1,10 @@ +.setup-script-process-cell { + display: flex; + align-items: center; + gap: $pad-small; + + .graphic { + width: $pad-xlarge; + scale: 60%; + } +} diff --git a/frontend/components/TableContainer/DataTable/SetupScriptProcessCell/index.ts b/frontend/components/TableContainer/DataTable/SetupScriptProcessCell/index.ts new file mode 100644 index 0000000000..dbc7987b1d --- /dev/null +++ b/frontend/components/TableContainer/DataTable/SetupScriptProcessCell/index.ts @@ -0,0 +1 @@ +export { default } from "./SetupScriptProcessCell"; diff --git a/frontend/components/TableContainer/DataTable/SetupScriptStatusCell/SetupScriptStatusCell.tsx b/frontend/components/TableContainer/DataTable/SetupScriptStatusCell/SetupScriptStatusCell.tsx new file mode 100644 index 0000000000..8753d9ab44 --- /dev/null +++ b/frontend/components/TableContainer/DataTable/SetupScriptStatusCell/SetupScriptStatusCell.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +import { SetupStepStatus } from "interfaces/setup"; + +import Icon from "components/Icon"; +import { IconNames } from "components/icons"; +import Spinner from "components/Spinner"; + +const baseClass = "setup-script-status-cell"; + +interface ISetupScriptStatusCell { + status: SetupStepStatus; +} + +const serverToUiStatus = ( + status: SetupStepStatus +): { label: string; icon: IconNames | "spinner" } => { + switch (status) { + case "pending": + return { label: "Pending", icon: "pending-outline" }; + case "running": + return { label: "Running", icon: "spinner" }; + case "success": + return { label: "Ran", icon: "success" }; + case "failure": + case "cancelled": + return { label: "Failed", icon: "error" }; + default: + return { label: "Pending", icon: "pending-outline" }; + } +}; + +const SetupScriptStatusCell = ({ status }: ISetupScriptStatusCell) => { + const { label, icon } = serverToUiStatus(status); + return ( +
+ {icon === "spinner" ? : } + {label} +
+ ); +}; + +export default SetupScriptStatusCell; diff --git a/frontend/components/TableContainer/DataTable/SetupScriptStatusCell/_styles.scss b/frontend/components/TableContainer/DataTable/SetupScriptStatusCell/_styles.scss new file mode 100644 index 0000000000..b72b27fb5a --- /dev/null +++ b/frontend/components/TableContainer/DataTable/SetupScriptStatusCell/_styles.scss @@ -0,0 +1,11 @@ +.setup-script-status-cell { + display: flex; + align-items: center; + gap: $pad-xsmall; + + .loading-spinner { + margin: 0; + width: $pad-medium; + height: $pad-medium; + } +} diff --git a/frontend/components/TableContainer/DataTable/SetupScriptStatusCell/index.ts b/frontend/components/TableContainer/DataTable/SetupScriptStatusCell/index.ts new file mode 100644 index 0000000000..bfa57df0a4 --- /dev/null +++ b/frontend/components/TableContainer/DataTable/SetupScriptStatusCell/index.ts @@ -0,0 +1 @@ +export { default } from "./SetupScriptStatusCell"; diff --git a/frontend/components/TableContainer/DataTable/SetupSoftwareProcessCell/_styles.scss b/frontend/components/TableContainer/DataTable/SetupSoftwareProcessCell/_styles.scss index a7a8955ea3..b5ef499b6a 100644 --- a/frontend/components/TableContainer/DataTable/SetupSoftwareProcessCell/_styles.scss +++ b/frontend/components/TableContainer/DataTable/SetupSoftwareProcessCell/_styles.scss @@ -2,4 +2,8 @@ display: flex; align-items: center; gap: $pad-small; + + .software-icon__small { + width: $pad-xlarge; + } } diff --git a/frontend/components/TableContainer/DataTable/SetupSoftwareStatusCell/SetupSoftwareStatusCell.tsx b/frontend/components/TableContainer/DataTable/SetupSoftwareStatusCell/SetupSoftwareStatusCell.tsx index 02287a4a26..305c1bc3b7 100644 --- a/frontend/components/TableContainer/DataTable/SetupSoftwareStatusCell/SetupSoftwareStatusCell.tsx +++ b/frontend/components/TableContainer/DataTable/SetupSoftwareStatusCell/SetupSoftwareStatusCell.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { SetupSoftwareStatus } from "interfaces/software"; +import { SetupStepStatus } from "interfaces/setup"; import Icon from "components/Icon"; import { IconNames } from "components/icons"; @@ -9,11 +9,11 @@ import Spinner from "components/Spinner"; const baseClass = "setup-software-status-cell"; interface ISetupSoftwareStatusCell { - status: SetupSoftwareStatus; + status: SetupStepStatus; } const serverToUiStatus = ( - status: SetupSoftwareStatus + status: SetupStepStatus ): { label: string; icon: IconNames | "spinner" } => { switch (status) { case "pending": diff --git a/frontend/interfaces/setup.ts b/frontend/interfaces/setup.ts new file mode 100644 index 0000000000..9422cd3386 --- /dev/null +++ b/frontend/interfaces/setup.ts @@ -0,0 +1,19 @@ +export const SETUP_STEP_STATUSES = [ + "pending", + "running", + "success", + "failure", + "cancelled", // server should be aggregating cancelled installs with failed, check here just in case +] as const; + +export type SetupStepStatus = typeof SETUP_STEP_STATUSES[number]; + +export const SETUP_STEP_TYPES = ["software_install", "script_run"]; + +export type SetupStepType = typeof SETUP_STEP_TYPES[number]; + +export interface ISetupStep { + name: string | null; + status: SetupStepStatus; + type?: SetupStepType; +} diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index 75885abdfb..c3062374f8 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -594,18 +594,3 @@ export interface IFleetMaintainedAppDetails { software_title_id?: number; // null unless the team already has the software added (as a Fleet-maintained app, App Store (app), or custom package) categories: SoftwareCategory[]; } - -export const SETUP_SOFTWARE_STATUSES = [ - "pending", - "running", - "success", - "failure", - "cancelled", // server should be aggregating cancelled installs with failed, check here just in case -] as const; - -export type SetupSoftwareStatus = typeof SETUP_SOFTWARE_STATUSES[number]; - -export interface ISetupSoftwareStatus { - name: string | null; - status: SetupSoftwareStatus; -} diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx index 227498307d..99ab256ba6 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx @@ -2,12 +2,12 @@ import React from "react"; import { screen, waitFor } from "@testing-library/react"; import { IDeviceUserResponse, IHostDevice } from "interfaces/host"; -import createMockHost, { createMockHostEndUser } from "__mocks__/hostMock"; +import createMockHost from "__mocks__/hostMock"; import mockServer from "test/mock-server"; import { createCustomRenderer, createMockRouter } from "test/test-utils"; import createMockLicense from "__mocks__/licenseMock"; -import { IGetSetupSoftwareStatusesResponse } from "services/entities/device_user"; +import { IGetSetupExperienceStatusesResponse } from "services/entities/device_user"; import { customDeviceHandler, @@ -28,6 +28,7 @@ const mockLocation = { query: undefined, order_key: undefined, order_direction: undefined, + setup_only: "", }, search: undefined, }; @@ -137,10 +138,12 @@ describe("Device User Page", () => { describe("Setup experience software installation", () => { const REGULAR_DUP_MATCHER = /Last fetched/; const SETTING_UP_YOUR_DEVICE_MATCHER = /Setting up your device/; + const CONFIG_COMPLETE_MATCHER = /Configuration complete/; const setupTest = async ( deviceUserResponseOverrides?: Partial, - setupExperienceOverrides?: Partial + setupExperienceOverrides?: Partial, + mockLocationOverrides = {} ) => { mockServer.use(customDeviceHandler(deviceUserResponseOverrides)); mockServer.use(defaultDeviceCertificatesHandler); @@ -154,12 +157,13 @@ describe("Device User Page", () => { ); - await screen.findByText(/My device/); - return user; }; @@ -174,7 +178,7 @@ describe("Device User Page", () => { }); }); - it("checks for setup experience software on Fleet Premium, and renders Setting Up Your Device if there is such software", async () => { + it("checks for setup experience steps on Fleet Premium, and renders Setting Up Your Device if there are such steps", async () => { const host = createMockHost() as IHostDevice; host.platform = "linux"; @@ -184,15 +188,20 @@ describe("Device User Page", () => { expect( screen.getByText(SETTING_UP_YOUR_DEVICE_MATCHER) ).toBeInTheDocument(); + expect(screen.getByText(/Installing/)).toBeInTheDocument(); + expect(screen.getByText(/Running/)).toBeInTheDocument(); }); expect(screen.queryByText(REGULAR_DUP_MATCHER)).toBeNull(); }); - it("checks for setup experience software on Fleet Premium, and renders the normal device user page if there is no such software", async () => { + it("checks for setup experience steps on Fleet Premium, and renders the normal device user page if there are such steps", async () => { const host = createMockHost() as IHostDevice; host.platform = "linux"; - await setupTest({ host }, { setup_experience_results: {} }); + await setupTest( + { host }, + { setup_experience_results: { software: [], scripts: [] } } + ); await waitFor(() => { expect(screen.getByText(REGULAR_DUP_MATCHER)).toBeInTheDocument(); @@ -200,6 +209,43 @@ describe("Device User Page", () => { expect(screen.queryByText(SETTING_UP_YOUR_DEVICE_MATCHER)).toBeNull(); }); + it("checks for setup experience steps on Fleet Premium, and renders Setting Up Your Device even if there are no such steps if setup_only=1 is in the query", async () => { + const host = createMockHost() as IHostDevice; + host.platform = "linux"; + + await setupTest( + { host }, + { setup_experience_results: { software: [], scripts: [] } }, + { query: { setup_only: "1" } } + ); + await waitFor(() => { + expect(screen.getByText(CONFIG_COMPLETE_MATCHER)).toBeInTheDocument(); + }); + + expect(screen.queryByText(REGULAR_DUP_MATCHER)).toBeNull(); + }); + it("checks for setup experience items on Fleet Premium, and renders Setting Up Your Device when all steps are complete if setup_only=1 is in the query", async () => { + const host = createMockHost() as IHostDevice; + host.platform = "linux"; + + await setupTest( + { host }, + { + setup_experience_results: { + software: [ + { type: "software_installer", name: "step 1", status: "success" }, + ], + scripts: [{ type: "script", name: "step 2", status: "failure" }], + }, + }, + { query: { setup_only: "1" } } + ); + await waitFor(() => { + expect(screen.getByText(CONFIG_COMPLETE_MATCHER)).toBeInTheDocument(); + }); + + expect(screen.queryByText(REGULAR_DUP_MATCHER)).toBeNull(); + }); }); describe("MDM enrollment", () => { diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index 7b9637dba9..8a37441fed 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -10,7 +10,7 @@ import { NotificationContext } from "context/notification"; import deviceUserAPI, { IGetDeviceCertsRequestParams, IGetDeviceCertificatesResponse, - IGetSetupSoftwareStatusesResponse, + IGetSetupExperienceStatusesResponse, } from "services/entities/device_user"; import diskEncryptionAPI from "services/entities/disk_encryption"; import { @@ -27,6 +27,7 @@ import { } from "interfaces/certificates"; import { isAppleDevice, isLinuxLike } from "interfaces/platform"; import { IHostSoftware } from "interfaces/software"; +import { ISetupStep } from "interfaces/setup"; import DeviceUserError from "components/DeviceUserError"; // @ts-ignore @@ -53,7 +54,7 @@ import AboutCard from "../cards/About"; import SoftwareCard from "../cards/Software"; import PoliciesCard from "../cards/Policies"; import InfoModal from "./InfoModal"; -import { getErrorMessage, getIsSettingUpSoftware } from "./helpers"; +import { getErrorMessage, hasRemainingSetupSteps } from "./helpers"; import FleetIcon from "../../../../../assets/images/fleet-avatar-24x24@2x.png"; import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal"; @@ -110,6 +111,7 @@ interface IDeviceUserPageProps { query?: string; order_key?: string; order_direction?: "asc" | "desc"; + setup_only?: string; }; search?: string; }; @@ -316,7 +318,9 @@ const DeviceUserPage = ({ const isPremiumTier = license?.tier === "premium"; const isAppleHost = isAppleDevice(host?.platform); const isSetupExperienceSoftwareEnabledPlatform = - isLinuxLike(host?.platform || "") || host?.platform === "windows"; + isLinuxLike(host?.platform || "") || + host?.platform === "windows" || + host?.platform === "darwin"; const checkForSetupExperienceSoftware = isSetupExperienceSoftwareEnabledPlatform && isPremiumTier; @@ -326,22 +330,35 @@ const DeviceUserPage = ({ const aboutData = normalizeEmptyValues(pick(host, HOST_ABOUT_DATA)); const { - data: softwareSetupStatuses, - isLoading: isLoadingSetupSoftware, - isError: isErrorSetupSoftware, + data: setupStepStatuses, + isLoading: isLoadingSetupSteps, + isError: isErrorSetupSteps, } = useQuery< - IGetSetupSoftwareStatusesResponse, + IGetSetupExperienceStatusesResponse, Error, - IGetSetupSoftwareStatusesResponse["setup_experience_results"]["software"] + ISetupStep[] | null | undefined >( ["software-setup-statuses", deviceAuthToken], - () => deviceUserAPI.getSetupSoftwareStatuses({ token: deviceAuthToken }), + () => deviceUserAPI.getSetupExperienceStatuses({ token: deviceAuthToken }), { ...DEFAULT_USE_QUERY_OPTIONS, - select: (res) => res.setup_experience_results.software, enabled: checkForSetupExperienceSoftware, // this can only become true once the above `dupResponse` is defined by its associated API call response, ensuring this call only fires once the frontend knows if this is a Fleet Premium instance - refetchInterval: (data) => (getIsSettingUpSoftware(data) ? 5000 : false), // refetch every 5s until finished + refetchInterval: (data) => (hasRemainingSetupSteps(data) ? 5000 : false), // refetch every 5s until finished refetchIntervalInBackground: true, + select: (response) => { + // Marshal the response to include a `type` property so we can differentiate + // between software and script setup steps in the UI. + return [ + ...(response.setup_experience_results.software ?? []).map((s) => ({ + ...s, + type: "software" as const, + })), + ...(response.setup_experience_results.scripts ?? []).map((s) => ({ + ...s, + type: "script" as const, + })), + ]; + }, } ); @@ -484,21 +501,21 @@ const DeviceUserPage = ({ !host || isLoadingHost || isLoadingDeviceCertificates || - isLoadingSetupSoftware + isLoadingSetupSteps ) { return ; } - if (isErrorSetupSoftware) { + if (isErrorSetupSteps) { return ; } if ( checkForSetupExperienceSoftware && - getIsSettingUpSoftware(softwareSetupStatuses) + (hasRemainingSetupSteps(setupStepStatuses) || location.query.setup_only) ) { // at this point, softwareSetupStatuses will be non-empty return ( ); diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SettingUpYourDevice.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SettingUpYourDevice.tsx index aef975837a..7095d69bf9 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SettingUpYourDevice.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SettingUpYourDevice.tsx @@ -1,34 +1,46 @@ import Card from "components/Card"; -import { ISetupSoftwareStatus } from "interfaces/software"; +import { ISetupStep } from "interfaces/setup"; import React from "react"; import InfoButton from "../InfoButton"; -import SetupSoftwareStatusTable from "./SetupSoftwareStatusTable"; +import SetupStatusTable from "./SetupStatusTable"; + +import { hasRemainingSetupSteps } from "../../helpers"; const baseClass = "setting-up-your-device"; interface ISettingUpYourDevice { - softwareStatuses: ISetupSoftwareStatus[]; + setupSteps: ISetupStep[]; toggleInfoModal: () => void; } const SettingUpYourDevice = ({ - softwareStatuses, + setupSteps, toggleInfoModal, }: ISettingUpYourDevice) => { + let title; + let message; + if (hasRemainingSetupSteps(setupSteps)) { + title = "Setting up your device..."; + message = ` + Your computer is currently being configured by your organization. + Please don't attempt to restart or shut down the computer unless + prompted to do so. + `; + } else { + title = "Configuration complete"; + message = + "Your computer has been successfully configured. Setup will continue momentarily."; + } + return (
- -

My device

- -
-

Setting up your device...

-

- Your computer is currently being configured by your organization. - Please don't attempt to restart or shut down the computer unless - prompted to do so. -

- +
+

{title}

+ +
+

{message}

+
); diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/SetupSoftwareStatusTable.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/SetupSoftwareStatusTable.tsx deleted file mode 100644 index c50bd7dd4c..0000000000 --- a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/SetupSoftwareStatusTable.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from "react"; - -import { ISetupSoftwareStatus } from "interfaces/software"; - -import TableContainer from "components/TableContainer"; -import EmptyTable from "components/EmptyTable"; - -import generateColumnConfigs from "./SetupSoftwareStatusTableConfig"; - -const baseClass = "setup-software-status-table"; - -interface ISetupSoftwareStatusTableProps { - statuses: ISetupSoftwareStatus[]; -} - -const SetupSoftwareStatusTable = ({ - statuses, -}: ISetupSoftwareStatusTableProps) => { - const columnConfigs = generateColumnConfigs(); - - return ( -
- ( - // will never be empty - - )} - /> -
- ); -}; - -export default SetupSoftwareStatusTable; diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/SetupSoftwareStatusTableConfig.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/SetupSoftwareStatusTableConfig.tsx deleted file mode 100644 index 8d83ab08e3..0000000000 --- a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/SetupSoftwareStatusTableConfig.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; - -import { CellProps, Column } from "react-table"; - -import { ISetupSoftwareStatus } from "interfaces/software"; - -import SetupSoftwareProcessCell from "components/TableContainer/DataTable/SetupSoftwareProcessCell"; -import SetupSoftwareStatusCell from "components/TableContainer/DataTable/SetupSoftwareStatusCell"; - -type ISetupSoftwareStatusTableConfig = Column; -type ITableCellProps = CellProps; - -const generateColumnConfigs = (): ISetupSoftwareStatusTableConfig[] => [ - { - Header: "Process", - accessor: "name", - disableSortBy: true, - Cell: (cellProps: ITableCellProps) => { - const { name } = cellProps.row.original; - - return ; - }, - }, - { - Header: "Status", - accessor: "status", - disableSortBy: true, - Cell: (cellProps: ITableCellProps) => { - const { status } = cellProps.row.original; - - return ; - }, - }, -]; - -export default generateColumnConfigs; diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/index.ts b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/index.ts deleted file mode 100644 index 81adfd7215..0000000000 --- a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupSoftwareStatusTable/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./SetupSoftwareStatusTable"; diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTable.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTable.tsx new file mode 100644 index 0000000000..0ad5ae690a --- /dev/null +++ b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTable.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +import { ISetupStep } from "interfaces/setup"; + +import TableContainer from "components/TableContainer"; +import EmptyTable from "components/EmptyTable"; + +import generateColumnConfigs from "./SetupStatusTableConfig"; + +const baseClass = "setup-status-table"; + +interface ISetupStatusTableProps { + statuses: ISetupStep[]; +} + +const SetupStatusTable = ({ statuses }: ISetupStatusTableProps) => { + const columnConfigs = generateColumnConfigs(); + + // Sort the statuses so that scripts are always at the bottom. + statuses.sort((a, b) => { + if (a.type === b.type) { + return 0; + } + if (a.type === "script") { + return 1; + } + return -1; + }); + + return ( +
+ ( + // will never be empty + + )} + /> +
+ ); +}; + +export default SetupStatusTable; diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTableConfig.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTableConfig.tsx new file mode 100644 index 0000000000..09567c67d3 --- /dev/null +++ b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/SetupStatusTableConfig.tsx @@ -0,0 +1,48 @@ +import React from "react"; + +import { CellProps, Column } from "react-table"; + +import { ISetupStep } from "interfaces/setup"; + +import SetupSoftwareProcessCell from "components/TableContainer/DataTable/SetupSoftwareProcessCell"; +import SetupSoftwareStatusCell from "components/TableContainer/DataTable/SetupSoftwareStatusCell"; +import SetupScriptProcessCell from "components/TableContainer/DataTable/SetupScriptProcessCell"; +import SetupScriptStatusCell from "components/TableContainer/DataTable/SetupScriptStatusCell"; + +type ISetupStatusTableConfig = Column; +type ITableCellProps = CellProps; + +const generateColumnConfigs = (): ISetupStatusTableConfig[] => [ + { + Header: "Process", + accessor: "name", + disableSortBy: true, + Cell: (cellProps: ITableCellProps) => { + const { name, type } = cellProps.row.original; + if (type === "software") { + return ; + } + if (type === "script") { + return ; + } + return null; + }, + }, + { + Header: "Status", + accessor: "status", + disableSortBy: true, + Cell: (cellProps: ITableCellProps) => { + const { status, type } = cellProps.row.original; + if (type === "software") { + return ; + } + if (type === "script") { + return ; + } + return null; + }, + }, +]; + +export default generateColumnConfigs; diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/index.ts b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/index.ts new file mode 100644 index 0000000000..f2f94e6b2f --- /dev/null +++ b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/SetupStatusTable/index.ts @@ -0,0 +1 @@ +export { default } from "./SetupStatusTable"; diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/_styles.scss b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/_styles.scss index 2e3590a040..8130936ea5 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/_styles.scss +++ b/frontend/pages/hosts/details/DeviceUserPage/components/SettingUpYourDevice/_styles.scss @@ -1,7 +1,11 @@ .setting-up-your-device { &__header { - display: inline-flex; + display: flex; justify-content: space-between; + + .info-button { + padding-top: 0; + } } &__title { font-weight: $bold; @@ -18,4 +22,8 @@ p { margin: 0; } + + .empty-table__container { + max-width: 100%; + } } diff --git a/frontend/pages/hosts/details/DeviceUserPage/helpers.ts b/frontend/pages/hosts/details/DeviceUserPage/helpers.ts index 5af643b21b..d0ba6974e5 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/helpers.ts +++ b/frontend/pages/hosts/details/DeviceUserPage/helpers.ts @@ -1,4 +1,4 @@ -import { ISetupSoftwareStatus } from "interfaces/software"; +import { ISetupStep } from "interfaces/setup"; const DEFAULT_ERROR_MESSAGE = "refetch error."; @@ -7,8 +7,8 @@ export const getErrorMessage = (e: unknown, hostName: string) => { return `Host "${hostName}" ${DEFAULT_ERROR_MESSAGE}`; }; -export const getIsSettingUpSoftware = ( - statuses: ISetupSoftwareStatus[] | null | undefined +export const hasRemainingSetupSteps = ( + statuses: ISetupStep[] | null | undefined ) => { if (!statuses || statuses.length === 0) { // not configured or no software selected diff --git a/frontend/services/entities/device_user.ts b/frontend/services/entities/device_user.ts index cb3ae3bda1..7509272f78 100644 --- a/frontend/services/entities/device_user.ts +++ b/frontend/services/entities/device_user.ts @@ -1,6 +1,7 @@ import { IDeviceUserResponse } from "interfaces/host"; import { IListOptions } from "interfaces/list_options"; -import { IDeviceSoftware, ISetupSoftwareStatus } from "interfaces/software"; +import { IDeviceSoftware } from "interfaces/software"; +import { ISetupStep } from "interfaces/setup"; import { IHostCertificate } from "interfaces/certificates"; import sendRequest from "services"; import endpoints from "utilities/endpoints"; @@ -51,11 +52,11 @@ export interface IGetDeviceCertsRequestParams extends IListOptions { export interface IGetVppInstallCommandResultsResponse { results: IMdmCommandResult[]; } -export interface IGetSetupSoftwareStatusesResponse { - setup_experience_results: { software?: ISetupSoftwareStatus[] }; +export interface IGetSetupExperienceStatusesResponse { + setup_experience_results: { software: ISetupStep[]; scripts: ISetupStep[] }; } -export interface IGetSetupSoftwareStatusesParams { +export interface IGetSetupExperienceStatusesParams { token: string; } @@ -173,11 +174,11 @@ export default { return sendRequest("GET", path); }, - getSetupSoftwareStatuses: ({ + getSetupExperienceStatuses: ({ token, - }: IGetSetupSoftwareStatusesParams): Promise => { - const { DEVICE_SETUP_SOFTWARE_STATUSES } = endpoints; - const path = DEVICE_SETUP_SOFTWARE_STATUSES(token); + }: IGetSetupExperienceStatusesParams): Promise => { + const { DEVICE_SETUP_EXPERIENCE_STATUSES } = endpoints; + const path = DEVICE_SETUP_EXPERIENCE_STATUSES(token); return sendRequest("POST", path); }, diff --git a/frontend/test/handlers/device-handler.ts b/frontend/test/handlers/device-handler.ts index 61dee8150e..08cf34cb58 100644 --- a/frontend/test/handlers/device-handler.ts +++ b/frontend/test/handlers/device-handler.ts @@ -12,7 +12,7 @@ import { baseUrl } from "test/test-utils"; import { IDeviceUserResponse } from "interfaces/host"; import { IGetDeviceSoftwareResponse, - IGetSetupSoftwareStatusesResponse, + IGetSetupExperienceStatusesResponse, } from "services/entities/device_user"; import { IGetHostCertificatesResponse } from "services/entities/hosts"; @@ -87,7 +87,7 @@ export const defaultDeviceCertificatesHandler = http.get( ); export const deviceSetupExperienceHandler = ( - overrides?: Partial + overrides?: Partial ) => http.post(baseUrl("/device/:token/setup_experience/status"), () => { return HttpResponse.json( @@ -96,5 +96,5 @@ export const deviceSetupExperienceHandler = ( }); export const emptySetupExperienceHandler = deviceSetupExperienceHandler({ - setup_experience_results: { software: [] }, + setup_experience_results: { software: [], scripts: [] }, }); diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index e36a1250d5..dbd596d610 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -62,7 +62,7 @@ export default { DEVICE_CERTIFICATES: (token: string): string => { return `/${API_VERSION}/fleet/device/${token}/certificates`; }, - DEVICE_SETUP_SOFTWARE_STATUSES: (token: string): string => { + DEVICE_SETUP_EXPERIENCE_STATUSES: (token: string): string => { return `/${API_VERSION}/fleet/device/${token}/setup_experience/status`; }, DEVICE_RESEND_PROFILE: (token: string, profileUUID: string) => diff --git a/server/fleet/setup_experience.go b/server/fleet/setup_experience.go index e8ca5d5c40..4c6d168df1 100644 --- a/server/fleet/setup_experience.go +++ b/server/fleet/setup_experience.go @@ -196,6 +196,8 @@ func IsSetupExperienceSupported(hostPlatform string) bool { type DeviceSetupExperienceStatusPayload struct { // Software holds the status of the software to install on the device. Software []*SetupExperienceStatusResult `json:"software,omitempty"` + // Scripts holds the status of the scripts to run on the device. + Scripts []*SetupExperienceStatusResult `json:"scripts,omitempty"` } // HostUUIDForSetupExperience returns the host "UUID" to use during the "Setup experience"