From e9440f1c0ebf388b01d841775618d76df72471c9 Mon Sep 17 00:00:00 2001 From: Denys Butenko Date: Tue, 7 Jan 2025 19:43:57 +0700 Subject: [PATCH] NAS-133354: QA iSCSI pages --- ...ctory-services-indicator.component.spec.ts | 2 + .../directory-services-indicator.component.ts | 25 ++++-- .../iscsi-wizard/iscsi-wizard.component.html | 6 +- .../iscsi-wizard.component.spec.ts | 73 +++++++++++++++- .../iscsi-wizard/iscsi-wizard.component.ts | 42 +++++----- .../pages/sharing/iscsi/iscsi.component.ts | 3 +- .../all-targets/all-targets.component.html | 1 + .../connections-card.component.ts | 4 +- ...re-channel-connections-card.component.html | 39 +++++---- ...channel-connections-card.component.spec.ts | 83 +++++++++++-------- ...ibre-channel-connections-card.component.ts | 10 ++- src/app/services/iscsi.service.ts | 2 + 12 files changed, 202 insertions(+), 88 deletions(-) diff --git a/src/app/modules/layout/topbar/directory-services-indicator/directory-services-indicator.component.spec.ts b/src/app/modules/layout/topbar/directory-services-indicator/directory-services-indicator.component.spec.ts index 26d5a5d3e57..d5f0f2d3e2d 100644 --- a/src/app/modules/layout/topbar/directory-services-indicator/directory-services-indicator.component.spec.ts +++ b/src/app/modules/layout/topbar/directory-services-indicator/directory-services-indicator.component.spec.ts @@ -7,6 +7,7 @@ import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectat import { of } from 'rxjs'; import { MockApiService } from 'app/core/testing/classes/mock-api.service'; import { mockCall, mockApi } from 'app/core/testing/utils/mock-api.utils'; +import { mockAuth } from 'app/core/testing/utils/mock-auth.utils'; import { DirectoryServiceState } from 'app/enums/directory-service-state.enum'; import { ApiEvent } from 'app/interfaces/api-message.interface'; import { DirectoryServicesState } from 'app/interfaces/directory-services-state.interface'; @@ -25,6 +26,7 @@ describe('DirectoryServicesIndicatorComponent', () => { const createComponent = createComponentFactory({ component: DirectoryServicesIndicatorComponent, providers: [ + mockAuth(), mockApi([ mockCall('directoryservices.get_state', { activedirectory: DirectoryServiceState.Healthy, diff --git a/src/app/modules/layout/topbar/directory-services-indicator/directory-services-indicator.component.ts b/src/app/modules/layout/topbar/directory-services-indicator/directory-services-indicator.component.ts index 76d7296f6ea..c4018b48f02 100644 --- a/src/app/modules/layout/topbar/directory-services-indicator/directory-services-indicator.component.ts +++ b/src/app/modules/layout/topbar/directory-services-indicator/directory-services-indicator.component.ts @@ -6,10 +6,12 @@ import { MatDialog, MatDialogRef, MatDialogState } from '@angular/material/dialo import { MatTooltip } from '@angular/material/tooltip'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateModule } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import { filter, Subscription, switchMap } from 'rxjs'; import { DirectoryServiceState } from 'app/enums/directory-service-state.enum'; +import { Role } from 'app/enums/role.enum'; import { helptextTopbar } from 'app/helptext/topbar'; import { DirectoryServicesState } from 'app/interfaces/directory-services-state.interface'; +import { AuthService } from 'app/modules/auth/auth.service'; import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component'; import { DirectoryServicesMonitorComponent, @@ -44,6 +46,7 @@ export class DirectoryServicesIndicatorComponent implements OnInit, OnDestroy { private api: ApiService, private matDialog: MatDialog, private cdr: ChangeDetectorRef, + private auth: AuthService, ) { } get isServicesMonitorOpen(): boolean { @@ -72,12 +75,20 @@ export class DirectoryServicesIndicatorComponent implements OnInit, OnDestroy { private loadDirectoryServicesStatus(): void { // TODO: Sync endpoints - this.api.call('directoryservices.get_state').pipe(untilDestroyed(this)).subscribe((state) => { - this.updateIconVisibility(state); - }); - this.statusSubscription = this.api.subscribe('directoryservices.status').pipe(untilDestroyed(this)).subscribe((event) => { - this.updateIconVisibility(event.fields); - }); + this.api.call('directoryservices.get_state') + .pipe(untilDestroyed(this)) + .subscribe((state) => { + this.updateIconVisibility(state); + }); + this.statusSubscription = this.auth.hasRole(Role.DirectoryServiceRead) + .pipe( + filter(Boolean), + switchMap(() => this.api.subscribe('directoryservices.status')), + untilDestroyed(this), + ) + .subscribe((event) => { + this.updateIconVisibility(event.fields); + }); } updateIconVisibility(servicesState: DirectoryServicesState): void { diff --git a/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.html b/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.html index ead562edaf5..1f98c7815c0 100644 --- a/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.html +++ b/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.html @@ -1,7 +1,7 @@ @@ -50,7 +50,7 @@ color="primary" type="submit" ixTest="save" - [disabled]="form.invalid || isLoading" + [disabled]="form.invalid || isLoading()" >{{ 'Save' | translate }} } @@ -77,7 +77,7 @@ color="primary" type="submit" ixTest="save" - [disabled]="form.invalid || isLoading" + [disabled]="form.invalid || isLoading()" >{{ 'Save' | translate }} diff --git a/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.spec.ts b/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.spec.ts index 80b358921c4..d359c58ed53 100644 --- a/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.spec.ts +++ b/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.spec.ts @@ -42,7 +42,7 @@ describe('IscsiWizardComponent', () => { let store$: Store; const slideInRef: SlideInRef = { - close: jest.fn(), + close: jest.fn(() => true), requireConfirmationWhen: jest.fn(), getData: jest.fn(() => undefined), }; @@ -120,7 +120,7 @@ describe('IscsiWizardComponent', () => { jest.spyOn(store$, 'dispatch'); }); - it('creates objects when wizard is submitted', fakeAsync(async () => { + it('iSCSI: creates objects when wizard is submitted', fakeAsync(async () => { spectator.tick(100); await form.fillForm({ @@ -191,4 +191,73 @@ describe('IscsiWizardComponent', () => { expect(spectator.inject(SlideInRef).close).toHaveBeenCalled(); })); + + it('fibre channel: creates objects when wizard is submitted', async () => { + await form.fillForm({ + Name: 'test-name', + Device: 'Create New', + 'Pool/Dataset': '/mnt/new_pool', + Size: 1024, + Portal: 'Create New', + Initiators: ['initiator1', 'initiator2'], + }); + + const addIpAddressButton = await loader.getHarness(IxListHarness.with({ label: 'IP Address' })); + await addIpAddressButton.pressAddButton(); + + await form.fillForm( + { + 'IP Address': '::', + }, + ); + + const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' })); + await saveButton.click(); + + expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(8, 'pool.dataset.create', [{ + name: 'new_pool/test-name', + type: 'VOLUME', + volsize: 1073741824, + }]); + + expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(9, 'iscsi.extent.create', [{ + blocksize: 512, + disk: 'zvol/my+pool/test_zvol', + insecure_tpc: true, + name: 'test-name', + rpm: 'SSD', + type: 'DISK', + xen: false, + }]); + + expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(10, 'iscsi.portal.create', [{ + comment: 'test-name', + listen: [{ ip: '::' }], + }]); + + expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(11, 'iscsi.initiator.create', [{ + comment: 'test-name', + initiators: ['initiator1', 'initiator2'], + }]); + + expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(12, 'iscsi.target.create', [{ + name: 'test-name', + mode: 'ISCSI', + groups: [{ + auth: null, + authmethod: 'NONE', + initiator: 14, + portal: 13, + }], + }]); + + expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(13, 'iscsi.targetextent.create', [{ + extent: 11, + target: 15, + }]); + + expect(store$.dispatch).toHaveBeenCalledWith(checkIfServiceIsEnabled({ serviceName: ServiceName.Iscsi })); + + expect(spectator.inject(SlideInRef).close).toHaveBeenCalled(); + }); }); diff --git a/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.ts b/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.ts index 39112d91f0a..141fb23fece 100644 --- a/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.ts +++ b/src/app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component.ts @@ -1,5 +1,6 @@ import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, + ChangeDetectionStrategy, Component, OnInit, + signal, } from '@angular/core'; import { Validators, ReactiveFormsModule } from '@angular/forms'; import { MatButton } from '@angular/material/button'; @@ -90,9 +91,9 @@ import { ExtentWizardStepComponent } from './steps/extent-wizard-step/extent-wiz ], }) export class IscsiWizardComponent implements OnInit { - isLoading = false; - toStop = false; - namesInUse: string[] = []; + isLoading = signal(false); + toStop = signal(false); + namesInUse = signal([]); createdZvol: Dataset | undefined; createdExtent: IscsiExtent | undefined; @@ -109,7 +110,7 @@ export class IscsiWizardComponent implements OnInit { extent: this.fb.group({ name: ['', [ Validators.required, - forbiddenValues(this.namesInUse), + forbiddenValues(this.namesInUse()), Validators.pattern(patterns.targetDeviceName), ]], type: [IscsiExtentType.Disk, [Validators.required]], @@ -252,17 +253,16 @@ export class IscsiWizardComponent implements OnInit { private api: ApiService, private errorHandler: ErrorHandlerService, private dialogService: DialogService, - private cdr: ChangeDetectorRef, private translate: TranslateService, private loader: AppLoaderService, private store$: Store, public slideInRef: SlideInRef, ) { this.iscsiService.getExtents().pipe(untilDestroyed(this)).subscribe((extents) => { - this.namesInUse.push(...extents.map((extent) => extent.name)); + this.namesInUse.set(extents.map((extent) => extent.name)); }); this.iscsiService.getTargets().pipe(untilDestroyed(this)).subscribe((targets) => { - this.namesInUse.push(...targets.map((target) => target.name)); + this.namesInUse.set(targets.map((target) => target.name)); }); } @@ -326,8 +326,7 @@ export class IscsiWizardComponent implements OnInit { } rollBack(): void { - this.isLoading = false; - this.cdr.markForCheck(); + this.isLoading.set(false); const requests = []; @@ -365,13 +364,13 @@ export class IscsiWizardComponent implements OnInit { } handleError(err: unknown): void { - this.toStop = true; + this.toStop.set(true); this.dialogService.error(this.errorHandler.parseError(err)); } async onSubmit(): Promise { - this.isLoading = true; - this.toStop = false; + this.isLoading.set(true); + this.toStop.set(false); this.createdZvol = undefined; this.createdExtent = undefined; @@ -387,7 +386,7 @@ export class IscsiWizardComponent implements OnInit { ); } - if (this.toStop) { + if (this.toStop()) { this.rollBack(); return; } @@ -397,7 +396,7 @@ export class IscsiWizardComponent implements OnInit { (err: unknown) => this.handleError(err), ); - if (this.toStop) { + if (this.toStop()) { this.rollBack(); return; } @@ -409,7 +408,7 @@ export class IscsiWizardComponent implements OnInit { ); } - if (this.toStop) { + if (this.toStop()) { this.rollBack(); return; } @@ -421,7 +420,7 @@ export class IscsiWizardComponent implements OnInit { ); } - if (this.toStop) { + if (this.toStop()) { this.rollBack(); return; } @@ -433,7 +432,7 @@ export class IscsiWizardComponent implements OnInit { ); } - if (this.toStop) { + if (this.toStop()) { this.rollBack(); return; } @@ -443,7 +442,7 @@ export class IscsiWizardComponent implements OnInit { (err: unknown) => this.handleError(err), ); - if (this.toStop) { + if (this.toStop()) { this.rollBack(); return; } @@ -456,15 +455,14 @@ export class IscsiWizardComponent implements OnInit { ).then(() => {}, (err: unknown) => this.handleError(err)); } - if (this.toStop) { + if (this.toStop()) { this.rollBack(); return; } this.store$.dispatch(checkIfServiceIsEnabled({ serviceName: ServiceName.Iscsi })); - this.isLoading = false; - this.cdr.markForCheck(); + this.isLoading.set(false); this.slideInRef.close({ response: true, error: null }); } } diff --git a/src/app/pages/sharing/iscsi/iscsi.component.ts b/src/app/pages/sharing/iscsi/iscsi.component.ts index 6d2064fee17..c24e9875b3d 100644 --- a/src/app/pages/sharing/iscsi/iscsi.component.ts +++ b/src/app/pages/sharing/iscsi/iscsi.component.ts @@ -8,7 +8,7 @@ import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; import { async, Subject } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { filter, map, startWith } from 'rxjs/operators'; import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive'; import { UiSearchDirective } from 'app/directives/ui-search.directive'; import { Role } from 'app/enums/role.enum'; @@ -49,6 +49,7 @@ export class IscsiComponent { protected readonly needRefresh$ = new Subject(); protected readonly navLinks$ = this.iscsiService.hasFibreChannel().pipe( + startWith(false), map((hasFibreChannel) => { const links = [ { diff --git a/src/app/pages/sharing/iscsi/target/all-targets/all-targets.component.html b/src/app/pages/sharing/iscsi/target/all-targets/all-targets.component.html index a68c6557187..caa07f17bad 100644 --- a/src/app/pages/sharing/iscsi/target/all-targets/all-targets.component.html +++ b/src/app/pages/sharing/iscsi/target/all-targets/all-targets.component.html @@ -19,6 +19,7 @@ @if (dataProvider.expandedRow; as target) {