Skip to content

Commit

Permalink
NAS-133051 / 25.04 / Reorganize iSCSI wizard + NAS-133051 new form co…
Browse files Browse the repository at this point in the history
…ntrols to handle fibre ports (#11207)

* NAS-133051: Reorganize iSCSI wizard

* NAS-133051: Build form controls to handle fibre ports

* NAS-133051: Add FibreChannelService

* NAS-133051: Add unit tests

* NAS-133051: Fix remarks
  • Loading branch information
bvasilenko authored Dec 26, 2024
1 parent 147c20c commit 713074e
Show file tree
Hide file tree
Showing 109 changed files with 1,215 additions and 207 deletions.
2 changes: 1 addition & 1 deletion src/app/interfaces/api/api-call-directory.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ export interface ApiCallDirectory {
'fc.capable': { params: []; response: boolean };

// Fibre Channel Host
'fc.fc_host.query': { params: []; response: FibreChannelHost[] };
'fc.fc_host.query': { params: QueryParams<FibreChannelHost>; response: FibreChannelHost[] };
'fc.fc_host.update': { params: [id: number, changes: Partial<FibreChannelHost>]; response: void };

// Fibre Channel Port
Expand Down
2 changes: 2 additions & 0 deletions src/app/interfaces/option.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ export interface ActionOption<T = BaseOptionValueType> extends Option<T> {
}

export const newOption = 'NEW';
export const nullOption = 'NULL';
export const skipOption = 'SKIP';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<ix-radio-group
[formControl]="isNewControl"
[options]="isNewOptions$"
></ix-radio-group>
<ng-container [formGroup]="form()">
@if(isNewControl.value) {
<ix-select
formControlName="host_id"
[label]="'Choose a new virtual port' | translate"
[options]="creatingPortOptions$"
[required]="true"
></ix-select>
} @else {
<ix-select
formControlName="port"
[label]="'Existing Ports' | translate"
[options]="existingPortOptions$"
[required]="true"
></ix-select>
}
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
ChangeDetectionStrategy, Component, input, OnInit,
} from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { FormBuilder } from '@ngneat/reactive-forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { map, Observable, of } from 'rxjs';
import { Option, nullOption, skipOption } from 'app/interfaces/option.interface';
import { IxRadioGroupComponent } from 'app/modules/forms/ix-forms/components/ix-radio-group/ix-radio-group.component';
import { IxSelectComponent } from 'app/modules/forms/ix-forms/components/ix-select/ix-select.component';
import { TargetFormComponent } from 'app/pages/sharing/iscsi/target/target-form/target-form.component';
import { ApiService } from 'app/services/websocket/api.service';

@UntilDestroy()
@Component({
selector: 'ix-fc-ports-controls',
templateUrl: './fc-ports-controls.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
ReactiveFormsModule,
IxSelectComponent,
IxRadioGroupComponent,
TranslateModule,
],
})
export class FcPortsControlsComponent implements OnInit {
form = input.required<TargetFormComponent['fcForm']>();
isEdit = input(false);

isNewControl = this.fb.control(false);

readonly isNewOptions$: Observable<Option<boolean>[]> = of([
{ label: this.translate.instant('Use an existing port'), value: false },
{ label: this.translate.instant('Create new virtual port'), value: true },
]);

readonly creatingPortOptions$ = this.api.call('fc.fc_host.query').pipe(map((hosts) => {
return hosts.map((host) => ({
label: `${this.translate.instant('Create')} ${host.alias}/${host.npiv + 1}`,
value: host.id,
}));
}));

readonly existingPortOptions$ = this.api.call('fcport.port_choices', [false]).pipe(map((ports) => {
const option = [{
label: this.translate.instant('Do not connect to a fibre channel port'),
value: nullOption,
}];

if (this.isEdit()) {
option.push({
label: this.translate.instant('Use current port'),
value: skipOption,
});
}
return option.concat(Object.entries(ports).map(([value]) => ({ label: value, value })));
}));

constructor(
private fb: FormBuilder,
private api: ApiService,
private translate: TranslateService,
) {}

ngOnInit(): void {
this.form().controls.host_id.disable();
this.isNewControl.valueChanges.pipe(untilDestroyed(this)).subscribe((isNew) => {
if (isNew) {
this.form().controls.port.disable();
this.form().controls.host_id.enable();
this.form().controls.port.setValue(null);
} else {
this.form().controls.port.enable();
this.form().controls.host_id.disable();
this.form().controls.host_id.setValue(null);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,39 @@
<ix-use-ix-icons-in-stepper></ix-use-ix-icons-in-stepper>

<mat-step #matStepInstance>
<ng-template matStepLabel>{{ 'Create or Choose Block Device' | translate }}</ng-template>
<ix-device-wizard-step [form]="form.controls.device"></ix-device-wizard-step>
<ng-template matStepLabel>{{ 'Target' | translate }}</ng-template>
<ix-target-wizard-step [form]="form.controls.target"></ix-target-wizard-step>
<div class="step-buttons">
<button
mat-button
matStepperNext
color="primary"
type="button"
ixTest="next"
[disabled]="form.controls.target.invalid"
>{{ 'Next' | translate }}</button>
</div>
</mat-step>

<mat-step #matStepInstance>
<ng-template matStepLabel>{{ 'Extent' | translate }}</ng-template>
<ix-extent-wizard-step [form]="form.controls.extent"></ix-extent-wizard-step>
<div class="step-buttons">
<button
mat-button
matStepperPrevious
color="accent"
type="button"
ixTest="back"
>{{ 'Back' | translate }}</button>
@if (isNewTarget) {
<button
mat-button
matStepperNext
color="primary"
type="button"
ixTest="next"
[disabled]="form.controls.device.invalid"
[disabled]="form.controls.extent.invalid"
>{{ 'Next' | translate }}</button>
} @else {
<button
Expand All @@ -33,32 +55,14 @@
}
</div>
</mat-step>
@if (isNewTarget) {
<mat-step #matStepInstance>
<ng-template matStepLabel>{{ 'Portal' | translate }}</ng-template>
<ix-portal-wizard-step [form]="form.controls.portal"></ix-portal-wizard-step>
<div class="step-buttons">
<button
mat-button
matStepperPrevious
color="accent"
type="button"
ixTest="back"
>{{ 'Back' | translate }}</button>
<button
mat-button
matStepperNext
color="primary"
type="button"
ixTest="next"
[disabled]="form.controls.portal.invalid"
>{{ 'Next' | translate }}</button>
</div>
</mat-step>

@if (isNewTarget) {
<mat-step #matStepInstance>
<ng-template matStepLabel>{{ 'Initiator' | translate }}</ng-template>
<ix-initiator-wizard-step [form]="form.controls.initiator"></ix-initiator-wizard-step>
<ng-template matStepLabel>{{ 'Protocol Options' | translate }}</ng-template>
<ix-protocol-options-wizard-step
[form]="form.controls.options"
[isFibreChannelMode]="isFibreChannelMode"
></ix-protocol-options-wizard-step>
<div class="step-buttons">
<button
mat-button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { provideMockStore } from '@ngrx/store/testing';
import { of } from 'rxjs';
import { mockCall, mockApi } from 'app/core/testing/utils/mock-api.utils';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { LicenseFeature } from 'app/enums/license-feature.enum';
import { ServiceName } from 'app/enums/service-name.enum';
import { ServiceStatus } from 'app/enums/service-status.enum';
import { Dataset } from 'app/interfaces/dataset.interface';
Expand All @@ -18,20 +19,22 @@ import {
IscsiAuthAccess, IscsiExtent, IscsiInitiatorGroup, IscsiPortal, IscsiTarget, IscsiTargetExtent,
} from 'app/interfaces/iscsi.interface';
import { Service } from 'app/interfaces/service.interface';
import { SystemInfo } from 'app/interfaces/system-info.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { IxListHarness } from 'app/modules/forms/ix-forms/components/ix-list/ix-list.harness';
import { IxFormHarness } from 'app/modules/forms/ix-forms/testing/ix-form.harness';
import { OldSlideInRef } from 'app/modules/slide-ins/old-slide-in-ref';
import { SLIDE_IN_DATA } from 'app/modules/slide-ins/slide-in.token';
import { IscsiWizardComponent } from 'app/pages/sharing/iscsi/iscsi-wizard/iscsi-wizard.component';
import { DeviceWizardStepComponent } from 'app/pages/sharing/iscsi/iscsi-wizard/steps/device-wizard-step/device-wizard-step.component';
import { InitiatorWizardStepComponent } from 'app/pages/sharing/iscsi/iscsi-wizard/steps/initiator-wizard-step/initiator-wizard-step.component';
import { PortalWizardStepComponent } from 'app/pages/sharing/iscsi/iscsi-wizard/steps/portal-wizard-step/portal-wizard-step.component';
import { ExtentWizardStepComponent } from 'app/pages/sharing/iscsi/iscsi-wizard/steps/extent-wizard-step/extent-wizard-step.component';
import { ProtocolOptionsWizardStepComponent } from 'app/pages/sharing/iscsi/iscsi-wizard/steps/protocol-options-wizard-step/protocol-options-wizard-step.component';
import { TargetWizardStepComponent } from 'app/pages/sharing/iscsi/iscsi-wizard/steps/target-wizard-step/target-wizard-step.component';
import { OldSlideInService } from 'app/services/old-slide-in.service';
import { ApiService } from 'app/services/websocket/api.service';
import { AppState } from 'app/store';
import { checkIfServiceIsEnabled } from 'app/store/services/services.actions';
import { selectServices } from 'app/store/services/services.selectors';
import { selectSystemInfo } from 'app/store/system-info/system-info.selectors';

describe('IscsiWizardComponent', () => {
let spectator: Spectator<IscsiWizardComponent>;
Expand All @@ -44,9 +47,9 @@ describe('IscsiWizardComponent', () => {
imports: [
ReactiveFormsModule,
MatStepperModule,
DeviceWizardStepComponent,
PortalWizardStepComponent,
InitiatorWizardStepComponent,
TargetWizardStepComponent,
ExtentWizardStepComponent,
ProtocolOptionsWizardStepComponent,
],
providers: [
mockAuth(),
Expand All @@ -55,6 +58,7 @@ describe('IscsiWizardComponent', () => {
confirm: jest.fn(() => of(true)),
}),
mockApi([
mockCall('fc.capable', true),
mockCall('iscsi.global.sessions', [] as IscsiGlobalSession[]),
mockCall('iscsi.extent.query', []),
mockCall('iscsi.target.query', []),
Expand All @@ -75,15 +79,26 @@ describe('IscsiWizardComponent', () => {
mockCall('iscsi.targetextent.create', { id: 16 } as IscsiTargetExtent),
]),
provideMockStore({
selectors: [{
selector: selectServices,
value: [{
service: ServiceName.Iscsi,
id: 4,
enable: false,
state: ServiceStatus.Stopped,
} as Service],
}],
selectors: [
{
selector: selectServices,
value: [{
service: ServiceName.Iscsi,
id: 4,
enable: false,
state: ServiceStatus.Stopped,
} as Service],
},
{
selector: selectSystemInfo,
value: {
version: 'TrueNAS-SCALE-22.12',
license: {
features: [LicenseFeature.FibreChannel],
},
} as SystemInfo,
},
],
}),
mockProvider(OldSlideInRef),
{ provide: SLIDE_IN_DATA, useValue: undefined },
Expand Down Expand Up @@ -124,13 +139,13 @@ describe('IscsiWizardComponent', () => {
await saveButton.click();
tick();

expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(7, 'pool.dataset.create', [{
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(8, 'iscsi.extent.create', [{
expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(9, 'iscsi.extent.create', [{
blocksize: 512,
disk: 'zvol/my+pool/test_zvol',
insecure_tpc: true,
Expand All @@ -140,18 +155,19 @@ describe('IscsiWizardComponent', () => {
xen: false,
}]);

expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(9, 'iscsi.portal.create', [{
expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(10, 'iscsi.portal.create', [{
comment: 'test-name',
listen: [{ ip: '::' }],
}]);

expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(10, 'iscsi.initiator.create', [{
expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(11, 'iscsi.initiator.create', [{
comment: 'test-name',
initiators: ['initiator1', 'initiator2'],
}]);

expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(11, 'iscsi.target.create', [{
expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(12, 'iscsi.target.create', [{
name: 'test-name',
mode: 'ISCSI',
groups: [{
auth: null,
authmethod: 'NONE',
Expand All @@ -160,7 +176,7 @@ describe('IscsiWizardComponent', () => {
}],
}]);

expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(12, 'iscsi.targetextent.create', [{
expect(spectator.inject(ApiService).call).toHaveBeenNthCalledWith(13, 'iscsi.targetextent.create', [{
extent: 11,
target: 15,
}]);
Expand Down
Loading

0 comments on commit 713074e

Please sign in to comment.