UI: Make more specific and move down a level gating of Setup Experience UX to facilitate appropriate granular access to Linux and Android features (#32754)

## For #32683 

- Gate Setup experience steps for MDM and ABM being enabled at the
individual sidenav level instead of the entire section
- Allow Linux software installation even when MDM/ABM not enabled
- Improve typing of sidenav 

### Setup experience > Install software > Linux can be accessed without
MDM/ABM, but not macOS:

![ezgif-1c8bb8d13011ea](https://github.com/user-attachments/assets/56ffdbc5-2b49-4263-9483-0ebfc1b2754f)

### Other setup experience tabs gated without MDM/ABM configured (note
specific conditions for End user authentication - Apple MDM OR Android
MDM, with informative Tooltips:

![ezgif-1d194f6b298edd](https://github.com/user-attachments/assets/79450034-b278-46e9-9089-330c126336f3)


- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
jacobshandling 2025-09-10 16:51:02 -07:00 committed by GitHub
parent e2fd468c22
commit 64d23817ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 380 additions and 202 deletions

View file

@ -1,37 +1,15 @@
import React, { useContext } from "react";
import PATHS from "router/paths";
import { InjectedRouter, Params } from "react-router/lib/Router";
import { AppContext } from "context/app";
import SideNav from "pages/admin/components/SideNav";
import Button from "components/buttons/Button/Button";
import PremiumFeatureMessage from "components/PremiumFeatureMessage";
import EmptyTable from "components/EmptyTable";
import SETUP_EXPERIENCE_NAV_ITEMS from "./SetupExperienceNavItems";
import TurnOnMdmMessage from "../../../components/TurnOnMdmMessage";
const baseClass = "setup-experience";
interface ISetupEmptyState {
router: InjectedRouter;
}
const SetupEmptyState = ({ router }: ISetupEmptyState) => {
const onClickEmptyConnect = () => {
router.push(PATHS.ADMIN_INTEGRATIONS_MDM);
};
return (
<EmptyTable
header="Setup experience for macOS hosts"
info="Connect Fleet to the Apple Business Manager to get started."
primaryButton={<Button onClick={onClickEmptyConnect}>Connect</Button>}
/>
);
};
interface ISetupExperienceProps {
params: Params;
location: { search: string };
@ -46,7 +24,7 @@ const SetupExperience = ({
teamIdForApi,
}: ISetupExperienceProps) => {
const { section } = params;
const { isPremiumTier, config } = useContext(AppContext);
const { isPremiumTier } = useContext(AppContext);
// Not premium shows premium message
if (!isPremiumTier) {
@ -57,22 +35,6 @@ const SetupExperience = ({
);
}
// MDM is not on so show messaging for user to enable it.
if (!config?.mdm.enabled_and_configured) {
return (
<TurnOnMdmMessage
header="Manage setup experience for macOS"
info="To install software and run scripts when Macs first boot, first turn on automatic enrollment."
buttonText="Turn on"
router={router}
/>
);
}
// User has not set up Apple Business Manager.
if (!config?.mdm.apple_bm_enabled_and_configured) {
return <SetupEmptyState router={router} />;
}
const DEFAULT_SETTINGS_SECTION = SETUP_EXPERIENCE_NAV_ITEMS[0];
const currentFormSection =

View file

@ -1,5 +1,7 @@
import PATHS from "router/paths";
import { InjectedRouter } from "react-router";
import { ISideNavItem } from "pages/admin/components/SideNav/SideNav";
import EndUserAuthentication from "./cards/EndUserAuthentication/EndUserAuthentication";
@ -8,14 +10,12 @@ import SetupAssistant from "./cards/SetupAssistant";
import InstallSoftware from "./cards/InstallSoftware";
import RunScript from "./cards/RunScript";
interface ISetupExperienceCardProps {
currentTeamId?: number;
export interface ISetupExperienceCardProps {
currentTeamId: number;
router: InjectedRouter;
}
// TODO: types
const SETUP_EXPERIENCE_NAV_ITEMS: ISideNavItem<
ISetupExperienceCardProps | any
>[] = [
const SETUP_EXPERIENCE_NAV_ITEMS: ISideNavItem<ISetupExperienceCardProps>[] = [
{
title: "1. End user authentication",
urlSection: "end-user-auth",

View file

@ -8,23 +8,10 @@
margin-top: 80px;
}
&__empty-state {
margin-top: $pad-xxlarge;
text-align: center;
h2 {
font-size: $small;
margin: 0 0 $pad-xsmall;
}
p {
font-size: $x-small;
margin: 0 0 $pad-medium;
}
button {
margin: 0 auto;
}
.turn-on-mdm-message {
@include tab-empty-state;
max-width: none;
padding: 64px 170px;
margin: 0;
}
}

View file

@ -1,10 +1,10 @@
import React from "react";
import { screen } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import { createCustomRenderer } from "test/test-utils";
import { createCustomRenderer, createMockRouter } from "test/test-utils";
import mockServer from "test/mock-server";
import {
createSetupExperienceBootstrapPackageHandler,
createSetupExperienceBootstrapMetadataHandler,
createSetupExperienceScriptHandler,
createSetupExperienceSoftwareHandler,
createSetuUpExperienceBootstrapSummaryHandler,
@ -16,6 +16,7 @@ import {
createMockSoftwarePackage,
createMockSoftwareTitle,
} from "__mocks__/softwareMock";
import { createMockMdmConfig } from "__mocks__/configMock";
import BootstrapPackage from "./BootstrapPackage";
@ -23,7 +24,7 @@ import BootstrapPackage from "./BootstrapPackage";
* sets up some default backend mocks for the tests. Override what you need
* with mockServer.use() in the test itself.
*/
const setuDefaultBackendMocks = () => {
const setupDefaultBackendMocks = () => {
mockServer.use(createGetConfigHandler());
// default is no run script or install software already added
@ -32,7 +33,7 @@ const setuDefaultBackendMocks = () => {
// default will be a bootstrap package already uploaded
mockServer.use(
createSetupExperienceBootstrapPackageHandler({ name: "foo-package.pkg" })
createSetupExperienceBootstrapMetadataHandler({ name: "foo-package.pkg" })
);
mockServer.use(
createSetuUpExperienceBootstrapSummaryHandler({
@ -44,11 +45,55 @@ const setuDefaultBackendMocks = () => {
};
describe("BootstrapPackage", () => {
it("renders the status table and bootstrap package if a package has been uploaded", async () => {
setuDefaultBackendMocks();
it("renders the 'turn on automatic enrollment' message when MDM isn't configured", async () => {
setupDefaultBackendMocks();
mockServer.use(
createGetConfigHandler({
mdm: createMockMdmConfig({ enabled_and_configured: false }),
})
);
const render = createCustomRenderer({
withBackendMock: true,
});
const render = createCustomRenderer({ withBackendMock: true });
render(<BootstrapPackage currentTeamId={0} />);
render(<BootstrapPackage router={createMockRouter()} currentTeamId={0} />);
await waitFor(() => {
expect(
screen.getByText(/turn on automatic enrollment/)
).toBeInTheDocument();
});
});
it("renders the 'turn on automatic enrollment' message when MDM is configured, but ABM is not", async () => {
setupDefaultBackendMocks();
mockServer.use(
createGetConfigHandler({
mdm: createMockMdmConfig({
enabled_and_configured: true,
apple_bm_enabled_and_configured: false,
}),
})
);
const render = createCustomRenderer({
withBackendMock: true,
});
render(<BootstrapPackage router={createMockRouter()} currentTeamId={0} />);
await waitFor(() => {
expect(
screen.getByText(/turn on automatic enrollment/)
).toBeInTheDocument();
});
});
it("renders the status table and bootstrap package if a package has been uploaded", async () => {
setupDefaultBackendMocks();
const render = createCustomRenderer({
withBackendMock: true,
});
render(<BootstrapPackage router={createMockRouter()} currentTeamId={0} />);
await screen.findByText(/status/gi);
@ -61,11 +106,14 @@ describe("BootstrapPackage", () => {
});
it("render the bootstrap package uploader if a package has not been uploaded", async () => {
setuDefaultBackendMocks();
setupDefaultBackendMocks();
mockServer.use(errorNoBootstrapPackageMetadataHandler);
const render = createCustomRenderer({ withBackendMock: true });
render(<BootstrapPackage currentTeamId={0} />);
const render = createCustomRenderer({
withBackendMock: true,
});
render(<BootstrapPackage router={createMockRouter()} currentTeamId={0} />);
await screen.findByText(/Upload a bootstrap package/gi);
@ -82,11 +130,16 @@ describe("BootstrapPackage", () => {
});
it("renders the advanced options as disabled if there is no bootstrap package uploaded", async () => {
setuDefaultBackendMocks();
setupDefaultBackendMocks();
mockServer.use(errorNoBootstrapPackageMetadataHandler);
const render = createCustomRenderer({ withBackendMock: true });
const { user } = render(<BootstrapPackage currentTeamId={0} />);
const render = createCustomRenderer({
withBackendMock: true,
});
const { user } = render(
<BootstrapPackage router={createMockRouter()} currentTeamId={0} />
);
await screen.findByText("Show advanced options");
await user.click(screen.getByText("Show advanced options"));
@ -98,7 +151,7 @@ describe("BootstrapPackage", () => {
});
it("renders the advanced options as disabled if there are already added install software", async () => {
setuDefaultBackendMocks();
setupDefaultBackendMocks();
mockServer.use(
createSetupExperienceSoftwareHandler({
software_titles: [
@ -111,8 +164,13 @@ describe("BootstrapPackage", () => {
})
);
const render = createCustomRenderer({ withBackendMock: true });
const { user } = render(<BootstrapPackage currentTeamId={0} />);
const render = createCustomRenderer({
withBackendMock: true,
});
const { user } = render(
<BootstrapPackage router={createMockRouter()} currentTeamId={0} />
);
await screen.findByText("Show advanced options");
await user.click(screen.getByText("Show advanced options"));
@ -124,11 +182,16 @@ describe("BootstrapPackage", () => {
});
it("renders the advanced options as disabled if there is alreaddy a run script added", async () => {
setuDefaultBackendMocks();
setupDefaultBackendMocks();
mockServer.use(createSetupExperienceScriptHandler());
const render = createCustomRenderer({ withBackendMock: true });
const { user } = render(<BootstrapPackage currentTeamId={0} />);
const render = createCustomRenderer({
withBackendMock: true,
});
const { user } = render(
<BootstrapPackage router={createMockRouter()} currentTeamId={0} />
);
await screen.findByText("Show advanced options");
await user.click(screen.getByText("Show advanced options"));

View file

@ -17,6 +17,7 @@ import { NotificationContext } from "context/notification";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
import Spinner from "components/Spinner";
import TurnOnMdmMessage from "components/TurnOnMdmMessage";
import SectionHeader from "components/SectionHeader";
import BootstrapPackagePreview from "./components/BootstrapPackagePreview";
@ -26,6 +27,8 @@ import DeleteBootstrapPackageModal from "./components/DeleteBootstrapPackageModa
import BootstrapAdvancedOptions from "./components/BootstrapAdvancedOptions";
import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer";
import { getInstallSoftwareDuringSetupCount } from "../InstallSoftware/components/AddInstallSoftware/helpers";
import { ISetupExperienceCardProps } from "../../SetupExperienceNavItems";
import getManualAgentInstallSetting from "../../helpers";
const baseClass = "bootstrap-package";
@ -33,22 +36,10 @@ const baseClass = "bootstrap-package";
// available for install so we can correctly display the selected count.
const PER_PAGE_SIZE = 3000;
export const getManualAgentInstallSetting = (
currentTeamId: number,
globalConfig?: IConfig,
teamConfig?: ITeamConfig
) => {
if (currentTeamId === API_NO_TEAM_ID) {
return globalConfig?.mdm.macos_setup.manual_agent_install || false;
}
return teamConfig?.mdm?.macos_setup.manual_agent_install || false;
};
interface IBootstrapPackageProps {
currentTeamId: number;
}
const BootstrapPackage = ({ currentTeamId }: IBootstrapPackageProps) => {
const BootstrapPackage = ({
currentTeamId,
router,
}: ISetupExperienceCardProps) => {
const { renderFlash } = useContext(NotificationContext);
const [
selectedManualAgentInstall,
@ -87,6 +78,7 @@ const BootstrapPackage = ({ currentTeamId }: IBootstrapPackageProps) => {
);
const {
data: globalConfig,
isLoading: isLoadingGlobalConfig,
refetch: refetchGlobalConfig,
} = useQuery<IConfig, Error>(
@ -94,11 +86,12 @@ const BootstrapPackage = ({ currentTeamId }: IBootstrapPackageProps) => {
() => configAPI.loadAll(),
{
...DEFAULT_USE_QUERY_OPTIONS,
enabled: currentTeamId === API_NO_TEAM_ID,
onSuccess: (data) => {
setSelectedManualAgentInstall(
getManualAgentInstallSetting(currentTeamId, data)
);
if (currentTeamId === API_NO_TEAM_ID) {
setSelectedManualAgentInstall(
getManualAgentInstallSetting(currentTeamId, data)
);
}
},
}
);
@ -211,10 +204,32 @@ const BootstrapPackage = ({ currentTeamId }: IBootstrapPackageProps) => {
isLoadingScript ||
isLoadingSoftware;
const renderContent = () => {
if (isLoading) {
return <Spinner />;
}
if (
!(
globalConfig?.mdm.enabled_and_configured &&
globalConfig?.mdm.apple_bm_enabled_and_configured
)
) {
return (
<TurnOnMdmMessage
header="Additional configuration required"
info="Supported on macOS. To customize, first turn on automatic enrollment."
buttonText="Turn on"
router={router}
/>
);
}
return renderBootstrapView();
};
return (
<section className={baseClass}>
<SectionHeader title="Bootstrap package" />
{isLoading ? <Spinner /> : renderBootstrapView()}
{renderContent()}
{showDeleteBootstrapPackageModal && (
<DeleteBootstrapPackageModal
onDelete={onDelete}

View file

@ -1,5 +1,4 @@
import React from "react";
import { InjectedRouter } from "react-router";
import PATHS from "router/paths";
import { useQuery } from "react-query";
@ -10,11 +9,13 @@ import { ITeamConfig } from "interfaces/team";
import SectionHeader from "components/SectionHeader/SectionHeader";
import Spinner from "components/Spinner";
import TurnOnMdmMessage from "components/TurnOnMdmMessage";
import RequireEndUserAuth from "./components/RequireEndUserAuth/RequireEndUserAuth";
import EndUserAuthForm from "./components/EndUserAuthForm/EndUserAuthForm";
import EndUserExperiencePreview from "./components/EndUserExperiencePreview";
import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer";
import { ISetupExperienceCardProps } from "../../SetupExperienceNavItems";
const baseClass = "end-user-authentication";
@ -45,15 +46,10 @@ const isIdPConfigured = ({
);
};
interface IEndUserAuthenticationProps {
currentTeamId: number;
router: InjectedRouter;
}
const EndUserAuthentication = ({
currentTeamId,
router,
}: IEndUserAuthenticationProps) => {
}: ISetupExperienceCardProps) => {
const { data: globalConfig, isLoading: isLoadingGlobalConfig } = useQuery<
IConfig,
Error
@ -80,27 +76,48 @@ const EndUserAuthentication = ({
);
const onClickConnect = () => {
router.push(PATHS.ADMIN_INTEGRATIONS_MDM);
router.push(PATHS.ADMIN_INTEGRATIONS_IDENTITY_PROVIDER);
};
const renderContent = () => {
if (!globalConfig || isLoadingGlobalConfig || isLoadingTeamConfig) {
return <Spinner />;
}
const mdmConfig = globalConfig.mdm;
if (
!(
mdmConfig.enabled_and_configured ||
mdmConfig.android_enabled_and_configured
)
) {
return (
<TurnOnMdmMessage
header="Additional configuration required"
info="Supported on macOS, iOS, iPadOS, and Android. To customize, first turn on MDM."
buttonText="Turn on"
router={router}
/>
);
}
return (
<SetupExperienceContentContainer>
{!isIdPConfigured(mdmConfig) ? (
<RequireEndUserAuth onClickConnect={onClickConnect} />
) : (
<EndUserAuthForm
currentTeamId={currentTeamId}
defaultIsEndUserAuthEnabled={defaultIsEndUserAuthEnabled}
/>
)}
<EndUserExperiencePreview />
</SetupExperienceContentContainer>
);
};
return (
<section className={baseClass}>
<SectionHeader title="End user authentication" />
{isLoadingGlobalConfig || isLoadingTeamConfig ? (
<Spinner />
) : (
<SetupExperienceContentContainer>
{!globalConfig || !isIdPConfigured(globalConfig.mdm) ? (
<RequireEndUserAuth onClickConnect={onClickConnect} />
) : (
<EndUserAuthForm
currentTeamId={currentTeamId}
defaultIsEndUserAuthEnabled={defaultIsEndUserAuthEnabled}
/>
)}
<EndUserExperiencePreview />
</SetupExperienceContentContainer>
)}
{renderContent()}
</section>
);
};

View file

@ -10,9 +10,19 @@ import Checkbox from "components/forms/fields/Checkbox";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import { NotificationContext } from "context/notification";
import { AppContext } from "context/app";
import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "end-user-auth-form";
const getTooltipCopy = (android = false) => {
return (
<>
{android ? "Android" : "Apple"} MDM must be turned on in <b>Settings</b>{" "}
&gt; <b>Integrations</b> &gt; <b>Mobile Device Management(MDM)</b> to turn
on end user authentication.
</>
);
};
interface IEndUserAuthFormProps {
currentTeamId: number;
defaultIsEndUserAuthEnabled: boolean;
@ -64,7 +74,15 @@ const EndUserAuthForm = ({
<p className={classes}>
Require end users to authenticate with your identity provider (IdP)
and agree to an end user license agreement (EULA) when they setup
their new macOS, iOS, iPadOS and Android hosts.{" "}
their new{" "}
<TooltipWrapper tipContent={getTooltipCopy()}>macOS</TooltipWrapper>,{" "}
<TooltipWrapper tipContent={getTooltipCopy()}>iOS</TooltipWrapper>,{" "}
<TooltipWrapper tipContent={getTooltipCopy()}>iPadOS</TooltipWrapper>{" "}
and{" "}
<TooltipWrapper tipContent={getTooltipCopy(true)}>
Android
</TooltipWrapper>{" "}
hosts.{" "}
<Link to={PATHS.ADMIN_INTEGRATIONS_IDENTITY_PROVIDER}>View IdP</Link>{" "}
and <Link to={PATHS.ADMIN_INTEGRATIONS_MDM}>EULA</Link>.
</p>

View file

@ -20,12 +20,14 @@ import Spinner from "components/Spinner";
import TabNav from "components/TabNav";
import TabText from "components/TabText";
import CustomLink from "components/CustomLink";
import TurnOnMdmMessage from "components/TurnOnMdmMessage";
import InstallSoftwarePreview from "./components/InstallSoftwarePreview";
import AddInstallSoftware from "./components/AddInstallSoftware";
import SelectSoftwareModal from "./components/SelectSoftwareModal";
import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer";
import { getManualAgentInstallSetting } from "../BootstrapPackage/BootstrapPackage";
import { ISetupExperienceCardProps } from "../../SetupExperienceNavItems";
import getManualAgentInstallSetting from "../../helpers";
const baseClass = "install-software";
@ -41,11 +43,10 @@ export const PLATFORM_BY_INDEX: SetupExperiencePlatform[] = [
"linux",
];
interface IInstallSoftwareProps {
currentTeamId: number;
}
const InstallSoftware = ({ currentTeamId }: IInstallSoftwareProps) => {
const InstallSoftware = ({
currentTeamId,
router,
}: ISetupExperienceCardProps) => {
const [showSelectSoftwareModal, setShowSelectSoftwareModal] = useState(false);
const [
selectedPlatform,
@ -81,7 +82,6 @@ const InstallSoftware = ({ currentTeamId }: IInstallSoftwareProps) => {
Error
>(["config", currentTeamId], () => configAPI.loadAll(), {
...DEFAULT_USE_QUERY_OPTIONS,
enabled: currentTeamId === API_NO_TEAM_ID,
});
const { data: teamConfig, isLoading: isLoadingTeamConfig } = useQuery<
@ -130,6 +130,21 @@ const InstallSoftware = ({ currentTeamId }: IInstallSoftwareProps) => {
}
if (softwareTitles || softwareTitles === null) {
const appleMdmAndAbmEnabled =
globalConfig?.mdm.enabled_and_configured &&
globalConfig?.mdm.apple_bm_enabled_and_configured;
// TODO - incorporate Windows MDM condition when implementing Windows software install on setup
if (platform === "macos" && !appleMdmAndAbmEnabled) {
return (
<TurnOnMdmMessage
header="Additional configuration required"
info="To customize, first turn on automatic enrollment."
buttonText="Turn on"
router={router}
/>
);
}
return (
<SetupExperienceContentContainer>
<AddInstallSoftware

View file

@ -4,5 +4,7 @@
}
&__windows {
@include tab-empty-state;
max-width: none;
padding: 64px 170px;
}
}

View file

@ -2,29 +2,75 @@ import React from "react";
import { screen } from "@testing-library/react";
import mockServer from "test/mock-server";
import { createCustomRenderer } from "test/test-utils";
import { createCustomRenderer, createMockRouter } from "test/test-utils";
import {
createSetupExperienceScriptHandler,
errorNoSetupExperienceScriptHandler,
} from "test/handlers/setup-experience-handlers";
import { createGetConfigHandler } from "test/handlers/config-handlers";
import { createMockMdmConfig } from "__mocks__/configMock";
import RunScript from "./RunScript";
describe("RunScript", () => {
it("should render the 'turn on automatic enrollment' message when MDM isn't configured", async () => {
mockServer.use(errorNoSetupExperienceScriptHandler);
mockServer.use(
createGetConfigHandler({
mdm: createMockMdmConfig({ enabled_and_configured: false }),
})
);
const render = createCustomRenderer({
withBackendMock: true,
});
render(<RunScript router={createMockRouter()} currentTeamId={1} />);
expect(
await screen.getByText(/turn on automatic enrollment/)
).toBeInTheDocument();
});
it("should render the 'turn on automatic enrollment' message when MDM is configured but not ABM", async () => {
mockServer.use(errorNoSetupExperienceScriptHandler);
mockServer.use(
createGetConfigHandler({
mdm: createMockMdmConfig({
enabled_and_configured: true,
apple_bm_enabled_and_configured: false,
}),
})
);
const render = createCustomRenderer({
withBackendMock: true,
});
render(<RunScript router={createMockRouter()} currentTeamId={1} />);
expect(
await screen.getByText(/turn on automatic enrollment/)
).toBeInTheDocument();
});
it("should render the script uploader when no script has been uploaded", async () => {
mockServer.use(errorNoSetupExperienceScriptHandler);
const render = createCustomRenderer({ withBackendMock: true });
mockServer.use(createGetConfigHandler());
const render = createCustomRenderer({
withBackendMock: true,
});
render(<RunScript currentTeamId={1} />);
render(<RunScript router={createMockRouter()} currentTeamId={1} />);
expect(await screen.findByRole("button", { name: "Upload" })).toBeVisible();
});
it("should render the uploaded script uploader when a script has been uploaded", async () => {
mockServer.use(createSetupExperienceScriptHandler());
const render = createCustomRenderer({ withBackendMock: true });
mockServer.use(createGetConfigHandler());
const render = createCustomRenderer({
withBackendMock: true,
});
render(<RunScript currentTeamId={1} />);
render(<RunScript router={createMockRouter()} currentTeamId={1} />);
expect(
await screen.findByText("Script will run during setup:")

View file

@ -6,6 +6,7 @@ import {
DEFAULT_USE_QUERY_OPTIONS,
LEARN_MORE_ABOUT_BASE_LINK,
} from "utilities/constants";
import mdmAPI, {
IGetSetupExperienceScriptResponse,
} from "services/entities/mdm";
@ -18,28 +19,26 @@ import SectionHeader from "components/SectionHeader";
import DataError from "components/DataError";
import Spinner from "components/Spinner";
import CustomLink from "components/CustomLink";
import TurnOnMdmMessage from "components/TurnOnMdmMessage";
import SetupExperiencePreview from "./components/SetupExperienceScriptPreview";
import SetupExperienceScriptUploader from "./components/SetupExperienceScriptUploader";
import SetupExperienceScriptCard from "./components/SetupExperienceScriptCard";
import DeleteSetupExperienceScriptModal from "./components/DeleteSetupExperienceScriptModal";
import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer";
import { getManualAgentInstallSetting } from "../BootstrapPackage/BootstrapPackage";
import { ISetupExperienceCardProps } from "../../SetupExperienceNavItems";
import getManualAgentInstallSetting from "../../helpers";
const baseClass = "run-script";
interface IRunScriptProps {
currentTeamId: number;
}
const RunScript = ({ currentTeamId }: IRunScriptProps) => {
const RunScript = ({ currentTeamId, router }: ISetupExperienceCardProps) => {
const [showDeleteScriptModal, setShowDeleteScriptModal] = useState(false);
const {
data: script,
error: scriptError,
isLoading,
isError,
isError: isScriptError,
refetch: refetchScript,
remove: removeScriptFromCache,
} = useQuery<IGetSetupExperienceScriptResponse, AxiosError>(
@ -53,7 +52,6 @@ const RunScript = ({ currentTeamId }: IRunScriptProps) => {
Error
>(["config", currentTeamId], () => configAPI.loadAll(), {
...DEFAULT_USE_QUERY_OPTIONS,
enabled: currentTeamId === API_NO_TEAM_ID,
});
const { data: teamConfig, isLoading: isLoadingTeamConfig } = useQuery<
@ -87,7 +85,23 @@ const RunScript = ({ currentTeamId }: IRunScriptProps) => {
<Spinner />;
}
if (isError && scriptError.status !== 404) {
if (
!(
globalConfig?.mdm.enabled_and_configured &&
globalConfig?.mdm.apple_bm_enabled_and_configured
)
) {
return (
<TurnOnMdmMessage
header="Additional configuration required"
info="Supported on macOS. To customize, first turn on automatic enrollment."
buttonText="Turn on"
router={router}
/>
);
}
if (isScriptError && scriptError.status !== 404) {
return <DataError />;
}
@ -123,6 +137,14 @@ const RunScript = ({ currentTeamId }: IRunScriptProps) => {
)}
</div>
<SetupExperiencePreview />
{showDeleteScriptModal && script && (
<DeleteSetupExperienceScriptModal
currentTeamId={currentTeamId}
scriptName={script.name}
onDeleted={onDelete}
onExit={() => setShowDeleteScriptModal(false)}
/>
)}
</SetupExperienceContentContainer>
);
};
@ -130,15 +152,7 @@ const RunScript = ({ currentTeamId }: IRunScriptProps) => {
return (
<section className={baseClass}>
<SectionHeader title="Run script" />
<>{renderContent()}</>
{showDeleteScriptModal && script && (
<DeleteSetupExperienceScriptModal
currentTeamId={currentTeamId}
scriptName={script.name}
onDeleted={onDelete}
onExit={() => setShowDeleteScriptModal(false)}
/>
)}
{renderContent()}
</section>
);
};

View file

@ -14,6 +14,7 @@ import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
import SectionHeader from "components/SectionHeader";
import Spinner from "components/Spinner";
import CustomLink from "components/CustomLink";
import TurnOnMdmMessage from "components/TurnOnMdmMessage";
import SetupAssistantPreview from "./components/SetupAssistantPreview";
import SetupAssistantProfileUploader from "./components/SetupAssistantProfileUploader";
@ -21,14 +22,14 @@ import SetupAssistantProfileCard from "./components/SetupAssistantProfileCard/Se
import DeleteAutoEnrollmentProfile from "./components/DeleteAutoEnrollmentProfile";
import AdvancedOptionsForm from "./components/AdvancedOptionsForm";
import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer";
import { ISetupExperienceCardProps } from "../../SetupExperienceNavItems";
const baseClass = "setup-assistant";
interface ISetupAssistantProps {
currentTeamId: number;
}
const SetupAssistant = ({ currentTeamId }: ISetupAssistantProps) => {
const SetupAssistant = ({
currentTeamId,
router,
}: ISetupExperienceCardProps) => {
const [showDeleteProfileModal, setShowDeleteProfileModal] = useState(false);
const { data: globalConfig, isLoading: isLoadingGlobalConfig } = useQuery<
@ -37,7 +38,6 @@ const SetupAssistant = ({ currentTeamId }: ISetupAssistantProps) => {
>(["config", currentTeamId], () => configAPI.loadAll(), {
...DEFAULT_USE_QUERY_OPTIONS,
retry: false,
enabled: currentTeamId === API_NO_TEAM_ID,
});
const { data: teamConfig, isLoading: isLoadingTeamConfig } = useQuery<
@ -90,45 +90,69 @@ const SetupAssistant = ({ currentTeamId }: ISetupAssistantProps) => {
isLoadingGlobalConfig || isLoadingTeamConfig || isLoadingEnrollmentProfile;
const enrollmentProfileNotFound = enrollmentProfileError?.status === 404;
const renderSetupAssistantView = () => {
return (
<SetupExperienceContentContainer>
<div className={`${baseClass}__upload-container`}>
<p className={`${baseClass}__section-description`}>
Add an automatic enrollment profile to customize the macOS Setup
Assistant.{" "}
<CustomLink
url="https://fleetdm.com/learn-more-about/setup-assistant"
text="Learn how"
newTab
/>
</p>
{enrollmentProfileNotFound || !enrollmentProfileData ? (
<SetupAssistantProfileUploader
currentTeamId={currentTeamId}
onUpload={onUpload}
/>
) : (
<SetupAssistantProfileCard
profile={enrollmentProfileData}
onDelete={() => setShowDeleteProfileModal(true)}
/>
)}
<AdvancedOptionsForm
key={String(defaultReleaseDeviceSetting)}
currentTeamId={currentTeamId}
defaultReleaseDevice={defaultReleaseDeviceSetting}
/>
</div>
<div className={`${baseClass}__preview-container`}>
<SetupAssistantPreview />
</div>
</SetupExperienceContentContainer>
);
};
const renderContent = () => {
if (isLoading) {
return <Spinner />;
}
if (
!(
globalConfig?.mdm.enabled_and_configured &&
globalConfig?.mdm.apple_bm_enabled_and_configured
)
) {
return (
<TurnOnMdmMessage
header="Additional configuration required"
info="Supported on macOS. To customize, first turn on automatic enrollment."
buttonText="Turn on"
router={router}
/>
);
}
return renderSetupAssistantView();
};
return (
<section className={baseClass}>
<SectionHeader title="Setup assistant" />
{isLoading ? (
<Spinner />
) : (
<SetupExperienceContentContainer>
<div className={`${baseClass}__upload-container`}>
<p className={`${baseClass}__section-description`}>
Add an automatic enrollment profile to customize the macOS Setup
Assistant.{" "}
<CustomLink
url="https://fleetdm.com/learn-more-about/setup-assistant"
text="Learn how"
newTab
/>
</p>
{enrollmentProfileNotFound || !enrollmentProfileData ? (
<SetupAssistantProfileUploader
currentTeamId={currentTeamId}
onUpload={onUpload}
/>
) : (
<SetupAssistantProfileCard
profile={enrollmentProfileData}
onDelete={() => setShowDeleteProfileModal(true)}
/>
)}
<AdvancedOptionsForm
key={String(defaultReleaseDeviceSetting)}
currentTeamId={currentTeamId}
defaultReleaseDevice={defaultReleaseDeviceSetting}
/>
</div>
<div className={`${baseClass}__preview-container`}>
<SetupAssistantPreview />
</div>
</SetupExperienceContentContainer>
)}
{renderContent()}
{showDeleteProfileModal && (
<DeleteAutoEnrollmentProfile
currentTeamId={currentTeamId}

View file

@ -0,0 +1,15 @@
import { IConfig } from "interfaces/config";
import { API_NO_TEAM_ID, ITeamConfig } from "interfaces/team";
const getManualAgentInstallSetting = (
currentTeamId: number,
globalConfig?: IConfig,
teamConfig?: ITeamConfig
) => {
if (currentTeamId === API_NO_TEAM_ID) {
return globalConfig?.mdm.macos_setup.manual_agent_install || false;
}
return teamConfig?.mdm?.macos_setup.manual_agent_install || false;
};
export default getManualAgentInstallSetting;

View file

@ -53,7 +53,7 @@ export const createSetupExperienceSoftwareHandler = (
);
});
export const createSetupExperienceBootstrapPackageHandler = (
export const createSetupExperienceBootstrapMetadataHandler = (
overrides?: Partial<IGetBootstrapPackageMetadataResponse>
) =>
http.get(setupExperienceBootstrapMetadataUrl, () => {