mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
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:  ### Other setup experience tabs gated without MDM/ABM configured (note specific conditions for End user authentication - Apple MDM OR Android MDM, with informative Tooltips:  - [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:
parent
e2fd468c22
commit
64d23817ad
14 changed files with 380 additions and 202 deletions
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>{" "}
|
||||
> <b>Integrations</b> > <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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,5 +4,7 @@
|
|||
}
|
||||
&__windows {
|
||||
@include tab-empty-state;
|
||||
max-width: none;
|
||||
padding: 64px 170px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:")
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
15
frontend/pages/ManageControlsPage/SetupExperience/helpers.ts
Normal file
15
frontend/pages/ManageControlsPage/SetupExperience/helpers.ts
Normal 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;
|
||||
|
|
@ -53,7 +53,7 @@ export const createSetupExperienceSoftwareHandler = (
|
|||
);
|
||||
});
|
||||
|
||||
export const createSetupExperienceBootstrapPackageHandler = (
|
||||
export const createSetupExperienceBootstrapMetadataHandler = (
|
||||
overrides?: Partial<IGetBootstrapPackageMetadataResponse>
|
||||
) =>
|
||||
http.get(setupExperienceBootstrapMetadataUrl, () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue