mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Fix bug for not being to reenable end user migration in UI (#30106)
Fixes #30063 This fixes an issue added in the [PR](https://github.com/fleetdm/fleet/pull/29968) where the user was not able to reenable the end user migration form. I've also added improved a11y attributes to the slider component, ensured we are functionally disabling the form controls during gitops mode and not just visually, and updated/added tests for the EndUserMigrationSection component. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] Added/updated automated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
parent
092b068657
commit
a5c69a60a4
8 changed files with 162 additions and 16 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -85,6 +85,7 @@ macoffice_rel_notes/
|
|||
|
||||
# IDE
|
||||
.vscode
|
||||
.cursor
|
||||
|
||||
# residual files when running the build-windows tool
|
||||
orbit/cmd/desktop/manifest.xml
|
||||
|
|
|
|||
1
changes/issue-30063-fix-end-user-migration-enable
Normal file
1
changes/issue-30063-fix-end-user-migration-enable
Normal file
|
|
@ -0,0 +1 @@
|
|||
- fixes an issue where users were not able to reenable end user migration in the UI.
|
||||
|
|
@ -209,7 +209,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
const createMockConfig = (overrides?: Partial<IConfig>): IConfig => {
|
||||
export const createMockConfig = (overrides?: Partial<IConfig>): IConfig => {
|
||||
return { ...DEFAULT_CONFIG_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,18 +13,18 @@ describe("Slider Component", () => {
|
|||
it("renders correctly with default props", () => {
|
||||
render(<Slider {...defaultProps} />);
|
||||
expect(screen.getByText("Off")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button")).toHaveClass("fleet-slider");
|
||||
expect(screen.getByRole("switch")).toHaveClass("fleet-slider");
|
||||
});
|
||||
|
||||
it("renders active state correctly", () => {
|
||||
render(<Slider {...defaultProps} value />);
|
||||
expect(screen.getByText("On")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button")).toHaveClass("fleet-slider--active");
|
||||
expect(screen.getByRole("switch")).toHaveClass("fleet-slider--active");
|
||||
});
|
||||
|
||||
it("calls onChange when clicked", () => {
|
||||
render(<Slider {...defaultProps} />);
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
fireEvent.click(screen.getByRole("switch"));
|
||||
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,8 +67,11 @@ const Slider = (props: ISliderProps): JSX.Element => {
|
|||
<FormField {...formFieldProps} type="slider">
|
||||
<div className={wrapperClassNames}>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={value}
|
||||
className={`button button--unstyled ${sliderBtnClass}`}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
ref={sliderRef}
|
||||
>
|
||||
<div className={sliderDotClass} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
import React from "react";
|
||||
import { screen } from "@testing-library/react";
|
||||
|
||||
import { createMockConfig, createMockMdmConfig } from "__mocks__/configMock";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { createCustomRenderer, createMockRouter } from "test/test-utils";
|
||||
|
||||
import EndUserMigrationSection from "./EndUserMigrationSection";
|
||||
|
||||
const createTestMockData = (
|
||||
configOverrides: Partial<IConfig>,
|
||||
isPremiumTier = true
|
||||
) => {
|
||||
return {
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier,
|
||||
config: createMockConfig({
|
||||
...configOverrides,
|
||||
}),
|
||||
setConfig: jest.fn(),
|
||||
},
|
||||
notification: {
|
||||
renderFlash: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe("EndUserMigrationSection", () => {
|
||||
const mockRouter = createMockRouter();
|
||||
|
||||
it("toggles form elements disabled state when slider is clicked", async () => {
|
||||
const render = createCustomRenderer(
|
||||
createTestMockData({
|
||||
mdm: createMockMdmConfig({
|
||||
macos_migration: {
|
||||
enable: false,
|
||||
mode: "voluntary",
|
||||
webhook_url: "",
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const { user } = render(<EndUserMigrationSection router={mockRouter} />);
|
||||
|
||||
// Verify slider is initially disabled (off)
|
||||
const slider = screen.getByRole("switch");
|
||||
expect(slider).not.toBeChecked();
|
||||
|
||||
// Verify form elements are disabled
|
||||
const voluntaryRadio = screen.getByRole("radio", { name: "Voluntary" });
|
||||
const forcedRadio = screen.getByRole("radio", { name: "Forced" });
|
||||
const webhookInput = screen.getByRole("textbox", { name: "Webhook URL" });
|
||||
expect(voluntaryRadio).toBeDisabled();
|
||||
expect(forcedRadio).toBeDisabled();
|
||||
expect(webhookInput).toBeDisabled();
|
||||
|
||||
// Click the slider to enable it form elements.
|
||||
// have to wait for the async state update
|
||||
user.click(slider);
|
||||
await screen.findByRole("switch", { checked: true });
|
||||
|
||||
expect(slider).toBeChecked();
|
||||
expect(voluntaryRadio).not.toBeDisabled();
|
||||
expect(forcedRadio).not.toBeDisabled();
|
||||
expect(webhookInput).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("disables form elements when gitops mode is enabled", async () => {
|
||||
const render = createCustomRenderer(
|
||||
createTestMockData({
|
||||
mdm: createMockMdmConfig({
|
||||
macos_migration: {
|
||||
enable: true,
|
||||
mode: "voluntary",
|
||||
webhook_url: "",
|
||||
},
|
||||
}),
|
||||
gitops: {
|
||||
gitops_mode_enabled: true,
|
||||
repository_url: "https://example.com/repo.git",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const { user } = render(<EndUserMigrationSection router={mockRouter} />);
|
||||
|
||||
// Verify slider is enabled but disabled due to gitops mode
|
||||
const slider = screen.getByRole("switch");
|
||||
expect(slider).toBeChecked();
|
||||
expect(slider).toBeDisabled();
|
||||
|
||||
// Verify form elements are disabled
|
||||
const voluntaryRadio = screen.getByRole("radio", { name: "Voluntary" });
|
||||
const forcedRadio = screen.getByRole("radio", { name: "Forced" });
|
||||
const webhookInput = screen.getByRole("textbox", { name: "Webhook URL" });
|
||||
|
||||
expect(voluntaryRadio).toBeDisabled();
|
||||
expect(forcedRadio).toBeDisabled();
|
||||
expect(webhookInput).toBeDisabled();
|
||||
|
||||
// clicking the slider should have no effect
|
||||
user.click(slider);
|
||||
expect(slider).toBeDisabled();
|
||||
expect(voluntaryRadio).toBeDisabled();
|
||||
expect(forcedRadio).toBeDisabled();
|
||||
expect(webhookInput).toBeDisabled();
|
||||
});
|
||||
|
||||
it("renders the connect button when MDM is not connected", () => {
|
||||
const render = createCustomRenderer(
|
||||
createTestMockData({
|
||||
mdm: createMockMdmConfig({
|
||||
apple_bm_enabled_and_configured: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
render(<EndUserMigrationSection router={mockRouter} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("Connect to Apple Business Manager to get started.")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Connect" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the premium feature message when not on premium tier", () => {
|
||||
const render = createCustomRenderer(createTestMockData({}, false));
|
||||
|
||||
render(<EndUserMigrationSection router={mockRouter} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("This feature is included in Fleet Premium.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -57,6 +57,7 @@ const validateWebhookUrl = (val: string) => {
|
|||
const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
|
||||
const { config, isPremiumTier, setConfig } = useContext(AppContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const [formData, setFormData] = useState<IEndUserMigrationFormData>({
|
||||
isEnabled: config?.mdm.macos_migration.enable || false,
|
||||
mode: config?.mdm.macos_migration.mode || "voluntary",
|
||||
|
|
@ -130,8 +131,10 @@ const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
|
|||
}
|
||||
};
|
||||
|
||||
const isGitOpsModeEnabled = config?.gitops.gitops_mode_enabled;
|
||||
|
||||
const formClasses = classnames(`${baseClass}__end-user-migration-form`, {
|
||||
disabled: !formData.isEnabled || config?.gitops.gitops_mode_enabled,
|
||||
disabled: !formData.isEnabled || isGitOpsModeEnabled,
|
||||
});
|
||||
|
||||
if (!isPremiumTier) {
|
||||
|
|
@ -166,18 +169,18 @@ const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
|
|||
alt="end user migration preview"
|
||||
className={`${baseClass}__migration-preview`}
|
||||
/>
|
||||
<Slider
|
||||
value={formData.isEnabled}
|
||||
onChange={toggleMigrationEnabled}
|
||||
activeText="Enabled"
|
||||
inactiveText="Disabled"
|
||||
disabled={isGitOpsModeEnabled}
|
||||
/>
|
||||
<div className={`form ${formClasses}`}>
|
||||
<Slider
|
||||
value={formData.isEnabled}
|
||||
onChange={toggleMigrationEnabled}
|
||||
activeText="Enabled"
|
||||
inactiveText="Disabled"
|
||||
className={`${baseClass}__enabled-slider`}
|
||||
/>
|
||||
<div className={`form-field ${baseClass}__mode-field`}>
|
||||
<div className="form-field__label">Mode</div>
|
||||
<Radio
|
||||
disabled={!formData.isEnabled}
|
||||
disabled={!formData.isEnabled || isGitOpsModeEnabled}
|
||||
checked={formData.mode === "voluntary"}
|
||||
value="voluntary"
|
||||
id="voluntary"
|
||||
|
|
@ -187,7 +190,7 @@ const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
|
|||
name="mode-type"
|
||||
/>
|
||||
<Radio
|
||||
disabled={!formData.isEnabled}
|
||||
disabled={!formData.isEnabled || isGitOpsModeEnabled}
|
||||
checked={formData.mode === "forced"}
|
||||
value="forced"
|
||||
id="forced"
|
||||
|
|
@ -208,7 +211,7 @@ const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
|
|||
page.
|
||||
</p>
|
||||
<InputField
|
||||
readOnly={!formData.isEnabled}
|
||||
readOnly={!formData.isEnabled || isGitOpsModeEnabled}
|
||||
name="webhook_url"
|
||||
label="Webhook URL"
|
||||
value={formData.webhookUrl}
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ describe("EditQueryForm - component", () => {
|
|||
expect(automationsSlider).toBeInTheDocument();
|
||||
|
||||
// Check if the automations are enabled
|
||||
const automationsButton = within(automationsSlider).getByRole("button");
|
||||
const automationsButton = within(automationsSlider).getByRole("switch");
|
||||
expect(automationsButton).toHaveClass("fleet-slider--active");
|
||||
|
||||
// Check if the warning icon is present
|
||||
|
|
|
|||
Loading…
Reference in a new issue