import { NvmeofGatewayNodeMode } from '~/app/shared/enum/nvmeof.enum';
import { NvmeofGatewayNodeAddModalComponent } from './nvmeof-gateway-node/nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component';
import { NvmeofSubsystemNamespacesListComponent } from './nvmeof-subsystem-namespaces-list/nvmeof-subsystem-namespaces-list.component';
+import { NvmeofSubsystemOverviewComponent } from './nvmeof-subsystem-overview/nvmeof-subsystem-overview.component';
import { NvmeSubsystemViewBreadcrumbResolver } from './nvme-subsystem-view/nvme-subsystem-view-breadcrumb.resolver';
import { NvmeSubsystemViewComponent } from './nvme-subsystem-view/nvme-subsystem-view.component';
+import { NvmeofSubsystemPerformanceComponent } from './nvmeof-subsystem-performance/nvmeof-subsystem-performance.component';
@NgModule({
imports: [
NvmeofGatewayNodeAddModalComponent,
NvmeofNamespaceExpandModalComponent,
NvmeSubsystemViewComponent,
- NvmeofEditHostKeyModalComponent
+ NvmeofEditHostKeyModalComponent,
+ NvmeofSubsystemOverviewComponent,
+ NvmeofSubsystemPerformanceComponent
],
exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
{
path: 'gateways',
component: NvmeofGatewayComponent,
- data: { breadcrumbs: 'Gateways' },
children: [
{
path: `${URLVerbs.EDIT}/:subsystem_nqn/namespace/:nsid`,
component: NvmeSubsystemViewComponent,
data: { breadcrumbs: NvmeSubsystemViewBreadcrumbResolver },
children: [
- { path: '', redirectTo: 'namespaces', pathMatch: 'full' },
+ { path: '', redirectTo: 'overview', pathMatch: 'full' },
+ {
+ path: 'overview',
+ component: NvmeofSubsystemOverviewComponent
+ },
{
path: 'hosts',
component: NvmeofInitiatorsListComponent
{
path: 'listeners',
component: NvmeofListenersListComponent
+ },
+ {
+ path: 'performance',
+ component: NvmeofSubsystemPerformanceComponent
}
]
}
private buildSidebarItems() {
const extras = { queryParams: { group: this.groupName } };
this.sidebarItems = [
+ {
+ label: $localize`Overview`,
+ route: [this.basePath, this.subsystemNQN, 'overview'],
+ routeExtras: extras
+ },
{
label: $localize`Initiators`,
route: [this.basePath, this.subsystemNQN, 'hosts'],
label: $localize`Listeners`,
route: [this.basePath, this.subsystemNQN, 'listeners'],
routeExtras: extras
+ },
+ {
+ label: $localize`Performance`,
+ route: [this.basePath, this.subsystemNQN, 'performance'],
+ routeExtras: extras
}
];
}
[maxLimit]="25"
identifier="hostname"
forceIdentifier="true"
- [autoReload]="false"
+ [autoReload]="true"
(updateSelection)="updateSelection($event)"
emptyStateTitle="No nodes available"
i18n-emptyStateTitle
<cd-table
[data]="subsystems"
[columns]="columns"
+ [autoReload]="true"
columnMode="flex"
selectionType="none"
identifier="nqn"
max_namespaces: 256,
namespace_count: 0,
subtype: 'NVMe',
- namespaces: []
+ namespaces: [],
+ has_dhchap_key: true
} as NvmeofSubsystem,
{
nqn: 'nqn.2014-08.org.nvmexpress:uuid:2222',
max_namespaces: 256,
namespace_count: 0,
subtype: 'NVMe',
- namespaces: []
+ namespaces: [],
+ has_dhchap_key: true
} as NvmeofSubsystem
];
isNavigation="true"
[cacheActive]="false">
<cds-tab
- heading="Gateway groups"
+ heading="Gateways"
[tabContent]="gateways_content"
i18n-heading
[active]="activeTab === Tabs.gateways"
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
import { NvmeofGatewayComponent } from './nvmeof-gateway.component';
import { SharedModule } from '~/app/shared/shared.module';
import { ComboBoxModule, GridModule, TabsModule } from 'carbon-components-angular';
import { of } from 'rxjs';
+import { BreadcrumbService } from '~/app/shared/services/breadcrumb.service';
describe('NvmeofGatewayComponent', () => {
let component: NvmeofGatewayComponent;
let fixture: ComponentFixture<NvmeofGatewayComponent>;
+ let breadcrumbService: BreadcrumbService;
+ let router: Router;
beforeEach(async () => {
await TestBed.configureTestingModule({
TabsModule
],
providers: [
+ BreadcrumbService,
{
provide: ActivatedRoute,
useValue: {
fixture = TestBed.createComponent(NvmeofGatewayComponent);
component = fixture.componentInstance;
+ breadcrumbService = TestBed.inject(BreadcrumbService);
+ router = TestBed.inject(Router);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
+
+ it('should set tab crumb on init', () => {
+ spyOn(breadcrumbService, 'setTabCrumb');
+ component.ngOnInit();
+ expect(breadcrumbService.setTabCrumb).toHaveBeenCalledWith('Gateways');
+ });
+
+ it('should update tab crumb on tab switch', () => {
+ spyOn(router, 'navigate');
+ spyOn(breadcrumbService, 'setTabCrumb');
+ component.onSelected(component.Tabs.subsystem);
+ expect(router.navigate).toHaveBeenCalledWith([], {
+ relativeTo: TestBed.inject(ActivatedRoute),
+ queryParams: { tab: component.Tabs.subsystem },
+ queryParamsHandling: 'merge'
+ });
+ expect(breadcrumbService.setTabCrumb).toHaveBeenCalledWith('Subsystem');
+ });
+
+ it('should clear tab crumb on destroy', () => {
+ spyOn(breadcrumbService, 'clearTabCrumb');
+ component.ngOnDestroy();
+ expect(breadcrumbService.clearTabCrumb).toHaveBeenCalled();
+ });
});
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
+import { Subject } from 'rxjs';
+import { filter, takeUntil } from 'rxjs/operators';
import _ from 'lodash';
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { BreadcrumbService } from '~/app/shared/services/breadcrumb.service';
enum TABS {
gateways = 'gateways',
namespace = 'namespace'
}
+const TAB_LABELS: Record<TABS, string> = {
+ [TABS.gateways]: $localize`Gateways`,
+ [TABS.subsystem]: $localize`Subsystem`,
+ [TABS.namespace]: $localize`Namespace`
+};
+
@Component({
selector: 'cd-nvmeof-gateway',
templateUrl: './nvmeof-gateway.component.html',
styleUrls: ['./nvmeof-gateway.component.scss'],
standalone: false
})
-export class NvmeofGatewayComponent implements OnInit {
+export class NvmeofGatewayComponent implements OnInit, OnDestroy {
selectedTab: TABS;
activeTab: TABS = TABS.gateways;
+ private readonly destroy$ = new Subject<void>();
@ViewChild('statusTpl', { static: true })
statusTpl: TemplateRef<any>;
selection = new CdTableSelection();
- constructor(public actionLabels: ActionLabelsI18n, private route: ActivatedRoute) {}
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ private route: ActivatedRoute,
+ private router: Router,
+ private breadcrumbService: BreadcrumbService
+ ) {}
ngOnInit() {
- this.route.queryParams.subscribe((params) => {
+ this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
if (params['tab'] && Object.values(TABS).includes(params['tab'])) {
this.activeTab = params['tab'] as TABS;
}
+ this.breadcrumbService.setTabCrumb(TAB_LABELS[this.activeTab]);
});
+
+ this.router.events
+ .pipe(
+ filter((event) => event instanceof NavigationEnd),
+ takeUntil(this.destroy$)
+ )
+ .subscribe(() => {
+ // Run after NavigationEnd handlers so tab crumb is not cleared by global breadcrumb reset.
+ setTimeout(() => this.breadcrumbService.setTabCrumb(TAB_LABELS[this.activeTab]));
+ });
+ }
+
+ ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ this.breadcrumbService.clearTabCrumb();
}
onSelected(tab: TABS) {
this.selectedTab = tab;
this.activeTab = tab;
+ this.router.navigate([], {
+ relativeTo: this.route,
+ queryParams: { tab },
+ queryParamsHandling: 'merge'
+ });
+ this.breadcrumbService.setTabCrumb(TAB_LABELS[tab]);
}
public get Tabs(): typeof TABS {
(fetchData)="listInitiators()"
[columns]="initiatorColumns"
selectionType="multiClick"
+ [autoReload]="false"
(updateSelection)="updateSelection($event)">
<div class="table-actions">
<cd-table-actions [permission]="permission"
const mockInitiators = [
{
nqn: '*',
- dhchap_key: ''
+ use_dhchap: ''
}
];
const mockSubsystem = {
nqn: 'nqn.2016-06.io.spdk:cnode1',
serial_number: '12345',
- psk: ''
+ has_dhchap_key: false
};
class MockNvmeOfService {
component.subsystemNQN = 'nqn.2016-06.io.spdk:cnode1';
component.group = 'group1';
component.ngOnInit();
- fixture.detectChanges();
});
it('should create', () => {
}));
it('should update authStatus when initiator has dhchap_key', fakeAsync(() => {
- const initiatorsWithKey = [{ nqn: 'nqn1', dhchap_key: 'key1' }];
+ const initiatorsWithKey = [{ nqn: 'nqn1', use_dhchap: 'key1' }];
spyOn(TestBed.inject(NvmeofService), 'getInitiators').and.returnValue(of(initiatorsWithKey));
component.listInitiators();
tick();
}));
it('should update authStatus when subsystem has psk', fakeAsync(() => {
- const subsystemWithPsk = { ...mockSubsystem, psk: 'psk1' };
+ const subsystemWithPsk = { ...mockSubsystem, has_dhchap_key: true };
+ component.initiators = [{ nqn: 'nqn1', use_dhchap: 'key1' }];
spyOn(TestBed.inject(NvmeofService), 'getSubsystem').and.returnValue(of(subsystemWithPsk));
component.getSubsystem();
tick();
(fetchData)="listListeners()"
[columns]="listenerColumns"
identifier="id"
+ [autoReload]="true"
forceIdentifier="true"
selectionType="single"
(updateSelection)="updateSelection($event)">
<cd-table [data]="namespaces"
columnMode="flex"
(fetchData)="fetchData()"
+ [autoReload]="false"
[columns]="namespacesColumns"
selectionType="single"
(updateSelection)="updateSelection($event)"
}
updateGroupSelectionState() {
- if (!this.group && this.gwGroups.length) {
- this.onGroupSelection(this.gwGroups[0]);
+ if (this.gwGroups.length) {
+ if (!this.group) {
+ this.onGroupSelection(this.gwGroups[0]);
+ } else {
+ this.gwGroups = this.gwGroups.map((g) => ({
+ ...g,
+ selected: g.content === this.group
+ }));
+ }
this.gwGroupsEmpty = false;
this.gwGroupPlaceholder = DEFAULT_PLACEHOLDER;
- } else if (!this.gwGroups.length) {
+ } else {
this.gwGroupsEmpty = true;
this.gwGroupPlaceholder = $localize`No groups available`;
}
<cd-table [data]="namespaces"
columnMode="flex"
(fetchData)="listNamespaces()"
+ [autoReload]="true"
[columns]="namespacesColumns"
selectionType="single"
(updateSelection)="updateSelection($event)"
--- /dev/null
+<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>
+
+ <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>
+
+ <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>
+
+ <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>
+ </div>
+ <div class="detail-item"></div>
+ </div>
+</cds-tile>
--- /dev/null
+@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;
+}
--- /dev/null
+import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ActivatedRoute } from '@angular/router';
+import { of } from 'rxjs';
+
+import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { GridModule, TilesModule } from 'carbon-components-angular';
+
+import { NvmeofSubsystemOverviewComponent } from './nvmeof-subsystem-overview.component';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('NvmeofSubsystemOverviewComponent', () => {
+ let component: NvmeofSubsystemOverviewComponent;
+ let fixture: ComponentFixture<NvmeofSubsystemOverviewComponent>;
+ let nvmeofService: NvmeofService;
+
+ const mockSubsystem = {
+ nqn: 'nqn.2016-06.io.spdk:cnode1',
+ serial_number: 'Ceph30487186726692',
+ model_number: 'Ceph bdev Controller',
+ min_cntlid: 1,
+ max_cntlid: 2040,
+ subtype: 'NVMe',
+ namespace_count: 3,
+ max_namespaces: 256,
+ enable_ha: true,
+ allow_any_host: true,
+ gw_group: 'gateway-prod',
+ psk: 'some-key'
+ };
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [NvmeofSubsystemOverviewComponent],
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ SharedModule,
+ NgbTooltipModule,
+ TilesModule,
+ GridModule
+ ],
+ providers: [
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ parent: {
+ params: of({ subsystem_nqn: 'nqn.2016-06.io.spdk:cnode1' })
+ },
+ queryParams: of({ group: 'group1' })
+ }
+ },
+ {
+ provide: NvmeofService,
+ useValue: {
+ getSubsystem: jest.fn().mockReturnValue(of(mockSubsystem))
+ }
+ }
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NvmeofSubsystemOverviewComponent);
+ component = fixture.componentInstance;
+ nvmeofService = TestBed.inject(NvmeofService);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should fetch subsystem on init', fakeAsync(() => {
+ component.ngOnInit();
+ tick();
+ expect(nvmeofService.getSubsystem).toHaveBeenCalledWith('nqn.2016-06.io.spdk:cnode1', 'group1');
+ }));
+
+ it('should store subsystem data', fakeAsync(() => {
+ component.ngOnInit();
+ tick();
+ expect(component.subsystem).toEqual(mockSubsystem);
+ expect(component.subsystem.serial_number).toBe('Ceph30487186726692');
+ expect(component.subsystem.model_number).toBe('Ceph bdev Controller');
+ expect(component.subsystem.max_cntlid).toBe(2040);
+ expect(component.subsystem.min_cntlid).toBe(1);
+ expect(component.subsystem.namespace_count).toBe(3);
+ expect(component.subsystem.max_namespaces).toBe(256);
+ expect(component.subsystem.gw_group).toBe('gateway-prod');
+ }));
+
+ it('should not fetch when subsystemNQN is missing', fakeAsync(() => {
+ TestBed.resetTestingModule();
+ TestBed.configureTestingModule({
+ declarations: [NvmeofSubsystemOverviewComponent],
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ SharedModule,
+ NgbTooltipModule,
+ TilesModule,
+ GridModule
+ ],
+ providers: [
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ parent: {
+ params: of({})
+ },
+ queryParams: of({ group: 'group1' })
+ }
+ },
+ {
+ provide: NvmeofService,
+ useValue: {
+ getSubsystem: jest.fn().mockReturnValue(of(mockSubsystem))
+ }
+ }
+ ]
+ }).compileComponents();
+
+ const newFixture = TestBed.createComponent(NvmeofSubsystemOverviewComponent);
+ const newComponent = newFixture.componentInstance;
+ const newService = TestBed.inject(NvmeofService);
+ newFixture.detectChanges();
+ tick();
+ expect(newService.getSubsystem).not.toHaveBeenCalled();
+ expect(newComponent.subsystem).toBeUndefined();
+ }));
+
+ it('should render detail labels in the template', fakeAsync(() => {
+ component.ngOnInit();
+ tick();
+ fixture.detectChanges();
+
+ const compiled = fixture.nativeElement;
+ const labels = compiled.querySelectorAll('.cds--type-label-01');
+ const labelTexts = Array.from(labels).map((el: HTMLElement) => el.textContent.trim());
+ expect(labelTexts).toContain('Serial number');
+ expect(labelTexts).toContain('Model Number');
+ expect(labelTexts).toContain('Gateway group');
+ expect(labelTexts).toContain('Maximum Controller Identifier');
+ expect(labelTexts).toContain('Minimum Controller Identifier');
+ expect(labelTexts).toContain('Namespaces');
+ expect(labelTexts).toContain('Maximum allowed namespaces');
+ }));
+
+ it('should display subsystem type 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('NVMe');
+ }));
+
+ 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 display HA status 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('Yes');
+ }));
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { NvmeofSubsystem } from '~/app/shared/models/nvmeof';
+
+@Component({
+ selector: 'cd-nvmeof-subsystem-overview',
+ templateUrl: './nvmeof-subsystem-overview.component.html',
+ styleUrls: ['./nvmeof-subsystem-overview.component.scss'],
+ standalone: false
+})
+export class NvmeofSubsystemOverviewComponent implements OnInit {
+ subsystemNQN: string;
+ groupName: string;
+ subsystem: NvmeofSubsystem;
+
+ constructor(private route: ActivatedRoute, private nvmeofService: NvmeofService) {}
+
+ ngOnInit() {
+ this.route.parent?.params.subscribe((params) => {
+ this.subsystemNQN = params['subsystem_nqn'];
+ this.fetchIfReady();
+ });
+ this.route.queryParams.subscribe((qp) => {
+ this.groupName = qp['group'];
+ this.fetchIfReady();
+ });
+ }
+
+ private fetchIfReady() {
+ if (this.subsystemNQN && this.groupName) {
+ this.fetchSubsystem();
+ }
+ }
+
+ fetchSubsystem() {
+ this.nvmeofService
+ .getSubsystem(this.subsystemNQN, this.groupName)
+ .subscribe((subsystem: NvmeofSubsystem) => {
+ this.subsystem = subsystem;
+ });
+ }
+}
--- /dev/null
+<cd-grafana *ngIf="permissions.grafana.read && subsystemNQN && groupName"
+ i18n-title
+ title="Subsystem details"
+ grafanaPath="ceph-nvme-of-gateways-performance?var-group={{groupName}}&var-subsystem={{subsystemNQN}}"
+ [type]="'metrics'"
+ uid="feeuv1dno43r4deed"
+ grafanaStyle="three">
+</cd-grafana>
+
+<cd-helper *ngIf="!permissions.grafana.read"
+ i18n>
+ Grafana permissions are required to view performance details.
+</cd-helper>
--- /dev/null
+import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ActivatedRoute } from '@angular/router';
+import { of } from 'rxjs';
+
+import { NvmeofSubsystemPerformanceComponent } from './nvmeof-subsystem-performance.component';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { Permissions } from '~/app/shared/models/permissions';
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('NvmeofSubsystemPerformanceComponent', () => {
+ let component: NvmeofSubsystemPerformanceComponent;
+ let fixture: ComponentFixture<NvmeofSubsystemPerformanceComponent>;
+
+ const mockPermissions = new Permissions({ grafana: ['read'] });
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [NvmeofSubsystemPerformanceComponent],
+ imports: [HttpClientTestingModule, RouterTestingModule, SharedModule],
+ providers: [
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ parent: {
+ params: of({ subsystem_nqn: 'nqn.2016-06.io.spdk:cnode1' })
+ },
+ queryParams: of({ group: 'group1' })
+ }
+ },
+ {
+ provide: AuthStorageService,
+ useValue: {
+ getPermissions: jest.fn().mockReturnValue(mockPermissions)
+ }
+ }
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NvmeofSubsystemPerformanceComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should set subsystemNQN and groupName from route params', fakeAsync(() => {
+ component.ngOnInit();
+ tick();
+ expect(component.subsystemNQN).toBe('nqn.2016-06.io.spdk:cnode1');
+ expect(component.groupName).toBe('group1');
+ }));
+
+ it('should have grafana read permission', () => {
+ expect(component.permissions.grafana.read).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-nvmeof-subsystem-performance',
+ templateUrl: './nvmeof-subsystem-performance.component.html',
+ styleUrls: ['./nvmeof-subsystem-performance.component.scss'],
+ standalone: false
+})
+export class NvmeofSubsystemPerformanceComponent implements OnInit {
+ subsystemNQN: string;
+ groupName: string;
+ permissions: Permissions;
+
+ constructor(private route: ActivatedRoute, private authStorageService: AuthStorageService) {
+ this.permissions = this.authStorageService.getPermissions();
+ }
+
+ ngOnInit() {
+ this.route.parent?.params.subscribe((params) => {
+ this.subsystemNQN = params['subsystem_nqn'];
+ });
+ this.route.queryParams.subscribe((qp) => {
+ this.groupName = qp['group'];
+ });
+ }
+}
</cd-table-key-value>
</ng-template>
</ng-container>
- <ng-container ngbNavItem="listeners">
- <a ngbNavLink
- i18n>Listeners</a>
- <ng-template ngbNavContent>
- <cd-nvmeof-listeners-list
- [subsystemNQN]="subsystemNQN"
- [group]="group">
- </cd-nvmeof-listeners-list>
- </ng-template>
- </ng-container>
- <ng-container ngbNavItem="namespaces">
- <a ngbNavLink
- i18n>Namespaces</a>
- <ng-template ngbNavContent>
- <cd-nvmeof-namespaces-list
- [subsystemNQN]="subsystemNQN"
- [group]="group">
- </cd-nvmeof-namespaces-list>
- </ng-template>
- </ng-container>
- <ng-container ngbNavItem="initiators">
- <a ngbNavLink
- i18n>Initiators</a>
- <ng-template ngbNavContent>
- <cd-nvmeof-initiators-list [subsystemNQN]="subsystemNQN"
- [group]="group">
- </cd-nvmeof-initiators-list>
- </ng-template>
- </ng-container>
<ng-container ngbNavItem="performance-details"
*ngIf="permissions.grafana.read">
<a ngbNavLink
subtype: 'NVMe',
nqn: 'nqn.2001-07.com.ceph:1720603703820',
namespace_count: 1,
+ has_dhchap_key: false,
max_namespaces: DEFAULT_MAX_NAMESPACE_PER_SUBSYSTEM
};
component.permissions = new Permissions({
[columns]="subsystemsColumns"
columnMode="flex"
selectionType="single"
- [hasDetails]="true"
- (setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)"
(fetchData)="fetchData()"
emptyStateTitle="No subsystems created"
name: $localize`Authentication`,
prop: 'authentication',
cellTemplate: this.authenticationTpl
- },
- {
- name: $localize`Traffic encryption`,
- prop: 'encryption',
- cellTemplate: this.encryptionTpl
}
];
import { AppConstants } from '~/app/shared/constants/app.constants';
import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs';
+import { BreadcrumbService } from '~/app/shared/services/breadcrumb.service';
@Component({
selector: 'cd-breadcrumbs',
*/
finished = false;
subscription: Subscription;
+ private tabCrumbSubscription: Subscription;
private defaultResolver = new BreadcrumbsResolver();
-
- constructor(private router: Router, private injector: Injector, private titleService: Title) {
+ private baseCrumbs: IBreadcrumb[] = [];
+ private currentTabCrumb: IBreadcrumb = null;
+
+ constructor(
+ private router: Router,
+ private injector: Injector,
+ private titleService: Title,
+ private breadcrumbService: BreadcrumbService
+ ) {
this.subscription = this.router.events
.pipe(filter((x) => x instanceof NavigationStart))
.subscribe(() => {
this.subscription = this.router.events
.pipe(filter((x) => x instanceof NavigationEnd))
.subscribe(() => {
+ this.breadcrumbService.clearTabCrumb();
const currentRoot = router.routerState.snapshot.root;
this._resolveCrumbs(currentRoot)
)
.subscribe((x) => {
this.finished = true;
- this.crumbs = x;
+ this.baseCrumbs = x;
+ this.crumbs = this.currentTabCrumb ? [...x, this.currentTabCrumb] : [...x];
const title = this.getTitleFromCrumbs(this.crumbs);
this.titleService.setTitle(title);
});
});
+
+ this.tabCrumbSubscription = this.breadcrumbService.tabCrumb$.subscribe((tabCrumb) => {
+ this.currentTabCrumb = tabCrumb;
+ if (tabCrumb) {
+ this.crumbs = [...this.baseCrumbs, tabCrumb];
+ } else {
+ this.crumbs = [...this.baseCrumbs];
+ }
+ const title = this.getTitleFromCrumbs(this.crumbs);
+ this.titleService.setTitle(title);
+ });
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
+ this.tabCrumbSubscription.unsubscribe();
}
private _resolveCrumbs(route: ActivatedRouteSnapshot): Observable<IBreadcrumb[]> {
enable_ha?: boolean;
gw_group?: string;
initiator_count?: number;
- psk?: string;
+ has_dhchap_key: boolean;
}
export interface NvmeofSubsystemData extends NvmeofSubsystem {
export interface NvmeofSubsystemInitiator {
nqn: string;
- dhchap_key?: string;
+ use_dhchap?: string;
}
export interface NvmeofListener {
const UNIDIRECTIONAL = 'Unidirectional';
const BIDIRECTIONAL = 'Bi-directional';
- if (subsystem.psk) {
- return BIDIRECTIONAL;
- }
-
let hostsList: NvmeofSubsystemInitiator[] = [];
if (_initiators && 'hosts' in _initiators && Array.isArray(_initiators.hosts)) {
hostsList = _initiators.hosts;
hostsList = _initiators as NvmeofSubsystemInitiator[];
}
- const hasDhchapKey = hostsList.some((host) => !!host.dhchap_key);
- if (hasDhchapKey) {
- return UNIDIRECTIONAL;
+ let auth = NO_AUTH;
+
+ const hostHasDhchapKey = hostsList.some((host) => !!host.use_dhchap);
+
+ if (hostHasDhchapKey) {
+ auth = UNIDIRECTIONAL;
+ }
+
+ if (subsystem.has_dhchap_key && hostHasDhchapKey) {
+ auth = BIDIRECTIONAL;
}
- return NO_AUTH;
+ return auth;
}
// Form control names for NvmeofNamespacesFormComponent
--- /dev/null
+import { Injectable } from '@angular/core';
+import { BehaviorSubject } from 'rxjs';
+
+import { IBreadcrumb } from '~/app/shared/models/breadcrumbs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class BreadcrumbService {
+ private tabCrumbSubject = new BehaviorSubject<IBreadcrumb>(null);
+ tabCrumb$ = this.tabCrumbSubject.asObservable();
+
+ setTabCrumb(text: string, path: string = null): void {
+ this.tabCrumbSubject.next({ text, path });
+ }
+
+ clearTabCrumb(): void {
+ this.tabCrumbSubject.next(null);
+ }
+}