-
+ >
-
+ >
{
{ username: 'root' },
{ username: 'games' },
]),
+ getUserByName: (username: string) => of({ username } as { username: string }),
+ getGroupByName: (groupName: string) => of({ group: groupName }),
}),
mockProvider(DialogService, {
confirm: jest.fn(() => of(true)),
diff --git a/src/app/pages/datasets/modules/permissions/containers/dataset-trivial-permissions/dataset-trivial-permissions.component.ts b/src/app/pages/datasets/modules/permissions/containers/dataset-trivial-permissions/dataset-trivial-permissions.component.ts
index c3e9b88e0d8..7aad4fae668 100644
--- a/src/app/pages/datasets/modules/permissions/containers/dataset-trivial-permissions/dataset-trivial-permissions.component.ts
+++ b/src/app/pages/datasets/modules/permissions/containers/dataset-trivial-permissions/dataset-trivial-permissions.component.ts
@@ -18,12 +18,11 @@ import { Role } from 'app/enums/role.enum';
import { helptextPermissions } from 'app/helptext/storage/volumes/datasets/dataset-permissions';
import { FilesystemSetPermParams } from 'app/interfaces/filesystem-stat.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
-import { GroupComboboxProvider } from 'app/modules/forms/ix-forms/classes/group-combobox-provider';
-import { UserComboboxProvider } from 'app/modules/forms/ix-forms/classes/user-combobox-provider';
import { IxCheckboxComponent } from 'app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.component';
-import { IxComboboxComponent } from 'app/modules/forms/ix-forms/components/ix-combobox/ix-combobox.component';
import { IxFieldsetComponent } from 'app/modules/forms/ix-forms/components/ix-fieldset/ix-fieldset.component';
+import { IxGroupComboboxComponent } from 'app/modules/forms/ix-forms/components/ix-group-combobox/ix-group-combobox.component';
import { IxPermissionsComponent } from 'app/modules/forms/ix-forms/components/ix-permissions/ix-permissions.component';
+import { IxUserComboboxComponent } from 'app/modules/forms/ix-forms/components/ix-user-combobox/ix-user-combobox.component';
import { FormErrorHandlerService } from 'app/modules/forms/ix-forms/services/form-error-handler.service';
import { IxValidatorsService } from 'app/modules/forms/ix-forms/services/ix-validators.service';
import { FakeProgressBarComponent } from 'app/modules/loader/components/fake-progress-bar/fake-progress-bar.component';
@@ -32,7 +31,6 @@ import { TestDirective } from 'app/modules/test-id/test.directive';
import { ApiService } from 'app/modules/websocket/api.service';
import { ErrorHandlerService } from 'app/services/errors/error-handler.service';
import { StorageService } from 'app/services/storage.service';
-import { UserService } from 'app/services/user.service';
@UntilDestroy()
@Component({
@@ -48,7 +46,8 @@ import { UserService } from 'app/services/user.service';
MatCardContent,
ReactiveFormsModule,
IxFieldsetComponent,
- IxComboboxComponent,
+ IxUserComboboxComponent,
+ IxGroupComboboxComponent,
IxCheckboxComponent,
IxPermissionsComponent,
RequiresRolesDirective,
@@ -70,7 +69,6 @@ export class DatasetTrivialPermissionsComponent implements OnInit {
private storageService = inject(StorageService);
private translate = inject(TranslateService);
private dialog = inject(DialogService);
- private userService = inject(UserService);
private validatorService = inject(IxValidatorsService);
private snackbar = inject(SnackbarService);
@@ -99,9 +97,6 @@ export class DatasetTrivialPermissionsComponent implements OnInit {
datasetPath: string;
datasetId: string;
- readonly userProvider = new UserComboboxProvider(this.userService);
- readonly groupProvider = new GroupComboboxProvider(this.userService);
-
readonly tooltips = {
user: helptextPermissions.userTooltip,
applyUser: helptextPermissions.applyUser.tooltip,
diff --git a/src/app/pages/services/components/service-smb/service-smb.component.html b/src/app/pages/services/components/service-smb/service-smb.component.html
index a91e11ea1ea..6b0de2a96a0 100644
--- a/src/app/pages/services/components/service-smb/service-smb.component.html
+++ b/src/app/pages/services/components/service-smb/service-smb.component.html
@@ -92,19 +92,17 @@
[tooltip]="tooltips.multichannel | translate"
>
-
+ >
-
+ >
{
userQueryDsCache: jest.fn(() => of([{
username: 'test-username',
}])),
+ getUserByName: (username: string) => of({ username } as User),
+ getGroupByName: (groupName: string) => of({ group: groupName }),
}),
mockProvider(SlideInRef, slideInRef),
mockAuth(),
diff --git a/src/app/pages/services/components/service-smb/service-smb.component.ts b/src/app/pages/services/components/service-smb/service-smb.component.ts
index 870c3f66d88..4c7c3ed1af3 100644
--- a/src/app/pages/services/components/service-smb/service-smb.component.ts
+++ b/src/app/pages/services/components/service-smb/service-smb.component.ts
@@ -14,17 +14,16 @@ import { choicesToOptions } from 'app/helpers/operators/options.operators';
import { mapToOptions } from 'app/helpers/options.helper';
import { helptextServiceSmb } from 'app/helptext/services/components/service-smb';
import { SmbConfigUpdate } from 'app/interfaces/smb-config.interface';
-import { GroupComboboxProvider } from 'app/modules/forms/ix-forms/classes/group-combobox-provider';
-import { UserComboboxProvider } from 'app/modules/forms/ix-forms/classes/user-combobox-provider';
import { FormActionsComponent } from 'app/modules/forms/ix-forms/components/form-actions/form-actions.component';
import { IxCheckboxComponent } from 'app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.component';
import { IxChipsComponent } from 'app/modules/forms/ix-forms/components/ix-chips/ix-chips.component';
-import { IxComboboxComponent } from 'app/modules/forms/ix-forms/components/ix-combobox/ix-combobox.component';
import { IxFieldsetComponent } from 'app/modules/forms/ix-forms/components/ix-fieldset/ix-fieldset.component';
+import { IxGroupComboboxComponent } from 'app/modules/forms/ix-forms/components/ix-group-combobox/ix-group-combobox.component';
import { IxInputComponent } from 'app/modules/forms/ix-forms/components/ix-input/ix-input.component';
import { IxListItemComponent } from 'app/modules/forms/ix-forms/components/ix-list/ix-list-item/ix-list-item.component';
import { IxListComponent } from 'app/modules/forms/ix-forms/components/ix-list/ix-list.component';
import { IxSelectComponent } from 'app/modules/forms/ix-forms/components/ix-select/ix-select.component';
+import { IxUserComboboxComponent } from 'app/modules/forms/ix-forms/components/ix-user-combobox/ix-user-combobox.component';
import { FormErrorHandlerService } from 'app/modules/forms/ix-forms/services/form-error-handler.service';
import { IxValidatorsService } from 'app/modules/forms/ix-forms/services/ix-validators.service';
import { ModalHeaderComponent } from 'app/modules/slide-ins/components/modal-header/modal-header.component';
@@ -33,7 +32,6 @@ import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service'
import { TestDirective } from 'app/modules/test-id/test.directive';
import { ApiService } from 'app/modules/websocket/api.service';
import { ErrorHandlerService } from 'app/services/errors/error-handler.service';
-import { UserService } from 'app/services/user.service';
interface BindIp {
bindIp: string;
@@ -55,7 +53,8 @@ interface BindIp {
IxChipsComponent,
IxCheckboxComponent,
IxSelectComponent,
- IxComboboxComponent,
+ IxUserComboboxComponent,
+ IxGroupComboboxComponent,
IxListComponent,
IxListItemComponent,
FormActionsComponent,
@@ -71,7 +70,6 @@ export class ServiceSmbComponent implements OnInit {
private errorHandler = inject(ErrorHandlerService);
private fb = inject(FormBuilder);
private translate = inject(TranslateService);
- private userService = inject(UserService);
private validatorsService = inject(IxValidatorsService);
private snackbar = inject(SnackbarService);
slideInRef = inject>(SlideInRef);
@@ -131,8 +129,6 @@ export class ServiceSmbComponent implements OnInit {
};
readonly unixCharsetOptions$ = this.api.call('smb.unixcharset_choices').pipe(choicesToOptions());
- readonly guestAccountProvider = new UserComboboxProvider(this.userService);
- readonly adminGroupProvider = new GroupComboboxProvider(this.userService);
readonly bindIpAddressOptions$ = combineLatest([
this.api.call('smb.bindip_choices').pipe(choicesToOptions()),
diff --git a/src/app/pages/services/components/service-ssh/service-ssh.component.html b/src/app/pages/services/components/service-ssh/service-ssh.component.html
index 87bd80b37e4..766522270c3 100644
--- a/src/app/pages/services/components/service-ssh/service-ssh.component.html
+++ b/src/app/pages/services/components/service-ssh/service-ssh.component.html
@@ -15,12 +15,11 @@
[tooltip]="tooltips.tcpport | translate"
>
-
+ >
{
mockProvider(FormErrorHandlerService),
mockProvider(DialogService),
mockProvider(SlideInRef, slideInRef),
+ mockProvider(UserService, {
+ groupQueryDsCache: jest.fn(() => of(fakeGroupDataSource)),
+ getGroupByName: jest.fn((groupName: string) => {
+ const existingGroup = fakeGroupDataSource.find((group) => group.group === groupName);
+ if (existingGroup) {
+ return of(existingGroup);
+ }
+ return of(null);
+ }),
+ getUserByName: jest.fn(() => of(null)),
+ }),
mockAuth(),
],
});
diff --git a/src/app/pages/services/components/service-ssh/service-ssh.component.ts b/src/app/pages/services/components/service-ssh/service-ssh.component.ts
index c506a2c4f7d..dfda9bcf39b 100644
--- a/src/app/pages/services/components/service-ssh/service-ssh.component.ts
+++ b/src/app/pages/services/components/service-ssh/service-ssh.component.ts
@@ -1,10 +1,12 @@
-import { ChangeDetectionStrategy, Component, OnInit, signal, inject } from '@angular/core';
+import {
+ ChangeDetectionStrategy, Component, OnInit, signal, inject,
+} from '@angular/core';
import { NonNullableFormBuilder, ReactiveFormsModule } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { MatCard, MatCardContent } from '@angular/material/card';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
-import { map, of } from 'rxjs';
+import { of } from 'rxjs';
import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive';
import { Role } from 'app/enums/role.enum';
import { SshSftpLogFacility, SshSftpLogLevel, SshWeakCipher } from 'app/enums/ssh.enum';
@@ -13,9 +15,8 @@ import { helptextServiceSsh } from 'app/helptext/services/components/service-ssh
import { SshConfigUpdate } from 'app/interfaces/ssh-config.interface';
import { FormActionsComponent } from 'app/modules/forms/ix-forms/components/form-actions/form-actions.component';
import { IxCheckboxComponent } from 'app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.component';
-import { ChipsProvider } from 'app/modules/forms/ix-forms/components/ix-chips/chips-provider';
-import { IxChipsComponent } from 'app/modules/forms/ix-forms/components/ix-chips/ix-chips.component';
import { IxFieldsetComponent } from 'app/modules/forms/ix-forms/components/ix-fieldset/ix-fieldset.component';
+import { IxGroupChipsComponent } from 'app/modules/forms/ix-forms/components/ix-group-chips/ix-group-chips.component';
import { IxInputComponent } from 'app/modules/forms/ix-forms/components/ix-input/ix-input.component';
import { IxSelectComponent } from 'app/modules/forms/ix-forms/components/ix-select/ix-select.component';
import { IxTextareaComponent } from 'app/modules/forms/ix-forms/components/ix-textarea/ix-textarea.component';
@@ -26,7 +27,6 @@ import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service'
import { TestDirective } from 'app/modules/test-id/test.directive';
import { ApiService } from 'app/modules/websocket/api.service';
import { ErrorHandlerService } from 'app/services/errors/error-handler.service';
-import { UserService } from 'app/services/user.service';
@UntilDestroy()
@Component({
@@ -41,7 +41,7 @@ import { UserService } from 'app/services/user.service';
ReactiveFormsModule,
IxFieldsetComponent,
IxInputComponent,
- IxChipsComponent,
+ IxGroupChipsComponent,
IxCheckboxComponent,
IxSelectComponent,
IxTextareaComponent,
@@ -57,7 +57,6 @@ export class ServiceSshComponent implements OnInit {
private errorHandler = inject(ErrorHandlerService);
private formErrorHandler = inject(FormErrorHandlerService);
private fb = inject(NonNullableFormBuilder);
- private userService = inject(UserService);
private translate = inject(TranslateService);
private snackbar = inject(SnackbarService);
slideInRef = inject>(SlideInRef);
@@ -67,12 +66,6 @@ export class ServiceSshComponent implements OnInit {
protected isFormLoading = signal(false);
isBasicMode = true;
- groupProvider: ChipsProvider = (query) => {
- return this.userService.groupQueryDsCache(query).pipe(
- map((groups) => groups.map((group) => group.group)),
- );
- };
-
form = this.fb.group({
tcpport: [null as number | null],
password_login_groups: [[] as string[]],
diff --git a/src/app/pages/sharing/nfs/nfs-form/nfs-form.component.html b/src/app/pages/sharing/nfs/nfs-form/nfs-form.component.html
index ab6ac3f61d2..86d4f0cedc8 100644
--- a/src/app/pages/sharing/nfs/nfs-form/nfs-form.component.html
+++ b/src/app/pages/sharing/nfs/nfs-form/nfs-form.component.html
@@ -48,33 +48,29 @@
[label]="'Read Only' | translate"
>
-
+ >
-
+ >
-
+ >
-
+ >
{
{ group: 'sys' },
{ group: 'operator' },
]),
+ getUserByName: (username: string) => of({ username } as { username: string }),
+ getGroupByName: (groupName: string) => of({ group: groupName }),
}),
mockProvider(DialogService, {
confirm: jest.fn(() => of(true)),
diff --git a/src/app/pages/sharing/nfs/nfs-form/nfs-form.component.ts b/src/app/pages/sharing/nfs/nfs-form/nfs-form.component.ts
index 1026a14e667..4bbf39fcc9d 100644
--- a/src/app/pages/sharing/nfs/nfs-form/nfs-form.component.ts
+++ b/src/app/pages/sharing/nfs/nfs-form/nfs-form.component.ts
@@ -20,19 +20,18 @@ import { helptextSharingNfs } from 'app/helptext/sharing';
import { DatasetCreate } from 'app/interfaces/dataset.interface';
import { NfsShare, NfsShareUpdate } from 'app/interfaces/nfs-share.interface';
import { Option } from 'app/interfaces/option.interface';
-import { GroupComboboxProvider } from 'app/modules/forms/ix-forms/classes/group-combobox-provider';
-import { UserComboboxProvider } from 'app/modules/forms/ix-forms/classes/user-combobox-provider';
import { FormActionsComponent } from 'app/modules/forms/ix-forms/components/form-actions/form-actions.component';
import { IxCheckboxComponent } from 'app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.component';
-import { IxComboboxComponent } from 'app/modules/forms/ix-forms/components/ix-combobox/ix-combobox.component';
import { ExplorerCreateDatasetComponent } from 'app/modules/forms/ix-forms/components/ix-explorer/explorer-create-dataset/explorer-create-dataset.component';
import { IxExplorerComponent } from 'app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component';
import { IxFieldsetComponent } from 'app/modules/forms/ix-forms/components/ix-fieldset/ix-fieldset.component';
+import { IxGroupComboboxComponent } from 'app/modules/forms/ix-forms/components/ix-group-combobox/ix-group-combobox.component';
import { IxInputComponent } from 'app/modules/forms/ix-forms/components/ix-input/ix-input.component';
import { IxIpInputWithNetmaskComponent } from 'app/modules/forms/ix-forms/components/ix-ip-input-with-netmask/ix-ip-input-with-netmask.component';
import { IxListItemComponent } from 'app/modules/forms/ix-forms/components/ix-list/ix-list-item/ix-list-item.component';
import { IxListComponent } from 'app/modules/forms/ix-forms/components/ix-list/ix-list.component';
import { IxSelectComponent } from 'app/modules/forms/ix-forms/components/ix-select/ix-select.component';
+import { IxUserComboboxComponent } from 'app/modules/forms/ix-forms/components/ix-user-combobox/ix-user-combobox.component';
import { FormErrorHandlerService } from 'app/modules/forms/ix-forms/services/form-error-handler.service';
import { IxValidatorsService } from 'app/modules/forms/ix-forms/services/ix-validators.service';
import { ipv4or6cidrValidator } from 'app/modules/forms/ix-forms/validators/ip-validation';
@@ -44,7 +43,6 @@ import { ApiService } from 'app/modules/websocket/api.service';
import { getRootDatasetsValidator } from 'app/pages/sharing/utils/root-datasets-validator';
import { DatasetService } from 'app/services/dataset/dataset.service';
import { FilesystemService } from 'app/services/filesystem.service';
-import { UserService } from 'app/services/user.service';
import { checkIfServiceIsEnabled } from 'app/store/services/services.actions';
import { ServicesState } from 'app/store/services/services.reducer';
import { selectIsEnterprise } from 'app/store/system-info/system-info.selectors';
@@ -65,7 +63,8 @@ import { selectIsEnterprise } from 'app/store/system-info/system-info.selectors'
ExplorerCreateDatasetComponent,
IxInputComponent,
IxCheckboxComponent,
- IxComboboxComponent,
+ IxUserComboboxComponent,
+ IxGroupComboboxComponent,
IxSelectComponent,
IxListComponent,
IxListItemComponent,
@@ -80,7 +79,6 @@ import { selectIsEnterprise } from 'app/store/system-info/system-info.selectors'
export class NfsFormComponent implements OnInit {
private api = inject(ApiService);
private formBuilder = inject(FormBuilder);
- private userService = inject(UserService);
private translate = inject(TranslateService);
private filesystemService = inject(FilesystemService);
private formErrorHandler = inject(FormErrorHandlerService);
@@ -131,8 +129,6 @@ export class NfsFormComponent implements OnInit {
protected readonly requiredRoles = [Role.SharingNfsWrite, Role.SharingWrite];
readonly helptext = helptextSharingNfs;
- readonly userProvider = new UserComboboxProvider(this.userService);
- readonly groupProvider = new GroupComboboxProvider(this.userService);
readonly treeNodeProvider = this.filesystemService.getFilesystemNodeProvider({ directoriesOnly: true });
readonly isEnterprise = toSignal(this.store$.select(selectIsEnterprise));
diff --git a/src/app/pages/sharing/smb/smb-form/smb-form.component.html b/src/app/pages/sharing/smb/smb-form/smb-form.component.html
index 61e9634dd23..5b6bc4613c6 100644
--- a/src/app/pages/sharing/smb/smb-form/smb-form.component.html
+++ b/src/app/pages/sharing/smb/smb-form/smb-form.component.html
@@ -137,21 +137,17 @@
@if (form.value.audit.enable) {
-
+ >
-
+ >
}
diff --git a/src/app/pages/sharing/smb/smb-form/smb-form.component.spec.ts b/src/app/pages/sharing/smb/smb-form/smb-form.component.spec.ts
index 1a483e5ca55..4b1ae8b4613 100644
--- a/src/app/pages/sharing/smb/smb-form/smb-form.component.spec.ts
+++ b/src/app/pages/sharing/smb/smb-form/smb-form.component.spec.ts
@@ -9,7 +9,7 @@ import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectat
import { Store } from '@ngrx/store';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { MockComponent } from 'ng-mocks';
-import { of, throwError } from 'rxjs';
+import { of, Subject, throwError } from 'rxjs';
import { GiB } from 'app/constants/bytes.constant';
import { fakeSuccessfulJob } from 'app/core/testing/utils/fake-job.utils';
import { mockApi, mockCall, mockJob } from 'app/core/testing/utils/mock-api.utils';
@@ -18,6 +18,7 @@ import { ServiceName } from 'app/enums/service-name.enum';
import { ServiceStatus } from 'app/enums/service-status.enum';
import { helptextSharingSmb } from 'app/helptext/sharing';
import { JsonRpcError } from 'app/interfaces/api-message.interface';
+import { DsUncachedGroup } from 'app/interfaces/ds-cache.interface';
import { FileSystemStat } from 'app/interfaces/filesystem-stat.interface';
import { Group } from 'app/interfaces/group.interface';
import { Service } from 'app/interfaces/service.interface';
@@ -47,6 +48,7 @@ import { RestartSmbDialog } from 'app/pages/sharing/smb/smb-form/restart-smb-dia
import { SmbUsersWarningComponent } from 'app/pages/sharing/smb/smb-form/smb-users-warning/smb-users-warning.component';
import { ApiCallError } from 'app/services/errors/error.classes';
import { FilesystemService } from 'app/services/filesystem.service';
+import { UserService } from 'app/services/user.service';
import { AppState } from 'app/store';
import { checkIfServiceIsEnabled } from 'app/store/services/services.actions';
import { selectServices } from 'app/store/services/services.selectors';
@@ -113,6 +115,7 @@ describe('SmbFormComponent', () => {
mockAuth(),
mockApi([
mockCall('group.query', [{ id: 1, group: 'test', builtin: false }] as Group[]),
+ mockCall('group.get_group_obj', { gr_gid: 1000, gr_name: 'test', gr_mem: [] }),
mockCall('sharing.smb.create', { ...existingShare }),
mockCall('sharing.smb.update', { ...existingShare }),
mockCall('sharing.smb.share_precheck', null),
@@ -163,6 +166,16 @@ describe('SmbFormComponent', () => {
mockProvider(FormErrorHandlerService, {
handleValidationErrors: jest.fn(),
}),
+ mockProvider(UserService, {
+ groupQueryDsCache: jest.fn(() => of([{ group: 'test', gid: 1 }])),
+ getGroupByName: jest.fn((groupName: string) => {
+ if (groupName === 'test') {
+ return of({ group: 'test', gid: 1 });
+ }
+ return of(null);
+ }),
+ getUserByName: jest.fn(() => of(null)),
+ }),
],
});
@@ -1184,6 +1197,164 @@ describe('SmbFormComponent', () => {
expect(errorElement).toBeTruthy();
expect(errorElement?.textContent).toContain('At least one group must be specified');
});
+
+ it('should show error when non-existent group is entered in watch list', fakeAsync(async () => {
+ // Mock API to return error for non-existent group
+ const userService = spectator.inject(UserService);
+ jest.spyOn(userService, 'getGroupByName').mockReturnValue(throwError(() => new Error('Group not found')));
+
+ // Fill in required fields and enable audit logging
+ await form.fillForm({
+ Path: '/mnt/pool123/test',
+ Name: 'TestShare',
+ Purpose: 'Default Share',
+ 'Enable Logging': true,
+ });
+
+ spectator.detectChanges();
+ await spectator.fixture.whenStable();
+
+ // Manually add a non-existent group using the form control
+ const auditGroup = (spectator.component as unknown as { form: FormGroup }).form.controls.audit as FormGroup;
+ auditGroup.controls.watch_list.setValue(['nonexistent']);
+ auditGroup.controls.watch_list.markAsTouched();
+ auditGroup.controls.watch_list.updateValueAndValidity();
+
+ // Wait for async validation and debounce
+ spectator.tick(600);
+ spectator.detectChanges();
+ await spectator.fixture.whenStable();
+
+ // Verify error message is displayed
+ const errorElement = spectator.query('ix-errors mat-error');
+ expect(errorElement?.textContent).toContain('The following groups do not exist: nonexistent');
+
+ // Verify save button is disabled
+ const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
+ expect(await saveButton.isDisabled()).toBe(true);
+ }));
+
+ it('should show error when non-existent group is entered in ignore list', fakeAsync(async () => {
+ // Mock API to return error for non-existent group
+ const userService = spectator.inject(UserService);
+ jest.spyOn(userService, 'getGroupByName').mockReturnValue(throwError(() => new Error('Group not found')));
+
+ // Fill in required fields and enable audit logging
+ await form.fillForm({
+ Path: '/mnt/pool123/test',
+ Name: 'TestShare',
+ Purpose: 'Default Share',
+ 'Enable Logging': true,
+ });
+
+ spectator.detectChanges();
+ await spectator.fixture.whenStable();
+
+ // Manually add a non-existent group using the form control
+ const auditGroup = (spectator.component as unknown as { form: FormGroup }).form.controls.audit as FormGroup;
+ auditGroup.controls.ignore_list.setValue(['nonexistent']);
+ auditGroup.controls.ignore_list.markAsTouched();
+ auditGroup.controls.ignore_list.updateValueAndValidity();
+
+ // Wait for async validation and debounce
+ spectator.tick(600);
+ spectator.detectChanges();
+ await spectator.fixture.whenStable();
+
+ // Verify error message is displayed
+ const errorElement = spectator.query('ix-errors mat-error');
+ expect(errorElement?.textContent).toContain('The following groups do not exist: nonexistent');
+
+ // Verify save button is disabled
+ const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
+ expect(await saveButton.isDisabled()).toBe(true);
+ }));
+
+ it('should pass validation when all entered groups exist', fakeAsync(async () => {
+ // Mock API to return success for existing groups
+ const userService = spectator.inject(UserService);
+ jest.spyOn(userService, 'getGroupByName').mockReturnValue(of({
+ gr_gid: 1000,
+ gr_name: 'test',
+ gr_mem: [],
+ } as DsUncachedGroup));
+
+ // Fill in required fields and enable audit logging
+ await form.fillForm({
+ Path: '/mnt/pool123/test',
+ Name: 'TestShare',
+ Purpose: 'Default Share',
+ 'Enable Logging': true,
+ });
+
+ spectator.detectChanges();
+ await spectator.fixture.whenStable();
+
+ // Add an existing group
+ const auditGroup = (spectator.component as unknown as { form: FormGroup }).form.controls.audit as FormGroup;
+ auditGroup.controls.watch_list.setValue(['test']);
+ auditGroup.controls.watch_list.markAsTouched();
+ auditGroup.controls.watch_list.updateValueAndValidity();
+
+ // Wait for async validation and debounce
+ spectator.tick(600);
+ spectator.detectChanges();
+ await spectator.fixture.whenStable();
+
+ // Verify no error message is displayed
+ const errorElement = spectator.query('ix-errors mat-error');
+ expect(errorElement).toBeFalsy();
+
+ // Verify save button is enabled
+ const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
+ expect(await saveButton.isDisabled()).toBe(false);
+ }));
+
+ it('should disable save button during async validation', fakeAsync(async () => {
+ // Mock API with a delayed response to catch the PENDING state
+ const userService = spectator.inject(UserService);
+ const delayedObservable$ = new Subject
();
+ jest.spyOn(userService, 'getGroupByName').mockReturnValue(delayedObservable$.asObservable());
+
+ // Fill in required fields and enable audit logging
+ await form.fillForm({
+ Path: '/mnt/pool123/test',
+ Name: 'TestShare',
+ Purpose: 'Default Share',
+ 'Enable Logging': true,
+ });
+
+ spectator.detectChanges();
+ await spectator.fixture.whenStable();
+
+ // Add a group to trigger async validation
+ const auditGroup = (spectator.component as unknown as { form: FormGroup }).form.controls.audit as FormGroup;
+ auditGroup.controls.watch_list.setValue(['test']);
+ auditGroup.controls.watch_list.markAsTouched();
+ auditGroup.controls.watch_list.updateValueAndValidity();
+
+ // Wait for debounce
+ spectator.tick(600);
+ spectator.detectChanges();
+
+ // Verify save button is disabled while validation is pending
+ const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
+ expect(await saveButton.isDisabled()).toBe(true);
+
+ // Complete the async validation
+ delayedObservable$.next({
+ gr_gid: 1000,
+ gr_name: 'test',
+ gr_mem: [],
+ } as DsUncachedGroup);
+ delayedObservable$.complete();
+
+ spectator.detectChanges();
+ await spectator.fixture.whenStable();
+
+ // Verify save button is now enabled
+ expect(await saveButton.isDisabled()).toBe(false);
+ }));
});
describe('Dataset Naming Schema null value', () => {
diff --git a/src/app/pages/sharing/smb/smb-form/smb-form.component.ts b/src/app/pages/sharing/smb/smb-form/smb-form.component.ts
index 6c5aa42b269..b0bbe31b57e 100644
--- a/src/app/pages/sharing/smb/smb-form/smb-form.component.ts
+++ b/src/app/pages/sharing/smb/smb-form/smb-form.component.ts
@@ -13,9 +13,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Store } from '@ngrx/store';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { isEqual } from 'lodash-es';
-import {
- endWith, Observable, of,
-} from 'rxjs';
+import { endWith, Observable, of } from 'rxjs';
import {
debounceTime, filter, map, switchMap, take, tap,
} from 'rxjs/operators';
@@ -41,12 +39,12 @@ import { ExplorerNodeData } from 'app/interfaces/tree-node.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { FormActionsComponent } from 'app/modules/forms/ix-forms/components/form-actions/form-actions.component';
import { IxCheckboxComponent } from 'app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.component';
-import { ChipsProvider } from 'app/modules/forms/ix-forms/components/ix-chips/chips-provider';
import { IxChipsComponent } from 'app/modules/forms/ix-forms/components/ix-chips/ix-chips.component';
import { IxErrorsComponent } from 'app/modules/forms/ix-forms/components/ix-errors/ix-errors.component';
import { ExplorerCreateDatasetComponent } from 'app/modules/forms/ix-forms/components/ix-explorer/explorer-create-dataset/explorer-create-dataset.component';
import { IxExplorerComponent } from 'app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component';
import { IxFieldsetComponent } from 'app/modules/forms/ix-forms/components/ix-fieldset/ix-fieldset.component';
+import { IxGroupChipsComponent } from 'app/modules/forms/ix-forms/components/ix-group-chips/ix-group-chips.component';
import { IxInputComponent } from 'app/modules/forms/ix-forms/components/ix-input/ix-input.component';
import { IxSelectComponent } from 'app/modules/forms/ix-forms/components/ix-select/ix-select.component';
import { WarningComponent } from 'app/modules/forms/ix-forms/components/warning/warning.component';
@@ -68,7 +66,6 @@ import { getRootDatasetsValidator } from 'app/pages/sharing/utils/root-datasets-
import { DatasetService } from 'app/services/dataset/dataset.service';
import { ErrorHandlerService } from 'app/services/errors/error-handler.service';
import { FilesystemService } from 'app/services/filesystem.service';
-import { UserService } from 'app/services/user.service';
import { checkIfServiceIsEnabled } from 'app/store/services/services.actions';
import { ServicesState } from 'app/store/services/services.reducer';
import { selectService } from 'app/store/services/services.selectors';
@@ -91,6 +88,7 @@ import { selectIsEnterprise } from 'app/store/system-info/system-info.selectors'
IxSelectComponent,
IxCheckboxComponent,
IxChipsComponent,
+ IxGroupChipsComponent,
IxErrorsComponent,
FormActionsComponent,
RequiresRolesDirective,
@@ -111,7 +109,6 @@ export class SmbFormComponent implements OnInit, AfterViewInit {
private datasetService = inject(DatasetService);
private translate = inject(TranslateService);
private router = inject(Router);
- private userService = inject(UserService);
protected loader = inject(LoaderService);
private errorHandler = inject(ErrorHandlerService);
private formErrorHandler = inject(FormErrorHandlerService);
@@ -146,12 +143,6 @@ export class SmbFormComponent implements OnInit, AfterViewInit {
private wasStripAclWarningShown = false;
private smbConfig = signal(null);
- protected groupProvider: ChipsProvider = (query) => {
- return this.userService.groupQueryDsCache(query).pipe(
- map((groups) => groups.map((group) => group.group)),
- );
- };
-
title: string = helptextSharingSmb.formTitleAdd;
createDatasetProps: Omit = {
@@ -169,7 +160,14 @@ export class SmbFormComponent implements OnInit, AfterViewInit {
}
get isAsyncValidatorPending(): boolean {
- return this.form.controls.name.status === 'PENDING' && this.form.controls.name.touched;
+ const nameControl = this.form.controls.name;
+ const auditGroup = this.form.controls.audit;
+ const watchListControl = auditGroup.controls.watch_list;
+ const ignoreListControl = auditGroup.controls.ignore_list;
+
+ return (nameControl.status === 'PENDING' && nameControl.touched)
+ || (watchListControl.status === 'PENDING' && watchListControl.touched)
+ || (ignoreListControl.status === 'PENDING' && ignoreListControl.touched);
}
readonly treeNodeProvider = this.filesystemService.getFilesystemNodeProvider({
diff --git a/src/app/pages/system/advanced/cron/cron-form/cron-form.component.html b/src/app/pages/system/advanced/cron/cron-form/cron-form.component.html
index c6553d24b28..1162c1ff23e 100644
--- a/src/app/pages/system/advanced/cron/cron-form/cron-form.component.html
+++ b/src/app/pages/system/advanced/cron/cron-form/cron-form.component.html
@@ -16,13 +16,12 @@
[tooltip]="tooltips.command | translate"
>
-
+ >
{
{ username: 'root' },
{ username: 'steven' },
] as User[]),
+ getUserByName: (username: string) => of({ username } as User),
}),
mockProvider(SlideInRef, componentRef),
mockAuth(),
diff --git a/src/app/pages/system/advanced/cron/cron-form/cron-form.component.ts b/src/app/pages/system/advanced/cron/cron-form/cron-form.component.ts
index d85f2c1a211..5582098817b 100644
--- a/src/app/pages/system/advanced/cron/cron-form/cron-form.component.ts
+++ b/src/app/pages/system/advanced/cron/cron-form/cron-form.component.ts
@@ -9,12 +9,11 @@ import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-r
import { Role } from 'app/enums/role.enum';
import { helptextCron } from 'app/helptext/system/cron-form';
import { Cronjob, CronjobUpdate } from 'app/interfaces/cronjob.interface';
-import { UserComboboxProvider } from 'app/modules/forms/ix-forms/classes/user-combobox-provider';
import { FormActionsComponent } from 'app/modules/forms/ix-forms/components/form-actions/form-actions.component';
import { IxCheckboxComponent } from 'app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.component';
-import { IxComboboxComponent } from 'app/modules/forms/ix-forms/components/ix-combobox/ix-combobox.component';
import { IxFieldsetComponent } from 'app/modules/forms/ix-forms/components/ix-fieldset/ix-fieldset.component';
import { IxInputComponent } from 'app/modules/forms/ix-forms/components/ix-input/ix-input.component';
+import { IxUserComboboxComponent } from 'app/modules/forms/ix-forms/components/ix-user-combobox/ix-user-combobox.component';
import { FormErrorHandlerService } from 'app/modules/forms/ix-forms/services/form-error-handler.service';
import { SchedulerComponent } from 'app/modules/scheduler/components/scheduler/scheduler.component';
import { crontabToSchedule } from 'app/modules/scheduler/utils/crontab-to-schedule.utils';
@@ -25,7 +24,6 @@ import { SlideInRef } from 'app/modules/slide-ins/slide-in-ref';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { TestDirective } from 'app/modules/test-id/test.directive';
import { ApiService } from 'app/modules/websocket/api.service';
-import { UserService } from 'app/services/user.service';
@UntilDestroy()
@Component({
@@ -39,7 +37,7 @@ import { UserService } from 'app/services/user.service';
ReactiveFormsModule,
IxFieldsetComponent,
IxInputComponent,
- IxComboboxComponent,
+ IxUserComboboxComponent,
SchedulerComponent,
IxCheckboxComponent,
FormActionsComponent,
@@ -55,7 +53,6 @@ export class CronFormComponent implements OnInit {
private translate = inject(TranslateService);
private errorHandler = inject(FormErrorHandlerService);
private snackbar = inject(SnackbarService);
- private userService = inject(UserService);
slideInRef = inject>(SlideInRef);
protected readonly requiredRoles = [Role.SystemCronWrite];
@@ -90,8 +87,6 @@ export class CronFormComponent implements OnInit {
stderr: helptextCron.stderrTooltip,
};
- readonly userProvider = new UserComboboxProvider(this.userService);
-
private editingCron: Cronjob | undefined;
constructor() {
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index e9ff432c97c..82668984e9d 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -646,6 +646,7 @@
"At least 1 GPU is required by the host for its functions.": "",
"At least 1 vdev is required to make an update to the pool.": "",
"At least one module must be defined in rsyncd.conf(5) of the rsync server or in the Rsync Modules of another system.": "",
+ "The following groups do not exist: {groups}": "",
"At least one pool must be available to use apps": "",
"At least one spare is recommended for dRAID. Spares cannot be added later.": "",
"Atleast {min} disk(s) are required for {vdevType} vdevs": "",