mgr/dashboard: Display users current quota usage

Fixes: https://tracker.ceph.com/issues/45965

Signed-off-by: Avan Thakkar <athakkar@redhat.com>
This commit is contained in:
Avan Thakkar 2020-08-01 16:06:13 +05:30 committed by Avan Thakkar
parent ba56112835
commit 9456884c29
9 changed files with 201 additions and 50 deletions

View 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')

View 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']

View 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>

View 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);

View 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();

View File

@ -19,3 +19,26 @@
[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>

View 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);
});
});

View 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 = () =>

View File

@ -7940,6 +7940,11 @@ paths:
name: daemon_name
schema:
type: string
- default: true
in: query
name: stats
schema:
type: boolean
responses:
'200':
content: