refactor(settings/auth): migrate auto user provision toggle to react [BE-12585] (#1865)

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Chaim Lev-Ari
2026-02-26 14:18:48 +02:00
committed by GitHub
parent 9ad6c16d43
commit a01dd005fd
13 changed files with 133 additions and 72 deletions

View File

@@ -31,6 +31,13 @@ export default class OAuthSettingsController {
this.removeTeamMembership = this.removeTeamMembership.bind(this);
this.onToggleAutoTeamMembership = this.onToggleAutoTeamMembership.bind(this);
this.onChangeAuthStyle = this.onChangeAuthStyle.bind(this);
this.onAutoUserProvisionChange = this.onAutoUserProvisionChange.bind(this);
}
onAutoUserProvisionChange(value) {
this.$scope.$evalAsync(() => {
this.settings.OAuthAutoCreateUsers = value;
});
}
onMicrosoftTenantIDChange() {

View File

@@ -29,12 +29,11 @@
</div>
<!-- !HideInternalAuth -->
<auto-user-provision-toggle ng-model="$ctrl.settings.OAuthAutoCreateUsers">
<field-description>
With automatic user provisioning enabled, Portainer will create user(s) automatically with the standard user role. If disabled, users must be created beforehand in Portainer
in order to login.
</field-description>
</auto-user-provision-toggle>
<auto-user-provision-toggle
value="$ctrl.settings.OAuthAutoCreateUsers"
on-change="($ctrl.onAutoUserProvisionChange)"
description="'With automatic user provisioning enabled, Portainer will create user(s) automatically with the standard user role. If disabled, users must be created beforehand in Portainer in order to login.'"
></auto-user-provision-toggle>
<div ng-if="$ctrl.settings.OAuthAutoCreateUsers">
<div class="form-group">

View File

@@ -14,6 +14,7 @@ import { HelmCertPanel } from '@/react/portainer/settings/SettingsView/HelmCertP
import { HiddenContainersPanel } from '@/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersPanel';
import { SSLSettingsPanelWrapper } from '@/react/portainer/settings/SettingsView/SSLSettingsPanel/SSLSettingsPanel';
import { AuthStyleField } from '@/react/portainer/settings/AuthenticationView/OAuth';
import { AutoUserProvisionToggle } from '@/react/portainer/settings/AuthenticationView/AutoUserProvisionToggle/AutoUserProvisionToggle';
export const settingsModule = angular
.module('portainer.app.react.components.settings', [])
@@ -55,4 +56,13 @@ export const settingsModule = angular
'readonly',
'size',
])
)
.component(
'autoUserProvisionToggle',
r2a(AutoUserProvisionToggle, [
'value',
'onChange',
'description',
'data-cy',
])
).name;

View File

@@ -1,13 +0,0 @@
<div class="col-sm-12 form-section-title"> Automatic user provisioning </div>
<div class="form-group">
<span class="col-sm-12 text-muted small" ng-transclude="description"> </span>
</div>
<div class="form-group">
<div class="col-sm-12 vertical-center">
<label for="tls" class="control-label !pt-0 text-left"> Automatic user provisioning </label>
<label class="switch my-0 ml-6">
<input type="checkbox" ng-model="$ctrl.ngModel" data-cy="auto-user-provision-toggle" />
<span class="slider round"></span>
</label>
</div>
</div>

View File

@@ -1,9 +0,0 @@
export const autoUserProvisionToggle = {
templateUrl: './auto-user-provision-toggle.html',
transclude: {
description: 'fieldDescription',
},
bindings: {
ngModel: '=',
},
};

View File

@@ -1,10 +1,8 @@
import angular from 'angular';
import ldapModule from './ldap';
import { autoUserProvisionToggle } from './auto-user-provision-toggle';
import { saveAuthSettingsButton } from './save-auth-settings-button';
export default angular
.module('portainer.settings.authentication', [ldapModule])
.component('saveAuthSettingsButton', saveAuthSettingsButton)
.component('autoUserProvisionToggle', autoUserProvisionToggle).name;
.component('saveAuthSettingsButton', saveAuthSettingsButton).name;

View File

