DashboardTimeSelectorComponent
],
- exports: [DashboardV3Component]
+ exports: [DashboardV3Component, CardComponent, CardRowComponent]
})
export class DashboardV3Module {}
--- /dev/null
+<div class="container-fluid">
+ <div class="row mx-0">
+ <cd-card cardTitle="Daemons"
+ i18n-title
+ link="/rgw/daemons"
+ class="col-sm-2 px-3 d-flex"
+ aria-label="Daemons card">
+ <span class="ms-4 me-4">
+ <h1 class="text-center">{{ rgwDaemonCount }}</h1>
+ </span>
+ </cd-card>
+
+ <cd-card cardTitle="Zoning"
+ i18n-title
+ class="col-sm-2 px-3 d-flex"
+ aria-label="Details card">
+ <span class="ms-4 me-4 text-center">
+ <h3>{{ rgwRealmCount }} Realms</h3>
+ <h3>{{ rgwZonegroupCount }} Zonegroups</h3>
+ <h3>{{ rgwZoneCount }} Zones</h3>
+ </span>
+ </cd-card>
+
+ <cd-card cardTitle="Buckets"
+ i18n-title
+ class="col-sm-2 px-3 d-flex"
+ aria-label="Details card">
+ <span class="ms-4 me-4 text-center">
+ <h2>{{ rgwBucketCount }} Buckets</h2>
+ <h2>{{ objectCount }} Objects</h2>
+ </span>
+ </cd-card>
+
+ <cd-card cardTitle="Users"
+ i18n-title
+ class="col-sm-2 px-3 d-flex"
+ aria-label="Details card">
+ <span class="ms-4 me-4 text-center">
+ <h1>{{ UserCount }}</h1>
+ </span>
+ </cd-card>
+
+ <cd-card cardTitle="Used Capacity"
+ i18n-title
+ class="col-sm-2 px-3 d-flex"
+ aria-label="Details card">
+ <span class="ms-4 me-4 text-center">
+ <h1>{{ totalPoolUsedBytes | dimlessBinary}}</h1>
+ </span>
+ </cd-card>
+
+ <cd-card cardTitle="Avg Object Size"
+ i18n-title
+ class="col-sm-2 px-3 d-flex"
+ aria-label="Details card">
+ <span class="ms-4 me-4 text-center">
+ <h1>{{ averageObjectSize | dimlessBinary}}</h1>
+ </span>
+ </cd-card>
+ </div>
+</div>
--- /dev/null
+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<RgwOverviewDashboardComponent>;
+ 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);
+ });
+});
--- /dev/null
+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();
+ }
+}
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: [
NgbTooltipModule,
NgxPipeFunctionModule,
TreeModule,
- DataTableModule
+ DataTableModule,
+ DashboardV3Module
],
exports: [
RgwDaemonListComponent,
RgwMultisiteMigrateComponent,
RgwMultisiteImportComponent,
RgwMultisiteExportComponent,
- CreateRgwServiceEntitiesComponent
+ CreateRgwServiceEntitiesComponent,
+ RgwOverviewDashboardComponent
]
})
export class RgwModule {}
}
]
},
+ {
+ path: 'overview',
+ data: { breadcrumbs: 'Overview' },
+ children: [{ path: '', component: RgwOverviewDashboardComponent }]
+ },
{
path: 'multisite',
canActivate: [FeatureTogglesGuardService, ModuleStatusGuardService],
<ul class="list-unstyled"
id="gateway-nav"
[ngbCollapse]="displayedSubMenu !== 'rgw'">
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_rgw_overview">
+ <a i18n
+ routerLink="/rgw/overview">Overview</a>
+ </li>
<li routerLinkActive="active"
class="tc_submenuitem tc_submenuitem_rgw_daemons">
<a i18n
total_avail_bytes: int
total_bytes: int
total_used_raw_bytes: int
+ total_objects: int
+ total_pool_bytes_used: int
+ average_object_size: int
class ClusterModel:
@classmethod
def get_capacity(cls) -> ClusterCapacity:
df = mgr.get('df')
+ total_objects = 0
+ total_pool_bytes_used = 0
+ average_object_size = 0
+ rgw_pools = cls.get_rgw_pools()
+ for pool in df['pools']:
+ if pool['name'] in rgw_pools:
+ total_objects = total_objects + pool['stats']['objects']
+ total_pool_bytes_used = total_pool_bytes_used + pool['stats']['bytes_used']
+ if total_objects != 0:
+ average_object_size = total_pool_bytes_used / total_objects
return ClusterCapacity(total_avail_bytes=df['stats']['total_avail_bytes'],
total_bytes=df['stats']['total_bytes'],
- total_used_raw_bytes=df['stats']['total_used_raw_bytes'])._asdict()
+ total_used_raw_bytes=df['stats']['total_used_raw_bytes'],
+ total_objects=total_objects,
+ total_pool_bytes_used=total_pool_bytes_used,
+ average_object_size=average_object_size)._asdict()
+ @classmethod
+ def get_rgw_pools(cls):
+ rgw_pool_names = []
+ osd_map = mgr.get('osd_map')
+ for pool in osd_map['pools']:
+ if 'rgw' in pool.get('application_metadata', {}):
+ rgw_pool_names.append(pool['pool_name'])
+ return rgw_pool_names
+