From cec046e76f1c53809df197994cb29477f455bac3 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 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 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.ts - Resolved conflicts due to variable name change and few other import conflicts. --- .../cephfs-detail.component.html | 4 +- .../osd/osd-list/osd-list.component.html | 4 +- .../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 | 88 ++++++++++++++---- .../usage-bar/usage-bar.component.html | 4 +- .../usage-bar/usage-bar.component.ts | 10 +-- 8 files changed, 196 insertions(+), 28 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 f8a5217f9746b..f82a0005de498 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 @@ -54,8 +54,8 @@ - + - + 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 0a83df29efc19..560cfba014bed 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 @@ -24,8 +24,8 @@ 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 3dcff15fcacfe..aa487bf909ec0 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 @@ -16,3 +16,24 @@ [selection]="selection"> + + + + + + 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 2d4736dd71ab7..4ecb6ae1a3ea5 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 { ActionLabels } from '../../../shared/constants/app.constants'; import { TableActionsComponent } from '../../../shared/datatable/table-actions/table-actions.component'; import { SharedModule } from '../../../shared/shared.module'; @@ -20,6 +22,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; }); @@ -199,4 +206,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 bfdcd04679773..f16a98b48c938 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,7 @@ -import { Component, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, 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'; @@ -13,6 +14,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'; @@ -24,9 +27,13 @@ const BASE_URL = 'rgw/bucket'; styleUrls: ['./rgw-bucket-list.component.scss'], providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }] }) -export class RgwBucketListComponent { +export class RgwBucketListComponent implements OnInit { @ViewChild(TableComponent) table: TableComponent; + @ViewChild('bucketSizeTpl') + bucketSizeTpl: TemplateRef; + @ViewChild('bucketObjectTpl') + bucketObjectTpl: TemplateRef; permission: Permission; tableActions: CdTableAction[]; @@ -36,25 +43,16 @@ export class RgwBucketListComponent { 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 + public actionLabels: ActionLabelsI18n, + private changeDetectorRef: ChangeDetectorRef ) { 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 = { @@ -78,10 +76,70 @@ export class RgwBucketListComponent { this.tableActions = [addAction, editAction, deleteAction]; } + 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; + }); + } + getBucketList(context: CdTableFetchDataContext) { 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 65d9a89d4d8f1..c31a2eb4ed13a 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 0a3b12e24e5e2..0d4256d8728a6 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,21 +7,21 @@ import { Component, Input, OnChanges } from '@angular/core'; }) export class UsageBarComponent implements OnChanges { @Input() - totalBytes: number; + total: number; @Input() - usedBytes: number; + used: number; @Input() decimals = 0; + @Input() + isBinary = true; usedPercentage: number; freePercentage: number; - freeBytes: number; constructor() {} ngOnChanges() { - this.usedPercentage = this.totalBytes > 0 ? (this.usedBytes / this.totalBytes) * 100 : 0; + this.usedPercentage = this.total > 0 ? (this.used / this.total) * 100 : 0; this.freePercentage = 100 - this.usedPercentage; - this.freeBytes = this.totalBytes - this.usedBytes; } } -- 2.39.5