Fleet UI: Update Setup experience > Users (#43354)

This commit is contained in:
RachelElysia
2026-04-23 12:28:36 -04:00
committed by GitHub
parent fe66e3fd08
commit b906e8061d
16 changed files with 309 additions and 189 deletions
+3
View File
@@ -68,6 +68,9 @@ export interface ITeam extends ITeamSummary {
require_all_software_macos: boolean | null;
lock_end_user_info: boolean | null;
};
macos_setup: {
enable_managed_local_account?: boolean;
};
windows_updates: {
deadline_days: number | null;
grace_period_days: number | null;
@@ -4,7 +4,7 @@ import { InjectedRouter } from "react-router";
import { ISideNavItem } from "pages/admin/components/SideNav/SideNav";
import EndUserAuthentication from "./cards/EndUserAuthentication/EndUserAuthentication";
import Users from "./cards/Users/Users";
import BootstrapPackage from "./cards/BootstrapPackage";
import SetupAssistant from "./cards/SetupAssistant";
import InstallSoftware from "./cards/InstallSoftware";
@@ -18,10 +18,10 @@ export interface ISetupExperienceCardProps {
const SETUP_EXPERIENCE_NAV_ITEMS: ISideNavItem<ISetupExperienceCardProps>[] = [
{
title: "1. End user authentication",
urlSection: "end-user-auth",
path: PATHS.CONTROLS_END_USER_AUTHENTICATION,
Card: EndUserAuthentication,
title: "1. Users",
urlSection: "users",
path: PATHS.CONTROLS_USERS,
Card: Users,
},
{
title: "2. Bootstrap package",
@@ -1,152 +0,0 @@
import React, { useContext, useState } from "react";
import classnames from "classnames";
import PATHS from "router/paths";
import mdmAPI from "services/entities/mdm";
import { NotificationContext } from "context/notification";
import { AppContext } from "context/app";
import Button from "components/buttons/Button";
import Checkbox from "components/forms/fields/Checkbox";
import CustomLink from "components/CustomLink";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import TooltipWrapper from "components/TooltipWrapper";
import RevealButton from "components/buttons/RevealButton";
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;
defaultLockEndUserInfo: boolean;
}
const EndUserAuthForm = ({
currentTeamId,
defaultIsEndUserAuthEnabled,
defaultLockEndUserInfo,
}: IEndUserAuthFormProps) => {
const { renderFlash } = useContext(NotificationContext);
const gitOpsModeEnabled = useContext(AppContext).config?.gitops
.gitops_mode_enabled;
const [isEndUserAuthEnabled, setEndUserAuthEnabled] = useState(
defaultIsEndUserAuthEnabled
);
const [lockEndUserInfo, setLockEndUserInfo] = useState(
defaultLockEndUserInfo
);
const [isUpdating, setIsUpdating] = useState(false);
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const onToggleEndUserAuth = (newCheckVal: boolean) => {
setEndUserAuthEnabled(newCheckVal);
// Sync lock end user info with EUA: enabling EUA enables it, disabling EUA disables it.
setLockEndUserInfo(newCheckVal);
};
const onChangeLockEndUserInfo = (newCheckVal: boolean) => {
setLockEndUserInfo(newCheckVal);
};
const onClickSave = async () => {
setIsUpdating(true);
const canLockEndUserInfo = isEndUserAuthEnabled && lockEndUserInfo;
try {
await mdmAPI.updateEndUserAuthentication(
currentTeamId,
isEndUserAuthEnabled,
canLockEndUserInfo
);
renderFlash("success", "Successfully updated.");
} catch {
renderFlash("error", "Couldnt update. Please try again.");
} finally {
setIsUpdating(false);
setLockEndUserInfo(canLockEndUserInfo);
}
};
const classes = classnames({ [`${baseClass}--disabled`]: gitOpsModeEnabled });
return (
<div className={baseClass}>
<form>
<p className={classes}>
Require end users to authenticate with your{" "}
<CustomLink
url={PATHS.ADMIN_INTEGRATIONS_SSO_END_USERS}
text="identity provider (IdP)"
/>{" "}
when they set up their new hosts.{" "}
<TooltipWrapper tipContent={getTooltipCopy()}>macOS</TooltipWrapper>{" "}
hosts will also be required to agree to an{" "}
<CustomLink
url={`${PATHS.ADMIN_INTEGRATIONS_MDM}#end-user-license-agreement`}
text="end user license agreement (EULA)"
/>{" "}
if configured.
</p>
<Checkbox
disabled={gitOpsModeEnabled}
value={isEndUserAuthEnabled}
onChange={onToggleEndUserAuth}
>
Turn on
</Checkbox>
<RevealButton
isShowing={showAdvancedOptions}
showText="Advanced options"
hideText="Advanced options"
caretPosition="after"
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
/>
{showAdvancedOptions && (
<Checkbox
disabled={!isEndUserAuthEnabled}
onChange={onChangeLockEndUserInfo}
value={lockEndUserInfo}
>
<TooltipWrapper
tipContent={
<span>
End user can&apos;t edit the local account&apos;s{" "}
<b>Account Name</b> and
<br />
<b>Full Name</b> in macOS Setup Assistant. These fields will
be
<br />
locked to values from your IdP.
</span>
}
>
Lock end user info
</TooltipWrapper>
</Checkbox>
)}
<GitOpsModeTooltipWrapper
renderChildren={(disableChildren) => (
<Button
disabled={disableChildren}
isLoading={isUpdating}
onClick={onClickSave}
>
Save
</Button>
)}
/>
</form>
</div>
);
};
export default EndUserAuthForm;
@@ -1,6 +0,0 @@
.end-user-auth-form {
form > p {
font-size: $x-small;
line-height: $line-height-large;
}
}
@@ -1 +0,0 @@
export { default } from "./EndUserAuthForm";
@@ -1 +0,0 @@
export { default } from "./EndUserAuthentication";
@@ -13,11 +13,29 @@ import GenericMsgWithNavButton from "components/GenericMsgWithNavButton";
import CustomLink from "components/CustomLink";
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
import EndUserAuthForm from "./components/EndUserAuthForm/EndUserAuthForm";
import UsersForm from "./components/UsersForm/UsersForm";
import SetupExperienceContentContainer from "../../components/SetupExperienceContentContainer";
import { ISetupExperienceCardProps } from "../../SetupExperienceNavItems";
const baseClass = "end-user-authentication";
const baseClass = "setup-experience-users";
const getEnabledManagedLocalAccount = (
currentTeamId: number,
globalConfig?: IConfig,
teamConfig?: ITeamConfig
) => {
if (globalConfig === undefined && teamConfig === undefined) {
return false;
}
if (currentTeamId === 0) {
return (
globalConfig?.mdm?.macos_setup?.enable_managed_local_account ?? false
);
}
return teamConfig?.mdm?.macos_setup?.enable_managed_local_account ?? false;
};
const getEnabledEndUserAuth = (
currentTeamId: number,
@@ -66,10 +84,7 @@ const isIdPConfigured = ({
);
};
const EndUserAuthentication = ({
currentTeamId,
router,
}: ISetupExperienceCardProps) => {
const Users = ({ currentTeamId, router }: ISetupExperienceCardProps) => {
const { data: globalConfig, isLoading: isLoadingGlobalConfig } = useQuery<
IConfig,
Error
@@ -101,6 +116,12 @@ const EndUserAuthentication = ({
teamConfig
);
const defaultEnableManagedLocalAccount = getEnabledManagedLocalAccount(
currentTeamId,
globalConfig,
teamConfig
);
const renderContent = () => {
if (!globalConfig || isLoadingGlobalConfig || isLoadingTeamConfig) {
return <Spinner />;
@@ -117,10 +138,11 @@ const EndUserAuthentication = ({
path={PATHS.ADMIN_INTEGRATIONS_SSO_END_USERS}
/>
) : (
<EndUserAuthForm
<UsersForm
currentTeamId={currentTeamId}
defaultIsEndUserAuthEnabled={defaultIsEndUserAuthEnabled}
defaultLockEndUserInfo={defaultLockEndUserInfo}
defaultEnableManagedLocalAccount={defaultEnableManagedLocalAccount}
/>
)}
</SetupExperienceContentContainer>
@@ -130,7 +152,7 @@ const EndUserAuthentication = ({
return (
<section className={baseClass}>
<SectionHeader
title="End user authentication"
title="Users"
details={
<CustomLink
newTab
@@ -144,4 +166,4 @@ const EndUserAuthentication = ({
);
};
export default EndUserAuthentication;
export default Users;
@@ -1,3 +1,3 @@
.end-user-authentication {
.setup-experience-users {
@include vertical-card-layout;
}
@@ -0,0 +1,86 @@
import React from "react";
import { screen } from "@testing-library/react";
import { createCustomRenderer } from "test/test-utils";
import UsersForm from "./UsersForm";
describe("UsersForm", () => {
const defaultProps = {
currentTeamId: 0,
defaultIsEndUserAuthEnabled: false,
defaultLockEndUserInfo: false,
defaultEnableManagedLocalAccount: false,
};
const render = createCustomRenderer({
withBackendMock: true,
});
it("renders the end user authentication and managed local account checkboxes", () => {
render(<UsersForm {...defaultProps} />);
expect(screen.getByText("End user authentication")).toBeInTheDocument();
expect(screen.getByText("Managed local account")).toBeInTheDocument();
});
it("renders help text for end user authentication with IdP link", () => {
render(<UsersForm {...defaultProps} />);
expect(
screen.getByText(/End users are required to authenticate/)
).toBeInTheDocument();
expect(screen.getByText("identity provider (IdP)")).toBeInTheDocument();
});
it("renders help text for managed local account", () => {
render(<UsersForm {...defaultProps} />);
expect(
screen.getByText(/Fleet generates a user \(_fleetadmin\)/)
).toBeInTheDocument();
});
it("hides lock end user info when end user auth is unchecked", () => {
render(<UsersForm {...defaultProps} />);
expect(screen.queryByText("Lock end user info")).not.toBeInTheDocument();
});
it("shows lock end user info inline when end user auth is checked", () => {
render(<UsersForm {...defaultProps} defaultIsEndUserAuthEnabled />);
expect(screen.getByText("Lock end user info")).toBeInTheDocument();
});
it("reveals lock end user info when end user auth is toggled on", async () => {
const { user } = render(<UsersForm {...defaultProps} />);
expect(screen.queryByText("Lock end user info")).not.toBeInTheDocument();
await user.click(
screen.getByRole("checkbox", { name: "End user authentication" })
);
expect(screen.getByText("Lock end user info")).toBeInTheDocument();
});
it("renders managed local account checkbox as unchecked by default", () => {
render(<UsersForm {...defaultProps} />);
expect(
screen.getByRole("checkbox", { name: "Managed local account" })
).not.toBeChecked();
});
it("renders managed local account checkbox as checked when default is true", () => {
render(<UsersForm {...defaultProps} defaultEnableManagedLocalAccount />);
expect(
screen.getByRole("checkbox", { name: "Managed local account" })
).toBeChecked();
});
it("does not show an 'Advanced options' reveal button", () => {
render(<UsersForm {...defaultProps} />);
expect(screen.queryByText("Advanced options")).not.toBeInTheDocument();
});
it("renders exactly one Save button", () => {
render(<UsersForm {...defaultProps} />);
const saveButtons = screen.getAllByRole("button", { name: "Save" });
expect(saveButtons).toHaveLength(1);
});
});
@@ -0,0 +1,168 @@
import React, { useContext, useEffect, useState } from "react";
import PATHS from "router/paths";
import mdmAPI from "services/entities/mdm";
import { NotificationContext } from "context/notification";
import { AppContext } from "context/app";
import Button from "components/buttons/Button";
import Checkbox from "components/forms/fields/Checkbox";
import CustomLink from "components/CustomLink";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "users-form";
interface IUsersFormProps {
currentTeamId: number;
defaultIsEndUserAuthEnabled: boolean;
defaultLockEndUserInfo: boolean;
defaultEnableManagedLocalAccount: boolean;
}
const UsersForm = ({
currentTeamId,
defaultIsEndUserAuthEnabled,
defaultLockEndUserInfo,
defaultEnableManagedLocalAccount,
}: IUsersFormProps) => {
const { renderFlash } = useContext(NotificationContext);
const gitOpsModeEnabled = useContext(AppContext).config?.gitops
.gitops_mode_enabled;
const [isEndUserAuthEnabled, setEndUserAuthEnabled] = useState(
defaultIsEndUserAuthEnabled
);
const [lockEndUserInfo, setLockEndUserInfo] = useState(
defaultLockEndUserInfo
);
const [enableManagedLocalAccount, setEnableManagedLocalAccount] = useState(
defaultEnableManagedLocalAccount
);
const [isUpdating, setIsUpdating] = useState(false);
// Re-sync local state when the parent refetches config (e.g. team switch).
// useState initializers only run on first mount, so without this the form
// would show and save the previous team's settings.
useEffect(() => {
setEndUserAuthEnabled(defaultIsEndUserAuthEnabled);
setLockEndUserInfo(defaultLockEndUserInfo);
setEnableManagedLocalAccount(defaultEnableManagedLocalAccount);
}, [
defaultIsEndUserAuthEnabled,
defaultLockEndUserInfo,
defaultEnableManagedLocalAccount,
]);
const onToggleEndUserAuth = (newCheckVal: boolean) => {
setEndUserAuthEnabled(newCheckVal);
// Sync lock end user info with EUA: enabling EUA enables it, disabling EUA disables it.
setLockEndUserInfo(newCheckVal);
};
const onChangeLockEndUserInfo = (newCheckVal: boolean) => {
setLockEndUserInfo(newCheckVal);
};
const onToggleManagedLocalAccount = (newCheckVal: boolean) => {
setEnableManagedLocalAccount(newCheckVal);
};
const onClickSave = async () => {
setIsUpdating(true);
const canLockEndUserInfo = isEndUserAuthEnabled && lockEndUserInfo;
try {
await mdmAPI.updateSetupExperienceSettings({
fleet_id: currentTeamId,
enable_end_user_authentication: isEndUserAuthEnabled,
lock_end_user_info: canLockEndUserInfo,
enable_managed_local_account: enableManagedLocalAccount,
});
renderFlash("success", "Successfully updated.");
} catch {
renderFlash("error", "Couldn't update settings. Please try again.");
}
setIsUpdating(false);
setLockEndUserInfo(canLockEndUserInfo);
};
return (
<div className={baseClass}>
<form onSubmit={onClickSave}>
<Checkbox
disabled={gitOpsModeEnabled}
value={isEndUserAuthEnabled}
onChange={onToggleEndUserAuth}
helpText={
<span>
End users are required to authenticate with your{" "}
<CustomLink
url={PATHS.ADMIN_INTEGRATIONS_SSO_END_USERS}
text="identity provider (IdP)"
/>{" "}
when setting up new hosts.
</span>
}
>
End user authentication
</Checkbox>
{isEndUserAuthEnabled && (
<div className={`${baseClass}__advanced-options`}>
<Checkbox
disabled={gitOpsModeEnabled}
onChange={onChangeLockEndUserInfo}
value={lockEndUserInfo}
>
<TooltipWrapper
tipContent={
<span>
End user can&apos;t edit the local account&apos;s{" "}
<b>Account Name</b> and
<br />
<b>Full Name</b> in macOS Setup Assistant. These fields will
be
<br />
locked to values from your IdP.
</span>
}
>
Lock end user info
</TooltipWrapper>
</Checkbox>
</div>
)}
<Checkbox
disabled={gitOpsModeEnabled}
value={enableManagedLocalAccount}
onChange={onToggleManagedLocalAccount}
helpText={
<span>
Fleet generates a user (_fleetadmin) and unique password for each
host, accessible in <b>Host details</b> &gt;{" "}
<b>Show managed account</b>.
</span>
}
>
<TooltipWrapper tipContent="Creates a hidden managed local admin account for remote troubleshooting on macOS hosts.">
Managed local account
</TooltipWrapper>
</Checkbox>
<GitOpsModeTooltipWrapper
renderChildren={(disableChildren) => (
<Button
disabled={disableChildren}
isLoading={isUpdating}
type="submit"
>
Save
</Button>
)}
/>
</form>
</div>
);
};
export default UsersForm;
@@ -0,0 +1,6 @@
.users-form {
&__advanced-options {
margin-left: $pad-large;
margin-bottom: $pad-small;
}
}
@@ -0,0 +1 @@
export { default } from "./UsersForm";
@@ -0,0 +1 @@
export { default } from "./Users";
+4
View File
@@ -325,6 +325,10 @@ const routes = (
<Route path="os-settings/:section" component={OSSettings} />
<Route path="setup-experience" component={SetupExperience} />
<Redirect
from="setup-experience/end-user-auth"
to="setup-experience/users"
/>
<Route
path="setup-experience/:section"
component={SetupExperience}
+1 -1
View File
@@ -16,7 +16,7 @@ export default {
CONTROLS_DISK_ENCRYPTION: `${URL_PREFIX}/controls/os-settings/disk-encryption`,
CONTROLS_PASSWORDS: `${URL_PREFIX}/controls/os-settings/passwords`,
CONTROLS_SETUP_EXPERIENCE: `${URL_PREFIX}/controls/setup-experience`,
CONTROLS_END_USER_AUTHENTICATION: `${URL_PREFIX}/controls/setup-experience/end-user-auth`,
CONTROLS_USERS: `${URL_PREFIX}/controls/setup-experience/users`,
CONTROLS_BOOTSTRAP_PACKAGE: `${URL_PREFIX}/controls/setup-experience/bootstrap-package`,
CONTROLS_SETUP_ASSISTANT: `${URL_PREFIX}/controls/setup-experience/setup-assistant`,
CONTROLS_INSTALL_SOFTWARE: (platform: SetupExperiencePlatform) =>
+2 -13
View File
@@ -54,8 +54,10 @@ export const isDDMProfile = (profile: IMdmProfile | IHostMdmProfile) => {
interface IUpdateSetupExperienceBody {
fleet_id?: number;
enable_end_user_authentication?: boolean;
lock_end_user_info?: boolean;
apple_enable_release_device_manually?: boolean;
macos_manual_agent_install?: boolean;
enable_managed_local_account?: boolean;
}
export interface IAppleSetupEnrollmentProfileResponse {
@@ -252,19 +254,6 @@ const mdmService = {
return sendRequest("GET", MDM_EULA(token));
},
updateEndUserAuthentication: (
teamId: number,
isEnabled: boolean,
canLockEndUserInfo: boolean
) => {
const { MDM_SETUP } = endpoints;
return sendRequest("PATCH", MDM_SETUP, {
fleet_id: teamId,
enable_end_user_authentication: isEnabled,
lock_end_user_info: canLockEndUserInfo,
});
},
updateRequireAllSoftwareMacOS: (teamId: number, isEnabled: boolean) => {
const { MDM_SETUP } = endpoints;
return sendRequest("PATCH", MDM_SETUP, {