Demo: https://www.youtube.com/watch?v=cWxZlu9WuwA Guide updates: https://github.com/fleetdm/fleet/pull/49603/changes IT admins can configure the fleet that hosts enrolling through user-driven Windows MDM enrollment (Windows Autopilot, Entra join) are automatically assigned to, via the Windows MDM settings page, the mdm.windows_enrollment.default_fleet config setting, or GitOps. - New windows_enrollment_config row stores the default team; the config API surfaces it by fleet name and hydrates reads from the row so team renames and deletions never serve a stale name. Deleting the fleet clears the setting. - New edited_windows_enrollment_default_fleet activity, emitted only when the value changes. - The OMA-DM session persists the device-reported SMBIOS serial on still-unlinked enrollments, and orbit enrollment reverse-links by that serial and assigns the default fleet before orbit's one-shot setup-experience init, so the default fleet's software, scripts, and profiles apply during the Autopilot ESP. The DevDetail and osquery link paths keep the same assignment as fallbacks, and the EUA-token link path now shares the same post-link bookkeeping. - Hosts are only assigned when new to Fleet in this enrollment cycle: existing hosts, including ones parked in Unassigned, keep their fleet on re-enrollment, matching macOS ABM behavior. - GitOps defers applying the setting until teams declared in the same run are created, and fleetctl generate-gitops exports it. - Windows MDM settings page redesign per Figma: programmatic enrollment toggle, User driven enrollment section with the Entra-gated Default fleet dropdown, and a Migration section. <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #41787 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually ## Database migrations - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). ## New Fleet configuration settings - [x] Verified that the setting is exported via `fleetctl generate-gitops` - [x] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - [x] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [x] Verified that any relevant UI is disabled when GitOps mode is enabled <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for assigning a default Fleet Premium fleet to new Windows MDM enrollments, including Autopilot and Entra join. * Default-fleet settings can be configured, cleared, and managed through Windows MDM settings and GitOps. * Assigned fleet software, scripts, and profiles can apply during out-of-box setup. * Added activity-feed visibility for default-fleet changes. * Improved Windows enrollment matching using hardware serial numbers. * **Documentation** * Documented default-fleet assignment for Windows enrollment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -99,4 +99,54 @@ describe("DropdownWrapper Component", () => {
|
||||
|
||||
expect(screen.getByText(/no results found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows the disabled tooltip on hover when disabled with content provided", async () => {
|
||||
const { container } = render(
|
||||
<DropdownWrapper
|
||||
options={sampleOptions}
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
name="test-dropdown"
|
||||
label="Test Dropdown"
|
||||
isDisabled
|
||||
disabledTooltipContent="Reason it is disabled"
|
||||
/>
|
||||
);
|
||||
|
||||
const tooltipAnchor = container.querySelector(
|
||||
".dropdown-wrapper__disabled-tooltip .component__tooltip-wrapper__element"
|
||||
);
|
||||
expect(tooltipAnchor).toBeInTheDocument();
|
||||
|
||||
// react-tooltip only mounts the tip content once the anchor is hovered
|
||||
await userEvent.hover(tooltipAnchor as Element);
|
||||
expect(
|
||||
await screen.findByText(/reason it is disabled/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The tooltip wraps the control only when isDisabled and disabledTooltipContent are both set.
|
||||
// Each case below drops one of those two operands, so neither can be removed from the condition.
|
||||
test.each([
|
||||
{
|
||||
caseName: "enabled",
|
||||
props: { disabledTooltipContent: "Reason it is disabled" },
|
||||
},
|
||||
{ caseName: "disabled without content", props: { isDisabled: true } },
|
||||
])("does not render the disabled tooltip when $caseName", ({ props }) => {
|
||||
const { container } = render(
|
||||
<DropdownWrapper
|
||||
options={sampleOptions}
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
name="test-dropdown"
|
||||
label="Test Dropdown"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
container.querySelector(".dropdown-wrapper__disabled-tooltip")
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import { PADDING } from "styles/var/padding";
|
||||
|
||||
import FormField from "components/forms/FormField";
|
||||
import DropdownOptionTooltipWrapper from "components/forms/fields/Dropdown/DropdownOptionTooltipWrapper";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Icon from "components/Icon";
|
||||
import { IconNames } from "components/icons";
|
||||
import { TooltipContent } from "interfaces/dropdownOption";
|
||||
@@ -131,6 +132,8 @@ export interface IDropdownWrapper {
|
||||
* not infer any of these on its own; without a value here screen readers
|
||||
* announce a bare "combobox". */
|
||||
ariaLabel?: string;
|
||||
/** Tooltip explaining why the dropdown is disabled. Shown above the control, on hover over the control only (not the label or help text), and only while `isDisabled` is true. */
|
||||
disabledTooltipContent?: React.ReactNode;
|
||||
/** Defaults to "auto" so a menu near the viewport bottom flips upward
|
||||
* instead of stretching the page and triggering a scrollbar-driven
|
||||
* layout shift. */
|
||||
@@ -381,6 +384,7 @@ const DropdownWrapper = ({
|
||||
nowrapMenu,
|
||||
customNoOptionsMessage,
|
||||
ariaLabel,
|
||||
disabledTooltipContent,
|
||||
menuPlacement = "auto",
|
||||
}: IDropdownWrapper) => {
|
||||
const wrapperClassNames = classnames(baseClass, className, {
|
||||
@@ -449,6 +453,39 @@ const DropdownWrapper = ({
|
||||
);
|
||||
};
|
||||
|
||||
const selectElement = (
|
||||
<Select<CustomOptionType, false>
|
||||
classNamePrefix="react-select"
|
||||
isSearchable={isSearchable}
|
||||
styles={generateCustomDropdownStyles(
|
||||
variant,
|
||||
isDisabled,
|
||||
nowrapMenu,
|
||||
maxMenuHeight
|
||||
)}
|
||||
options={options}
|
||||
components={{
|
||||
Option: CustomOption,
|
||||
DropdownIndicator: CustomDropdownIndicator,
|
||||
IndicatorSeparator: () => null,
|
||||
ValueContainer,
|
||||
}}
|
||||
value={getCurrentValue()}
|
||||
onChange={handleChange}
|
||||
isDisabled={isDisabled}
|
||||
noOptionsMessage={() => customNoOptionsMessage ?? "No results found"}
|
||||
tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility
|
||||
placeholder={placeholder}
|
||||
onMenuOpen={onMenuOpen}
|
||||
menuPlacement={menuPlacement}
|
||||
// Resolve accessible name: explicit prop wins, otherwise fall back
|
||||
// to the placeholder (usually "Select X"), otherwise the required
|
||||
// `name` (often a kebab-case identifier — least readable but
|
||||
// guaranteed present).
|
||||
aria-label={ariaLabel ?? placeholder ?? name}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<FormField
|
||||
name={name}
|
||||
@@ -457,36 +494,19 @@ const DropdownWrapper = ({
|
||||
type="dropdown"
|
||||
className={wrapperClassNames}
|
||||
>
|
||||
<Select<CustomOptionType, false>
|
||||
classNamePrefix="react-select"
|
||||
isSearchable={isSearchable}
|
||||
styles={generateCustomDropdownStyles(
|
||||
variant,
|
||||
isDisabled,
|
||||
nowrapMenu,
|
||||
maxMenuHeight
|
||||
)}
|
||||
options={options}
|
||||
components={{
|
||||
Option: CustomOption,
|
||||
DropdownIndicator: CustomDropdownIndicator,
|
||||
IndicatorSeparator: () => null,
|
||||
ValueContainer,
|
||||
}}
|
||||
value={getCurrentValue()}
|
||||
onChange={handleChange}
|
||||
isDisabled={isDisabled}
|
||||
noOptionsMessage={() => customNoOptionsMessage ?? "No results found"}
|
||||
tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility
|
||||
placeholder={placeholder}
|
||||
onMenuOpen={onMenuOpen}
|
||||
menuPlacement={menuPlacement}
|
||||
// Resolve accessible name: explicit prop wins, otherwise fall back
|
||||
// to the placeholder (usually "Select X"), otherwise the required
|
||||
// `name` (often a kebab-case identifier — least readable but
|
||||
// guaranteed present).
|
||||
aria-label={ariaLabel ?? placeholder ?? name}
|
||||
/>
|
||||
{isDisabled && disabledTooltipContent ? (
|
||||
<TooltipWrapper
|
||||
className={`${baseClass}__disabled-tooltip`}
|
||||
tipContent={disabledTooltipContent}
|
||||
position="top"
|
||||
underline={false}
|
||||
showArrow
|
||||
>
|
||||
{selectElement}
|
||||
</TooltipWrapper>
|
||||
) : (
|
||||
selectElement
|
||||
)}
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Wraps the <Select> control when disabledTooltipContent is provided; the wrapper must fill the form field's width
|
||||
// like the bare control does.
|
||||
&__disabled-tooltip {
|
||||
&.component__tooltip-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.component__tooltip-wrapper__element {
|
||||
// The element is otherwise a max-content flex item, which collapses the percentage-width <Select> inside it to
|
||||
// its text width.
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
// Table dropdowns have height 40px
|
||||
&__table-filter {
|
||||
height: 36px;
|
||||
|
||||
Reference in New Issue
Block a user