]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Display users current bucket quota usage
authorAvan Thakkar <athakkar@redhat.com>
Fri, 17 Apr 2020 08:51:48 +0000 (14:21 +0530)
committerAvan Thakkar <athakkar@redhat.com>
Thu, 25 Jun 2020 05:50:40 +0000 (11:20 +0530)
Fixes: https://tracker.ceph.com/issues/45011
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
(cherry picked from commit 966d887f7a5e0765b4210ab4edcb1cea3e03ac35)

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html
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/shared/components/usage-bar/usage-bar.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts

index 862a8b491a19995db7e53cdbe02b49d7bca23abe..c1d33d8e0d243543429364674dfac62cf97ee055 100644 (file)
@@ -31,8 +31,8 @@
 <!-- templates -->
 <ng-template #poolUsageTpl
              let-row="row">
-  <cd-usage-bar [totalBytes]="row.size"
-                [usedBytes]="row.used"></cd-usage-bar>
+  <cd-usage-bar [total]="row.size"
+                [used]="row.used"></cd-usage-bar>
 </ng-template>
 
 <ng-template #activityTmpl
index d1a5fb653dd0eaf6b828323875b383d3cd85a482..727d44b35f2baf90956e12324915ba707ec165f3 100644 (file)
@@ -76,7 +76,7 @@
 
 <ng-template #osdUsageTpl
              let-row="row">
-  <cd-usage-bar [totalBytes]="row.stats.stat_bytes"
-                [usedBytes]="row.stats.stat_bytes_used">
+  <cd-usage-bar [total]="row.stats.stat_bytes"
+                [used]="row.stats.stat_bytes_used">
   </cd-usage-bar>
 </ng-template>
index 02a98a5de7cd0fc3eff52af2340fc53b25018f39..1bc7dbcdb74211a4153d9a840f7717f94ab56820 100644 (file)
@@ -51,7 +51,7 @@
 <ng-template #poolUsageTpl
              let-row="row">
   <cd-usage-bar *ngIf="row.stats?.max_avail?.latest"
-                [totalBytes]="row.stats.bytes_used.latest + row.stats.max_avail.latest"
-                [usedBytes]="row.stats.bytes_used.latest">
+                [total]="row.stats.bytes_used.latest + row.stats.max_avail.latest"
+                [used]="row.stats.bytes_used.latest">
   </cd-usage-bar>
 </ng-template>
index a35475abec4e01613c12571ba8de182e3a947079..a8844d6575e948fa978698ae70eb0d5c4f18ee9c 100644 (file)
                          [selection]="expandedRow">
   </cd-rgw-bucket-details>
 </cd-table>
+
+<ng-template #bucketSizeTpl
+             let-row="row">
+  <cd-usage-bar *ngIf="row.bucket_quota.max_size > 0 && row.bucket_quota.enabled; else noSizeQuota"
+                [total]="row.bucket_quota.max_size"
+                [used]="row.bucket_size">
+  </cd-usage-bar>
+
+  <ng-template #noSizeQuota>No Limit</ng-template>
+</ng-template>
+
+<ng-template #bucketObjectTpl
+             let-row="row">
+  <cd-usage-bar *ngIf="row.bucket_quota.max_objects > 0 && row.bucket_quota.enabled; else noObjectQuota"
+                [total]="row.bucket_quota.max_objects"
+                [used]="row.num_objects"
+                [isBinary]="false">
+  </cd-usage-bar>
+
+  <ng-template #noObjectQuota>No Limit</ng-template>
+</ng-template>
index 435a151ad38ec80af400358c54c19e0ef4e78a78..4cc380c3d16ba06eb6a73c9b4be243f0b8d6df06 100644 (file)
@@ -5,12 +5,14 @@ import { RouterTestingModule } from '@angular/router/testing';
 
 import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
 import { ModalModule } from 'ngx-bootstrap/modal';
+import { of } from 'rxjs';
 
 import {
   configureTestBed,
   i18nProviders,
   PermissionHelper
 } from '../../../../testing/unit-test-helper';
+import { RgwBucketService } from '../../../shared/api/rgw-bucket.service';
 import { TableActionsComponent } from '../../../shared/datatable/table-actions/table-actions.component';
 import { SharedModule } from '../../../shared/shared.module';
 import { RgwBucketDetailsComponent } from '../rgw-bucket-details/rgw-bucket-details.component';
