Hide the different meanings of max_buckets from the user by improving the UI.
Fixes: https://tracker.ceph.com/issues/44322
Signed-off-by: Volker Theile <vtheile@suse.com>
/**
* Helper method to select an option inside a select element.
* This method will also expect that the option was set.
+ * @param option The option text (not value) to be selected.
*/
async selectOption(selectionName: string, option: string) {
await element(by.cssContainingText(`select[name=${selectionName}] option`, option)).click();
/**
* Helper method to expect a set option inside a select element.
+ * @param option The selected option text (not value) that is to
+ * be expected.
*/
async expectSelectOption(selectionName: string, option: string) {
return expect(
await element(by.id('email')).sendKeys(email);
// Enter max buckets
+ await this.selectOption('max_buckets_mode', 'Custom');
await element(by.id('max_buckets')).click();
await element(by.id('max_buckets')).clear();
await element(by.id('max_buckets')).sendKeys(maxbuckets);
await element(by.id('email')).sendKeys(new_email);
// Change the max buckets field
+ await this.selectOption('max_buckets_mode', 'Custom');
await element(by.id('max_buckets')).click();
await element(by.id('max_buckets')).clear();
await element(by.id('max_buckets')).sendKeys(new_maxbuckets);
);
// check that username field is marked invalid if username has been cleared off
+ await username_field.click();
for (let i = 0; i < uname.length; i++) {
await username_field.sendKeys(protractor.Key.BACK_SPACE);
}
);
// put negative max buckets to make field invalid
+ await this.expectSelectOption('max_buckets_mode', 'Custom');
await element(by.id('max_buckets')).click();
await element(by.id('max_buckets')).clear();
await element(by.id('max_buckets')).sendKeys('-5');
await expect(element(by.id('max_buckets')).getAttribute('class')).toContain('ng-invalid');
await username_field.click(); // trigger validation check
await expect(element(by.css('#max_buckets + .invalid-feedback')).getText()).toMatch(
- 'The entered value must be >= 0.'
+ 'The entered value must be >= 1.'
);
await this.navigateTo();
);
// put negative max buckets to make field invalid
+ await this.expectSelectOption('max_buckets_mode', 'Custom');
await element(by.id('max_buckets')).click();
await element(by.id('max_buckets')).clear();
await element(by.id('max_buckets')).sendKeys('-5');
await expect(element(by.id('max_buckets')).getAttribute('class')).toContain('ng-invalid');
await element(by.id('email')).click(); // trigger validation check
await expect(element(by.css('#max_buckets + .invalid-feedback')).getText()).toMatch(
- 'The entered value must be >= 0.'
+ 'The entered value must be >= 1.'
);
await this.navigateTo();
<tr>
<td i18n
class="bold">Maximum buckets</td>
- <td>{{ user.max_buckets }}</td>
+ <td>{{ user.max_buckets | map:maxBucketsMap }}</td>
</tr>
<tr *ngIf="user.subusers && user.subusers.length">
<td i18n
// Details tab
user: any;
+ maxBucketsMap: {};
// Keys tab
keys: any = [];
flexGrow: 1
}
];
+ this.maxBucketsMap = {
+ '-1': this.i18n('Disabled'),
+ 0: this.i18n('Unlimited')
+ };
}
ngOnChanges() {
<!-- Max. buckets -->
<div class="form-group row">
- <label class="cd-col-form-label required"
- for="max_buckets"
+ <label class="cd-col-form-label"
+ for="max_buckets_mode"
i18n>Max. buckets</label>
+ <div class="cd-col-form-input">
+ <select class="form-control custom-select"
+ formControlName="max_buckets_mode"
+ name="max_buckets_mode"
+ id="max_buckets_mode">
+ <option i18n
+ value="-1">Disabled</option>
+ <option i18n
+ value="0">Unlimited</option>
+ <option i18n
+ value="1">Custom</option>
+ </select>
+ </div>
+ </div>
+ <div *ngIf="1 == userForm.get('max_buckets_mode').value"
+ class="form-group row">
+ <label class="cd-col-form-label"></label>
<div class="cd-col-form-input">
<input id="max_buckets"
class="form-control"
type="number"
- formControlName="max_buckets">
+ formControlName="max_buckets"
+ min="1">
<span class="invalid-feedback"
*ngIf="userForm.showError('max_buckets', frm, 'required')"
i18n>This field is required.</span>
<span class="invalid-feedback"
*ngIf="userForm.showError('max_buckets', frm, 'min')"
- i18n>The entered value must be >= 0.</span>
+ i18n>The entered value must be >= 1.</span>
</div>
</div>
}));
});
+ describe('max buckets', () => {
+ it('disable creation (create)', () => {
+ spyOn(rgwUserService, 'create');
+ formHelper.setValue('max_buckets_mode', -1, true);
+ component.onSubmit();
+ expect(rgwUserService.create).toHaveBeenCalledWith({
+ access_key: '',
+ display_name: null,
+ email: '',
+ generate_key: true,
+ max_buckets: -1,
+ secret_key: '',
+ suspended: false,
+ uid: null
+ });
+ });
+
+ it('disable creation (edit)', () => {
+ spyOn(rgwUserService, 'update');
+ component.editing = true;
+ formHelper.setValue('max_buckets_mode', -1, true);
+ component.onSubmit();
+ expect(rgwUserService.update).toHaveBeenCalledWith(null, {
+ display_name: null,
+ email: null,
+ max_buckets: -1,
+ suspended: false
+ });
+ });
+
+ it('unlimited buckets (create)', () => {
+ spyOn(rgwUserService, 'create');
+ formHelper.setValue('max_buckets_mode', 0, true);
+ component.onSubmit();
+ expect(rgwUserService.create).toHaveBeenCalledWith({
+ access_key: '',
+ display_name: null,
+ email: '',
+ generate_key: true,
+ max_buckets: 0,
+ secret_key: '',
+ suspended: false,
+ uid: null
+ });
+ });
+
+ it('unlimited buckets (edit)', () => {
+ spyOn(rgwUserService, 'update');
+ component.editing = true;
+ formHelper.setValue('max_buckets_mode', 0, true);
+ component.onSubmit();
+ expect(rgwUserService.update).toHaveBeenCalledWith(null, {
+ display_name: null,
+ email: null,
+ max_buckets: 0,
+ suspended: false
+ });
+ });
+
+ it('custom (create)', () => {
+ spyOn(rgwUserService, 'create');
+ formHelper.setValue('max_buckets_mode', 1, true);
+ formHelper.setValue('max_buckets', 100, true);
+ component.onSubmit();
+ expect(rgwUserService.create).toHaveBeenCalledWith({
+ access_key: '',
+ display_name: null,
+ email: '',
+ generate_key: true,
+ max_buckets: 100,
+ secret_key: '',
+ suspended: false,
+ uid: null
+ });
+ });
+
+ it('custom (edit)', () => {
+ spyOn(rgwUserService, 'update');
+ component.editing = true;
+ formHelper.setValue('max_buckets_mode', 1, true);
+ formHelper.setValue('max_buckets', 100, true);
+ component.onSubmit();
+ expect(rgwUserService.update).toHaveBeenCalledWith(null, {
+ display_name: null,
+ email: null,
+ max_buckets: 100,
+ suspended: false
+ });
+ });
+ });
+
describe('submit form', () => {
let notificationService: NotificationService;
[CdValidators.email],
[CdValidators.unique(this.rgwUserService.emailExists, this.rgwUserService)]
],
- max_buckets: [1000, [Validators.required, Validators.min(0)]],
+ max_buckets_mode: [1],
+ max_buckets: [
+ 1000,
+ [
+ CdValidators.requiredIf({ max_buckets_mode: '1' }),
+ CdValidators.number(false),
+ Validators.min(1)
+ ]
+ ],
suspended: [false],
// S3 key
generate_key: [true],
const defaults = _.clone(this.userForm.value);
// Extract the values displayed in the form.
let value = _.pick(resp[0], _.keys(this.userForm.value));
+ // Map the max. buckets values.
+ switch (value['max_buckets']) {
+ case -1:
+ value['max_buckets_mode'] = -1;
+ value['max_buckets'] = '';
+ break;
+ case 0:
+ value['max_buckets_mode'] = 0;
+ value['max_buckets'] = '';
+ break;
+ default:
+ value['max_buckets_mode'] = 1;
+ break;
+ }
// Map the quota values.
['user', 'bucket'].forEach((type) => {
const quota = resp[1][type + '_quota'];
* @return {Boolean} Returns TRUE if the general user settings have been modified.
*/
private _isGeneralDirty(): boolean {
- return ['display_name', 'email', 'max_buckets', 'suspended'].some((path) => {
- return this.userForm.get(path).dirty;
- });
+ return ['display_name', 'email', 'max_buckets_mode', 'max_buckets', 'suspended'].some(
+ (path) => {
+ return this.userForm.get(path).dirty;
+ }
+ );
}
/**
secret_key: this.userForm.getValue('secret_key')
});
}
+ const maxBucketsMode = parseInt(this.userForm.getValue('max_buckets_mode'), 10);
+ if (_.includes([-1, 0], maxBucketsMode)) {
+ // -1 => Disable bucket creation.
+ // 0 => Unlimited bucket creation.
+ _.merge(result, { max_buckets: maxBucketsMode });
+ }
return result;
}
* configuration has been modified.
*/
private _getUpdateArgs() {
- const result: Record<string, string> = {};
+ const result: Record<string, any> = {};
const keys = ['display_name', 'email', 'max_buckets', 'suspended'];
for (const key of keys) {
result[key] = this.userForm.getValue(key);
}
+ const maxBucketsMode = parseInt(this.userForm.getValue('max_buckets_mode'), 10);
+ if (_.includes([-1, 0], maxBucketsMode)) {
+ // -1 => Disable bucket creation.
+ // 0 => Unlimited bucket creation.
+ result['max_buckets'] = maxBucketsMode;
+ }
return result;
}
{
name: this.i18n('Max. buckets'),
prop: 'max_buckets',
- flexGrow: 1
+ flexGrow: 1,
+ cellTransformation: CellTemplate.map,
+ customTemplateConfig: {
+ '-1': this.i18n('Disabled'),
+ 0: this.i18n('Unlimited')
+ }
}
];
const getUserUri = () =>
return this.http.get(`${this.url}/${uid}/quota`);
}
- create(args: Record<string, string>) {
+ create(args: Record<string, any>) {
let params = new HttpParams();
_.keys(args).forEach((key) => {
params = params.append(key, args[key]);
return this.http.post(this.url, null, { params: params });
}
- update(uid: string, args: Record<string, string>) {
+ update(uid: string, args: Record<string, any>) {
let params = new HttpParams();
_.keys(args).forEach((key) => {
params = params.append(key, args[key]);
<span *ngIf="!last"> </span>
</span>
</ng-template>
+
+<ng-template #mapTpl
+ let-column="column"
+ let-value="value">
+ <span>{{ value | map:column?.customTemplateConfig }}</span>
+</ng-template>
classAddingTpl: TemplateRef<any>;
@ViewChild('badgeTpl', { static: true })
badgeTpl: TemplateRef<any>;
+ @ViewChild('mapTpl', { static: true })
+ mapTpl: TemplateRef<any>;
// This is the array with the items to be shown.
@Input()
this.cellTemplates.executing = this.executingTpl;
this.cellTemplates.classAdding = this.classAddingTpl;
this.cellTemplates.badge = this.badgeTpl;
+ this.cellTemplates.map = this.mapTpl;
}
useCustomClass(value: any): string {
// supports an optional custom configuration:
// {
// ...
+ // cellTransformation: CellTemplate.badge,
// customTemplateConfig: {
// class?: string; // Additional class name.
// prefix?: any; // Prefix of the value to be displayed.
// }
// }
// }
- badge = 'badge'
+ badge = 'badge',
+ // Maps the value using the given dictionary.
+ // {
+ // ...
+ // cellTransformation: CellTemplate.map,
+ // customTemplateConfig: {
+ // [key: any]: any
+ // }
+ // }
+ map = 'map'
}
--- /dev/null
+import { MapPipe } from './map.pipe';
+
+describe('MapPipe', () => {
+ const pipe = new MapPipe();
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('map value [1]', () => {
+ expect(pipe.transform('foo')).toBe('foo');
+ });
+
+ it('map value [2]', () => {
+ expect(pipe.transform('foo', { '-1': 'disabled', 0: 'unlimited' })).toBe('foo');
+ });
+
+ it('map value [3]', () => {
+ expect(pipe.transform(-1, { '-1': 'disabled', 0: 'unlimited' })).toBe('disabled');
+ });
+
+ it('map value [4]', () => {
+ expect(pipe.transform(0, { '-1': 'disabled', 0: 'unlimited' })).toBe('unlimited');
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+import * as _ from 'lodash';
+
+@Pipe({
+ name: 'map'
+})
+export class MapPipe implements PipeTransform {
+ transform(value: string | number, map?: object): any {
+ if (!_.isPlainObject(map)) {
+ return value;
+ }
+ return _.get(map, value, value);
+ }
+}
import { IscsiBackstorePipe } from './iscsi-backstore.pipe';
import { JoinPipe } from './join.pipe';
import { LogPriorityPipe } from './log-priority.pipe';
+import { MapPipe } from './map.pipe';
import { MillisecondsPipe } from './milliseconds.pipe';
import { NotAvailablePipe } from './not-available.pipe';
import { OrdinalPipe } from './ordinal.pipe';
IopsPipe,
UpperFirstPipe,
RbdConfigurationSourcePipe,
- DurationPipe
+ DurationPipe,
+ MapPipe
],
exports: [
ArrayPipe,
IopsPipe,
UpperFirstPipe,
RbdConfigurationSourcePipe,
- DurationPipe
+ DurationPipe,
+ MapPipe
],
providers: [
ArrayPipe,
IopsPipe,
MillisecondsPipe,
NotAvailablePipe,
- UpperFirstPipe
+ UpperFirstPipe,
+ MapPipe
]
})
export class PipesModule {}