class RgwUserAccountsController(RESTController):
@allow_empty_body
- def create(self, account_name: Optional[str] = None,
- account_id: Optional[str] = None, email: Optional[str] = None):
- return RgwAccounts.create_account(account_name, account_id, email)
+ def create(self, account_name: Optional[str] = None, tenant: str = None,
+ account_id: Optional[str] = None, email: Optional[str] = None,
+ max_buckets: str = None, max_users: str = None,
+ max_roles: str = None, max_group: str = None,
+ max_access_keys: str = None):
+ return RgwAccounts.create_account(account_name, tenant, account_id, email,
+ max_buckets, max_users, max_roles,
+ max_group, max_access_keys)
def list(self, detailed: bool = False):
detailed = str_to_bool(detailed)
'max_size': (str, 'Max size')})
@RESTController.Resource(method='PUT', path='/quota')
@allow_empty_body
- def set_quota(self, quota_type: str, account_id: str, max_size: str, max_objects: str):
- return RgwAccounts.set_quota(quota_type, account_id, max_size, max_objects)
+ def set_quota(self, quota_type: str, account_id: str, max_size: str, max_objects: str,
+ enabled: bool):
+ return RgwAccounts.set_quota(quota_type, account_id, max_size, max_objects, enabled)
@EndpointDoc("Enable/Disable RGW Account/Bucket quota",
parameters={'account_id': (str, 'Account id')})
-export interface Accounts {
+export interface Account {
id: string;
tenant: string;
name: string;
--- /dev/null
+<!-- Account quota -->
+<div *ngIf="selection.quota">
+ <legend i18n>Account quota</legend>
+ <cd-table-key-value [data]="quota">
+ </cd-table-key-value>
+</div>
+
+<!-- Bucket quota -->
+<div *ngIf="selection.bucket_quota">
+ <legend i18n>Bucket quota</legend>
+ <cd-table-key-value [data]="bucket_quota">
+ </cd-table-key-value>
+</div>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwUserAccountsDetailsComponent } from './rgw-user-accounts-details.component';
+
+describe('RgwUserAccountsDetailsComponent', () => {
+ let component: RgwUserAccountsDetailsComponent;
+ let fixture: ComponentFixture<RgwUserAccountsDetailsComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [RgwUserAccountsDetailsComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(RgwUserAccountsDetailsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+
+@Component({
+ selector: 'cd-rgw-user-accounts-details',
+ templateUrl: './rgw-user-accounts-details.component.html',
+ styleUrls: ['./rgw-user-accounts-details.component.scss']
+})
+export class RgwUserAccountsDetailsComponent implements OnChanges {
+ @Input()
+ selection: any;
+ quota = {};
+ bucket_quota = {};
+
+ constructor(private dimlessBinary: DimlessBinaryPipe) {}
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.selection && changes.selection.currentValue) {
+ this.quota = this.createDisplayValues('quota');
+ this.bucket_quota = this.createDisplayValues('bucket_quota');
+ }
+ }
+
+ createDisplayValues(quota_type: string) {
+ return {
+ Enabled: this.selection[quota_type].enabled ? 'Yes' : 'No',
+ 'Maximum Size':
+ this.selection[quota_type].max_size <= -1
+ ? 'Unlimited'
+ : this.dimlessBinary.transform(this.selection[quota_type].max_size),
+ 'Maximum objects':
+ this.selection[quota_type].max_objects <= -1
+ ? 'Unlimited'
+ : this.selection[quota_type].max_objects
+ };
+ }
+}
--- /dev/null
+<div cdsCol
+ [columnNumbers]="{md: 4}">
+ <ng-container *cdFormLoading="loading">
+ <form name="accountForm"
+ #formDir="ngForm"
+ [formGroup]="accountForm"
+ novalidate>
+
+ <div i18n="form title"
+ class="form-header">{{ action | titlecase }} {{ resource | upperFirst }}
+ </div>
+
+ <!-- Account Name -->
+ <div class="form-item">
+ <cds-text-label label="Account Name"
+ for="acc_name"
+ cdRequiredField="Account Name"
+ [invalid]="!accountForm.controls.account_name.valid && accountForm.controls.account_name.dirty"
+ [invalidText]="accountIdError"
+ i18n>Account Name
+ <input cdsText
+ type="text"
+ placeholder="Enter account name"
+ id="acc_name"
+ name="acc_name"
+ formControlName="account_name"/>
+ </cds-text-label>
+ <ng-template #accountIdError>
+ <span *ngIf="accountForm.showError('account_name', formDir, 'required')"
+ class="invalid-feedback">
+ <ng-container i18n>This field is required.</ng-container>
+ </span>
+ </ng-template>
+ </div>
+ <!-- Tenant -->
+ <div class="form-item">
+ <cds-text-label label="tenant"
+ for="tenant"
+ i18n>Tenant
+ <input cdsText
+ type="text"
+ placeholder="Enter tenant"
+ id="tenant"
+ name="tenant"
+ formControlName="tenant"/>
+ </cds-text-label>
+ </div>
+
+ <!-- Email -->
+ <div class="form-item">
+ <cds-text-label label="email"
+ for="email"
+ [invalid]="!accountForm.controls.email.valid && accountForm.controls.email.dirty"
+ [invalidText]="emailError"
+ i18n>Email
+ <input cdsText
+ type="text"
+ placeholder="Enter email"
+ id="email"
+ name="email"
+ formControlName="email"/>
+ </cds-text-label>
+ <ng-template #emailError>
+ <span *ngIf="accountForm.showError('email', formDir, 'email')">
+ <ng-container i18n> Please enter a valid email </ng-container>
+ </span>
+ </ng-template>
+ </div>
+
+ <div class="form-item form-item-append"
+ cdsRow>
+ <div cdsCol>
+ <!-- Max. bucket mode -->
+ <ng-container *ngTemplateOutlet="selectModeTemplate;context: { formControl: 'max_buckets_mode' }"></ng-container>
+ </div>
+ <div cdsCol>
+ <!-- Max buckets -->
+ <span *ngIf="1 == accountForm.get('max_buckets_mode').value">
+ <ng-container *ngTemplateOutlet="accountMaxValueTemplate;context: { formControl: 'max_buckets' }"></ng-container>
+ </span>
+ </div>
+ </div>
+
+ <div class="form-item form-item-append"
+ cdsRow>
+ <div cdsCol>
+ <!-- Max. users mode -->
+ <ng-container *ngTemplateOutlet="selectModeTemplate;context: { formControl: 'max_users_mode' }"></ng-container>
+ </div>
+ <div cdsCol>
+ <!-- Max users -->
+ <span *ngIf="1 == accountForm.get('max_users_mode').value">
+ <ng-container *ngTemplateOutlet="accountMaxValueTemplate;context: { formControl: 'max_users' }"></ng-container>
+ </span>
+ </div>
+ </div>
+
+ <div class="form-item form-item-append"
+ cdsRow>
+ <div cdsCol>
+ <!-- Max. roles mode -->
+ <ng-container *ngTemplateOutlet="selectModeTemplate;context: { formControl: 'max_roles_mode' }"></ng-container>
+ </div>
+ <div cdsCol>
+ <!-- Max roles -->
+ <span *ngIf="1 == accountForm.get('max_roles_mode').value">
+ <ng-container *ngTemplateOutlet="accountMaxValueTemplate;context: { formControl: 'max_roles' }"></ng-container>
+ </span>
+ </div>
+ </div>
+
+ <div class="form-item form-item-append"
+ cdsRow>
+ <div cdsCol>
+ <!-- Max. group mode -->
+ <ng-container *ngTemplateOutlet="selectModeTemplate;context: { formControl: 'max_group_mode' }"></ng-container>
+ </div>
+ <div cdsCol>
+ <!-- Max group -->
+ <span *ngIf="1 == accountForm.get('max_group_mode').value">
+ <ng-container *ngTemplateOutlet="accountMaxValueTemplate;context: { formControl: 'max_group' }"></ng-container>
+ </span>
+ </div>
+ </div>
+
+ <div class="form-item form-item-append"
+ cdsRow>
+ <div cdsCol>
+ <!-- Max. acess keys mode -->
+ <ng-container *ngTemplateOutlet="selectModeTemplate;context: { formControl: 'max_access_keys_mode' }"></ng-container>
+ </div>
+ <div cdsCol>
+ <!-- Max acess keys -->
+ <span *ngIf="1 == accountForm.get('max_access_keys_mode').value">
+ <ng-container *ngTemplateOutlet="accountMaxValueTemplate;context: { formControl: 'max_access_keys' }"></ng-container>
+ </span>
+ </div>
+ </div>
+
+ <!-- Account Quota -->
+ <div class="form-item">
+ <ng-container *ngTemplateOutlet="quotaTemplate;context: { formControl: {
+ enabled: 'account_quota_enabled',
+ unlimitedSize: 'account_quota_max_size_unlimited',
+ maxSize: 'account_quota_max_size',
+ unlimitedObjects: 'account_quota_max_objects_unlimited',
+ maxObjects: 'account_quota_max_objects'
+ },
+ quotaType: 'account'
+ }">
+ </ng-container>
+ </div>
+
+ <!-- Bucket Quota -->
+ <div class="form-item">
+ <ng-container *ngTemplateOutlet="quotaTemplate;
+ context: {
+ formControl: {
+ enabled: 'bucket_quota_enabled',
+ unlimitedSize: 'bucket_quota_max_size_unlimited',
+ maxSize: 'bucket_quota_max_size',
+ unlimitedObjects: 'bucket_quota_max_objects_unlimited',
+ maxObjects: 'bucket_quota_max_objects'
+ },
+ quotaType: 'bucket'
+ }">
+ </ng-container>
+ </div>
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="formDir"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+
+ </form>
+ </ng-container>
+</div>
+
+<ng-template #selectModeTemplate
+ [formGroup]="accountForm"
+ let-formControl="formControl">
+ <cds-select [formControlName]="formControl"
+ [name]="formControl"
+ [id]="formControl"
+ label="{{formControl.split('_')[1] | upperFirst}} Mode"
+ (change)="onModeChange($event.target.value, formControl)">
+ <option value="-1">Disabled</option>
+ <option value="0">Unlimited</option>
+ <option value="1">Custom</option>
+ </cds-select>
+</ng-template>
+
+<ng-template #accountMaxValueTemplate
+ let-formControl="formControl"
+ [formGroup]="accountForm">
+<cds-number [id]="formControl"
+ [name]="formControl"
+ formControlName="{{formControl}}"
+ label="{{formControl.split('_')[0] | upperFirst}}. {{formControl.split('_').length > 2 ? formControl.split('_')[1]+' '+formControl.split('_')[2]: formControl.split('_')[1]}}"
+ [min]="1"
+ [invalid]="!accountForm.controls[formControl].valid && accountForm.controls[formControl].dirty"
+ [invalidText]="maxValError"></cds-number>
+<ng-template #maxValError>
+ <span *ngIf="accountForm.showError(formControl, formDir, 'required')"
+ i18n>This field is required.</span>
+ <span *ngIf="accountForm.showError(formControl, formDir, 'pattern')"
+ i18n>The entered value must be a number greater than 0</span>
+</ng-template>
+</ng-template>
+
+<ng-template #quotaTemplate
+ let-quotaType="quotaType"
+ let-formControl="formControl"
+ [formGroup]="accountForm">
+ <fieldset class="cds--fieldset">
+ <legend class="cds--label">{{quotaType | upperFirst}} Quota</legend>
+ <!-- Enabled -->
+ <cds-checkbox [formControlName]="formControl.enabled">
+ Enabled
+ </cds-checkbox >
+ <!-- Unlimited size -->
+ <cds-checkbox *ngIf="accountForm.controls[formControl.enabled].value"
+ [formControlName]="formControl.unlimitedSize">
+ Unlimited size
+ </cds-checkbox>
+ <!-- Maximum size -->
+ <div class="form-item"
+ *ngIf="accountForm.controls[formControl.enabled].value && !accountForm.getValue(formControl.unlimitedSize)">
+ <cds-text-label [label]="formControl.maxSize"
+ [for]="formControl.maxSize"
+ [invalid]="!accountForm.controls[formControl.maxSize].valid && accountForm.controls[formControl.maxSize].dirty"
+ [invalidText]="quotaSizeError"
+ i18n>Max. size
+ <input cdsText
+ type="text"
+ placeholder="Enter size"
+ [id]="formControl.maxSize"
+ [name]="formControl.maxSize"
+ [formControlName]="formControl.maxSize"
+ cdDimlessBinary/>
+ </cds-text-label>
+ <ng-template #quotaSizeError>
+ <span *ngIf="accountForm.showError(formControl.maxSize, formDir, 'required')"
+ i18n>This field is required.</span>
+ <span *ngIf="accountForm.showError(formControl.maxSize, formDir, 'quotaMaxSize')"
+ i18n>The value is not valid.</span>
+ <span *ngIf="accountForm.showError(formControl.maxSize, formDir, 'pattern')"
+ i18n>Size must be a number or in a valid format. eg: 5 GiB</span>
+ </ng-template>
+ </div>
+ <!-- Unlimited objects -->
+ <cds-checkbox *ngIf="accountForm.controls[formControl.enabled].value"
+ [formControlName]="formControl.unlimitedObjects">
+ Unlimited objects
+ </cds-checkbox>
+ <!-- Maximum objects -->
+ <div class="form-item"
+ *ngIf="accountForm.controls[formControl.enabled].value && !accountForm.getValue(formControl.unlimitedObjects)">
+ <cds-text-label [label]="formControl.maxObjects"
+ [for]="formControl.maxObjects"
+ [invalid]="!accountForm.controls[formControl.maxObjects].valid && accountForm.controls[formControl.maxObjects].dirty"
+ [invalidText]="quotaObjectError"
+ i18n>Max. objects
+ <input cdsText
+ type="number"
+ placeholder="Enter number of objects"
+ [id]="formControl.maxObjects"
+ [name]="formControl.maxObjects"
+ [formControlName]="formControl.maxObjects"/>
+ </cds-text-label>
+ <ng-template #quotaObjectError>
+ <span *ngIf="accountForm.showError(formControl.maxObjects, formDir, 'required')"
+ i18n>This field is required.</span>
+ <span *ngIf="accountForm.showError(formControl.maxObjects, formDir, 'pattern')"
+ i18n>Please enter a valid number</span>
+ </ng-template>
+ </div>
+ </fieldset>
+</ng-template>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwUserAccountsFormComponent } from './rgw-user-accounts-form.component';
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { RouterTestingModule } from '@angular/router/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule } from 'ngx-toastr';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { of } from 'rxjs';
+import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service';
+import { ModalModule } from 'carbon-components-angular';
+
+class MockRgwUserAccountsService {
+ create = jest.fn().mockReturnValue(of(null));
+}
+
+describe('RgwUserAccountsFormComponent', () => {
+ let component: RgwUserAccountsFormComponent;
+ let fixture: ComponentFixture<RgwUserAccountsFormComponent>;
+ let rgwUserAccountsService: MockRgwUserAccountsService;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [RgwUserAccountsFormComponent],
+ imports: [
+ ComponentsModule,
+ ToastrModule.forRoot(),
+ HttpClientTestingModule,
+ PipesModule,
+ RouterTestingModule,
+ ModalModule
+ ],
+ providers: [{ provide: RgwUserAccountsService, useClass: MockRgwUserAccountsService }]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(RgwUserAccountsFormComponent);
+ rgwUserAccountsService = (TestBed.inject(
+ RgwUserAccountsService
+ ) as unknown) as MockRgwUserAccountsService;
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should call create method of MockRgwUserAccountsService and show success notification', () => {
+ component.editing = false;
+ const spy = jest.spyOn(component, 'submit');
+ const createDataSpy = jest.spyOn(rgwUserAccountsService, 'create').mockReturnValue(of(null));
+ component.submit();
+ expect(spy).toHaveBeenCalled();
+ expect(createDataSpy).toHaveBeenCalled();
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+import { AbstractControl, ValidationErrors, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { Account } from '../models/rgw-user-accounts';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdValidators, isEmptyInputValue } from '~/app/shared/forms/cd-validators';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { Observable, concat as observableConcat } from 'rxjs';
+
+@Component({
+ selector: 'cd-rgw-user-accounts-form',
+ templateUrl: './rgw-user-accounts-form.component.html',
+ styleUrls: ['./rgw-user-accounts-form.component.scss']
+})
+export class RgwUserAccountsFormComponent extends CdForm {
+ accountForm: CdFormGroup;
+ action: string;
+ resource: string;
+ editing: boolean = false;
+ submitObservables: Observable<Object>[] = [];
+
+ constructor(
+ private router: Router,
+ private actionLabels: ActionLabelsI18n,
+ private rgwUserAccountsService: RgwUserAccountsService,
+ private notificationService: NotificationService,
+ private formBuilder: CdFormBuilder
+ ) {
+ super();
+ this.editing = this.router.url.includes('rgw/accounts/edit');
+ this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
+ this.resource = $localize`Account`;
+ this.createForm();
+ this.loadingReady();
+ }
+
+ private createForm() {
+ this.accountForm = this.formBuilder.group({
+ account_id: [''],
+ tenant: [''],
+ account_name: ['', Validators.required],
+ email: ['', CdValidators.email],
+ max_users_mode: [1],
+ max_users: [
+ 1000,
+ [CdValidators.requiredIf({ max_users_mode: '1' }), CdValidators.number(false)]
+ ],
+ max_roles_mode: [1],
+ max_roles: [
+ 1000,
+ [CdValidators.requiredIf({ max_roles_mode: '1' }), CdValidators.number(false)]
+ ],
+ max_group_mode: [1],
+ max_group: [
+ 1000,
+ [CdValidators.requiredIf({ max_group_mode: '1' }), CdValidators.number(false)]
+ ],
+ max_access_keys_mode: [1],
+ max_access_keys: [
+ 4,
+ [CdValidators.requiredIf({ max_access_keys_mode: '1' }), CdValidators.number(false)]
+ ],
+ max_buckets_mode: [1],
+ max_buckets: [
+ 1000,
+ [CdValidators.requiredIf({ max_buckets_mode: '1' }), CdValidators.number(false)]
+ ],
+ account_quota_enabled: [false],
+ account_quota_max_size_unlimited: [true],
+ account_quota_max_size: [
+ null,
+ [
+ CdValidators.composeIf(
+ {
+ account_quota_enabled: true,
+ account_quota_max_size_unlimited: false
+ },
+ [Validators.required, this.quotaMaxSizeValidator]
+ )
+ ]
+ ],
+ account_quota_max_objects_unlimited: [true],
+ account_quota_max_objects: [
+ null,
+ [
+ CdValidators.requiredIf({
+ account_quota_enabled: true,
+ account_quota_max_objects_unlimited: false
+ }),
+ Validators.pattern(/^[0-9]+$/)
+ ]
+ ],
+ bucket_quota_enabled: [false],
+ bucket_quota_max_size_unlimited: [true],
+ bucket_quota_max_size: [
+ null,
+ [
+ CdValidators.composeIf(
+ {
+ bucket_quota_enabled: true,
+ bucket_quota_max_size_unlimited: false
+ },
+ [Validators.required, this.quotaMaxSizeValidator]
+ )
+ ]
+ ],
+ bucket_quota_max_objects_unlimited: [true],
+ bucket_quota_max_objects: [
+ null,
+ [
+ CdValidators.requiredIf({
+ bucket_quota_enabled: true,
+ bucket_quota_max_objects_unlimited: false
+ }),
+ Validators.pattern(/^[0-9]+$/)
+ ]
+ ]
+ });
+ }
+
+ /**
+ * Validate the quota maximum size, e.g. 1096, 1K, 30M or 1.9MiB.
+ */
+ quotaMaxSizeValidator(control: AbstractControl): ValidationErrors | null {
+ if (isEmptyInputValue(control.value)) {
+ return null;
+ }
+ const m = RegExp('^(\\d+(\\.\\d+)?)\\s*(B|K(B|iB)?|M(B|iB)?|G(B|iB)?|T(B|iB)?)?$', 'i').exec(
+ control.value
+ );
+ if (m === null) {
+ return { quotaMaxSize: true };
+ }
+ const bytes = new FormatterService().toBytes(control.value);
+ return bytes < 1024 ? { quotaMaxSize: true } : null;
+ }
+
+ submit() {
+ let notificationTitle: string = '';
+ if (this.accountForm.invalid) {
+ return;
+ }
+
+ if (this.accountForm.pending) {
+ this.accountForm.setErrors({ cdSubmitButton: true });
+ return;
+ }
+
+ if (!this.editing) {
+ const formvalue = this.accountForm.value;
+ const createPayload = {
+ account_id: formvalue.account_id,
+ account_name: formvalue.account_name,
+ email: formvalue.email,
+ tenant: formvalue.tenant,
+ max_users: this.getValueFromFormControl('max_users'),
+ max_buckets: this.getValueFromFormControl('max_buckets'),
+ max_roles: this.getValueFromFormControl('max_roles'),
+ max_group: this.getValueFromFormControl('max_group'),
+ max_access_keys: this.getValueFromFormControl('max_access_keys')
+ };
+ notificationTitle = $localize`Account created successfully`;
+ this.rgwUserAccountsService.create(createPayload).subscribe({
+ next: (account: Account) => {
+ this.accountForm.get('account_id').setValue(account.id);
+ this.setQuotaConfig();
+ this.notificationService.show(NotificationType.success, notificationTitle);
+ },
+ error: () => {
+ // Reset the 'Submit' button.
+ this.accountForm.setErrors({ cdSubmitButton: true });
+ }
+ });
+ }
+ }
+
+ setQuotaConfig() {
+ const accountId: string = this.accountForm.get('account_id').value;
+ // Check if account quota has been modified.
+ if (this._isQuotaConfDirty('account')) {
+ const accountQuotaArgs = this._getQuotaArgs('account');
+ this.submitObservables.push(
+ this.rgwUserAccountsService.setQuota(accountId, accountQuotaArgs)
+ );
+ }
+ // Check if bucket quota has been modified.
+ if (this._isQuotaConfDirty('bucket')) {
+ const bucketQuotaArgs = this._getQuotaArgs('bucket');
+ this.submitObservables.push(this.rgwUserAccountsService.setQuota(accountId, bucketQuotaArgs));
+ }
+ // Finally execute all observables one by one in serial.
+ observableConcat(...this.submitObservables).subscribe({
+ error: () => {
+ // Reset the 'Submit' button.
+ this.accountForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.goToListView();
+ }
+ });
+ if (this.submitObservables.length == 0) {
+ this.goToListView();
+ }
+ }
+
+ /**
+ * Helper function to get the arguments for the API request when any
+ * quota configuration has been modified.
+ */
+ private _getQuotaArgs(quotaType: string) {
+ const result = {
+ quota_type: quotaType,
+ enabled: this.accountForm.getValue(`${quotaType}_quota_enabled`),
+ max_size: '-1',
+ max_objects: '-1'
+ };
+ if (!this.accountForm.getValue(`${quotaType}_quota_max_size_unlimited`)) {
+ // Convert the given value to bytes.
+ const bytes = new FormatterService().toBytes(
+ this.accountForm.getValue(`${quotaType}_quota_max_size`)
+ );
+ // Finally convert the value to KiB.
+ result['max_size'] = (bytes / 1024).toFixed(0) as any;
+ }
+ if (!this.accountForm.getValue(`${quotaType}_quota_max_objects_unlimited`)) {
+ result['max_objects'] = `${this.accountForm.getValue(`${quotaType}_quota_max_objects`)}`;
+ }
+ return result;
+ }
+
+ /**
+ * Check if any quota has been modified.
+ * @return {Boolean} Returns TRUE if the quota has been modified.
+ */
+ private _isQuotaConfDirty(quotaType: string): boolean {
+ if (this.accountForm.get(`${quotaType}_quota_enabled`).value) {
+ return [
+ `${quotaType}_quota_enabled`,
+ `${quotaType}_quota_max_size_unlimited`,
+ `${quotaType}_quota_max_size`,
+ `${quotaType}_quota_max_objects_unlimited`,
+ `${quotaType}_quota_max_objects`
+ ].some((path) => {
+ return this.accountForm.get(path).dirty;
+ });
+ }
+ return false;
+ }
+
+ onModeChange(mode: string, formControlName: string) {
+ if (mode === '1') {
+ // If 'Custom' mode is selected, then ensure that the form field
+ // 'Max. buckets' contains a valid value. Set it to default if
+ // necessary.
+ if (!this.accountForm.get(formControlName).valid) {
+ this.accountForm.patchValue({
+ [formControlName]: 1000
+ });
+ }
+ }
+ }
+
+ goToListView(): void {
+ this.router.navigate(['rgw/accounts']);
+ }
+
+ getValueFromFormControl(formControlName: string) {
+ const formvalue = this.accountForm.value;
+ return formvalue[`${formControlName}_mode`] == 1
+ ? formvalue[formControlName]
+ : formvalue[`${formControlName}_mode`];
+ }
+}
[columns]="columns"
columnMode="flex"
selectionType="multiClick"
- [hasDetails]="false"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)"
identifier="id"
(fetchData)="getAccountsList($event)">
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
+ <cd-rgw-user-accounts-details *cdTableDetail
+ [selection]="expandedRow">
+ </cd-rgw-user-accounts-details>
</cd-table>
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RgwUserAccountsComponent } from './rgw-user-accounts.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule } from 'ngx-toastr';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ComponentsModule } from '~/app/shared/components/components.module';
describe('RgwUserAccountsComponent', () => {
let component: RgwUserAccountsComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
- declarations: [RgwUserAccountsComponent]
+ declarations: [RgwUserAccountsComponent],
+ imports: [
+ ComponentsModule,
+ ToastrModule.forRoot(),
+ HttpClientTestingModule,
+ PipesModule,
+ RouterTestingModule
+ ]
}).compileComponents();
fixture = TestBed.createComponent(RgwUserAccountsComponent);
import { Component, OnInit, ViewChild } from '@angular/core';
-import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
import { TableComponent } from '~/app/shared/datatable/table/table.component';
import { CdTableAction } from '~/app/shared/models/cd-table-action';
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
import { Permission } from '~/app/shared/models/permissions';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
-import { Accounts } from '../models/rgw-user-accounts';
+import { Account } from '../models/rgw-user-accounts';
import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { Router } from '@angular/router';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+
+const BASE_URL = 'rgw/accounts';
@Component({
selector: 'cd-rgw-user-accounts',
templateUrl: './rgw-user-accounts.component.html',
- styleUrls: ['./rgw-user-accounts.component.scss']
+ styleUrls: ['./rgw-user-accounts.component.scss'],
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
})
-export class RgwUserAccountsComponent implements OnInit {
+export class RgwUserAccountsComponent extends ListWithDetails implements OnInit {
@ViewChild(TableComponent, { static: true })
table: TableComponent;
permission: Permission;
tableActions: CdTableAction[] = [];
columns: CdTableColumn[] = [];
- accounts: Accounts[] = [];
+ accounts: Account[] = [];
selection: CdTableSelection = new CdTableSelection();
constructor(
private authStorageService: AuthStorageService,
public actionLabels: ActionLabelsI18n,
+ private router: Router,
private rgwUserAccountsService: RgwUserAccountsService
- ) {}
+ ) {
+ super();
+ }
ngOnInit() {
this.permission = this.authStorageService.getPermissions().rgw;
flexGrow: 1
}
];
+ const addAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ click: () => this.router.navigate([`${BASE_URL}/${URLVerbs.CREATE}`]),
+ name: this.actionLabels.CREATE,
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+ };
+ this.tableActions = [addAction];
}
- getAccountsList(context: CdTableFetchDataContext) {
+ getAccountsList(context?: CdTableFetchDataContext) {
this.rgwUserAccountsService.list(true).subscribe({
- next: (accounts) => {
+ next: (accounts: Account[]) => {
this.accounts = accounts;
},
error: () => {
CodeSnippetModule,
InputModule,
CheckboxModule,
- TreeviewModule
+ TreeviewModule,
+ SelectModule,
+ NumberModule
} from 'carbon-components-angular';
import { CephSharedModule } from '../shared/ceph-shared.module';
import { RgwUserAccountsComponent } from './rgw-user-accounts/rgw-user-accounts.component';
+import { RgwUserAccountsFormComponent } from './rgw-user-accounts-form/rgw-user-accounts-form.component';
+import { RgwUserAccountsDetailsComponent } from './rgw-user-accounts-details/rgw-user-accounts-details.component';
@NgModule({
imports: [
IconModule,
NgbProgressbar,
InputModule,
- CheckboxModule
+ CheckboxModule,
+ SelectModule,
+ NumberModule
],
exports: [
RgwDaemonListComponent,
RgwMultisiteSyncFlowModalComponent,
RgwMultisiteSyncPipeModalComponent,
RgwMultisiteTabsComponent,
- RgwUserAccountsComponent
+ RgwUserAccountsComponent,
+ RgwUserAccountsFormComponent,
+ RgwUserAccountsDetailsComponent
],
providers: [TitleCasePipe]
})
{
path: 'accounts',
data: { breadcrumbs: 'Accounts' },
- children: [{ path: '', component: RgwUserAccountsComponent }]
+ children: [
+ { path: '', component: RgwUserAccountsComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: RgwUserAccountsFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ }
+ ]
},
{
path: 'roles',
import { RgwUserAccountsService } from './rgw-user-accounts.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { Accounts } from '~/app/ceph/rgw/models/rgw-user-accounts';
+import { Account } from '~/app/ceph/rgw/models/rgw-user-accounts';
-const mockAccountData: Accounts[] = [
+const mockAccountData: Account[] = [
{
id: 'RGW80617806988089685',
tenant: '',
}
return this.http.get(this.url, { params });
}
+
+ get(account_id: string): Observable<any> {
+ let params = new HttpParams();
+ if (account_id) {
+ params = params.append('account_id', account_id);
+ }
+ return this.http.get(`${this.url}/get`, { params });
+ }
+
+ create(payload: any): Observable<any> {
+ return this.http.post(this.url, payload);
+ }
+
+ setQuota(
+ account_id: string,
+ payload: { quota_type: string; max_size: string; max_objects: string; enabled: boolean }
+ ) {
+ return this.http.put(`${this.url}/${account_id}/quota`, payload);
+ }
}
type: integer
email:
type: string
+ max_access_keys:
+ type: string
+ max_buckets:
+ type: string
+ max_group:
+ type: string
+ max_roles:
+ type: string
+ max_users:
+ type: string
+ tenant:
+ type: string
type: object
responses:
'201':
return cls.send_rgw_cmd(get_account_cmd)
@classmethod
- def create_account(cls, account_name: Optional[str] = None,
- account_id: Optional[str] = None, email: Optional[str] = None):
+ def create_account(cls, account_name: Optional[str] = None, tenant: str = None,
+ account_id: Optional[str] = None, email: Optional[str] = None,
+ max_buckets: str = None, max_users: str = None,
+ max_roles: str = None, max_group: str = None,
+ max_access_keys: str = None):
create_accounts_cmd = ['account', 'create']
if account_name:
if email:
create_accounts_cmd += ['--email', email]
+ if tenant:
+ create_accounts_cmd += ['--tenant', tenant]
+
+ if max_buckets:
+ create_accounts_cmd += ['--max_buckets', str(max_buckets)]
+
+ if max_users:
+ create_accounts_cmd += ['--max_users', str(max_users)]
+
+ if max_roles:
+ create_accounts_cmd += ['--max_roles', str(max_roles)]
+
+ if max_group:
+ create_accounts_cmd += ['--max_groups', str(max_group)]
+
+ if max_access_keys:
+ create_accounts_cmd += ['--max_access_keys', str(max_access_keys)]
+
return cls.send_rgw_cmd(create_accounts_cmd)
@classmethod
return cls.send_rgw_cmd(account_stats_cmd)
@classmethod
- def set_quota(cls, quota_type: str, account_id: str, max_size: str, max_objects: str):
+ def set_quota(cls, quota_type: str, account_id: str, max_size: str, max_objects: str,
+ enabled: bool):
set_quota_cmd = ['quota', 'set', '--quota-scope', quota_type, '--account-id', account_id,
'--max-size', max_size, '--max-objects', max_objects]
-
+ if enabled:
+ cls.set_quota_status(quota_type, account_id, 'enable')
+ else:
+ cls.set_quota_status(quota_type, account_id, 'disable')
return cls.send_rgw_cmd(set_quota_cmd)
@classmethod