@@ -19,6 +21,8 @@ import { RgwBucketListComponent } from './rgw-bucket-list.component';
 describe('RgwBucketListComponent', () => {
   let component: RgwBucketListComponent;
   let fixture: ComponentFixture<RgwBucketListComponent>;
+  let rgwBucketService: RgwBucketService;
+  let rgwBucketServiceListSpy: jasmine.Spy;
 
   configureTestBed({
     declarations: [RgwBucketListComponent, RgwBucketDetailsComponent],
@@ -34,6 +38,9 @@ describe('RgwBucketListComponent', () => {
   });
 
   beforeEach(() => {
+    rgwBucketService = TestBed.get(RgwBucketService);
+    rgwBucketServiceListSpy = spyOn(rgwBucketService, 'list');
+    rgwBucketServiceListSpy.and.returnValue(of(null));
     fixture = TestBed.createComponent(RgwBucketListComponent);
     component = fixture.componentInstance;
   });
@@ -84,4 +91,86 @@ describe('RgwBucketListComponent', () => {
       }
     });
   });
+
+  it('should test if bucket data is tranformed correctly', () => {
+    rgwBucketServiceListSpy.and.returnValue(
+      of([
+        {
+          bucket: 'bucket',
+          owner: 'testid',
+          usage: {
+            'rgw.main': {
+              size_actual: 4,
+              num_objects: 2
+            },
+            'rgw.another': {
+              size_actual: 6,
+              num_objects: 6
+            }
+          },
+          bucket_quota: {
+            max_size: 20,
+            max_objects: 10,
+            enabled: true
+          }
+        }
+      ])
+    );
+    fixture.detectChanges();
+    expect(component.buckets).toEqual([
+      {
+        bucket: 'bucket',
+        owner: 'testid',
+        usage: {
+          'rgw.main': { size_actual: 4, num_objects: 2 },
+          'rgw.another': { size_actual: 6, num_objects: 6 }
+        },
+        bucket_quota: {
+          max_size: 20,
+          max_objects: 10,
+          enabled: true
+        },
+        bucket_size: 10,
+        num_objects: 8,
+        size_usage: 0.5,
+        object_usage: 0.8
+      }
+    ]);
+  });
+  it('should usage bars only if quota enabled', () => {
+    rgwBucketServiceListSpy.and.returnValue(
+      of([
+        {
+          bucket: 'bucket',
+          owner: 'testid',
+          bucket_quota: {
+            max_size: 1024,
+            max_objects: 10,
+            enabled: true
+          }
+        }
+      ])
+    );
+    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([
+        {
+          bucket: 'bucket',
+          owner: 'testid',
+          bucket_quota: {
+            max_size: 1024,
+            max_objects: 10,
+            enabled: false
+          }
+        }
+      ])
+    );
+    fixture.detectChanges();
+    const usageBars = fixture.debugElement.nativeElement.querySelectorAll('cd-usage-bar');
+    expect(usageBars.length).toBe(0);
+  });
 });
index 1a99482b5bbe5f0a0b9fec97d5405398cbdf6b33..a1f79c312c88924264a4d3d93d0c77e595ea6011 100644 (file)
@@ -1,6 +1,14 @@
-import { Component, NgZone, ViewChild } from '@angular/core';
+import {
+  ChangeDetectorRef,
+  Component,
+  NgZone,
+  OnInit,
+  TemplateRef,
+  ViewChild
+} from '@angular/core';
 
 import { I18n } from '@ngx-translate/i18n-polyfill';
+import * as _ from 'lodash';
 import { BsModalService } from 'ngx-bootstrap/modal';
 import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs';
 
@@ -15,6 +23,8 @@ import { CdTableColumn } from '../../../shared/models/cd-table-column';
 import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { Permission } from '../../../shared/models/permissions';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
 import { URLBuilderService } from '../../../shared/services/url-builder.service';
 
@@ -26,9 +36,13 @@ const BASE_URL = 'rgw/bucket';
   styleUrls: ['./rgw-bucket-list.component.scss'],
   providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
 })
