From a685dde61b0735dfa5a55b76fef3c9f483e3a421 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Wed, 5 Jul 2023 19:41:55 +0530 Subject: [PATCH] mgr/dashboard: add single stat cards to rgw overview dashboard Signed-off-by: Aashish Sharma --- .../ceph/dashboard-v3/dashboard-v3.module.ts | 2 +- .../rgw-overview-dashboard.component.html | 61 +++++ .../rgw-overview-dashboard.component.scss | 0 .../rgw-overview-dashboard.component.spec.ts | 209 ++++++++++++++++++ .../rgw-overview-dashboard.component.ts | 102 +++++++++ .../frontend/src/app/ceph/rgw/rgw.module.ts | 13 +- .../navigation/navigation.component.html | 5 + src/pybind/mgr/dashboard/services/cluster.py | 27 ++- 8 files changed, 415 insertions(+), 4 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts index 2c3b4cc369fc7..6e55f98531dd1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts @@ -42,6 +42,6 @@ import { PgSummaryPipe } from './pg-summary.pipe'; DashboardTimeSelectorComponent ], - exports: [DashboardV3Component] + exports: [DashboardV3Component, CardComponent, CardRowComponent] }) export class DashboardV3Module {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html new file mode 100644 index 0000000000000..eaf7f47048d36 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html @@ -0,0 +1,61 @@ +
+
+ + +

{{ rgwDaemonCount }}

+
+
+ + + +

{{ rgwRealmCount }} Realms

+

{{ rgwZonegroupCount }} Zonegroups

+

{{ rgwZoneCount }} Zones

+
+
+ + + +

{{ rgwBucketCount }} Buckets

+

{{ objectCount }} Objects

+
+
+ + + +

{{ UserCount }}

+
+
+ + + +

{{ totalPoolUsedBytes | dimlessBinary}}

+
+
+ + + +

{{ averageObjectSize | dimlessBinary}}

+
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts new file mode 100644 index 0000000000000..238ac788be247 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts @@ -0,0 +1,209 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RgwOverviewDashboardComponent } from './rgw-overview-dashboard.component'; +import { of } from 'rxjs'; +import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; +import { RgwDaemon } from '../models/rgw-daemon'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { CardComponent } from '../../dashboard-v3/card/card.component'; +import { CardRowComponent } from '../../dashboard-v3/card-row/card-row.component'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; +import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; +import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; +import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; +import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service'; +import { RgwUserService } from '~/app/shared/api/rgw-user.service'; +import { HealthService } from '~/app/shared/api/health.service'; + +describe('RgwOverviewDashboardComponent', () => { + let component: RgwOverviewDashboardComponent; + let fixture: ComponentFixture; + const daemon: RgwDaemon = { + id: '8000', + service_map_id: '4803', + version: 'ceph version', + server_hostname: 'ceph', + realm_name: 'realm1', + zonegroup_name: 'zg1-realm1', + zone_name: 'zone1-zg1-realm1', + default: true, + port: 80 + }; + + const realmList = { + default_info: '20f61d29-7e45-4418-8e19-b7e962e4860b', + realms: ['realm2', 'realm1'] + }; + + const zonegroupList = { + default_info: '20f61d29-7e45-4418-8e19-b7e962e4860b', + zonegroups: ['zg-1', 'zg-2', 'zg-3'] + }; + + const zoneList = { + default_info: '20f61d29-7e45-4418-8e19-b7e962e4860b', + zones: ['zone4', 'zone5', 'zone6', 'zone7'] + }; + + const bucketList = [ + { + bucket: 'bucket', + owner: 'testid', + usage: { + 'rgw.main': { + size_actual: 4, + num_objects: 2 + }, + 'rgw.none': { + size_actual: 6, + num_objects: 6 + } + }, + bucket_quota: { + max_size: 20, + max_objects: 10, + enabled: true + } + }, + { + bucket: 'bucket2', + owner: 'testid', + usage: { + 'rgw.main': { + size_actual: 4, + num_objects: 2 + }, + 'rgw.none': { + size_actual: 6, + num_objects: 6 + } + }, + bucket_quota: { + max_size: 20, + max_objects: 10, + enabled: true + } + } + ]; + + const userList = [ + { + user_id: 'testid', + stats: { + size_actual: 6, + num_objects: 6 + }, + user_quota: { + max_size: 20, + max_objects: 10, + enabled: true + } + }, + { + user_id: 'testid2', + stats: { + size_actual: 6, + num_objects: 6 + }, + user_quota: { + max_size: 20, + max_objects: 10, + enabled: true + } + } + ]; + + const healthData = { + total_objects: '290', + total_pool_bytes_used: 9338880 + }; + + let listDaemonsSpy: jest.SpyInstance; + let listZonesSpy: jest.SpyInstance; + let listZonegroupsSpy: jest.SpyInstance; + let listRealmsSpy: jest.SpyInstance; + let listBucketsSpy: jest.SpyInstance; + let listUsersSpy: jest.SpyInstance; + let healthDataSpy: jest.SpyInstance; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + RgwOverviewDashboardComponent, + CardComponent, + CardRowComponent, + DimlessBinaryPipe + ], + imports: [HttpClientTestingModule] + }).compileComponents(); + }); + + beforeEach(() => { + listDaemonsSpy = jest + .spyOn(TestBed.inject(RgwDaemonService), 'list') + .mockReturnValue(of([daemon])); + listRealmsSpy = jest + .spyOn(TestBed.inject(RgwRealmService), 'list') + .mockReturnValue(of(realmList)); + listZonegroupsSpy = jest + .spyOn(TestBed.inject(RgwZonegroupService), 'list') + .mockReturnValue(of(zonegroupList)); + listZonesSpy = jest.spyOn(TestBed.inject(RgwZoneService), 'list').mockReturnValue(of(zoneList)); + listBucketsSpy = jest + .spyOn(TestBed.inject(RgwBucketService), 'list') + .mockReturnValue(of(bucketList)); + listUsersSpy = jest.spyOn(TestBed.inject(RgwUserService), 'list').mockReturnValue(of(userList)); + healthDataSpy = jest + .spyOn(TestBed.inject(HealthService), 'getClusterCapacity') + .mockReturnValue(of(healthData)); + fixture = TestBed.createComponent(RgwOverviewDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render all cards', () => { + fixture.detectChanges(); + const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card'); + expect(dashboardCards.length).toBe(6); + }); + + it('should get corresponding data into Daemons', () => { + expect(listDaemonsSpy).toHaveBeenCalled(); + expect(component.rgwDaemonCount).toEqual(1); + }); + + it('should get corresponding data into Realms', () => { + expect(listRealmsSpy).toHaveBeenCalled(); + expect(component.rgwRealmCount).toEqual(2); + }); + + it('should get corresponding data into Zonegroups', () => { + expect(listZonegroupsSpy).toHaveBeenCalled(); + expect(component.rgwZonegroupCount).toEqual(3); + }); + + it('should get corresponding data into Zones', () => { + expect(listZonesSpy).toHaveBeenCalled(); + expect(component.rgwZoneCount).toEqual(4); + }); + + it('should get corresponding data into Buckets', () => { + expect(listBucketsSpy).toHaveBeenCalled(); + expect(component.rgwBucketCount).toEqual(2); + }); + + it('should get corresponding data into Users', () => { + expect(listUsersSpy).toHaveBeenCalled(); + expect(component.UserCount).toEqual(2); + }); + + it('should get corresponding data into Objects and capacity', () => { + expect(healthDataSpy).toHaveBeenCalled(); + expect(component.objectCount).toEqual('290'); + expect(component.totalPoolUsedBytes).toEqual(9338880); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts new file mode 100644 index 0000000000000..2614a81cbe941 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts @@ -0,0 +1,102 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; + +import _ from 'lodash'; +import { Subscription } from 'rxjs'; + +import { HealthService } from '~/app/shared/api/health.service'; +import { Permissions } from '~/app/shared/models/permissions'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { + FeatureTogglesMap$, + FeatureTogglesService +} from '~/app/shared/services/feature-toggles.service'; +import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service'; +import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; +import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; +import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; +import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; +import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service'; +import { RgwUserService } from '~/app/shared/api/rgw-user.service'; + +@Component({ + selector: 'cd-rgw-overview-dashboard', + templateUrl: './rgw-overview-dashboard.component.html', + styleUrls: ['./rgw-overview-dashboard.component.scss'] +}) +export class RgwOverviewDashboardComponent implements OnInit, OnDestroy { + interval = new Subscription(); + permissions: Permissions; + enabledFeature$: FeatureTogglesMap$; + rgwDaemonCount = 0; + rgwRealmCount = 0; + rgwZonegroupCount = 0; + rgwZoneCount = 0; + rgwBucketCount = 0; + objectCount = 0; + UserCount = 0; + totalPoolUsedBytes = 0; + averageObjectSize = 0; + realmData: any; + daemonSub: Subscription; + realmSub: Subscription; + ZonegroupSub: Subscription; + ZoneSUb: Subscription; + UserSub: Subscription; + HealthSub: Subscription; + BucketSub: Subscription; + + constructor( + private authStorageService: AuthStorageService, + private featureToggles: FeatureTogglesService, + private healthService: HealthService, + private refreshIntervalService: RefreshIntervalService, + private rgwDaemonService: RgwDaemonService, + private rgwRealmService: RgwRealmService, + private rgwZonegroupService: RgwZonegroupService, + private rgwZoneService: RgwZoneService, + private rgwBucketService: RgwBucketService, + private rgwUserService: RgwUserService + ) { + this.permissions = this.authStorageService.getPermissions(); + this.enabledFeature$ = this.featureToggles.get(); + } + + ngOnInit() { + this.interval = this.refreshIntervalService.intervalData$.subscribe(() => { + this.daemonSub = this.rgwDaemonService.list().subscribe((data: any) => { + this.rgwDaemonCount = data.length; + }); + this.realmSub = this.rgwRealmService.list().subscribe((data: any) => { + this.rgwRealmCount = data['realms'].length; + }); + this.ZonegroupSub = this.rgwZonegroupService.list().subscribe((data: any) => { + this.rgwZonegroupCount = data['zonegroups'].length; + }); + this.ZoneSUb = this.rgwZoneService.list().subscribe((data: any) => { + this.rgwZoneCount = data['zones'].length; + }); + this.BucketSub = this.rgwBucketService.list().subscribe((data: any) => { + this.rgwBucketCount = data.length; + }); + this.UserSub = this.rgwUserService.list().subscribe((data: any) => { + this.UserCount = data.length; + }); + this.HealthSub = this.healthService.getClusterCapacity().subscribe((data: any) => { + this.objectCount = data['total_objects']; + this.totalPoolUsedBytes = data['total_pool_bytes_used']; + this.averageObjectSize = data['average_object_size']; + }); + }); + } + + ngOnDestroy() { + this.interval.unsubscribe(); + this.daemonSub.unsubscribe(); + this.realmSub.unsubscribe(); + this.ZonegroupSub.unsubscribe(); + this.ZoneSUb.unsubscribe(); + this.BucketSub.unsubscribe(); + this.UserSub.unsubscribe(); + this.HealthSub.unsubscribe(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index a0082209b96d1..fa0e72584b900 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -41,6 +41,8 @@ import { RgwMultisiteMigrateComponent } from './rgw-multisite-migrate/rgw-multis import { RgwMultisiteImportComponent } from './rgw-multisite-import/rgw-multisite-import.component'; import { RgwMultisiteExportComponent } from './rgw-multisite-export/rgw-multisite-export.component'; import { CreateRgwServiceEntitiesComponent } from './create-rgw-service-entities/create-rgw-service-entities.component'; +import { RgwOverviewDashboardComponent } from './rgw-overview-dashboard/rgw-overview-dashboard.component'; +import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module'; @NgModule({ imports: [ @@ -54,7 +56,8 @@ import { CreateRgwServiceEntitiesComponent } from './create-rgw-service-entities NgbTooltipModule, NgxPipeFunctionModule, TreeModule, - DataTableModule + DataTableModule, + DashboardV3Module ], exports: [ RgwDaemonListComponent, @@ -91,7 +94,8 @@ import { CreateRgwServiceEntitiesComponent } from './create-rgw-service-entities RgwMultisiteMigrateComponent, RgwMultisiteImportComponent, RgwMultisiteExportComponent, - CreateRgwServiceEntitiesComponent + CreateRgwServiceEntitiesComponent, + RgwOverviewDashboardComponent ] }) export class RgwModule {} @@ -165,6 +169,11 @@ const routes: Routes = [ } ] }, + { + path: 'overview', + data: { breadcrumbs: 'Overview' }, + children: [{ path: '', component: RgwOverviewDashboardComponent }] + }, { path: 'multisite', canActivate: [FeatureTogglesGuardService, ModuleStatusGuardService], diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index 6a05aea981791..941f8a762e96c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -269,6 +269,11 @@