@@ -5,8 +5,9 @@ import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.ser
export default class AdSettingsController {
/* @ngInject */
constructor(LDAPService) {
constructor(LDAPService, $scope) {
this.LDAPService = LDAPService;
this.$scope = $scope;
this.domainSuffix = '';
this.limitedFeatureId = FeatureId.HIDE_INTERNAL_AUTH;
@@ -15,6 +16,14 @@ export default class AdSettingsController {
this.searchGroups = this.searchGroups.bind(this);
this.parseDomainName = this.parseDomainName.bind(this);
this.onAccountChange = this.onAccountChange.bind(this);
this.onAutoUserProvisionChange = this.onAutoUserProvisionChange.bind(this);
this.onAutoUserProvisionChange = this.onAutoUserProvisionChange.bind(this);
}
onAutoUserProvisionChange(value) {
this.$scope.$evalAsync(() => {
this.settings.AutoCreateUsers = value;
});
}
parseDomainName(account) {

View File

@@ -2,12 +2,11 @@
<div class="be-indicator-container">
<div class="limited-be-link vertical-center"><be-feature-indicator feature="$ctrl.limitedFeatureId"></be-feature-indicator></div>
<div class="limited-be-content">
<auto-user-provision-toggle ng-model="$ctrl.settings.AutoCreateUsers">
<field-description>
With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role and assign them to team(s) which matches to LDAP group
name(s). If disabled, users must be created in Portainer beforehand.
</field-description>
</auto-user-provision-toggle>
<auto-user-provision-toggle
value="$ctrl.settings.AutoCreateUsers"
on-change="($ctrl.onAutoUserProvisionChange)"
description="'With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role and assign them to team(s) which matches to LDAP group name(s). If disabled, users must be created in Portainer beforehand.'"
></auto-user-provision-toggle>
<div>
<div class="col-sm-12 form-section-title"> Information </div>

View File

@@ -12,8 +12,8 @@ const DEFAULT_USER_FILTER = '(objectClass=inetOrgPerson)';
export default class LdapSettingsController {
/* @ngInject */
constructor(LDAPService) {
Object.assign(this, { LDAPService, SERVER_TYPES });
constructor(LDAPService, $scope) {
Object.assign(this, { LDAPService, SERVER_TYPES, $scope });
this.tlscaCert = null;
this.settingsDrafts = {};
@@ -24,8 +24,15 @@ export default class LdapSettingsController {
this.searchUsers = this.searchUsers.bind(this);
this.searchGroups = this.searchGroups.bind(this);
this.onChangeServerType = this.onChangeServerType.bind(this);
this.onAutoUserProvisionChange = this.onAutoUserProvisionChange.bind(this);
this.onAutoUserProvisionChange = this.onAutoUserProvisionChange.bind(this);
}
onAutoUserProvisionChange(value) {
this.$scope.$evalAsync(() => {
this.settings.AutoCreateUsers = value;
});
}
onTlscaCertChange(file) {
this.tlscaCert = file;
}

View File

@@ -1,10 +1,9 @@
<div>
<auto-user-provision-toggle ng-model="$ctrl.settings.AutoCreateUsers">
<field-description>
With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role and assign them to team(s) which matches to LDAP group name(s).
If disabled, users must be created in Portainer beforehand.
</field-description>
</auto-user-provision-toggle>
<auto-user-provision-toggle
value="$ctrl.settings.AutoCreateUsers"
on-change="($ctrl.onAutoUserProvisionChange)"
description="'With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role and assign them to team(s) which matches to LDAP group name(s). If disabled, users must be created in Portainer beforehand.'"
></auto-user-provision-toggle>
<div class="col-sm-12 form-section-title"> Server Type </div>

View File

@@ -153,7 +153,7 @@ BE-6604 (Parent: Authentication Settings Migration)
### [BE-12593: LDAP Settings](https://linear.app/portainer/issue/BE-12593) (Parent Issue)
- [ ] PR 3: [BE-12585](https://linear.app/portainer/issue/BE-12585) AutoUserProvisionToggle → shared component
- [x] PR 3: [BE-12585](https://linear.app/portainer/issue/BE-12585) AutoUserProvisionToggle → shared component
- **LDAP Shared Components** (Parallel - No Dependencies)
- [ ] PR 4: [BE-12586](https://linear.app/portainer/issue/BE-12586) LdapSettingsDnBuilder
- [ ] PR 5: [BE-12587](https://linear.app/portainer/issue/BE-12587) LdapSettingsGroupDnBuilder
@@ -190,31 +190,6 @@ BE-6604 (Parent: Authentication Settings Migration)
- [ ] PR 19: [BE-12601](https://linear.app/portainer/issue/BE-12601) AuthenticationView container (Formik) - **Blocked by BE-12593, BE-12596, BE-12600**
- [ ] PR 20: [BE-12602](https://linear.app/portainer/issue/BE-12602) Complete React migration - **Blocked by PR 19**
## Current PR: PR 2 - Completed ✅
**What was implemented:**
- Created `AuthenticationMethodSelector.tsx` component in both CE and EE
- Component wraps BoxSelector with authentication method options
- Registered component via react2angular bridge in `portainer/react/components/settings.ts`
- Updated Angular templates (`settingsAuthentication.html`) to use `<authentication-method-selector>`
- Added comprehensive unit tests with 8 test cases covering all authentication methods
- Tests use `userEvent`, `toBeVisible()`, and proper accessibility queries
**Files changed:**
- `package/server-ce/app/react/portainer/settings/AuthenticationView/AuthenticationMethodSelector.tsx` (new)
- `package/server-ce/app/react/portainer/settings/AuthenticationView/AuthenticationMethodSelector.test.tsx` (new)
- `package/server-ce/app/portainer/react/components/settings.ts` (modified)
- `package/server-ce/app/portainer/views/settings/authentication/settingsAuthentication.html` (modified)
- `package/server-ee/app/react/portainer/settings/AuthenticationView/AuthenticationMethodSelector.tsx` (new)
- `package/server-ee/app/react/portainer/settings/AuthenticationView/AuthenticationMethodSelector.test.tsx` (new)
- `package/server-ee/app/portainer/react/components/settings.ts` (modified)
- `package/server-ee/app/portainer/views/settings/authentication/settingsAuthentication.html` (modified)
**Integration:** Seamless - replaced `<box-selector>` with `<authentication-method-selector>` in templates
**Status:** Completed ✅
## Linear Issue Comparison
The Linear issue BE-6604 provides a detailed breakdown of the component structure. Our migration plan aligns with it:

View File

@@ -0,0 +1,45 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, test, vi } from 'vitest';
import { AutoUserProvisionToggle } from './AutoUserProvisionToggle';
describe('AutoUserProvisionToggle', () => {
test('renders with description', () => {
render(
<AutoUserProvisionToggle
value={false}
onChange={() => {}}
description="Test description for automatic user provisioning"
/>
);
expect(
screen.getByText(/test description for automatic user provisioning/i)
).toBeVisible();
expect(
screen.getByRole('checkbox', { name: /automatic user provisioning/i })
).toBeVisible();
});
test('calls onChange when toggled', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
render(
<AutoUserProvisionToggle
value={false}
onChange={onChange}
description="Enable automatic user provisioning"
/>
);
const toggle = screen.getByRole('checkbox', {
name: /automatic user provisioning/i,
});
await user.click(toggle);
expect(onChange).toHaveBeenCalled();
expect(onChange.mock.calls[0][0]).toBe(true);
});
});

View File

@@ -0,0 +1,35 @@
import { AutomationTestingProps } from '@/types';
import { FormSection } from '@@/form-components/FormSection';
import { SwitchField } from '@@/form-components/SwitchField';
interface AutoUserProvisionToggleProps extends Partial<AutomationTestingProps> {
value: boolean;
onChange: (value: boolean) => void;
description: string;
}
export function AutoUserProvisionToggle({
value,
onChange,
description,
'data-cy': dataCy = 'auto-user-provision-toggle',
}: AutoUserProvisionToggleProps) {
return (
<FormSection title="Automatic user provisioning">
<div className="form-group">
<span className="col-sm-12 text-muted small">{description}</span>
</div>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
label="Automatic user provisioning"
checked={value}
onChange={onChange}
data-cy={dataCy}
/>
</div>
</div>
</FormSection>
);
}