From: Naman Munet Date: Thu, 26 Dec 2024 11:48:03 +0000 (+0530) Subject: mgr/dashboard: added Create Account Functionality X-Git-Tag: v20.0.0~305^2~3 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=7db16e1b471c4a3d3c7e8091a88dc6df0aa96239;p=ceph.git mgr/dashboard: added Create Account Functionality Fixes: https://tracker.ceph.com/issues/69140 Signed-off-by: Naman Munet --- diff --git a/src/pybind/mgr/dashboard/controllers/rgw_iam.py b/src/pybind/mgr/dashboard/controllers/rgw_iam.py index 458bbbb73218..1a293a7a17af 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw_iam.py +++ b/src/pybind/mgr/dashboard/controllers/rgw_iam.py @@ -11,9 +11,14 @@ from . import APIDoc, APIRouter, EndpointDoc, RESTController, allow_empty_body 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) @@ -41,8 +46,9 @@ class RgwUserAccountsController(RESTController): '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')}) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-accounts.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-accounts.ts index 2cc05048ec4d..2d1d446e2aba 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-accounts.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-user-accounts.ts @@ -1,4 +1,4 @@ -export interface Accounts { +export interface Account { id: string; tenant: string; name: string; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.html new file mode 100644 index 000000000000..77a4008580d6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.html @@ -0,0 +1,13 @@ + +
+ Account quota + + +
+ + +
+ Bucket quota + + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.spec.ts new file mode 100644 index 000000000000..ac43cfe4aa62 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RgwUserAccountsDetailsComponent } from './rgw-user-accounts-details.component'; + +describe('RgwUserAccountsDetailsComponent', () => { + let component: RgwUserAccountsDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RgwUserAccountsDetailsComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(RgwUserAccountsDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.ts new file mode 100644 index 000000000000..a8062052090d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.ts @@ -0,0 +1,37 @@ +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 + }; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html new file mode 100644 index 000000000000..279d36de94c4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html @@ -0,0 +1,278 @@ +
+ +
+ +
{{ action | titlecase }} {{ resource | upperFirst }} +
+ + +
+ Account Name + + + + + This field is required. + + +
+ +
+ Tenant + + +
+ + +
+ Email + + + + + Please enter a valid email + + +
+ +
+
+ + +
+
+ + + + +
+
+ +
+
+ + +
+
+ + + + +
+
+ +
+
+ + +
+
+ + + + +
+
+ +
+
+ + +
+
+ + + + +
+
+ +
+
+ + +
+
+ + + + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+ + + + + + + + + + + + + This field is required. + The entered value must be a number greater than 0 + + + + +
+ {{quotaType | upperFirst}} Quota + + + Enabled + + + + Unlimited size + + +
+ Max. size + + + + This field is required. + The value is not valid. + Size must be a number or in a valid format. eg: 5 GiB + +
+ + + Unlimited objects + + +
+ Max. objects + + + + This field is required. + Please enter a valid number + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts new file mode 100644 index 000000000000..0cd52faffc6c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts @@ -0,0 +1,56 @@ +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; + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts new file mode 100644 index 000000000000..7d2734b61f63 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts @@ -0,0 +1,279 @@ +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[] = []; + + 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`]; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.html index b10972a492a4..bddb749b466a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.html @@ -12,7 +12,8 @@ [columns]="columns" columnMode="flex" selectionType="multiClick" - [hasDetails]="false" + [hasDetails]="true" + (setExpandedRow)="setExpandedRow($event)" (updateSelection)="updateSelection($event)" identifier="id" (fetchData)="getAccountsList($event)"> @@ -21,4 +22,8 @@ [selection]="selection" [tableActions]="tableActions"> + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.spec.ts index d6bdd8a270c8..ff25b3ca17e7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.spec.ts @@ -1,6 +1,11 @@ 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; @@ -8,7 +13,14 @@ describe('RgwUserAccountsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [RgwUserAccountsComponent] + declarations: [RgwUserAccountsComponent], + imports: [ + ComponentsModule, + ToastrModule.forRoot(), + HttpClientTestingModule, + PipesModule, + RouterTestingModule + ] }).compileComponents(); fixture = TestBed.createComponent(RgwUserAccountsComponent); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.ts index 64686d251c3c..882a3f40cd2e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.ts @@ -1,6 +1,6 @@ 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'; @@ -8,28 +8,38 @@ import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data 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; @@ -80,11 +90,19 @@ export class RgwUserAccountsComponent implements OnInit { 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: () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index caee3d69b13a..012e7c2fd21a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -73,10 +73,14 @@ import { 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: [ @@ -104,7 +108,9 @@ import { RgwUserAccountsComponent } from './rgw-user-accounts/rgw-user-accounts. IconModule, NgbProgressbar, InputModule, - CheckboxModule + CheckboxModule, + SelectModule, + NumberModule ], exports: [ RgwDaemonListComponent, @@ -156,7 +162,9 @@ import { RgwUserAccountsComponent } from './rgw-user-accounts/rgw-user-accounts. RgwMultisiteSyncFlowModalComponent, RgwMultisiteSyncPipeModalComponent, RgwMultisiteTabsComponent, - RgwUserAccountsComponent + RgwUserAccountsComponent, + RgwUserAccountsFormComponent, + RgwUserAccountsDetailsComponent ], providers: [TitleCasePipe] }) @@ -189,7 +197,14 @@ const routes: Routes = [ { path: 'accounts', data: { breadcrumbs: 'Accounts' }, - children: [{ path: '', component: RgwUserAccountsComponent }] + children: [ + { path: '', component: RgwUserAccountsComponent }, + { + path: URLVerbs.CREATE, + component: RgwUserAccountsFormComponent, + data: { breadcrumbs: ActionLabels.CREATE } + } + ] }, { path: 'roles', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.spec.ts index 6ea9571336e6..8a3fd3f01201 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.spec.ts @@ -2,9 +2,9 @@ import { TestBed } from '@angular/core/testing'; 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: '', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.ts index 0a6722244303..971261013af9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.ts @@ -17,4 +17,23 @@ export class RgwUserAccountsService { } return this.http.get(this.url, { params }); } + + get(account_id: string): Observable { + 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 { + 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); + } } diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index de1b3e8b60e2..c548ec32c0d7 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -10848,6 +10848,18 @@ paths: 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': diff --git a/src/pybind/mgr/dashboard/services/rgw_iam.py b/src/pybind/mgr/dashboard/services/rgw_iam.py index 5f490323441a..43a3f2be181d 100644 --- a/src/pybind/mgr/dashboard/services/rgw_iam.py +++ b/src/pybind/mgr/dashboard/services/rgw_iam.py @@ -42,8 +42,11 @@ class RgwAccounts: 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: @@ -55,6 +58,24 @@ class RgwAccounts: 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 @@ -83,10 +104,14 @@ class RgwAccounts: 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