Windows managed local account: design feedback on the Users tab and error tooltip (#50675)
Fixes from @melpike's feedback. Keep the Windows tab visible when Windows MDM is off rather than hiding it, and disable Create hidden admin with a tooltip pointing at Windows MDM, matching how the macOS tab already behaves. - Extracted `TurnOnMdmTooltipWrapper` to reduce code duplication Reword the password-policy error fleetd reports so it reads as sentences. The whole string is generated by Fleet, not by Windows, and it is now shown to admins verbatim in the Show managed account tooltip. Left-align that tooltip. <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #43488 # Checklist for submitter ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Windows account setup remains visible when Windows MDM is unavailable. * Added guidance for enabling Apple or Windows MDM when required. * Managed-account error tooltips now show detailed failure information when available. * **Bug Fixes** * Prevented unavailable managed-account options from being selected. * Improved Windows account error messages and password-policy wording. * Improved visual feedback for disabled actions in dropdown menus. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -31,3 +31,9 @@
|
||||
.actions-dropdown__option:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// Drop the stale highlight for as long as a disabled option is hovered.
|
||||
.actions-dropdown-select__menu-list:has(.actions-dropdown-select__option--is-disabled:hover)
|
||||
.actions-dropdown-select__option--is-focused {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
+17
-7
@@ -158,17 +158,27 @@ describe("UsersForm", () => {
|
||||
});
|
||||
|
||||
describe("platform tabs", () => {
|
||||
it("hides the Windows tab when Windows MDM is not enabled and configured", () => {
|
||||
renderWithMdmEnabled(<UsersForm {...defaultProps} />);
|
||||
it("keeps the Windows tab but disables the checkbox when Windows MDM is not configured", async () => {
|
||||
const { user } = renderWithMdmEnabled(<UsersForm {...defaultProps} />);
|
||||
expect(screen.getByRole("tab", { name: "macOS" })).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("tab", { name: "Windows" }));
|
||||
|
||||
// The Checkbox renders a div with role="checkbox", so aria-disabled is the disabled signal.
|
||||
expect(
|
||||
screen.queryByRole("tab", { name: "Windows" })
|
||||
).not.toBeInTheDocument();
|
||||
screen.getByRole("checkbox", { name: "Create hidden admin" })
|
||||
).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("shows the Windows tab when Windows MDM is enabled and configured", () => {
|
||||
renderWithWindowsMdmEnabled(<UsersForm {...defaultProps} />);
|
||||
expect(screen.getByRole("tab", { name: "Windows" })).toBeInTheDocument();
|
||||
it("enables the checkbox when Windows MDM is enabled and configured", async () => {
|
||||
const { user } = renderWithWindowsMdmEnabled(
|
||||
<UsersForm {...defaultProps} />
|
||||
);
|
||||
await user.click(screen.getByRole("tab", { name: "Windows" }));
|
||||
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Create hidden admin" })
|
||||
).toHaveAttribute("aria-disabled", "false");
|
||||
});
|
||||
|
||||
// Which section each tab renders. The sections' own contents are their components' concern, so this only pins the wiring.
|
||||
|
||||
+16
-17
@@ -196,11 +196,9 @@ const UsersForm = ({
|
||||
<Tab>
|
||||
<TabText>macOS</TabText>
|
||||
</Tab>
|
||||
{isWindowsMdmEnabledAndConfigured && (
|
||||
<Tab>
|
||||
<TabText>Windows</TabText>
|
||||
</Tab>
|
||||
)}
|
||||
<Tab>
|
||||
<TabText>Windows</TabText>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<LocalAccountSection
|
||||
@@ -212,18 +210,19 @@ const UsersForm = ({
|
||||
isMacMdmEnabledAndConfigured={!!isMacMdmEnabledAndConfigured}
|
||||
/>
|
||||
</TabPanel>
|
||||
{isWindowsMdmEnabledAndConfigured && (
|
||||
<TabPanel>
|
||||
<WindowsAccountSection
|
||||
enableManagedLocalAccount={
|
||||
formData.enableManagedLocalAccountWindows
|
||||
}
|
||||
onEnableManagedLocalAccountChange={
|
||||
onEnableManagedLocalAccountWindowsChange
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
)}
|
||||
<TabPanel>
|
||||
<WindowsAccountSection
|
||||
enableManagedLocalAccount={
|
||||
formData.enableManagedLocalAccountWindows
|
||||
}
|
||||
onEnableManagedLocalAccountChange={
|
||||
onEnableManagedLocalAccountWindowsChange
|
||||
}
|
||||
isWindowsMdmEnabledAndConfigured={
|
||||
!!isWindowsMdmEnabledAndConfigured
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</TabNav>
|
||||
<GitOpsModeTooltipWrapper
|
||||
|
||||
+6
-19
@@ -7,6 +7,8 @@ import CustomLink from "components/CustomLink";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import SettingsSection from "pages/admin/components/SettingsSection";
|
||||
|
||||
import TurnOnMdmTooltipWrapper from "../TurnOnMdmTooltipWrapper";
|
||||
|
||||
const baseClass = "users-form";
|
||||
|
||||
interface IEndUserAuthSectionProps {
|
||||
@@ -71,24 +73,9 @@ const EndUserAuthSection = ({
|
||||
</TooltipWrapper>
|
||||
{endUserAuthEnabled && (
|
||||
<div className={`${baseClass}__advanced-options`}>
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
!isMacMdmEnabledAndConfigured ? (
|
||||
<span>
|
||||
To enable, first turn on{" "}
|
||||
<CustomLink
|
||||
url={PATHS.ADMIN_INTEGRATIONS_MDM_APPLE}
|
||||
text="Apple MDM"
|
||||
variant="tooltip-link"
|
||||
/>
|
||||
.
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
disableTooltip={!!isMacMdmEnabledAndConfigured}
|
||||
underline={false}
|
||||
position="left"
|
||||
showArrow
|
||||
<TurnOnMdmTooltipWrapper
|
||||
platform="apple"
|
||||
isMdmEnabledAndConfigured={!!isMacMdmEnabledAndConfigured}
|
||||
>
|
||||
<Checkbox
|
||||
disabled={
|
||||
@@ -107,7 +94,7 @@ const EndUserAuthSection = ({
|
||||
>
|
||||
Lock end user info
|
||||
</Checkbox>
|
||||
</TooltipWrapper>
|
||||
</TurnOnMdmTooltipWrapper>
|
||||
</div>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
+5
-23
@@ -1,14 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Radio from "components/forms/fields/Radio";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
import { EndUserLocalAccountType } from "interfaces/mdm";
|
||||
|
||||
import ManagedAccountCheckbox from "../ManagedAccountCheckbox";
|
||||
import TurnOnMdmTooltipWrapper from "../TurnOnMdmTooltipWrapper";
|
||||
import { IUsersFormData } from "../../UsersForm";
|
||||
|
||||
const baseClass = "local-account-section";
|
||||
@@ -38,24 +35,9 @@ const LocalAccountSection = ({
|
||||
localAccountType !== EndUserLocalAccountType.ADMIN;
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
!isMacMdmEnabledAndConfigured ? (
|
||||
<span>
|
||||
To enable, first turn on{" "}
|
||||
<CustomLink
|
||||
url={PATHS.ADMIN_INTEGRATIONS_MDM_APPLE}
|
||||
text="Apple MDM"
|
||||
variant="tooltip-link"
|
||||
/>
|
||||
.
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
disableTooltip={isMacMdmEnabledAndConfigured}
|
||||
underline={false}
|
||||
position="left"
|
||||
showArrow
|
||||
<TurnOnMdmTooltipWrapper
|
||||
platform="apple"
|
||||
isMdmEnabledAndConfigured={isMacMdmEnabledAndConfigured}
|
||||
>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="left"
|
||||
@@ -127,7 +109,7 @@ const LocalAccountSection = ({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
</TurnOnMdmTooltipWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
||||
const MDM_BY_PLATFORM = {
|
||||
apple: { url: PATHS.ADMIN_INTEGRATIONS_MDM_APPLE, text: "Apple MDM" },
|
||||
windows: { url: PATHS.ADMIN_INTEGRATIONS_MDM_WINDOWS, text: "Windows MDM" },
|
||||
} as const;
|
||||
|
||||
interface ITurnOnMdmTooltipWrapperProps {
|
||||
platform: keyof typeof MDM_BY_PLATFORM;
|
||||
isMdmEnabledAndConfigured: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Explains that a control is disabled because the platform's MDM isn't turned on yet, and links to the page that turns it on. */
|
||||
const TurnOnMdmTooltipWrapper = ({
|
||||
platform,
|
||||
isMdmEnabledAndConfigured,
|
||||
children,
|
||||
}: ITurnOnMdmTooltipWrapperProps) => {
|
||||
const { url, text } = MDM_BY_PLATFORM[platform];
|
||||
|
||||
return (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
!isMdmEnabledAndConfigured ? (
|
||||
<span>
|
||||
To enable, first turn on{" "}
|
||||
<CustomLink url={url} text={text} variant="tooltip-link" />.
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
disableTooltip={isMdmEnabledAndConfigured}
|
||||
underline={false}
|
||||
position="left"
|
||||
showArrow
|
||||
>
|
||||
{children}
|
||||
</TooltipWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TurnOnMdmTooltipWrapper;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { default } from "./TurnOnMdmTooltipWrapper";
|
||||
+21
-13
@@ -5,18 +5,21 @@ import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
|
||||
import ManagedAccountCheckbox from "../ManagedAccountCheckbox";
|
||||
import TurnOnMdmTooltipWrapper from "../TurnOnMdmTooltipWrapper";
|
||||
|
||||
const baseClass = "windows-account-section";
|
||||
|
||||
interface IWindowsAccountSectionProps {
|
||||
enableManagedLocalAccount: boolean;
|
||||
onEnableManagedLocalAccountChange: (value: boolean) => void;
|
||||
isWindowsMdmEnabledAndConfigured: boolean;
|
||||
}
|
||||
|
||||
/** Windows tab of the Users card. The tab is only rendered when Windows MDM is configured, so unlike the macOS section there is no "turn on MDM first" state to handle here. */
|
||||
/** Windows tab of the Users card. The managed account checkbox mirrors the macOS tab, including the disabled state that points admins at Windows MDM when it isn't turned on yet. */
|
||||
const WindowsAccountSection = ({
|
||||
enableManagedLocalAccount,
|
||||
onEnableManagedLocalAccountChange,
|
||||
isWindowsMdmEnabledAndConfigured,
|
||||
}: IWindowsAccountSectionProps) => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -32,18 +35,23 @@ const WindowsAccountSection = ({
|
||||
</div>
|
||||
|
||||
<h3 className={`${baseClass}__sub-header`}>Managed account</h3>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="left"
|
||||
tipOffset={8}
|
||||
isInputField
|
||||
renderChildren={(gitopsEnabled) => (
|
||||
<ManagedAccountCheckbox
|
||||
disabled={!!gitopsEnabled}
|
||||
value={enableManagedLocalAccount}
|
||||
onChange={onEnableManagedLocalAccountChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<TurnOnMdmTooltipWrapper
|
||||
platform="windows"
|
||||
isMdmEnabledAndConfigured={isWindowsMdmEnabledAndConfigured}
|
||||
>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="left"
|
||||
tipOffset={8}
|
||||
isInputField
|
||||
renderChildren={(gitopsEnabled) => (
|
||||
<ManagedAccountCheckbox
|
||||
disabled={!!gitopsEnabled || !isWindowsMdmEnabledAndConfigured}
|
||||
value={enableManagedLocalAccount}
|
||||
onChange={onEnableManagedLocalAccountChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TurnOnMdmTooltipWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,3 +13,9 @@
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
// Left-align error tooltip.
|
||||
.host-actions-dropdown__managed-account-error {
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -857,7 +857,11 @@ const modifyOptions = (
|
||||
);
|
||||
} else if (managedAccountStatus === "failed") {
|
||||
// The reason the host reported is the actionable part, so prefer it over generic copy.
|
||||
managedAccountOption.tooltipContent = managedAccountDetail || (
|
||||
managedAccountOption.tooltipContent = managedAccountDetail ? (
|
||||
<span className="host-actions-dropdown__managed-account-error">
|
||||
{managedAccountDetail}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
The managed account failed to be
|
||||
<br />
|
||||
|
||||
@@ -128,7 +128,7 @@ func ensureUser(username, password string) error {
|
||||
0, // parm_err
|
||||
)
|
||||
if ret != 0 {
|
||||
return accountError(fmt.Sprintf("resetting password for %s", username), ret, len(password))
|
||||
return accountError(fmt.Sprintf("Resetting password for %s", username), ret, len(password))
|
||||
}
|
||||
// Resetting the password is not enough to make the account usable again. If it was disabled, locked out, or had
|
||||
// its never-expire flag removed after we created it, Fleet would escrow a password that cannot actually log in.
|
||||
@@ -154,7 +154,7 @@ func ensureUser(username, password string) error {
|
||||
0, // parm_err
|
||||
)
|
||||
if ret != 0 {
|
||||
return accountError(fmt.Sprintf("creating %s", username), ret, len(password))
|
||||
return accountError(fmt.Sprintf("Creating %s", username), ret, len(password))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -166,8 +166,8 @@ func ensureUser(username, password string) error {
|
||||
func accountError(op string, ret uintptr, passwordLen int) error {
|
||||
if ret == nerrPasswordTooShort || ret == errorPasswordRestriction {
|
||||
return fmt.Errorf(
|
||||
"%s: this device's password policy rejected the generated %d-character password; "+
|
||||
"check the minimum password length and any custom password filter on the host",
|
||||
"%s. This device's password policy rejected the generated %d-character password. "+
|
||||
"Check any custom password filter on the host.",
|
||||
op, passwordLen)
|
||||
}
|
||||
return fmt.Errorf("%s: %w", op, windows.Errno(ret))
|
||||
|
||||
Reference in New Issue
Block a user