[narrow]="true"
[fullWidth]="true">
<div cdsCol
- [columnNumbers]="{sm: 4, md: 8}">
+ [columnNumbers]="{ sm: 4, md: 8 }">
<div cdsRow
class="form-heading form-item cds-mt-5">
- <cd-help-text [formAllFieldsRequired]="true"></cd-help-text>
+ <cd-help-text [formAllFieldsRequired]="true"></cd-help-text>
</div>
<div cdsRow
class="form-item">
autofocus
formControlName="groupName"
cdRequiredField="Gateway group name"
- [invalid]="groupName.isInvalid"
- />
+ [invalid]="groupName.isInvalid" />
</cds-text-label>
<span
class="invalid-feedback"
class="invalid-feedback"
*ngIf="groupForm.get('groupName')?.hasError('invalidChars') && (groupForm.get('groupName')?.dirty || groupForm.get('groupName')?.touched)"
i18n>Special characters are not allowed.</span>
-
</div>
</div>
- <!-- CDS Checkbox -->
<div cdsRow
class="form-item">
<div cdsCol>
<cds-checkbox id="enableCds"
- formControlName="enableEncryption"
+ formControlName="enable_auth"
i18n>Enable encryption
</cds-checkbox>
</div>
</div>
- <!-- Encryption Configuration (shown when checkbox is checked) -->
- @if (groupForm.controls.enableEncryption.value) {
+ @if (groupForm.controls.enable_auth.value) {
<div cdsRow
class="form-item">
<div cdsCol>
cdValidate
#encryptionConfigRef="cdValidate"
[invalid]="encryptionConfigRef.isInvalid"
- formControlName="encryptionConfig"
+ formControlName="encryptionKey"
cols="100"
- rows="4">
- </textarea>
+ rows="4"></textarea>
</cds-textarea-label>
<span
class="invalid-feedback"
- *ngIf="groupForm.showError('encryptionConfig', formDir, 'required')"
+ *ngIf="groupForm.showError('encryptionKey', formDir, 'required')"
i18n>This field is required.</span>
</div>
</div>
}
- <!-- Target Nodes Selection -->
- <div
- cdsRow
- class="form-item"
- >
- <div cdsCol>
- <h1 class="cds--type-heading-02">Select target nodes</h1>
- <cd-help-text>
- <span i18n>
- Gateway nodes to run NVMe-oF target pods/services
- </span>
- </cd-help-text>
- </div>
- <div
- cdsCol
- class="cds-pt-3 cds-pb-3"
- >
- <cd-nvmeof-gateway-node
- (hostsLoaded)="onHostsLoaded($event)"
- ></cd-nvmeof-gateway-node>
- </div>
+
+ <div cdsRow
+ class="form-item">
+ <div cdsCol>
+ <h1 class="cds--type-heading-02">Select target nodes</h1>
+ <cd-help-text>
+ <span i18n>
+ Gateway nodes to run NVMe-oF target pods/services
+ </span>
+ </cd-help-text>
+ </div>
+ <div cdsCol
+ class="cds-pt-3 cds-pb-3">
+ <cd-nvmeof-gateway-node
+ (hostsLoaded)="onHostsLoaded($event)"></cd-nvmeof-gateway-node>
+ </div>
</div>
<div cdsRow class="form-item cds-mb-0">
<div cdsCol>
<div cdsRow class="form-item">
<div cdsCol>
<div class="cds--stack cds--stack-horizontal">
- <label class="cds--type-label-01"
+ <label class="cds--type-label-01"
i18n>Enable encryption</label>
<cds-tag type="green"
size="sm"
i18n>Recommended</cds-tag>
</div>
- <cds-checkbox id="enableEncryption"
+ <cds-checkbox id="enableEncryption"
formControlName="enableEncryption">
<span
class="cds--type-body-01"
</div>
@if (!groupForm.controls.enableEncryption.value) {
- <div cdsRow
+ <div cdsRow
class="cds-mt-2">
<div cdsCol>
<cd-alert-panel type="warning"
spacingClass="mb-0"
i18n>
- <span class="cds--type-heading-01">Encryption is required if you plan to use DH-CHAP or mTLS authentication.</span>
+ <span class="cds--type-heading-01">Encryption is required if you plan to use DH-CHAP or mTLS authentication.</span>
</cd-alert-panel>
</div>
</div>
@if (groupForm.controls.enableMtls.value) {
<div cdsRow class="form-item">
<div cdsCol>
- <label class="cds--label fw-bold"
+ <label class="cds--label fw-bold"
i18n>Choose Certificate Authority</label>
<cds-radio-group
formControlName="certificateType"
[form]="groupForm"
[submitText]="(action | titlecase) + ' ' + (resource)"
[disabled]="isCreateDisabled"
- wrappingClass="text-right"
- >
+ wrappingClass="text-right">
</cd-form-button-panel>
</div>
</div>
<cds-tile *ngIf="subsystem">
- <h4 class="cds--type-heading-03 tile-title"
- i18n>Subsystem details</h4>
-
- <div class="details-grid">
- <div class="detail-item">
- <span class="cds--type-label-01"
- i18n>Serial number</span>
- <span class="cds--type-body-compact-01">{{ subsystem.serial_number }}</span>
- </div>
- <div class="detail-item">
- <span class="cds--type-label-01"
- i18n>Model Number</span>
- <span class="cds--type-body-compact-01">{{ subsystem.model_number }}</span>
- </div>
- <div class="detail-item">
- <span class="cds--type-label-01"
- i18n>Gateway group</span>
- <span class="cds--type-body-compact-01">{{ subsystem.gw_group || groupName }}</span>
+ <div [cdsStack]="'vertical'"
+ [gap]="6">
+ <div cdsGrid
+ [useCssGrid]="true"
+ [condensed]="true"
+ [fullWidth]="true">
+ <div cdsCol
+ [columnNumbers]="{ sm: 4, md: 8, lg: 12 }">
+ <h3 class="cds--type-heading-03"
+ i18n>Subsystem details</h3>
+ </div>
</div>
- <div class="detail-item">
- <span class="cds--type-label-01"
- i18n>Subsystem Type</span>
- <span class="cds--type-body-compact-01">{{ subsystem.subtype }}</span>
- </div>
- <div class="detail-item">
- <span class="cds--type-label-01"
- i18n>HA Enabled</span>
- <span class="cds--type-body-compact-01">{{ subsystem.enable_ha ? 'Yes' : 'No' }}</span>
- </div>
- <div class="detail-item">
- <span class="cds--type-label-01"
- i18n>Hosts allowed</span>
- <span class="cds--type-body-compact-01">{{ subsystem.allow_any_host ? 'Any host' : 'Restricted' }}</span>
- </div>
+ @for (row of getRows(); track row) {
+ <div cdsGrid
+ [useCssGrid]="true"
+ [condensed]="true"
+ [fullWidth]="true">
- <div class="detail-item">
- <span class="cds--type-label-01"
- i18n>Maximum Controller Identifier</span>
- <span class="cds--type-body-compact-01">{{ subsystem.max_cntlid }}</span>
- </div>
- <div class="detail-item">
- <span class="cds--type-label-01"
- i18n>Minimum Controller Identifier</span>
- <span class="cds--type-body-compact-01">{{ subsystem.min_cntlid }}</span>
- </div>
- <div class="detail-item"></div>
+ @for (detail of getDetailsForRow(row); track detail.label) {
+ @if (detail.type === 'auth') {
+ <div cdsCol
+ [columnNumbers]="{ sm: 4, md: 4, lg: 4 }"
+ [cdsStack]="'vertical'"
+ [gap]="2">
+ <span class="cds--type-label-01"
+ i18n>Authentication</span>
+ <div>
+ <cd-icon [type]="getStatusIcon(detail)"></cd-icon>
+ <span class="cds--type-label-02">
+ {{ detail.value ? 'Bi-directional' : 'No authentication' }}
+ </span>
+ <a cdsLink
+ [routerLink]="['../hosts']"
+ [queryParams]="{ group: groupName }"
+ [cdsStack]="'horizontal'"
+ [gap]="1">
+ <span i18n>Edit</span>
+ <cd-icon type="edit"></cd-icon>
+ </a>
+ </div>
+ </div>
+ }
- <div class="detail-item">
- <span class="cds--type-label-01"
- i18n>Namespaces</span>
- <span class="cds--type-body-compact-01">{{ subsystem.namespace_count }}</span>
- </div>
- <div class="detail-item">
- <span class="cds--type-label-01"
- i18n>Maximum allowed namespaces</span>
- <span class="cds--type-body-compact-01">{{ subsystem.max_namespaces }}</span>
+ @else if (detail.type === 'listeners') {
+ <div cdsCol
+ [columnNumbers]="{ sm: 4, md: 4, lg: 4 }"
+ [cdsStack]="'vertical'"
+ [gap]="2">
+ <div>
+ <span class="cds--type-label-01">{{ detail.label }}</span>
+ @if (detail.tooltip) {
+ <cd-icon type="infoCircle"
+ class="cds-ml-2"
+ [ngbTooltip]="detail.tooltip"></cd-icon>
+ }
+ </div>
+ <span class="cds--type-label-02">{{ detail.value }}</span>
+ </div>
+ }
+
+ @else if (detail.type === 'host-access') {
+ <div cdsCol
+ [columnNumbers]="getColNumbers(detail)"
+ [cdsStack]="'vertical'"
+ [gap]="2">
+ <span class="cds--type-label-01">{{ detail.label }}</span>
+ <div class="cds--type-label-02">
+ <span>{{ detail.value ? 'Allow all hosts' : 'Restrict to specific hosts' }}</span>
+ <a cdsLink
+ [routerLink]="['../hosts']"
+ [queryParams]="{ group: groupName }"
+ [cdsStack]="'horizontal'"
+ class="cds-ml-2"
+ [gap]="1">
+ <span i18n>Edit</span>
+ <cd-icon type="edit"></cd-icon>
+ </a>
+ </div>
+ </div>
+ }
+
+ <!-- Default: plain text row -->
+ @else {
+ <div cdsCol
+ [columnNumbers]="getColNumbers(detail)"
+ [cdsStack]="'vertical'"
+ [gap]="2">
+ <span class="cds--type-label-01">{{ detail.label }}</span>
+ <span class="cds--type-label-02">{{ getDisplayValue(detail.value) }}</span>
+ </div>
+ }
+
+ }
+
+ @if (getDetailsForRow(row).length < 3 && !isFullWidthRow(row)) {
+ @for (filler of getFillerCount(row); track filler) {
+ <div cdsCol
+ [columnNumbers]="{ sm: 4, md: 4, lg: 4 }"></div>
+ }
+ }
</div>
- <div class="detail-item"></div>
+ }
</div>
</cds-tile>
@use '@carbon/layout';
-
-.tile-title {
- margin-bottom: layout.$spacing-06;
-}
-
-.details-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- row-gap: layout.$spacing-06;
- column-gap: layout.$spacing-07;
-}
-
-.detail-item {
- display: flex;
- flex-direction: column;
- gap: layout.$spacing-02;
-}
enable_ha: true,
allow_any_host: true,
gw_group: 'gateway-prod',
- psk: 'some-key'
+ has_dhchap_key: true,
+ network_mask: []
};
beforeEach(async () => {
expect(labelTexts).toContain('Serial number');
expect(labelTexts).toContain('Model Number');
expect(labelTexts).toContain('Gateway group');
+ expect(labelTexts).toContain('Host access');
+ expect(labelTexts).toContain('Authentication');
+ expect(labelTexts).toContain('Listeners');
expect(labelTexts).toContain('Maximum Controller Identifier');
expect(labelTexts).toContain('Minimum Controller Identifier');
expect(labelTexts).toContain('Namespaces');
expect(labelTexts).toContain('Maximum allowed namespaces');
}));
- it('should not display MTLS label in overview details', fakeAsync(() => {
+ it('should display host access and auth state from subsystem data', fakeAsync(() => {
component.ngOnInit();
tick();
fixture.detectChanges();
- const values = fixture.nativeElement.querySelectorAll('.cds--type-body-compact-01');
- const valueTexts = Array.from(values).map((el: HTMLElement) => el.textContent.trim());
- expect(valueTexts).not.toContain('MTLS');
- }));
-
- it('should display hosts allowed from subsystem data', fakeAsync(() => {
- component.ngOnInit();
- tick();
- fixture.detectChanges();
-
- const values = fixture.nativeElement.querySelectorAll('.cds--type-body-compact-01');
- const valueTexts = Array.from(values).map((el: HTMLElement) => el.textContent.trim());
- expect(valueTexts).toContain('Any host');
- }));
-
- it('should not render Edit link for Hosts allowed', fakeAsync(() => {
- component.ngOnInit();
- tick();
- fixture.detectChanges();
-
- const editLink = fixture.nativeElement.querySelector('a[cdsLink]');
- expect(editLink).toBeFalsy();
+ const hostAccessText = fixture.nativeElement.textContent;
+ expect(hostAccessText).toContain('Allow all hosts');
+ expect(hostAccessText).toContain('Bi-directional');
+ expect(hostAccessText).toContain('Edit');
}));
});
import { ActivatedRoute } from '@angular/router';
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
import { NvmeofSubsystem } from '~/app/shared/models/nvmeof';
+import { ICON_TYPE } from '~/app/shared/enum/icons.enum';
+
+export interface SubsystemDetail {
+ label: string;
+ value: string | number | boolean;
+ type: 'text' | 'host-access' | 'auth' | 'listeners';
+ tooltip?: string;
+ row: number;
+}
@Component({
selector: 'cd-nvmeof-subsystem-overview',
standalone: false
})
export class NvmeofSubsystemOverviewComponent implements OnInit {
- subsystemNQN: string;
- groupName: string;
- subsystem: NvmeofSubsystem;
+ subsystemNQN!: string;
+ groupName!: string;
+ subsystem!: NvmeofSubsystem;
+ details: SubsystemDetail[] = [];
constructor(private route: ActivatedRoute, private nvmeofService: NvmeofService) {}
}
fetchSubsystem() {
- this.nvmeofService
- .getSubsystem(this.subsystemNQN, this.groupName)
- .subscribe((subsystem: NvmeofSubsystem) => {
- this.subsystem = subsystem;
- });
+ this.nvmeofService.getSubsystem(this.subsystemNQN, this.groupName).subscribe((subsystem) => {
+ this.subsystem = subsystem as NvmeofSubsystem;
+ this.buildDetails();
+ });
+ }
+
+ private buildDetails() {
+ this.details = [
+ {
+ label: $localize`Serial number`,
+ value: this.subsystem.serial_number,
+ type: 'text',
+ row: 1
+ },
+ { label: $localize`Model Number`, value: this.subsystem.model_number, type: 'text', row: 1 },
+ {
+ label: $localize`Gateway group`,
+ value: this.subsystem.gw_group || this.groupName,
+ type: 'text',
+ row: 1
+ },
+ {
+ label: $localize`Host access`,
+ value: this.subsystem.allow_any_host ?? false,
+ type: 'host-access',
+ row: 2
+ },
+ {
+ label: $localize`Authentication`,
+ value: this.subsystem.has_dhchap_key,
+ type: 'auth',
+ row: 2
+ },
+ {
+ label: $localize`Listeners`,
+ value:
+ (this.subsystem.network_mask?.length ?? 0) > 0
+ ? $localize`Auto-fetched`
+ : $localize`Manually selected`,
+ type: 'listeners',
+ tooltip: $localize`Listeners are automatically fetched from the gateway`,
+ row: 3
+ },
+ {
+ label: $localize`Maximum Controller Identifier`,
+ value: this.subsystem.max_cntlid,
+ type: 'text',
+ row: 3
+ },
+ {
+ label: $localize`Minimum Controller Identifier`,
+ value: this.subsystem.min_cntlid,
+ type: 'text',
+ row: 3
+ },
+ { label: $localize`Namespaces`, value: this.subsystem.namespace_count, type: 'text', row: 4 },
+ {
+ label: $localize`Maximum allowed namespaces`,
+ value: this.subsystem.max_namespaces,
+ type: 'text',
+ row: 4
+ }
+ ];
+ }
+
+ getRows(): number[] {
+ return [...new Set(this.details.map((d) => d.row))];
+ }
+
+ getDetailsForRow(row: number): SubsystemDetail[] {
+ return this.details.filter((d) => d.row === row);
+ }
+
+ getDisplayValue(value: string | number | boolean): string {
+ if (typeof value === 'boolean') {
+ return value ? $localize`Enabled` : $localize`Disabled`;
+ }
+ return String(value);
+ }
+
+ getStatusIcon(detail: SubsystemDetail): keyof typeof ICON_TYPE {
+ return detail.value ? 'success' : 'error';
+ }
+
+ getColNumbers(detail: SubsystemDetail): { sm: number; md: number; lg: number } {
+ return detail.type === 'auth' ? { sm: 4, md: 8, lg: 12 } : { sm: 4, md: 4, lg: 4 };
+ }
+
+ isFullWidthRow(row: number): boolean {
+ return this.getDetailsForRow(row).some((d) => d.type === 'auth');
+ }
+
+ getFillerCount(row: number): number[] {
+ const needed = 3 - this.getDetailsForRow(row).length;
+ return Array.from({ length: needed });
}
}
gw_group?: string;
initiator_count?: number;
has_dhchap_key: boolean;
+ network_mask?: string[];
}
export interface NvmeofSubsystemData extends NvmeofSubsystem {