import { NvmeSubsystemViewComponent } from './nvme-subsystem-view/nvme-subsystem-view.component';
import { NvmeofSubsystemPerformanceComponent } from './nvmeof-subsystem-performance/nvmeof-subsystem-performance.component';
import { NvmeofTabsComponent } from './nvmeof-tabs/nvmeof-tabs.component';
+import { NvmeofSetupCardsComponent } from './nvmeof-setup-cards/nvmeof-setup-cards.component';
+import { NvmeofGatewayGroupFilterComponent } from './nvmeof-gateway-group-filter/nvmeof-gateway-group-filter.component';
@NgModule({
imports: [
ContainedListModule,
SideNavModule,
LayoutModule,
- ThemeModule
+ ThemeModule,
+ NvmeofSetupCardsComponent,
+ NvmeofGatewayGroupFilterComponent
],
declarations: [
RbdListComponent,
--- /dev/null
+<div class="nvmeof-gateway-group-filter">
+ <span class="nvmeof-gateway-group-filter__label cds--type-body-compact-01"
+ i18n>Gateway group:</span>
+ <cds-combo-box type="single"
+ class="nvmeof-gateway-group-filter__combo"
+ [placeholder]="placeholder"
+ [items]="items"
+ [disabled]="disabled"
+ (selected)="onSelected($event)"
+ (clear)="onClear()">
+ <cds-dropdown-list></cds-dropdown-list>
+ </cds-combo-box>
+</div>
--- /dev/null
+:host {
+ display: contents;
+}
+
+.nvmeof-gateway-group-filter {
+ display: flex;
+ align-items: center;
+ gap: var(--cds-spacing-03);
+ margin-left: var(--cds-spacing-05);
+
+ &__label {
+ white-space: nowrap;
+ color: var(--cds-text-secondary);
+ }
+
+ &__combo {
+ min-inline-size: 14rem;
+ max-inline-size: 20rem;
+ }
+}
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NvmeofGatewayGroupFilterComponent } from './nvmeof-gateway-group-filter.component';
+
+describe('NvmeofGatewayGroupFilterComponent', () => {
+ let fixture: ComponentFixture<NvmeofGatewayGroupFilterComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [NvmeofGatewayGroupFilterComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(NvmeofGatewayGroupFilterComponent);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(fixture.componentInstance).toBeTruthy();
+ });
+
+ it('should render combo box with filter class', () => {
+ const combo = fixture.nativeElement.querySelector('cds-combo-box');
+ expect(combo).toBeTruthy();
+ expect(combo.classList.contains('nvmeof-gateway-group-filter__combo')).toBe(true);
+ });
+});
--- /dev/null
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { ComboBoxModule } from 'carbon-components-angular';
+import { GroupsComboboxItem } from '~/app/shared/api/nvmeof.service';
+
+@Component({
+ selector: 'cd-nvmeof-gateway-group-filter',
+ templateUrl: './nvmeof-gateway-group-filter.component.html',
+ styleUrls: ['./nvmeof-gateway-group-filter.component.scss'],
+ standalone: true,
+ imports: [ComboBoxModule]
+})
+export class NvmeofGatewayGroupFilterComponent {
+ @Input() items: GroupsComboboxItem[] = [];
+ @Input() disabled = false;
+ @Input() placeholder = $localize`Enter group name`;
+
+ @Output() selected = new EventEmitter<GroupsComboboxItem>();
+ @Output() cleared = new EventEmitter<void>();
+
+ onSelected(item: GroupsComboboxItem): void {
+ this.selected.emit(item);
+ }
+
+ onClear(): void {
+ this.cleared.emit();
+ }
+}
-<cd-nvmeof-tabs></cd-nvmeof-tabs>
+<cd-nvmeof-tabs [showSetupCards]="(gatewayGroup$ | async)?.length === 0"></cd-nvmeof-tabs>
<ng-container *ngIf="gatewayGroup$ | async as gateways">
<cd-table
import { Component, OnInit, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
import { Router } from '@angular/router';
import { BehaviorSubject, forkJoin, Observable, of } from 'rxjs';
-import { catchError, map, switchMap } from 'rxjs/operators';
+import { catchError, map, shareReplay, switchMap } from 'rxjs/operators';
import { GatewayGroup, NvmeofService } from '~/app/shared/api/nvmeof.service';
import { HostService } from '~/app/shared/api/host.service';
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
return of([]);
})
)
- )
+ ),
+ shareReplay({ bufferSize: 1, refCount: true })
);
this.checkNodesAvailability();
}
-<cd-nvmeof-tabs></cd-nvmeof-tabs>
-
-<div cdsGrid
- [useCssGrid]="true"
- [narrow]="true"
- [fullWidth]="true">
-<div cdsCol
- [columnNumbers]="{sm: 4, md: 8}">
- <div class="cds-mt-3 form-item"
- cdsRow>
- <cds-combo-box
- type="single"
- label="Selected Gateway Group"
- i18n-label
- [placeholder]="gwGroupPlaceholder"
- [items]="gwGroups"
- (selected)="onGroupSelection($event)"
- (clear)="onGroupClear()"
- [disabled]="gwGroupsEmpty">
- <cds-dropdown-list></cds-dropdown-list>
- </cds-combo-box>
- </div>
-</div>
-</div>
+<cd-nvmeof-tabs [showSetupCards]="(namespaces$ | async)?.length === 0"></cd-nvmeof-tabs>
<ng-container *ngIf="namespaces$ | async as namespaces">
<cd-table [data]="namespaces"
+ [compactSearchField]="true"
columnMode="flex"
(fetchData)="fetchData()"
[columns]="namespacesColumns"
emptyStateMessage="Namespaces are storage volumes mapped to subsystems for host access. Create a namespace to start provisioning storage within a subsystem."
i18n-emptyStateMessage>
+ <div class="table-filter">
+ <cd-nvmeof-gateway-group-filter [items]="gwGroups"
+ [placeholder]="gwGroupPlaceholder"
+ [disabled]="gwGroupsEmpty"
+ (selected)="onGroupSelection($event)"
+ (cleared)="onGroupClear()">
+ </cd-nvmeof-gateway-group-filter>
+ </div>
+
<div class="table-actions">
<cd-table-actions [permission]="permission"
[selection]="selection"
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';
-import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { of } from 'rxjs';
import { take } from 'rxjs/operators';
import { RouterTestingModule } from '@angular/router/testing';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { NvmeofSubsystemsDetailsComponent } from '../nvmeof-subsystems-details/nvmeof-subsystems-details.component';
import { NvmeofNamespacesListComponent } from './nvmeof-namespaces-list.component';
+import { NvmeofGatewayGroupFilterComponent } from '../nvmeof-gateway-group-filter/nvmeof-gateway-group-filter.component';
const mockNamespaces = [
{
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [NvmeofNamespacesListComponent, NvmeofSubsystemsDetailsComponent],
- imports: [HttpClientModule, RouterTestingModule, SharedModule],
+ imports: [
+ HttpClientModule,
+ RouterTestingModule,
+ SharedModule,
+ NvmeofGatewayGroupFilterComponent
+ ],
providers: [
{ provide: NvmeofService, useClass: MockNvmeOfService },
{ provide: AuthStorageService, useClass: MockAuthStorageService },
{ provide: ModalCdsService, useClass: MockModalCdsService },
{ provide: TaskWrapperService, useClass: MockTaskWrapperService }
- ],
- schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ ]
}).compileComponents();
fixture = TestBed.createComponent(NvmeofNamespacesListComponent);
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
-import { catchError, map, switchMap, takeUntil } from 'rxjs/operators';
+import { catchError, map, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
const DEFAULT_PLACEHOLDER = $localize`Enter group name`;
catchError(() => of([]))
);
}),
+ shareReplay({ bufferSize: 1, refCount: true }),
takeUntil(this.destroy$)
);
}
--- /dev/null
+<cd-productive-card class="nvmeof-setup-cards">
+ <ng-template #header>
+ <div cdsStack="vertical"
+ gap="3">
+ <h2 class="cds--type-heading-compact-03"
+ i18n>Recommended first-time setup</h2>
+ <p class="cds--type-body-01"
+ i18n>
+ Start your NVMe over Fabrics configuration by creating the essential resources in sequence.
+ </p>
+ </div>
+ </ng-template>
+
+ <div class="nvmeof-setup-cards__columns">
+
+ <div class="nvmeof-setup-cards__column">
+ <div class="nvmeof-setup-cards__column-content">
+ <div class="nvmeof-setup-cards__step-row">
+ <span class="cds--type-label-01"
+ i18n>1.</span>
+ <span
+ class="cds--type-heading-compact-01"
+ i18n>Create Gateway groups</span>
+ </div>
+ <p class="cds--type-label-01"
+ i18n>
+ Group NVMe gateway nodes to enable high availability and<br>load balancing for storage targets.
+ </p>
+ <div class="nvmeof-setup-cards__info">
+ <cd-icon type="info"></cd-icon>
+ <span class="cds--type-label-01 "
+ i18n>No gateway groups configured for this cluster yet.</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="nvmeof-setup-cards__column">
+ <div class="nvmeof-setup-cards__column-content">
+ <div class="nvmeof-setup-cards__step-row">
+ <span class="cds--type-label-01"
+ i18n>2.</span>
+ <span
+ class="cds--type-heading-compact-01"
+ i18n>Create Subsystems</span>
+ </div>
+ <p class="cds--type-label-01"
+ i18n>
+ Define storage targets by creating NVMe subsystems and <br> configuring security, listeners, and host access.
+ </p>
+ <div class="nvmeof-setup-cards__info">
+ <cd-icon type="info"></cd-icon>
+ <span class="cds--type-label-01"
+ i18n>No subsystem configured for this cluster yet.</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="nvmeof-setup-cards__column">
+ <div class="nvmeof-setup-cards__column-content">
+ <div class="nvmeof-setup-cards__step-row">
+ <span class="cds--type-label-01"
+ i18n>3.</span>
+ <span class="cds--type-heading-compact-01"
+ i18n>Create Namespaces</span>
+ </div>
+ <p class="cds--type-label-01"
+ i18n>
+ Create storage namespaces backed by Ceph block images. This <br>completes your NVMe over Fabrics setup.
+ </p>
+ <div class="nvmeof-setup-cards__info">
+ <cd-icon type="info"></cd-icon>
+ <span class="cds--type-label-01"
+ i18n>No namespace available or mapped yet.</span>
+ </div>
+ </div>
+ </div>
+
+ </div>
+</cd-productive-card>
--- /dev/null
+.nvmeof-setup-cards__columns {
+ display: flex;
+ align-items: stretch;
+ padding-bottom: var(--cds-spacing-05);
+}
+
+.nvmeof-setup-cards__column {
+ flex: 1 1 0;
+ min-width: 0;
+ padding: var(--cds-spacing-03) var(--cds-spacing-05) var(--cds-spacing-05);
+ display: flex;
+ flex-direction: column;
+}
+
+.nvmeof-setup-cards__column-content {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ gap: var(--cds-spacing-03);
+}
+
+.nvmeof-setup-cards__step-row {
+ display: flex;
+ align-items: center;
+ gap: var(--cds-spacing-03);
+}
+
+.nvmeof-setup-cards__info {
+ display: flex;
+ align-items: center;
+ margin-top: auto;
+ gap: var(--cds-spacing-03);
+}
+
+:host ::ng-deep .nvmeof-setup-cards {
+ &.productive-card .productive-card-header {
+ padding: var(--cds-spacing-03) var(--cds-spacing-05);
+ border-bottom: 1px solid var(--cds-border-subtle);
+ }
+
+ &.productive-card .productive-card-section {
+ padding: 0;
+ }
+
+ .productive-card-section {
+ padding: var(--cds-spacing-02);
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { Component } from '@angular/core';
+import { RouterModule } from '@angular/router';
+import { LayoutModule, LayerModule, LinkModule, TilesModule } from 'carbon-components-angular';
+import { ProductiveCardComponent } from '~/app/shared/components/productive-card/productive-card.component';
+import { ComponentsModule } from '~/app/shared/components/components.module';
+
+@Component({
+ selector: 'cd-nvmeof-setup-cards',
+ templateUrl: './nvmeof-setup-cards.component.html',
+ styleUrl: './nvmeof-setup-cards.component.scss',
+ standalone: true,
+ imports: [
+ CommonModule,
+ RouterModule,
+ LayoutModule,
+ LayerModule,
+ TilesModule,
+ LinkModule,
+ ProductiveCardComponent,
+ ComponentsModule
+ ]
+})
+export class NvmeofSetupCardsComponent {}
-<cd-nvmeof-tabs></cd-nvmeof-tabs>
+<cd-nvmeof-tabs [showSetupCards]="(subsystems$ | async)?.length === 0"></cd-nvmeof-tabs>
-<div cdsGrid
- [useCssGrid]="true"
- [narrow]="true"
- [fullWidth]="true">
-<div cdsCol
- [columnNumbers]="{sm: 4, md: 8}">
- <div class="cds-mt-3 form-item"
- cdsRow>
- <cds-combo-box
- type="single"
- label="Selected Gateway Group"
- i18n-label
- [placeholder]="gwGroupPlaceholder"
- [items]="gwGroups"
- (selected)="onGroupSelection($event)"
- (clear)="onGroupClear()"
- [disabled]="gwGroupsEmpty">
- <cds-dropdown-list></cds-dropdown-list>
- </cds-combo-box>
- </div>
-</div>
-</div>
<ng-container *ngIf="subsystems$ | async as subsystems">
<cd-table #table
[data]="subsystems"
[columns]="subsystemsColumns"
+ [compactSearchField]="true"
columnMode="flex"
selectionType="single"
(updateSelection)="updateSelection($event)"
emptyStateMessage="Subsystems group NVMe namespaces and manage host access. Create a subsystem to start mapping NVMe volumes to hosts."
i18n-emptyStateMessage>
+ <div class="table-filter">
+ <cd-nvmeof-gateway-group-filter [items]="gwGroups"
+ [placeholder]="gwGroupPlaceholder"
+ [disabled]="gwGroupsEmpty"
+ (selected)="onGroupSelection($event)"
+ (cleared)="onGroupClear()">
+ </cd-nvmeof-gateway-group-filter>
+ </div>
+
<div class="table-actions">
<cd-table-actions [permission]="permissions.nvmeof"
[selection]="selection"
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { NvmeofSubsystemsComponent } from './nvmeof-subsystems.component';
import { NvmeofSubsystemsDetailsComponent } from '../nvmeof-subsystems-details/nvmeof-subsystems-details.component';
-import { ComboBoxModule, GridModule } from 'carbon-components-angular';
+import { NvmeofGatewayGroupFilterComponent } from '../nvmeof-gateway-group-filter/nvmeof-gateway-group-filter.component';
import { CephServiceSpec } from '~/app/shared/models/service.interface';
const mockSubsystems = [
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [NvmeofSubsystemsComponent, NvmeofSubsystemsDetailsComponent],
- imports: [HttpClientModule, RouterTestingModule, SharedModule, ComboBoxModule, GridModule],
+ imports: [
+ HttpClientModule,
+ RouterTestingModule,
+ SharedModule,
+ NvmeofGatewayGroupFilterComponent
+ ],
providers: [
{ provide: NvmeofService, useClass: MockNvmeOfService },
{ provide: AuthStorageService, useClass: MockAuthStorageService },
import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
import { CephServiceSpec } from '~/app/shared/models/service.interface';
import { BehaviorSubject, forkJoin, Observable, of, Subject } from 'rxjs';
-import { catchError, map, switchMap, takeUntil, tap } from 'rxjs/operators';
+import { catchError, map, shareReplay, switchMap, takeUntil, tap } from 'rxjs/operators';
import { DeletionImpact } from '~/app/shared/enum/delete-confirmation-modal-impact.enum';
const BASE_URL = 'block/nvmeof/subsystems';
tap((subs) => {
this.subsystems = subs;
}),
+ shareReplay({ bufferSize: 1, refCount: true }),
takeUntil(this.destroy$)
);
}
<fieldset>
- <legend>
+ <legend class="cds-mb-5">
<h1 class="cds--type-heading-03">NVMe over Fabrics (TCP)</h1>
- <cd-help-text>Monitor and manage NVMe-over-TCP resources for high-performance block storage.</cd-help-text>
+ <cd-help-text>Monitor and manage NVMe-over-TCP resources for high-<br>performance block storage.</cd-help-text>
</legend>
</fieldset>
-<section>
+@if (showSetupCards) {
+<cd-nvmeof-setup-cards></cd-nvmeof-setup-cards>
+}
+<section class="cds-mt-5">
<cds-tabs type="contained"
followFocus="true"
isNavigation="true"
-import { Component, OnInit } from '@angular/core';
+import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
const NVMEOF_PATH = 'block/nvmeof';
standalone: false
})
export class NvmeofTabsComponent implements OnInit {
+ @Input() showSetupCards = false;
+
selectedTab: TABS;
activeTab: TABS = TABS.gateways;
</cds-table-toolbar-actions>
<!-- end batch actions -->
<cds-table-toolbar-content>
+ <!-- gateway group / custom filter slot -->
+ <ng-content select=".table-filter"></ng-content>
+ <!-- end custom filter slot -->
<!-- search -->
<cds-table-toolbar-search *ngIf="searchField"
[expandable]="false"
// Display search field inside tool header?
@Input()
searchField? = true;
+ // Limit toolbar search width (e.g. when a prefix filter is shown).
+ @Input()
+ compactSearchField? = false;
// Display the table header?
@Input()
header? = true;