Skip to content

Commit c609ce5

Browse files
authored
Merge pull request ceph#60514 from rhcs-dashboard/fix-68733-main
mgr/dashboard: fix total objects/Avg object size in RGW Overview Page Reviewed-by: Afreen Misbah <[email protected]>
2 parents 14e491c + 74b0749 commit c609ce5

File tree

6 files changed

+226
-157
lines changed

6 files changed

+226
-157
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export interface Bucket {
2+
bucket: string;
3+
tenant: string;
4+
versioning: string;
5+
zonegroup: string;
6+
placement_rule: string;
7+
explicit_placement: {
8+
data_pool: string;
9+
data_extra_pool: string;
10+
index_pool: string;
11+
};
12+
id: string;
13+
marker: string;
14+
index_type: string;
15+
index_generation: number;
16+
num_shards: number;
17+
reshard_status: string;
18+
judge_reshard_lock_time: string;
19+
object_lock_enabled: boolean;
20+
mfa_enabled: boolean;
21+
owner: string;
22+
ver: string;
23+
master_ver: string;
24+
mtime: string;
25+
creation_time: string;
26+
max_marker: string;
27+
usage: Record<string, any>;
28+
bucket_quota: {
29+
enabled: boolean;
30+
check_on_raw: boolean;
31+
max_size: number;
32+
max_size_kb: number;
33+
max_objects: number;
34+
};
35+
read_tracker: number;
36+
bid: string;
37+
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Component, NgZone, OnInit, TemplateRef, ViewChild } from '@angular/core';
1+
import { Component, NgZone, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
22

33
import _ from 'lodash';
4-
import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs';
4+
import { forkJoin as observableForkJoin, Observable, Subscriber, Subscription } from 'rxjs';
5+
import { switchMap } from 'rxjs/operators';
56

67
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
78
import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
@@ -21,6 +22,7 @@ import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
2122
import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
2223
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
2324
import { URLBuilderService } from '~/app/shared/services/url-builder.service';
25+
import { Bucket } from '../models/rgw-bucket';
2426

2527
const BASE_URL = 'rgw/bucket';
2628

@@ -30,7 +32,7 @@ const BASE_URL = 'rgw/bucket';
3032
styleUrls: ['./rgw-bucket-list.component.scss'],
3133
providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
3234
})
33-
export class RgwBucketListComponent extends ListWithDetails implements OnInit {
35+
export class RgwBucketListComponent extends ListWithDetails implements OnInit, OnDestroy {
3436
@ViewChild(TableComponent, { static: true })
3537
table: TableComponent;
3638
@ViewChild('bucketSizeTpl', { static: true })
@@ -43,9 +45,10 @@ export class RgwBucketListComponent extends ListWithDetails implements OnInit {
4345
permission: Permission;
4446
tableActions: CdTableAction[];
4547
columns: CdTableColumn[] = [];
46-
buckets: object[] = [];
48+
buckets: Bucket[] = [];
4749
selection: CdTableSelection = new CdTableSelection();
4850
declare staleTimeout: number;
51+
private subs: Subscription = new Subscription();
4952

5053
constructor(
5154
private authStorageService: AuthStorageService,
@@ -126,33 +129,18 @@ export class RgwBucketListComponent extends ListWithDetails implements OnInit {
126129
this.setTableRefreshTimeout();
127130
}
128131

129-
transformBucketData() {
130-
_.forEach(this.buckets, (bucketKey) => {
131-
const maxBucketSize = bucketKey['bucket_quota']['max_size'];
132-
const maxBucketObjects = bucketKey['bucket_quota']['max_objects'];
133-
bucketKey['bucket_size'] = 0;
134-
bucketKey['num_objects'] = 0;
135-
if (!_.isEmpty(bucketKey['usage'])) {
136-
bucketKey['bucket_size'] = bucketKey['usage']['rgw.main']['size_actual'];
137-
bucketKey['num_objects'] = bucketKey['usage']['rgw.main']['num_objects'];
138-
}
139-
bucketKey['size_usage'] =
140-
maxBucketSize > 0 ? bucketKey['bucket_size'] / maxBucketSize : undefined;
141-
bucketKey['object_usage'] =
142-
maxBucketObjects > 0 ? bucketKey['num_objects'] / maxBucketObjects : undefined;
143-
});
144-
}
145-
146132
getBucketList(context: CdTableFetchDataContext) {
147133
this.setTableRefreshTimeout();
148-
this.rgwBucketService.list(true).subscribe(
149-
(resp: object[]) => {
150-
this.buckets = resp;
151-
this.transformBucketData();
152-
},
153-
() => {
154-
context.error();
155-
}
134+
this.subs.add(
135+
this.rgwBucketService
136+
.fetchAndTransformBuckets()
137+
.pipe(switchMap(() => this.rgwBucketService.buckets$))
138+
.subscribe({
139+
next: (buckets) => {
140+
this.buckets = buckets;
141+
},
142+
error: () => context.error()
143+
})
156144
);
157145
}
158146

@@ -198,4 +186,8 @@ export class RgwBucketListComponent extends ListWithDetails implements OnInit {
198186
}
199187
});
200188
}
189+
190+
ngOnDestroy() {
191+
this.subs.unsubscribe();
192+
}
201193
}
Lines changed: 86 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
1-
import { ComponentFixture, TestBed } from '@angular/core/testing';
2-
1+
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
2+
import { of, BehaviorSubject, combineLatest } from 'rxjs';
33
import { RgwOverviewDashboardComponent } from './rgw-overview-dashboard.component';
4-
import { of } from 'rxjs';
4+
import { HttpClientTestingModule } from '@angular/common/http/testing';
5+
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
56
import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
67
import { RgwDaemon } from '../models/rgw-daemon';
7-
import { HttpClientTestingModule } from '@angular/common/http/testing';
8+
import { CardComponent } from '~/app/shared/components/card/card.component';
9+
import { CardRowComponent } from '~/app/shared/components/card-row/card-row.component';
810
import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
11+
import { NO_ERRORS_SCHEMA } from '@angular/core';
912
import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
10-
import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
1113
import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
12-
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
13-
import { HealthService } from '~/app/shared/api/health.service';
14-
import { CardRowComponent } from '~/app/shared/components/card-row/card-row.component';
15-
import { CardComponent } from '~/app/shared/components/card/card.component';
16-
import { NO_ERRORS_SCHEMA } from '@angular/core';
17-
import { configureTestBed } from '~/testing/unit-test-helper';
14+
import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
1815

1916
describe('RgwOverviewDashboardComponent', () => {
2017
let component: RgwOverviewDashboardComponent;
2118
let fixture: ComponentFixture<RgwOverviewDashboardComponent>;
19+
let listDaemonsSpy: jest.SpyInstance;
20+
let listRealmsSpy: jest.SpyInstance;
21+
let listZonegroupsSpy: jest.SpyInstance;
22+
let listZonesSpy: jest.SpyInstance;
23+
let fetchAndTransformBucketsSpy: jest.SpyInstance;
24+
let totalBucketsAndUsersSpy: jest.SpyInstance;
25+
26+
const totalNumObjectsSubject = new BehaviorSubject<number>(290);
27+
const totalUsedCapacitySubject = new BehaviorSubject<number>(9338880);
28+
const averageObjectSizeSubject = new BehaviorSubject<number>(1280);
29+
const bucketsCount = 2;
30+
const usersCount = 5;
2231
const daemon: RgwDaemon = {
2332
id: '8000',
2433
service_map_id: '4803',
@@ -47,95 +56,105 @@ describe('RgwOverviewDashboardComponent', () => {
4756
zones: ['zone4', 'zone5', 'zone6', 'zone7']
4857
};
4958

50-
const bucketAndUserList = {
51-
buckets_count: 2,
52-
users_count: 2
53-
};
54-
55-
const healthData = {
56-
total_objects: '290',
57-
total_pool_bytes_used: 9338880
58-
};
59-
60-
let listDaemonsSpy: jest.SpyInstance;
61-
let listZonesSpy: jest.SpyInstance;
62-
let listZonegroupsSpy: jest.SpyInstance;
63-
let listRealmsSpy: jest.SpyInstance;
64-
let listBucketsSpy: jest.SpyInstance;
65-
let healthDataSpy: jest.SpyInstance;
66-
67-
configureTestBed({
68-
declarations: [
69-
RgwOverviewDashboardComponent,
70-
CardComponent,
71-
CardRowComponent,
72-
DimlessBinaryPipe
73-
],
74-
schemas: [NO_ERRORS_SCHEMA],
75-
imports: [HttpClientTestingModule]
76-
});
77-
7859
beforeEach(() => {
60+
TestBed.configureTestingModule({
61+
declarations: [
62+
RgwOverviewDashboardComponent,
63+
CardComponent,
64+
CardRowComponent,
65+
DimlessBinaryPipe
66+
],
67+
schemas: [NO_ERRORS_SCHEMA],
68+
providers: [
69+
{ provide: RgwDaemonService, useValue: { list: jest.fn() } },
70+
{ provide: RgwRealmService, useValue: { list: jest.fn() } },
71+
{ provide: RgwZonegroupService, useValue: { list: jest.fn() } },
72+
{ provide: RgwZoneService, useValue: { list: jest.fn() } },
73+
{
74+
provide: RgwBucketService,
75+
useValue: {
76+
fetchAndTransformBuckets: jest.fn(),
77+
totalNumObjects$: totalNumObjectsSubject.asObservable(),
78+
totalUsedCapacity$: totalUsedCapacitySubject.asObservable(),
79+
averageObjectSize$: averageObjectSizeSubject.asObservable(),
80+
getTotalBucketsAndUsersLength: jest.fn()
81+
}
82+
}
83+
],
84+
imports: [HttpClientTestingModule]
85+
}).compileComponents();
86+
fixture = TestBed.createComponent(RgwOverviewDashboardComponent);
87+
component = fixture.componentInstance;
7988
listDaemonsSpy = jest
8089
.spyOn(TestBed.inject(RgwDaemonService), 'list')
8190
.mockReturnValue(of([daemon]));
91+
fetchAndTransformBucketsSpy = jest
92+
.spyOn(TestBed.inject(RgwBucketService), 'fetchAndTransformBuckets')
93+
.mockReturnValue(of(null));
94+
totalBucketsAndUsersSpy = jest
95+
.spyOn(TestBed.inject(RgwBucketService), 'getTotalBucketsAndUsersLength')
96+
.mockReturnValue(of({ buckets_count: bucketsCount, users_count: usersCount }));
8297
listRealmsSpy = jest
8398
.spyOn(TestBed.inject(RgwRealmService), 'list')
8499
.mockReturnValue(of(realmList));
85100
listZonegroupsSpy = jest
86101
.spyOn(TestBed.inject(RgwZonegroupService), 'list')
87102
.mockReturnValue(of(zonegroupList));
88103
listZonesSpy = jest.spyOn(TestBed.inject(RgwZoneService), 'list').mockReturnValue(of(zoneList));
89-
listBucketsSpy = jest
90-
.spyOn(TestBed.inject(RgwBucketService), 'getTotalBucketsAndUsersLength')
91-
.mockReturnValue(of(bucketAndUserList));
92-
healthDataSpy = jest
93-
.spyOn(TestBed.inject(HealthService), 'getClusterCapacity')
94-
.mockReturnValue(of(healthData));
95-
fixture = TestBed.createComponent(RgwOverviewDashboardComponent);
96-
component = fixture.componentInstance;
97104
fixture.detectChanges();
98105
});
99106

100-
it('should create', () => {
107+
it('should create the component', () => {
101108
expect(component).toBeTruthy();
102109
});
103110

104111
it('should render all cards', () => {
105-
fixture.detectChanges();
106112
const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card');
107113
expect(dashboardCards.length).toBe(5);
108114
});
109115

110-
it('should get corresponding data into Daemons', () => {
111-
expect(listDaemonsSpy).toHaveBeenCalled();
112-
expect(component.rgwDaemonCount).toEqual(1);
113-
});
114-
115-
it('should get corresponding data into Realms', () => {
116+
it('should get data for Realms', () => {
116117
expect(listRealmsSpy).toHaveBeenCalled();
117118
expect(component.rgwRealmCount).toEqual(2);
118119
});
119120

120-
it('should get corresponding data into Zonegroups', () => {
121+
it('should get data for Zonegroups', () => {
121122
expect(listZonegroupsSpy).toHaveBeenCalled();
122123
expect(component.rgwZonegroupCount).toEqual(3);
123124
});
124125

125-
it('should get corresponding data into Zones', () => {
126+
it('should get data for Zones', () => {
126127
expect(listZonesSpy).toHaveBeenCalled();
127128
expect(component.rgwZoneCount).toEqual(4);
128129
});
129130

130-
it('should get corresponding data into Buckets', () => {
131-
expect(listBucketsSpy).toHaveBeenCalled();
132-
expect(component.rgwBucketCount).toEqual(2);
133-
expect(component.UserCount).toEqual(2);
134-
});
135-
136-
it('should get corresponding data into Objects and capacity', () => {
137-
expect(healthDataSpy).toHaveBeenCalled();
138-
expect(component.objectCount).toEqual('290');
131+
it('should set component properties from services using combineLatest', fakeAsync(() => {
132+
component.interval = of(null).subscribe(() => {
133+
component.fetchDataSub = combineLatest([
134+
TestBed.inject(RgwDaemonService).list(),
135+
TestBed.inject(RgwBucketService).fetchAndTransformBuckets(),
136+
totalNumObjectsSubject.asObservable(),
137+
totalUsedCapacitySubject.asObservable(),
138+
averageObjectSizeSubject.asObservable(),
139+
TestBed.inject(RgwBucketService).getTotalBucketsAndUsersLength()
140+
]).subscribe(([daemonData, _, objectCount, usedCapacity, averageSize, bucketData]) => {
141+
component.rgwDaemonCount = daemonData.length;
142+
component.objectCount = objectCount;
143+
component.totalPoolUsedBytes = usedCapacity;
144+
component.averageObjectSize = averageSize;
145+
component.rgwBucketCount = bucketData.buckets_count;
146+
component.UserCount = bucketData.users_count;
147+
});
148+
});
149+
tick();
150+
expect(listDaemonsSpy).toHaveBeenCalled();
151+
expect(fetchAndTransformBucketsSpy).toHaveBeenCalled();
152+
expect(totalBucketsAndUsersSpy).toHaveBeenCalled();
153+
expect(component.rgwDaemonCount).toEqual(1);
154+
expect(component.objectCount).toEqual(290);
139155
expect(component.totalPoolUsedBytes).toEqual(9338880);
140-
});
156+
expect(component.averageObjectSize).toEqual(1280);
157+
expect(component.rgwBucketCount).toEqual(bucketsCount);
158+
expect(component.UserCount).toEqual(usersCount);
159+
}));
141160
});

0 commit comments

Comments
 (0)