From c7623497d44e3739bbdf67a596426060c0d30953 Mon Sep 17 00:00:00 2001 From: Avan Thakkar Date: Fri, 17 Apr 2020 14:21:48 +0530 Subject: [PATCH] mgr/dashboard: Display users current bucket quota usage Fixes: https://tracker.ceph.com/issues/45011 Signed-off-by: Avan Thakkar (cherry picked from commit 966d887f7a5e0765b4210ab4edcb1cea3e03ac35) (cherry picked from commit 4fabba0bb772d480dcddc83272c83e7714726fc1) Conflicts: 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.spec.ts - Replace cd-usage-bar totalBytes and usedBytes with total and bytes Signed-off-by: Ernesto Puerta --- .../cephfs-detail.component.html | 4 +- .../osd/osd-list/osd-list.component.html | 27 +----- .../pool/pool-list/pool-list.component.html | 4 +- .../rgw-bucket-list.component.html | 21 ++++ .../rgw-bucket-list.component.spec.ts | 89 +++++++++++++++++ .../rgw-bucket-list.component.ts | 95 ++++++++++++++++--- .../usage-bar/usage-bar.component.html | 4 +- .../usage-bar/usage-bar.component.ts | 10 +- 8 files changed, 204 insertions(+), 50 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.html index 862a8b491a19..c1d33d8e0d24 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.html @@ -31,8 +31,8 @@ - + - + @@ -68,27 +68,6 @@ i18n>The {selection.hasSingleSelection, select, true {OSD is} false {OSDs are}} not safe to be {{ actionDescription }}! {{ message }} - OSD {{ osdIds | join }} will be + OSD {{ getSelectedOsdIds() | join }} will be {{ actionDescription }} if you proceed. - - - - -
-
- - -
-
-
-
-
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html index 8d6205817d81..40f68d8b64a3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html @@ -30,8 +30,8 @@ + [total]="row.stats.bytes_used.latest + row.stats.max_avail.latest" + [used]="row.stats.bytes_used.latest"> 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 a35475abec4e..a8844d6575e9 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 @@ -22,3 +22,24 @@ [selection]="expandedRow"> + + + + + + 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 98cd5fed526c..340552b17f18 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 @@ -5,12 +5,14 @@ import { RouterTestingModule } from '@angular/router/testing'; import { ModalModule } from 'ngx-bootstrap/modal'; import { TabsModule } from 'ngx-bootstrap/tabs'; +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; + 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); + }); }); 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 1a99482b5bbe..a1f79c312c88 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,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; + @ViewChild('bucketObjectTpl', { static: true }) + bucketObjectTpl: TemplateRef; 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(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html index e75d09087843..ebab648817b0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html @@ -2,11 +2,11 @@ - + - +
Used:  {{ usedBytes | dimlessBinary }} {{ isBinary ? (used | dimlessBinary) : (used | dimless) }}
Free: {{ freeBytes | dimlessBinary }}{{ isBinary ? (total - used | dimlessBinary) : (total - used | dimless) }}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts index e8b4ddc148ee..40e110e3f272 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts @@ -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; } } -- 2.47.3