From c796e645d8689e2166ae2b2d91bbfba431f9a6cf Mon Sep 17 00:00:00 2001 From: Avan Thakkar Date: Sat, 1 Aug 2020 16:06:13 +0530 Subject: [PATCH] mgr/dashboard: Display users current quota usage Fixes: https://tracker.ceph.com/issues/45965 Signed-off-by: Avan Thakkar (cherry picked from commit 9456884c2996d31ce9c39e39011d295c5e62bcf7) --- qa/tasks/mgr/dashboard/test_rgw.py | 11 ++- src/pybind/mgr/dashboard/controllers/rgw.py | 7 +- .../rgw-bucket-list.component.html | 6 +- .../rgw-bucket-list.component.spec.ts | 15 ++- .../rgw-bucket-list.component.ts | 65 ++++++------- .../rgw-user-list.component.html | 23 +++++ .../rgw-user-list.component.spec.ts | 95 ++++++++++++++++++- .../rgw-user-list/rgw-user-list.component.ts | 24 ++++- src/pybind/mgr/dashboard/openapi.yaml | 5 + 9 files changed, 201 insertions(+), 50 deletions(-) diff --git a/qa/tasks/mgr/dashboard/test_rgw.py b/qa/tasks/mgr/dashboard/test_rgw.py index 17495bf6718fe..5029c12d32259 100644 --- a/qa/tasks/mgr/dashboard/test_rgw.py +++ b/qa/tasks/mgr/dashboard/test_rgw.py @@ -62,8 +62,8 @@ class RgwTestCase(DashboardTestCase): cls._radosgw_admin_cmd(['user', 'rm', '--uid=teuth-test-user', '--purge-data']) super(RgwTestCase, cls).tearDownClass() - def get_rgw_user(self, uid): - return self._get('/api/rgw/user/{}'.format(uid)) + def get_rgw_user(self, uid, stats=True): + return self._get('/api/rgw/user/{}?stats={}'.format(uid, stats)) class RgwApiCredentialsTest(RgwTestCase): @@ -510,6 +510,13 @@ class RgwUserTest(RgwTestCase): self.assertStatus(200) self._assert_user_data(data) self.assertEqual(data['user_id'], 'admin') + self.assertTrue(data['stats']) + self.assertIsInstance(data['stats'], dict) + # Test without stats. + data = self.get_rgw_user('admin', False) + self.assertStatus(200) + self._assert_user_data(data) + self.assertEqual(data['user_id'], 'admin') def test_list(self): data = self._get('/api/rgw/user') diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 9e41e5a4d6927..1826a304306d5 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -364,9 +364,10 @@ class RgwUser(RgwRESTController): marker = result['marker'] return users - def get(self, uid, daemon_name=None): - # type: (str, Optional[str]) -> dict - result = self.proxy(daemon_name, 'GET', 'user', {'uid': uid}) + def get(self, uid, daemon_name=None, stats=True) -> dict: + query_params = '?stats' if stats else '' + result = self.proxy(daemon_name, 'GET', 'user{}'.format(query_params), + {'uid': uid, 'stats': stats}) if not self._keys_allowed(): del result['keys'] del result['swift_keys'] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html index 45bedb46f304d..b5e75841afe63 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html @@ -27,7 +27,8 @@ [used]="row.bucket_size"> - No Limit + No Limit - No Limit + No Limit diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts index a158a31bae53f..3e48c34e8b97b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts @@ -33,14 +33,16 @@ describe('RgwBucketListComponent', () => { beforeEach(() => { rgwBucketService = TestBed.inject(RgwBucketService); rgwBucketServiceListSpy = spyOn(rgwBucketService, 'list'); - rgwBucketServiceListSpy.and.returnValue(of(null)); + rgwBucketServiceListSpy.and.returnValue(of([])); fixture = TestBed.createComponent(RgwBucketListComponent); component = fixture.componentInstance; + spyOn(component, 'timeConditionReached').and.stub(); + fixture.detectChanges(); }); it('should create', () => { - fixture.detectChanges(); expect(component).toBeTruthy(); + expect(rgwBucketServiceListSpy).toHaveBeenCalledTimes(1); }); it('should test all TableActions combinations', () => { @@ -109,7 +111,8 @@ describe('RgwBucketListComponent', () => { } ]) ); - fixture.detectChanges(); + component.getBucketList(null); + expect(rgwBucketServiceListSpy).toHaveBeenCalledTimes(2); expect(component.buckets).toEqual([ { bucket: 'bucket', @@ -130,6 +133,7 @@ describe('RgwBucketListComponent', () => { } ]); }); + it('should usage bars only if quota enabled', () => { rgwBucketServiceListSpy.and.returnValue( of([ @@ -144,10 +148,13 @@ describe('RgwBucketListComponent', () => { } ]) ); + component.getBucketList(null); + expect(rgwBucketServiceListSpy).toHaveBeenCalledTimes(2); fixture.detectChanges(); const usageBars = fixture.debugElement.nativeElement.querySelectorAll('cd-usage-bar'); expect(usageBars.length).toBe(2); }); + it('should not show any usage bars if quota disabled', () => { rgwBucketServiceListSpy.and.returnValue( of([ @@ -162,6 +169,8 @@ describe('RgwBucketListComponent', () => { } ]) ); + component.getBucketList(null); + expect(rgwBucketServiceListSpy).toHaveBeenCalledTimes(2); fixture.detectChanges(); const usageBars = fixture.debugElement.nativeElement.querySelectorAll('cd-usage-bar'); expect(usageBars.length).toBe(0); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts index 000f6c59d8e67..0adc33a1057d6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts @@ -1,11 +1,4 @@ -import { - ChangeDetectorRef, - Component, - NgZone, - OnInit, - TemplateRef, - ViewChild -} from '@angular/core'; +import { Component, NgZone, OnInit, TemplateRef, ViewChild } from '@angular/core'; import _ from 'lodash'; import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs'; @@ -60,39 +53,13 @@ export class RgwBucketListComponent extends ListWithDetails implements OnInit { private modalService: ModalService, private urlBuilder: URLBuilderService, public actionLabels: ActionLabelsI18n, - private ngZone: NgZone, - private changeDetectorRef: ChangeDetectorRef + private ngZone: NgZone ) { super(); - this.permission = this.authStorageService.getPermissions().rgw; - const getBucketUri = () => - this.selection.first() && `${encodeURIComponent(this.selection.first().bid)}`; - const addAction: CdTableAction = { - permission: 'create', - icon: Icons.add, - routerLink: () => this.urlBuilder.getCreate(), - name: this.actionLabels.CREATE, - canBePrimary: (selection: CdTableSelection) => !selection.hasSelection - }; - const editAction: CdTableAction = { - permission: 'update', - icon: Icons.edit, - routerLink: () => this.urlBuilder.getEdit(getBucketUri()), - name: this.actionLabels.EDIT - }; - const deleteAction: CdTableAction = { - permission: 'delete', - icon: Icons.destroy, - click: () => this.deleteAction(), - disable: () => !this.selection.hasSelection, - name: this.actionLabels.DELETE, - canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection - }; - this.tableActions = [addAction, editAction, deleteAction]; - this.timeConditionReached(); } ngOnInit() { + this.permission = this.authStorageService.getPermissions().rgw; this.columns = [ { name: $localize`Name`, @@ -129,6 +96,31 @@ export class RgwBucketListComponent extends ListWithDetails implements OnInit { flexGrow: 0.8 } ]; + const getBucketUri = () => + this.selection.first() && `${encodeURIComponent(this.selection.first().bid)}`; + const addAction: CdTableAction = { + permission: 'create', + icon: Icons.add, + routerLink: () => this.urlBuilder.getCreate(), + name: this.actionLabels.CREATE, + canBePrimary: (selection: CdTableSelection) => !selection.hasSelection + }; + const editAction: CdTableAction = { + permission: 'update', + icon: Icons.edit, + routerLink: () => this.urlBuilder.getEdit(getBucketUri()), + name: this.actionLabels.EDIT + }; + const deleteAction: CdTableAction = { + permission: 'delete', + icon: Icons.destroy, + click: () => this.deleteAction(), + disable: () => !this.selection.hasSelection, + name: this.actionLabels.DELETE, + canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection + }; + this.tableActions = [addAction, editAction, deleteAction]; + this.timeConditionReached(); } transformBucketData() { @@ -171,7 +163,6 @@ export class RgwBucketListComponent extends ListWithDetails implements OnInit { (resp: object[]) => { this.buckets = resp; this.transformBucketData(); - this.changeDetectorRef.detectChanges(); }, () => { context.error(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html index d751069689e67..6c6d7677ec70d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html @@ -19,3 +19,26 @@ [selection]="expandedRow"> + + + + + + No Limit + + + + + + + No Limit + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts index adfd4513bb4dc..379957d88ec73 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts @@ -4,6 +4,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { RgwUserService } from '~/app/shared/api/rgw-user.service'; import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component'; import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper'; @@ -12,6 +15,8 @@ import { RgwUserListComponent } from './rgw-user-list.component'; describe('RgwUserListComponent', () => { let component: RgwUserListComponent; let fixture: ComponentFixture; + let rgwUserService: RgwUserService; + let rgwUserServiceListSpy: jasmine.Spy; configureTestBed({ declarations: [RgwUserListComponent], @@ -20,13 +25,18 @@ describe('RgwUserListComponent', () => { }); beforeEach(() => { + rgwUserService = TestBed.inject(RgwUserService); + rgwUserServiceListSpy = spyOn(rgwUserService, 'list'); + rgwUserServiceListSpy.and.returnValue(of([])); fixture = TestBed.createComponent(RgwUserListComponent); component = fixture.componentInstance; + spyOn(component, 'timeConditionReached').and.stub(); + fixture.detectChanges(); }); it('should create', () => { - fixture.detectChanges(); expect(component).toBeTruthy(); + expect(rgwUserServiceListSpy).toHaveBeenCalledTimes(1); }); it('should test all TableActions combinations', () => { @@ -70,4 +80,87 @@ describe('RgwUserListComponent', () => { } }); }); + + it('should test if rgw-user data is tranformed correctly', () => { + rgwUserServiceListSpy.and.returnValue( + of([ + { + user_id: 'testid', + stats: { + size_actual: 6, + num_objects: 6 + }, + user_quota: { + max_size: 20, + max_objects: 10, + enabled: true + } + } + ]) + ); + component.getUserList(null); + expect(rgwUserServiceListSpy).toHaveBeenCalledTimes(2); + expect(component.users).toEqual([ + { + user_id: 'testid', + stats: { + size_actual: 6, + num_objects: 6 + }, + user_quota: { + max_size: 20, + max_objects: 10, + enabled: true + } + } + ]); + }); + + it('should usage bars only if quota enabled', () => { + rgwUserServiceListSpy.and.returnValue( + of([ + { + user_id: 'testid', + stats: { + size_actual: 6, + num_objects: 6 + }, + user_quota: { + max_size: 1024, + max_objects: 10, + enabled: true + } + } + ]) + ); + component.getUserList(null); + expect(rgwUserServiceListSpy).toHaveBeenCalledTimes(2); + fixture.detectChanges(); + const usageBars = fixture.debugElement.nativeElement.querySelectorAll('cd-usage-bar'); + expect(usageBars.length).toBe(2); + }); + + it('should not show any usage bars if quota disabled', () => { + rgwUserServiceListSpy.and.returnValue( + of([ + { + user_id: 'testid', + stats: { + size_actual: 6, + num_objects: 6 + }, + user_quota: { + max_size: 1024, + max_objects: 10, + enabled: false + } + } + ]) + ); + component.getUserList(null); + expect(rgwUserServiceListSpy).toHaveBeenCalledTimes(2); + fixture.detectChanges(); + const usageBars = fixture.debugElement.nativeElement.querySelectorAll('cd-usage-bar'); + expect(usageBars.length).toBe(0); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts index 9a90e572d6489..a2eacd9b9aada 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts @@ -1,5 +1,6 @@ -import { Component, NgZone, ViewChild } from '@angular/core'; +import { Component, NgZone, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import * as _ from 'lodash'; import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs'; import { RgwUserService } from '~/app/shared/api/rgw-user.service'; @@ -27,9 +28,13 @@ const BASE_URL = 'rgw/user'; styleUrls: ['./rgw-user-list.component.scss'], providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }] }) -export class RgwUserListComponent extends ListWithDetails { +export class RgwUserListComponent extends ListWithDetails implements OnInit { @ViewChild(TableComponent, { static: true }) table: TableComponent; + @ViewChild('userSizeTpl', { static: true }) + userSizeTpl: TemplateRef; + @ViewChild('userObjectTpl', { static: true }) + userObjectTpl: TemplateRef; permission: Permission; tableActions: CdTableAction[]; columns: CdTableColumn[] = []; @@ -47,6 +52,9 @@ export class RgwUserListComponent extends ListWithDetails { private ngZone: NgZone ) { super(); + } + + ngOnInit() { this.permission = this.authStorageService.getPermissions().rgw; this.columns = [ { @@ -85,6 +93,18 @@ export class RgwUserListComponent extends ListWithDetails { '-1': $localize`Disabled`, 0: $localize`Unlimited` } + }, + { + name: $localize`Capacity Limit %`, + prop: 'size_usage', + cellTemplate: this.userSizeTpl, + flexGrow: 0.8 + }, + { + name: $localize`Object Limit %`, + prop: 'object_usage', + cellTemplate: this.userObjectTpl, + flexGrow: 0.8 } ]; const getUserUri = () => diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 5ecd9d5df86a7..d0f489c197b8f 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -7940,6 +7940,11 @@ paths: name: daemon_name schema: type: string + - default: true + in: query + name: stats + schema: + type: boolean responses: '200': content: -- 2.39.5