]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Display users current quota usage 36402/head
authorAvan Thakkar <athakkar@redhat.com>
Sat, 1 Aug 2020 10:36:13 +0000 (16:06 +0530)
committerAvan Thakkar <athakkar@localhost.localdomain>
Thu, 18 Mar 2021 13:18:37 +0000 (18:48 +0530)
Fixes: https://tracker.ceph.com/issues/45965
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
qa/tasks/mgr/dashboard/test_rgw.py
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts
src/pybind/mgr/dashboard/openapi.yaml

index 17495bf6718fe67281d77f58f4e0fe781f66e86a..5029c12d3225963ae564b51b07ce8e6d723d64ff 100644 (file)
@@ -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')
index 9e41e5a4d6927658751a5270f170e4ed36215b6f..1826a304306d5bfcebb4357ac60a6152f85baf0a 100644 (file)
@@ -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']
index 45bedb46f304d4cb2b09b8248740a0071b74d307..b5e75841afe639a90b6bc657e8d80442d473bfa1 100644 (file)
@@ -27,7 +27,8 @@
                 [used]="row.bucket_size">
   </cd-usage-bar>
 
-  <ng-template #noSizeQuota>No Limit</ng-template>
+  <ng-template #noSizeQuota
+               i18n>No Limit</ng-template>
 </ng-template>
 
 <ng-template #bucketObjectTpl
@@ -38,5 +39,6 @@
                 [isBinary]="false">
   </cd-usage-bar>
 
-  <ng-template #noObjectQuota>No Limit</ng-template>
+  <ng-template #noObjectQuota
+               i18n>No Limit</ng-template>
 </ng-template>
index a158a31bae53f9de3f61e2be46bf39ed96d9c155..3e48c34e8b97b285a2f5f464602b59172d7eea31 100644 (file)
@@ -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);
index 000f6c59d8e67295b5adb3a3ab4be718ee5ebf4f..0adc33a1057d6ec387cb13968f32011805431cc5 100644 (file)
@@ -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();
index d751069689e675ea266003461e2b645aa1d1ca4e..6c6d7677ec70d2948f362eabb61f3595278bd90e 100644 (file)
                        [selection]="expandedRow">
   </cd-rgw-user-details>
 </cd-table>
+
+<ng-template #userSizeTpl
+             let-row="row">
+  <cd-usage-bar *ngIf="row.user_quota.max_size > 0 && row.user_quota.enabled; else noSizeQuota"
+                [total]="row.user_quota.max_size"
+                [used]="row.stats.size_actual">
+  </cd-usage-bar>
+
+  <ng-template #noSizeQuota
+               i18n>No Limit</ng-template>
+</ng-template>
+
+<ng-template #userObjectTpl
+             let-row="row">
+  <cd-usage-bar *ngIf="row.user_quota.max_objects > 0 && row.user_quota.enabled; else noObjectQuota"
+                [total]="row.user_quota.max_objects"
+                [used]="row.stats.num_objects"
+                [isBinary]="false">
+  </cd-usage-bar>
+
+  <ng-template #noObjectQuota
+               i18n>No Limit</ng-template>
+</ng-template>
index adfd4513bb4dc5eb99dd3f1f432969f8e2452906..379957d88ec73d521126932cdb7aecf4552fb6b5 100644 (file)
@@ -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<RgwUserListComponent>;
+  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);
+  });
 });
index 9a90e572d64890b2dbd2a766dac62223047b4e8c..a2eacd9b9aadad15f3d6cc338058ba5659eb1b83 100644 (file)
@@ -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<any>;
+  @ViewChild('userObjectTpl', { static: true })
+  userObjectTpl: TemplateRef<any>;
   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 = () =>
index 5ecd9d5df86a742929cab8c98bf0723fdb5cef9c..d0f489c197b8f45cbc2920402a4cb1d88952c71f 100644 (file)
@@ -7940,6 +7940,11 @@ paths:
         name: daemon_name
         schema:
           type: string
+      - default: true
+        in: query
+        name: stats
+        schema:
+          type: boolean
       responses:
         '200':
           content: