From 2f41ccea608f238298e3add617961ec085a3b71b Mon Sep 17 00:00:00 2001 From: Afreen Misbah Date: Thu, 8 May 2025 09:39:59 +0530 Subject: [PATCH] mgr/dashboard: Add default state when gateway groups are empty Fixes https://tracker.ceph.com/issues/71247 - after upgrades the nvmeof service spec does not contain `group` field - this causes UI combobox internal errors - checking for `group` in spec and disabling the selector Signed-off-by: Afreen Misbah (cherry picked from commit 9a7c907bfc2345a79ecc1f850cc851f21f74fca5) --- .../nvmeof-gateway.component.html | 7 ++-- .../nvmeof-gateway.component.spec.ts | 9 +++- .../nvmeof-gateway.component.ts | 42 +++++++++---------- .../nvmeof-subsystems.component.html | 7 ++-- .../nvmeof-subsystems.component.spec.ts | 14 +++++++ .../nvmeof-subsystems.component.ts | 37 ++++++++-------- .../src/app/shared/api/nvmeof.service.ts | 29 +++++++++++++ 7 files changed, 98 insertions(+), 47 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html index 5e3c6b2af107f..4778764d97636 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html @@ -6,11 +6,12 @@ + (clear)="onGroupClear()" + [disabled]="gwGroupsEmpty"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts index 1c8bf5485661b..f4fb0c50d870c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts @@ -7,6 +7,7 @@ import { SharedModule } from '~/app/shared/shared.module'; import { ComboBoxModule, GridModule } from 'carbon-components-angular'; import { NvmeofTabsComponent } from '../nvmeof-tabs/nvmeof-tabs.component'; import { CephServiceService } from '~/app/shared/api/ceph-service.service'; +import { CephServiceSpec } from '~/app/shared/models/service.interface'; const mockServiceDaemons = [ { @@ -60,7 +61,7 @@ const mockGateways = [ } ]; -const mockGwGroups = [ +const mockformattedGwGroups = [ { content: 'default', serviceName: 'nvmeof.rbd.default' @@ -96,6 +97,10 @@ class MockNvmeOfService { listGatewayGroups() { return of(mockServices); } + + formatGwGroupsList(_data: CephServiceSpec[][]) { + return mockformattedGwGroups; + } } class MockCephServiceService { @@ -130,7 +135,7 @@ describe('NvmeofGatewayComponent', () => { it('should load gateway groups correctly', () => { expect(component.gwGroups.length).toBe(2); - expect(component.gwGroups).toStrictEqual(mockGwGroups); + expect(component.gwGroups).toStrictEqual(mockformattedGwGroups); }); it('should set service name of gateway groups correctly', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts index 5364d7ea342bf..ea66250a348eb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts @@ -5,17 +5,11 @@ import _ from 'lodash'; import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; -import { NvmeofService } from '../../../shared/api/nvmeof.service'; +import { GroupsComboboxItem, NvmeofService } from '../../../shared/api/nvmeof.service'; import { CephServiceSpec } from '~/app/shared/models/service.interface'; import { CephServiceService } from '~/app/shared/api/ceph-service.service'; import { Daemon } from '~/app/shared/models/daemon.interface'; -type ComboBoxItem = { - content: string; - serviceName: string; - selected?: boolean; -}; - type Gateway = { id: string; hostname: string; @@ -28,6 +22,8 @@ enum TABS { 'overview' } +const DEFAULT_PLACEHOLDER = $localize`Enter group name`; + @Component({ selector: 'cd-nvmeof-gateway', templateUrl: './nvmeof-gateway.component.html', @@ -35,7 +31,6 @@ enum TABS { }) export class NvmeofGatewayComponent implements OnInit { selectedTab: TABS; - selectedGatewayGroup: string = null; onSelected(tab: TABS) { this.selectedTab = tab; @@ -51,8 +46,11 @@ export class NvmeofGatewayComponent implements OnInit { gateways: Gateway[] = []; gatewayColumns: any; selection = new CdTableSelection(); - gwGroups: ComboBoxItem[] = []; + gwGroups: GroupsComboboxItem[] = []; groupService: string = null; + selectedGatewayGroup: string = null; + gwGroupsEmpty: boolean = false; + gwGroupPlaceholder: string = DEFAULT_PLACEHOLDER; constructor( private nvmeofService: NvmeofService, @@ -61,7 +59,7 @@ export class NvmeofGatewayComponent implements OnInit { ) {} ngOnInit() { - this.getGatewayGroups(); + this.setGatewayGroups(); this.gatewayColumns = [ { name: $localize`Gateway ID`, @@ -109,7 +107,7 @@ export class NvmeofGatewayComponent implements OnInit { } // Gateway groups - onGroupSelection(selected: ComboBoxItem) { + onGroupSelection(selected: GroupsComboboxItem) { selected.selected = true; this.groupService = selected.serviceName; this.selectedGatewayGroup = selected.content; @@ -121,18 +119,20 @@ export class NvmeofGatewayComponent implements OnInit { this.getGateways(); } - getGatewayGroups() { + setGatewayGroups() { this.nvmeofService.listGatewayGroups().subscribe((response: CephServiceSpec[][]) => { - this.gwGroups = response?.[0]?.length - ? response[0].map((group: CephServiceSpec) => { - return { - content: group?.spec?.group, - serviceName: group?.service_name - }; - }) - : []; + if (response?.[0]?.length) { + this.gwGroups = this.nvmeofService.formatGwGroupsList(response, true); + } else this.gwGroups = []; // Select first group if no group is selected - if (!this.groupService && this.gwGroups.length) this.onGroupSelection(this.gwGroups[0]); + if (!this.groupService && this.gwGroups.length) { + this.onGroupSelection(this.gwGroups[0]); + this.gwGroupsEmpty = false; + this.gwGroupPlaceholder = DEFAULT_PLACEHOLDER; + } else { + this.gwGroupsEmpty = true; + this.gwGroupPlaceholder = $localize`No groups available`; + } }); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.html index 38456d06d03d8..d3d0253db192c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.html @@ -6,11 +6,12 @@ + (clear)="onGroupClear()" + [disabled]="gwGroupsEmpty"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.spec.ts index c508cf74a7784..3246f7286f51d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.spec.ts @@ -12,6 +12,7 @@ import { NvmeofSubsystemsComponent } from './nvmeof-subsystems.component'; import { NvmeofTabsComponent } from '../nvmeof-tabs/nvmeof-tabs.component'; import { NvmeofSubsystemsDetailsComponent } from '../nvmeof-subsystems-details/nvmeof-subsystems-details.component'; import { ComboBoxModule, GridModule } from 'carbon-components-angular'; +import { CephServiceSpec } from '~/app/shared/models/service.interface'; const mockSubsystems = [ { @@ -49,11 +50,24 @@ const mockGroups = [ 2 ]; +const mockformattedGwGroups = [ + { + content: 'default' + }, + { + content: 'foo' + } +]; + class MockNvmeOfService { listSubsystems() { return of(mockSubsystems); } + formatGwGroupsList(_data: CephServiceSpec[][]) { + return mockformattedGwGroups; + } + listGatewayGroups() { return of(mockGroups); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts index b1f28d9cb6faf..f6f75f1356f1a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems/nvmeof-subsystems.component.ts @@ -12,16 +12,12 @@ import { Icons } from '~/app/shared/enum/icons.enum'; import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; import { FinishedTask } from '~/app/shared/models/finished-task'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; -import { NvmeofService } from '~/app/shared/api/nvmeof.service'; +import { NvmeofService, GroupsComboboxItem } from '~/app/shared/api/nvmeof.service'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { CephServiceSpec } from '~/app/shared/models/service.interface'; -type ComboBoxItem = { - content: string; - selected?: boolean; -}; - const BASE_URL = 'block/nvmeof/subsystems'; +const DEFAULT_PLACEHOLDER = $localize`Enter group name`; @Component({ selector: 'cd-nvmeof-subsystems', @@ -35,8 +31,10 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit selection = new CdTableSelection(); tableActions: CdTableAction[]; subsystemDetails: any[]; - gwGroups: ComboBoxItem[] = []; + gwGroups: GroupsComboboxItem[] = []; group: string = null; + gwGroupsEmpty: boolean = false; + gwGroupPlaceholder: string = DEFAULT_PLACEHOLDER; constructor( private nvmeofService: NvmeofService, @@ -55,7 +53,7 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit this.route.queryParams.subscribe((params) => { if (params?.['group']) this.onGroupSelection({ content: params?.['group'] }); }); - this.getGatewayGroups(); + this.setGatewayGroups(); this.subsystemsColumns = [ { name: $localize`NQN`, @@ -124,7 +122,7 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit } // Gateway groups - onGroupSelection(selected: ComboBoxItem) { + onGroupSelection(selected: GroupsComboboxItem) { selected.selected = true; this.group = selected.content; this.getSubsystems(); @@ -135,17 +133,20 @@ export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit this.getSubsystems(); } - getGatewayGroups() { + setGatewayGroups() { this.nvmeofService.listGatewayGroups().subscribe((response: CephServiceSpec[][]) => { - if (response?.[0].length) { - this.gwGroups = response[0].map((group: CephServiceSpec) => { - return { - content: group?.spec?.group - }; - }); - } + if (response?.[0]?.length) { + this.gwGroups = this.nvmeofService.formatGwGroupsList(response); + } else this.gwGroups = []; // Select first group if no group is selected - if (!this.group && this.gwGroups.length) this.onGroupSelection(this.gwGroups[0]); + if (!this.group && this.gwGroups.length) { + this.onGroupSelection(this.gwGroups[0]); + this.gwGroupsEmpty = false; + this.gwGroupPlaceholder = DEFAULT_PLACEHOLDER; + } else { + this.gwGroupsEmpty = true; + this.gwGroupPlaceholder = $localize`No groups available`; + } }); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts index 4a3148eb752d7..ff9647a3b53ea 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts @@ -4,9 +4,16 @@ import { HttpClient } from '@angular/common/http'; import _ from 'lodash'; import { Observable, of as observableOf } from 'rxjs'; import { catchError, mapTo } from 'rxjs/operators'; +import { CephServiceSpec } from '../models/service.interface'; export const MAX_NAMESPACE = 1024; +export type GroupsComboboxItem = { + content: string; + serviceName?: string; + selected?: boolean; +}; + type NvmeofRequest = { gw_group: string; }; @@ -40,6 +47,28 @@ const UI_API_PATH = 'ui-api/nvmeof'; export class NvmeofService { constructor(private http: HttpClient) {} + // formats the gateway groups to be consumed for combobox item + formatGwGroupsList( + data: CephServiceSpec[][], + isGatewayList: boolean = false + ): GroupsComboboxItem[] { + return data[0].reduce((gwGrpList: GroupsComboboxItem[], group: CephServiceSpec) => { + if (isGatewayList && group?.spec?.group && group?.service_name) { + gwGrpList.push({ + content: group.spec.group, + serviceName: group.service_name + }); + } else { + if (group?.spec?.group) { + gwGrpList.push({ + content: group.spec.group + }); + } + } + return gwGrpList; + }, []); + } + // Gateway groups listGatewayGroups() { return this.http.get(`${API_PATH}/gateway/group`); -- 2.39.5