-export class RgwBucketListComponent extends ListWithDetails {
+export class RgwBucketListComponent extends ListWithDetails implements OnInit {
   @ViewChild(TableComponent, { static: true })
   table: TableComponent;
+  @ViewChild('bucketSizeTpl', { static: true })
+  bucketSizeTpl: TemplateRef<any>;
+  @ViewChild('bucketObjectTpl', { static: true })
+  bucketObjectTpl: TemplateRef<any>;
 
   permission: Permission;
   tableActions: CdTableAction[];
@@ -40,27 +54,18 @@ export class RgwBucketListComponent extends ListWithDetails {
 
   constructor(
     private authStorageService: AuthStorageService,
+    private dimlessBinaryPipe: DimlessBinaryPipe,
+    private dimlessPipe: DimlessPipe,
     private rgwBucketService: RgwBucketService,
     private bsModalService: BsModalService,
     private i18n: I18n,
     private urlBuilder: URLBuilderService,
     public actionLabels: ActionLabelsI18n,
-    private ngZone: NgZone
+    private ngZone: NgZone,
+    private changeDetectorRef: ChangeDetectorRef
   ) {
     super();
     this.permission = this.authStorageService.getPermissions().rgw;
-    this.columns = [
-      {
-        name: this.i18n('Name'),
-        prop: 'bid',
-        flexGrow: 1
-      },
-      {
-        name: this.i18n('Owner'),
-        prop: 'owner',
-        flexGrow: 1
-      }
-    ];
     const getBucketUri = () =>
       this.selection.first() && `${encodeURIComponent(this.selection.first().bid)}`;
     const addAction: CdTableAction = {
@@ -88,6 +93,64 @@ export class RgwBucketListComponent extends ListWithDetails {
     this.timeConditionReached();
   }
 
+  ngOnInit() {
+    this.columns = [
+      {
+        name: this.i18n('Name'),
+        prop: 'bid',
+        flexGrow: 2
+      },
+      {
+        name: this.i18n('Owner'),
+        prop: 'owner',
+        flexGrow: 3
+      },
+      {
+        name: this.i18n('Used Capacity'),
+        prop: 'bucket_size',
+        flexGrow: 0.5,
+        pipe: this.dimlessBinaryPipe
+      },
+      {
+        name: this.i18n('Capacity Limit %'),
+        prop: 'size_usage',
+        cellTemplate: this.bucketSizeTpl,
+        flexGrow: 1
+      },
+      {
+        name: this.i18n('Objects'),
+        prop: 'num_objects',
+        flexGrow: 0.5,
+        pipe: this.dimlessPipe
+      },
+      {
+        name: this.i18n('Object Limit %'),
+        prop: 'object_usage',
+        cellTemplate: this.bucketObjectTpl,
+        flexGrow: 1
+      }
+    ];
+  }
+
+  transformBucketData() {
+    _.forEach(this.buckets, (bucketKey) => {
+      const usageList = bucketKey['usage'];
+      const maxBucketSize = bucketKey['bucket_quota']['max_size'];
+      const maxBucketObjects = bucketKey['bucket_quota']['max_objects'];
+      let totalBucketSize = 0;
+      let numOfObjects = 0;
+      _.forEach(usageList, (usageKey) => {
+        totalBucketSize = totalBucketSize + usageKey.size_actual;
+        numOfObjects = numOfObjects + usageKey.num_objects;
+      });
+      bucketKey['bucket_size'] = totalBucketSize;
+      bucketKey['num_objects'] = numOfObjects;
+      bucketKey['size_usage'] = maxBucketSize > 0 ? totalBucketSize / maxBucketSize : undefined;
+      bucketKey['object_usage'] =
+        maxBucketObjects > 0 ? numOfObjects / maxBucketObjects : undefined;
+    });
+  }
+
   timeConditionReached() {
     clearTimeout(this.staleTimeout);
     this.ngZone.runOutsideAngular(() => {
@@ -105,6 +168,8 @@ export class RgwBucketListComponent extends ListWithDetails {
     this.rgwBucketService.list().subscribe(
       (resp: object[]) => {
         this.buckets = resp;
+        this.transformBucketData();
+        this.changeDetectorRef.detectChanges();
       },
       () => {
         context.error();
index e6d3ef251dcd98efe8e3d3242bb5c9f33cc86eb0..42f995efdbbf86f940fc658460602ecb50064f07 100644 (file)
@@ -2,11 +2,11 @@
   <table>
     <tr>
       <td class="text-left">Used:&nbsp;</td>
-      <td class="text-right"><strong> {{ usedBytes | dimlessBinary }}</strong></td>
+      <td class="text-right"><strong> {{ isBinary ? (used | dimlessBinary) : (used | dimless) }}</strong></td>
     </tr>
     <tr>
       <td class="text-left">Free:&nbsp;</td>
-      <td class="'text-right"><strong>{{ freeBytes | dimlessBinary }}</strong></td>
+      <td class="'text-right"><strong>{{ isBinary ? (total - used | dimlessBinary) : (total - used | dimless) }}</strong></td>
     </tr>
   </table>
 </ng-template>
index e8b4ddc148eedb7012ffb39bdd4422c94e1a2701..40e110e3f27229a311319969d5e8b7e21bd98717 100644 (file)
@@ -7,19 +7,19 @@ import { Component, Input, OnChanges } from '@angular/core';
 })
 export class UsageBarComponent implements OnChanges {
   @Input()
-  totalBytes: number;
+  total: number;
   @Input()
-  usedBytes: number;
+  used: number;
+  @Input()
+  isBinary = true;
 
   usedPercentage: number;
   freePercentage: number;
-  freeBytes: number;
 
   constructor() {}
 
   ngOnChanges() {
-    this.usedPercentage = Math.round((this.usedBytes / this.totalBytes) * 100);
+    this.usedPercentage = this.total > 0 ? Math.round((this.used / this.total) * 100) : 0;
     this.freePercentage = 100 - this.usedPercentage;
-    this.freeBytes = this.totalBytes - this.usedBytes;
   }
 }