]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Added unit tests and fixed minor issues 61033/head
authorNaman Munet <naman.munet@ibm.com>
Fri, 10 Jan 2025 11:46:39 +0000 (17:16 +0530)
committerNaman Munet <naman.munet@ibm.com>
Wed, 22 Jan 2025 11:35:34 +0000 (17:05 +0530)
Fixes: https://tracker.ceph.com/issues/69140
Signed-off-by: Naman Munet <naman.munet@ibm.com>
12 files changed:
src/pybind/mgr/dashboard/controllers/rgw_iam.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_iam.py
src/pybind/mgr/dashboard/tests/test_rgw_iam.py

index 80d4b85862f672aa98ebcd97dbf125de2fdd4cf9..d9a87dc56b81c3bb350cb8eb1a5af154e0bfe39e 100644 (file)
@@ -9,54 +9,121 @@ from . import APIDoc, APIRouter, EndpointDoc, RESTController, allow_empty_body
 @APIRouter('rgw/accounts', Scope.RGW)
 @APIDoc("RGW User Accounts API", "RgwUserAccounts")
 class RgwUserAccountsController(RESTController):
-
+    @EndpointDoc("Update RGW account info",
+                 parameters={'account_name': (str, 'Account name'),
+                             'email': (str, 'Email'),
+                             'tenant': (str, 'Tenant'),
+                             'max_buckets': (int, 'Max buckets'),
+                             'max_users': (int, 'Max users'),
+                             'max_roles': (int, 'Max roles'),
+                             'max_group': (int, 'Max groups'),
+                             'max_access_keys': (int, 'Max access keys')})
     @allow_empty_body
-    def create(self, account_name: Optional[str] = None, tenant: 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):
+    def create(self, account_name: str, tenant: Optional[str] = None,
+               email: Optional[str] = None, max_buckets: Optional[int] = None,
+               max_users: Optional[int] = None, max_roles: Optional[int] = None,
+               max_group: Optional[int] = None,
+               max_access_keys: Optional[int] = None):
+        """
+        Create an account
+
+        :param account_name: Account name
+        :return: Returns account resource.
+        :rtype: Dict[str, Any]
+        """
         return RgwAccounts.create_account(account_name, tenant, email,
                                           max_buckets, max_users, max_roles,
                                           max_group, max_access_keys)
 
     def list(self, detailed: bool = False):
+        """
+        List all account ids or all detailed account info based on the 'detailed' query parameter.
+
+        - If detailed=True, returns detailed account info.
+        - If detailed=False, returns only account ids.
+        """
         detailed = str_to_bool(detailed)
         return RgwAccounts.get_accounts(detailed)
 
     @EndpointDoc("Get RGW Account by id",
                  parameters={'account_id': (str, 'Account id')})
     def get(self, account_id: str):
+        """
+        Get an account by account id
+        """
         return RgwAccounts.get_account(account_id)
 
     @EndpointDoc("Delete RGW Account",
                  parameters={'account_id': (str, 'Account id')})
     def delete(self, account_id):
+        """
+        Removes an account
+
+        :param account_id: account identifier
+        :return: None.
+        """
         return RgwAccounts.delete_account(account_id)
 
     @EndpointDoc("Update RGW account info",
-                 parameters={'account_id': (str, 'Account id')})
+                 parameters={'account_id': (str, 'Account id'),
+                             'account_name': (str, 'Account name'),
+                             'email': (str, 'Email'),
+                             'tenant': (str, 'Tenant'),
+                             'max_buckets': (int, 'Max buckets'),
+                             'max_users': (int, 'Max users'),
+                             'max_roles': (int, 'Max roles'),
+                             'max_group': (int, 'Max groups'),
+                             'max_access_keys': (int, 'Max access keys')})
     @allow_empty_body
-    def set(self, account_id: str, account_name: Optional[str] = None,
-            email: Optional[str] = None, tenant: str = None,
-            max_buckets: str = None, max_users: str = None,
-            max_roles: str = None, max_group: str = None,
-            max_access_keys: str = None):
+    def set(self, account_id: str, account_name: str,
+            email: Optional[str] = None, tenant: Optional[str] = None,
+            max_buckets: Optional[int] = None, max_users: Optional[int] = None,
+            max_roles: Optional[int] = None, max_group: Optional[int] = None,
+            max_access_keys: Optional[int] = None):
+        """
+        Modifies an account
+
+        :param account_id: Account identifier
+        :return: Returns modified account resource.
+        :rtype: Dict[str, Any]
+        """
         return RgwAccounts.modify_account(account_id, account_name, email, tenant,
                                           max_buckets, max_users, max_roles,
                                           max_group, max_access_keys)
 
     @EndpointDoc("Set RGW Account/Bucket quota",
                  parameters={'account_id': (str, 'Account id'),
-                             'max_size': (str, 'Max size')})
+                             'quota_type': (str, 'Quota type'),
+                             'max_size': (str, 'Max size'),
+                             'max_objects': (str, 'Max objects')})
     @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,
                   enabled: bool):
+        """
+        Modifies quota
+
+        :param account_id: Account identifier
+        :param quota_type: 'account' or 'bucket'
+        :return: Returns modified quota.
+        :rtype: Dict[str, Any]
+        """
         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')})
+                 parameters={'account_id': (str, 'Account id'),
+                             'quota_type': (str, 'Quota type'),
+                             'quota_status': (str, 'Quota status')})
     @RESTController.Resource(method='PUT', path='/quota/status')
     @allow_empty_body
     def set_quota_status(self, quota_type: str, account_id: str, quota_status: str):
+        """
+        Enable/Disable quota
+
+        :param account_id: Account identifier
+        :param quota_type: 'account' or 'bucket'
+        :param quota_status: 'enable' or 'disable'
+        :return: Returns modified quota.
+        :rtype: Dict[str, Any]
+        """
         return RgwAccounts.set_quota_status(quota_type, account_id, quota_status)
index ac43cfe4aa623139a45e087010aeefa239414373..1aad6167fa238d77729b801fd1df88513766e61c 100644 (file)
@@ -1,6 +1,9 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { RgwUserAccountsDetailsComponent } from './rgw-user-accounts-details.component';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { TableKeyValueComponent } from '~/app/shared/datatable/table-key-value/table-key-value.component';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
 
 describe('RgwUserAccountsDetailsComponent', () => {
   let component: RgwUserAccountsDetailsComponent;
@@ -8,11 +11,13 @@ describe('RgwUserAccountsDetailsComponent', () => {
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
-      declarations: [RgwUserAccountsDetailsComponent]
+      declarations: [RgwUserAccountsDetailsComponent, TableKeyValueComponent],
+      providers: [DimlessBinaryPipe, CdDatePipe]
     }).compileComponents();
 
     fixture = TestBed.createComponent(RgwUserAccountsDetailsComponent);
     component = fixture.componentInstance;
+    component.selection = { quota: {}, bucket_quota: {} };
     fixture.detectChanges();
   });
 
index a8062052090dd0cf76ab598397f116bca261c3bc..f1840235057213d2be413de7229b632d815c9d22 100644 (file)
@@ -24,14 +24,16 @@ export class RgwUserAccountsDetailsComponent implements OnChanges {
   createDisplayValues(quota_type: string) {
     return {
       Enabled: this.selection[quota_type].enabled ? 'Yes' : 'No',
-      'Maximum Size':
-        this.selection[quota_type].max_size <= -1
+      'Maximum size': this.selection[quota_type].enabled
+        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
+          : this.dimlessBinary.transform(this.selection[quota_type].max_size)
+        : '-',
+      'Maximum objects': this.selection[quota_type].enabled
+        ? this.selection[quota_type].max_objects <= -1
           ? 'Unlimited'
           : this.selection[quota_type].max_objects
+        : '-'
     };
   }
 }
index b9ccddd0a56cdcea3be601521491842ce9921d97..3856c42a8e6c9f3dc6c0acd06dd39263132cd512 100644 (file)
               [name]="formControl"
               [id]="formControl"
               label="{{formControl.split('_')[1] | upperFirst}} Mode"
-              (change)="onModeChange($event.target.value, formControl)">
-    <option value="-1">Disabled</option>
+              (change)="onModeChange($event.target.value, formControl)"
+              [helperText]="getHelperTextForMode(formControl)">
     <option value="0">Unlimited</option>
+    <option value="-1">Disabled</option>
     <option value="1">Custom</option>
   </cds-select>
 </ng-template>
             formControlName="{{formControl}}"
             label="{{formControl.split('_')[0] | upperFirst}}. {{formControl.split('_').length > 2 ? formControl.split('_')[1]+' '+formControl.split('_')[2]: formControl.split('_')[1]}}"
             [min]="1"
+            cdRequiredField="{{formControl.split('_')[0] | upperFirst}}. {{formControl.split('_').length > 2 ? formControl.split('_')[1]+' '+formControl.split('_')[2]: formControl.split('_')[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.controls[formControl].value == 0 && accountForm.showError(formControl, formDir, 'min')"
+        i18n>Enter number greater than 0</span>
   <span *ngIf="accountForm.showError(formControl, formDir, 'pattern')"
-        i18n>The entered value must be a number greater than 0</span>
+        i18n>Enter a valid positive number</span>
 </ng-template>
 </ng-template>
 
              [formGroup]="accountForm">
   <fieldset class="cds--fieldset">
     <legend class="cds--label">{{quotaType | upperFirst}} Quota</legend>
+    <div *ngIf="quotaType == 'account';else bucket"
+         class="quota-heading">Set quota on account owned by users.</div>
+    <ng-template #bucket>
+      <div class="quota-heading">
+        Set quota on buckets owned by an account.
+      </div>
+    </ng-template>
     <!-- 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 class="quota-sub-block"
+         *ngIf="accountForm.controls[formControl.enabled].value">
+      <!-- Unlimited size -->
+      <cds-checkbox *ngIf="accountForm.controls[formControl.enabled].value"
+                    [formControlName]="formControl.unlimitedSize">
+                    Unlimited size
+      </cds-checkbox>
+      <!-- Maximum size -->
+      <div class="input-wrapper"
+           *ngIf="accountForm.controls[formControl.enabled].value && !accountForm.getValue(formControl.unlimitedSize)">
+        <cds-text-label [label]="formControl.maxSize"
+                        [for]="formControl.maxSize"
+                        cdRequiredField="Max. size"
+                        [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>Enter a valid value.</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="input-wrapper"
+           *ngIf="accountForm.controls[formControl.enabled].value && !accountForm.getValue(formControl.unlimitedObjects)">
+        <cds-text-label [label]="formControl.maxObjects"
+                        [for]="formControl.maxObjects"
+                        cdRequiredField="Max. objects"
+                        [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>Enter a valid positive number</span>
+        </ng-template>
+      </div>
     </div>
   </fieldset>
 </ng-template>
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6c6969a7718e9022d4b86ec387d657fb50d8665d 100644 (file)
@@ -0,0 +1,21 @@
+@use '@carbon/layout';
+
+::ng-deep .cds--form__helper-text {
+  white-space: pre-line;
+}
+
+.quota-heading {
+  margin-bottom: layout.$spacing-03;
+}
+
+.quota-sub-block {
+  margin: layout.$spacing-03 0 0 layout.$spacing-06;
+
+  .input-wrapper {
+    margin: layout.$spacing-03 0 0 layout.$spacing-01;
+
+    &:first-of-type {
+      margin-bottom: layout.$spacing-06;
+    }
+  }
+}
index 0cd52faffc6ca25dc5e414f52168873a45b26079..87499ce2affe7aa5daa0fdd3618d14a058102fbf 100644 (file)
@@ -9,9 +9,13 @@ 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';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RgwUserAccountsComponent } from '../rgw-user-accounts/rgw-user-accounts.component';
 
 class MockRgwUserAccountsService {
   create = jest.fn().mockReturnValue(of(null));
+  modify = jest.fn().mockReturnValue(of(null));
+  setQuota = jest.fn().mockReturnValue(of(null));
 }
 
 describe('RgwUserAccountsFormComponent', () => {
@@ -27,8 +31,11 @@ describe('RgwUserAccountsFormComponent', () => {
         ToastrModule.forRoot(),
         HttpClientTestingModule,
         PipesModule,
-        RouterTestingModule,
-        ModalModule
+        RouterTestingModule.withRoutes([
+          { path: 'rgw/accounts', component: RgwUserAccountsComponent }
+        ]),
+        ModalModule,
+        ReactiveFormsModule
       ],
       providers: [{ provide: RgwUserAccountsService, useClass: MockRgwUserAccountsService }]
     }).compileComponents();
@@ -47,10 +54,96 @@ describe('RgwUserAccountsFormComponent', () => {
 
   it('should call create method of MockRgwUserAccountsService and show success notification', () => {
     component.editing = false;
+    component.accountForm.get('name').setValue('test');
+    const payload = {
+      account_name: 'test',
+      email: '',
+      tenant: '',
+      max_users: 1000,
+      max_buckets: 1000,
+      max_roles: 1000,
+      max_group: 1000,
+      max_access_keys: 4
+    };
     const spy = jest.spyOn(component, 'submit');
     const createDataSpy = jest.spyOn(rgwUserAccountsService, 'create').mockReturnValue(of(null));
     component.submit();
+    expect(component.accountForm.valid).toBe(true);
     expect(spy).toHaveBeenCalled();
     expect(createDataSpy).toHaveBeenCalled();
+    expect(createDataSpy).toHaveBeenCalledWith(payload);
+  });
+
+  it('should call modify method of MockRgwUserAccountsService and show success notification', () => {
+    component.editing = true;
+    component.accountForm.get('name').setValue('test');
+    component.accountForm.get('id').setValue('RGW12312312312312312');
+    component.accountForm.get('email').setValue('test@test.com');
+    const payload = {
+      account_id: 'RGW12312312312312312',
+      account_name: 'test',
+      email: 'test@test.com',
+      tenant: '',
+      max_users: 1000,
+      max_buckets: 1000,
+      max_roles: 1000,
+      max_group: 1000,
+      max_access_keys: 4
+    };
+    const spy = jest.spyOn(component, 'submit');
+    const modifyDataSpy = jest.spyOn(rgwUserAccountsService, 'modify').mockReturnValue(of(null));
+    component.submit();
+    expect(component.accountForm.valid).toBe(true);
+    expect(spy).toHaveBeenCalled();
+    expect(modifyDataSpy).toHaveBeenCalled();
+    expect(modifyDataSpy).toHaveBeenCalledWith(payload);
+  });
+
+  it('should call setQuota for "account" if account quota is dirty', () => {
+    component.accountForm.get('id').setValue('123');
+    component.accountForm.get('account_quota_enabled').setValue(true);
+    component.accountForm.get('account_quota_max_size_unlimited').setValue(false);
+    component.accountForm.get('account_quota_max_size').setValue('1 GiB');
+    component.accountForm.get('account_quota_max_objects_unlimited').setValue(false);
+    component.accountForm.get('account_quota_max_objects').setValue('100');
+    component.accountForm.get('account_quota_max_size').markAsDirty();
+    const accountId = '123';
+
+    const spySetQuota = jest.spyOn(rgwUserAccountsService, 'setQuota').mockReturnValue(of(null));
+    const spyGoToListView = jest.spyOn(component, 'goToListView');
+
+    component.setQuotaConfig();
+    const accountQuotaArgs = {
+      quota_type: 'account',
+      enabled: component.accountForm.getValue('account_quota_enabled'),
+      max_size: '1073741824',
+      max_objects: component.accountForm.getValue('account_quota_max_objects')
+    };
+    expect(spySetQuota).toHaveBeenCalledWith(accountId, accountQuotaArgs);
+    expect(spyGoToListView).toHaveBeenCalled();
+  });
+
+  it('should call setQuota for "bucket" if account quota is dirty', () => {
+    component.accountForm.get('id').setValue('123');
+    component.accountForm.get('bucket_quota_enabled').setValue(true);
+    component.accountForm.get('bucket_quota_max_size_unlimited').setValue(false);
+    component.accountForm.get('bucket_quota_max_size').setValue('1 GiB');
+    component.accountForm.get('bucket_quota_max_objects_unlimited').setValue(false);
+    component.accountForm.get('bucket_quota_max_objects').setValue('100');
+    component.accountForm.get('bucket_quota_max_size').markAsDirty();
+    const accountId = '123';
+
+    const spySetQuota = jest.spyOn(rgwUserAccountsService, 'setQuota').mockReturnValue(of(null));
+    const spyGoToListView = jest.spyOn(component, 'goToListView');
+
+    component.setQuotaConfig();
+    const bucketQuotaArgs = {
+      quota_type: 'bucket',
+      enabled: component.accountForm.getValue('bucket_quota_enabled'),
+      max_size: '1073741824',
+      max_objects: component.accountForm.getValue('bucket_quota_max_objects')
+    };
+    expect(spySetQuota).toHaveBeenCalledWith(accountId, bucketQuotaArgs);
+    expect(spyGoToListView).toHaveBeenCalled();
   });
 });
index 8e0d81835667fbe5a4249f9ace1cca3b8f9effed..7c082f6867daa2d292b6d0dc6c81a5fdcb99e94b 100644 (file)
@@ -94,15 +94,15 @@ export class RgwUserAccountsFormComponent extends CdForm implements OnInit {
   mapValuesForMode(value: any, formControlName: string) {
     switch (value[formControlName]) {
       case -1:
-        value[`${formControlName}_mode`] = -1;
+        value[`${formControlName}_mode`] = '-1';
         value[formControlName] = '';
         break;
       case 0:
-        value[`${formControlName}_mode`] = 0;
+        value[`${formControlName}_mode`] = '0';
         value[formControlName] = '';
         break;
       default:
-        value[`${formControlName}_mode`] = 1;
+        value[`${formControlName}_mode`] = '1';
         break;
     }
   }
@@ -113,30 +113,50 @@ export class RgwUserAccountsFormComponent extends CdForm implements OnInit {
       tenant: [''],
       name: ['', Validators.required],
       email: ['', CdValidators.email],
-      max_users_mode: [1],
+      max_users_mode: ['1'],
       max_users: [
         1000,
-        [CdValidators.requiredIf({ max_users_mode: '1' }), CdValidators.number(false)]
+        [
+          CdValidators.requiredIf({ max_users_mode: '1' }),
+          CdValidators.number(false),
+          Validators.min(1)
+        ]
       ],
-      max_roles_mode: [1],
+      max_roles_mode: ['1'],
       max_roles: [
         1000,
-        [CdValidators.requiredIf({ max_roles_mode: '1' }), CdValidators.number(false)]
+        [
+          CdValidators.requiredIf({ max_roles_mode: '1' }),
+          CdValidators.number(false),
+          Validators.min(1)
+        ]
       ],
-      max_groups_mode: [1],
+      max_groups_mode: ['1'],
       max_groups: [
         1000,
-        [CdValidators.requiredIf({ max_groups_mode: '1' }), CdValidators.number(false)]
+        [
+          CdValidators.requiredIf({ max_groups_mode: '1' }),
+          CdValidators.number(false),
+          Validators.min(1)
+        ]
       ],
-      max_access_keys_mode: [1],
+      max_access_keys_mode: ['1'],
       max_access_keys: [
         4,
-        [CdValidators.requiredIf({ max_access_keys_mode: '1' }), CdValidators.number(false)]
+        [
+          CdValidators.requiredIf({ max_access_keys_mode: '1' }),
+          CdValidators.number(false),
+          Validators.min(1)
+        ]
       ],
-      max_buckets_mode: [1],
+      max_buckets_mode: ['1'],
       max_buckets: [
         1000,
-        [CdValidators.requiredIf({ max_buckets_mode: '1' }), CdValidators.number(false)]
+        [
+          CdValidators.requiredIf({ max_buckets_mode: '1' }),
+          CdValidators.number(false),
+          Validators.min(1)
+        ]
       ],
       account_quota_enabled: [false],
       account_quota_max_size_unlimited: [true],
@@ -307,7 +327,7 @@ export class RgwUserAccountsFormComponent extends CdForm implements OnInit {
         this.accountForm.getValue(`${quotaType}_quota_max_size`)
       );
       // Finally convert the value to KiB.
-      result['max_size'] = (bytes / 1024).toFixed(0) as any;
+      result['max_size'] = bytes.toFixed(0) as any;
     }
     if (!this.accountForm.getValue(`${quotaType}_quota_max_objects_unlimited`)) {
       result['max_objects'] = `${this.accountForm.getValue(`${quotaType}_quota_max_objects`)}`;
@@ -354,4 +374,17 @@ export class RgwUserAccountsFormComponent extends CdForm implements OnInit {
       ? formvalue[formControlName]
       : formvalue[`${formControlName}_mode`];
   }
+
+  getHelperTextForMode(formControl: string) {
+    const resourceName =
+      formControl.split('_').length > 3
+        ? formControl.split('_')[1] + ' ' + formControl.split('_')[2]
+        : formControl.split('_')[1];
+    if (this.accountForm.getValue(formControl) == -1) {
+      return `${resourceName[0].toUpperCase() + resourceName.slice(1, -1)} creation is disabled.`;
+    } else if (this.accountForm.getValue(formControl) == 0) {
+      return `Unlimited ${resourceName.slice(0, -1)} creation allowed.`;
+    }
+    return '';
+  }
 }
index ab53cd71baad6f81d4117fe66d830570d23e33c9..047b89f621d99aaffa5cff9698c4d5fad3ea0ab2 100644 (file)
@@ -2,8 +2,7 @@
 <legend i18n>
   User Accounts
   <cd-help-text>
-    This feature allows administrators to assign unique credentials to individual users or applications,
-    ensuring granular access control and improved security across the cluster.
+    Administrators can assign unique credentials to users or applications, enabling granular access control and enhancing security across the cluster.
   </cd-help-text>
 </legend>
 <cd-table #table
@@ -16,7 +15,8 @@
           (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)"
           identifier="id"
-          (fetchData)="getAccountsList($event)">
+          (fetchData)="getAccountsList($event)"
+          [status]="tableStatus">
   <cd-table-actions class="table-actions"
                     [permission]="permission"
                     [selection]="selection"
index a41955302863fca969fefe720524ce52019622b3..dd4cf1b21aca4fb6866f50d0fc899f1da4c8bd46 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, OnInit, ViewChild } from '@angular/core';
+import { Component, NgZone, OnInit, ViewChild } from '@angular/core';
 
 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
 import { TableComponent } from '~/app/shared/datatable/table/table.component';
@@ -19,6 +19,7 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
 import { Observable, Subscriber, forkJoin as observableForkJoin } from 'rxjs';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { FinishedTask } from '~/app/shared/models/finished-task';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
 
 const BASE_URL = 'rgw/accounts';
 
@@ -36,6 +37,7 @@ export class RgwUserAccountsComponent extends ListWithDetails implements OnInit
   columns: CdTableColumn[] = [];
   accounts: Account[] = [];
   selection: CdTableSelection = new CdTableSelection();
+  declare staleTimeout: number;
 
   constructor(
     private authStorageService: AuthStorageService,
@@ -43,7 +45,8 @@ export class RgwUserAccountsComponent extends ListWithDetails implements OnInit
     private router: Router,
     private rgwUserAccountsService: RgwUserAccountsService,
     private cdsModalService: ModalCdsService,
-    private taskWrapper: TaskWrapperService
+    private taskWrapper: TaskWrapperService,
+    protected ngZone: NgZone
   ) {
     super();
   }
@@ -52,8 +55,8 @@ export class RgwUserAccountsComponent extends ListWithDetails implements OnInit
     this.permission = this.authStorageService.getPermissions().rgw;
     this.columns = [
       {
-        name: $localize`Account Id`,
-        prop: 'id',
+        name: $localize`Name`,
+        prop: 'name',
         flexGrow: 1
       },
       {
@@ -62,8 +65,8 @@ export class RgwUserAccountsComponent extends ListWithDetails implements OnInit
         flexGrow: 1
       },
       {
-        name: $localize`Full name`,
-        prop: 'name',
+        name: $localize`Account id`,
+        prop: 'id',
         flexGrow: 1
       },
       {
@@ -72,29 +75,54 @@ export class RgwUserAccountsComponent extends ListWithDetails implements OnInit
         flexGrow: 1
       },
       {
-        name: $localize`Max Users`,
+        name: $localize`Max users`,
         prop: 'max_users',
-        flexGrow: 1
+        flexGrow: 1,
+        cellTransformation: CellTemplate.map,
+        customTemplateConfig: {
+          '-1': $localize`Disabled`,
+          0: $localize`Unlimited`
+        }
       },
       {
-        name: $localize`Max Roles`,
+        name: $localize`Max roles`,
         prop: 'max_roles',
-        flexGrow: 1
+        flexGrow: 1,
+        cellTransformation: CellTemplate.map,
+        customTemplateConfig: {
+          '-1': $localize`Disabled`,
+          0: $localize`Unlimited`
+        }
       },
       {
-        name: $localize`Max Groups`,
+        name: $localize`Max groups`,
         prop: 'max_groups',
-        flexGrow: 1
+        flexGrow: 1,
+        cellTransformation: CellTemplate.map,
+        customTemplateConfig: {
+          '-1': $localize`Disabled`,
+          0: $localize`Unlimited`
+        }
       },
       {
         name: $localize`Max. buckets`,
         prop: 'max_buckets',
-        flexGrow: 1
+        flexGrow: 1,
+        cellTransformation: CellTemplate.map,
+        customTemplateConfig: {
+          '-1': $localize`Disabled`,
+          0: $localize`Unlimited`
+        }
       },
       {
-        name: $localize`Max Access Keys`,
+        name: $localize`Max access keys`,
         prop: 'max_access_keys',
-        flexGrow: 1
+        flexGrow: 1,
+        cellTransformation: CellTemplate.map,
+        customTemplateConfig: {
+          '-1': $localize`Disabled`,
+          0: $localize`Unlimited`
+        }
       }
     ];
     const getEditURL = () => {
@@ -124,9 +152,11 @@ export class RgwUserAccountsComponent extends ListWithDetails implements OnInit
       name: this.actionLabels.DELETE
     };
     this.tableActions = [addAction, editAction, deleteAction];
+    this.setTableRefreshTimeout();
   }
 
   getAccountsList(context?: CdTableFetchDataContext) {
+    this.setTableRefreshTimeout();
     this.rgwUserAccountsService.list(true).subscribe({
       next: (accounts: Account[]) => {
         this.accounts = accounts;
index d744eb6c42c0fc7f319380fa2d4d47ec4ea36ca4..6a5b9030042f695193403a39f521eba231af6be7 100644 (file)
@@ -10810,6 +10810,10 @@ paths:
       - Prometheus
   /api/rgw/accounts:
     get:
+      description: "\n        List all account ids or all detailed account info based\
+        \ on the 'detailed' query parameter.\n\n        - If detailed=True, returns\
+        \ detailed account info.\n        - If detailed=False, returns only account\
+        \ ids.\n        "
       parameters:
       - default: false
         in: query
@@ -10836,6 +10840,9 @@ paths:
       tags:
       - RgwUserAccounts
     post:
+      description: "\n        Create an account\n\n        :param account_name: Account\
+        \ name\n        :return: Returns account resource.\n        :rtype: Dict[str,\
+        \ Any]\n        "
       parameters: []
       requestBody:
         content:
@@ -10843,21 +10850,31 @@ paths:
             schema:
               properties:
                 account_name:
-                  type: integer
+                  description: Account name
+                  type: string
                 email:
+                  description: Email
                   type: string
                 max_access_keys:
-                  type: string
+                  description: Max access keys
+                  type: integer
                 max_buckets:
-                  type: string
+                  description: Max buckets
+                  type: integer
                 max_group:
-                  type: string
+                  description: Max groups
+                  type: integer
                 max_roles:
-                  type: string
+                  description: Max roles
+                  type: integer
                 max_users:
-                  type: string
+                  description: Max users
+                  type: integer
                 tenant:
+                  description: Tenant
                   type: string
+              required:
+              - account_name
               type: object
       responses:
         '201':
@@ -10881,10 +10898,13 @@ paths:
             trace.
       security:
       - jwt: []
+      summary: Update RGW account info
       tags:
       - RgwUserAccounts
   /api/rgw/accounts/{account_id}:
     delete:
+      description: "\n        Removes an account\n\n        :param account_id: account\
+        \ identifier\n        :return: None.\n        "
       parameters:
       - description: Account id
         in: path
@@ -10918,6 +10938,7 @@ paths:
       tags:
       - RgwUserAccounts
     get:
+      description: "\n        Get an account by account id\n        "
       parameters:
       - description: Account id
         in: path
@@ -10946,6 +10967,9 @@ paths:
       tags:
       - RgwUserAccounts
     put:
+      description: "\n        Modifies an account\n\n        :param account_id: Account\
+        \ identifier\n        :return: Returns modified account resource.\n      \
+        \  :rtype: Dict[str, Any]\n        "
       parameters:
       - description: Account id
         in: path
@@ -10959,21 +10983,31 @@ paths:
             schema:
               properties:
                 account_name:
-                  type: integer
+                  description: Account name
+                  type: string
                 email:
+                  description: Email
                   type: string
                 max_access_keys:
-                  type: string
+                  description: Max access keys
+                  type: integer
                 max_buckets:
-                  type: string
+                  description: Max buckets
+                  type: integer
                 max_group:
-                  type: string
+                  description: Max groups
+                  type: integer
                 max_roles:
-                  type: string
+                  description: Max roles
+                  type: integer
                 max_users:
-                  type: string
+                  description: Max users
+                  type: integer
                 tenant:
+                  description: Tenant
                   type: string
+              required:
+              - account_name
               type: object
       responses:
         '200':
@@ -11002,6 +11036,9 @@ paths:
       - RgwUserAccounts
   /api/rgw/accounts/{account_id}/quota:
     put:
+      description: "\n        Modifies quota\n\n        :param account_id: Account\
+        \ identifier\n        :param quota_type: 'account' or 'bucket'\n        :return:\
+        \ Returns modified quota.\n        :rtype: Dict[str, Any]\n        "
       parameters:
       - description: Account id
         in: path
@@ -11017,11 +11054,13 @@ paths:
                 enabled:
                   type: string
                 max_objects:
+                  description: Max objects
                   type: string
                 max_size:
                   description: Max size
                   type: string
                 quota_type:
+                  description: Quota type
                   type: string
               required:
               - quota_type
@@ -11056,6 +11095,10 @@ paths:
       - RgwUserAccounts
   /api/rgw/accounts/{account_id}/quota/status:
     put:
+      description: "\n        Enable/Disable quota\n\n        :param account_id: Account\
+        \ identifier\n        :param quota_type: 'account' or 'bucket'\n        :param\
+        \ quota_status: 'enable' or 'disable'\n        :return: Returns modified quota.\n\
+        \        :rtype: Dict[str, Any]\n        "
       parameters:
       - description: Account id
         in: path
@@ -11069,8 +11112,10 @@ paths:
             schema:
               properties:
                 quota_status:
+                  description: Quota status
                   type: string
                 quota_type:
+                  description: Quota type
                   type: string
               required:
               - quota_type
index 896f1a932396a7b48a85111f1886cc942f47c940..88730dd96a1d6ac495f1b83ce378db3b9cda1ffa 100644 (file)
@@ -42,10 +42,10 @@ class RgwAccounts:
         return cls.send_rgw_cmd(get_account_cmd)
 
     @classmethod
-    def create_account(cls, account_name: Optional[str] = None, tenant: 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):
+    def create_account(cls, account_name: str, tenant: Optional[str] = None,
+                       email: Optional[str] = None, max_buckets: Optional[int] = None,
+                       max_users: Optional[int] = None, max_roles: Optional[int] = None,
+                       max_group: Optional[int] = None, max_access_keys: Optional[int] = None):
         create_accounts_cmd = ['account', 'create']
 
         create_accounts_cmd += cls.get_common_args_list(account_name, email,
@@ -56,11 +56,11 @@ class RgwAccounts:
         return cls.send_rgw_cmd(create_accounts_cmd)
 
     @classmethod
-    def modify_account(cls, account_id: str, account_name: Optional[str] = None,
-                       email: Optional[str] = None, tenant: str = None,
-                       max_buckets: str = None, max_users: str = None,
-                       max_roles: str = None, max_group: str = None,
-                       max_access_keys: str = None):
+    def modify_account(cls, account_id: str, account_name: str,
+                       email: Optional[str] = None, tenant: Optional[str] = None,
+                       max_buckets: Optional[int] = None, max_users: Optional[int] = None,
+                       max_roles: Optional[int] = None, max_group: Optional[int] = None,
+                       max_access_keys: Optional[int] = None):
         modify_accounts_cmd = ['account', 'modify', '--account-id', account_id]
 
         modify_accounts_cmd += cls.get_common_args_list(account_name, email,
@@ -101,11 +101,11 @@ class RgwAccounts:
         return cls.send_rgw_cmd(set_quota_status_cmd)
 
     @classmethod
-    def get_common_args_list(cls, account_name: Optional[str] = None,
-                             email: Optional[str] = None, tenant: str = None,
-                             max_buckets: str = None, max_users: str = None,
-                             max_roles: str = None, max_group: str = None,
-                             max_access_keys: str = None):
+    def get_common_args_list(cls, account_name: str, email: Optional[str] = None,
+                             tenant: Optional[str] = None, max_buckets: Optional[int] = None,
+                             max_users: Optional[int] = None, max_roles: Optional[int] = None,
+                             max_group: Optional[int] = None,
+                             max_access_keys: Optional[int] = None):
         common_cmd_list = []
         if account_name:
             common_cmd_list += ['--account-name', account_name]
index 133b5a0d390c385eb459516123fca375526f0291..90bb9ac16c872ae34fc1369c184b519ad35a7e54 100644 (file)
@@ -39,12 +39,14 @@ class TestRgwUserAccountsController(TestCase):
         mock_create_account.return_value = mockReturnVal
 
         controller = RgwUserAccountsController()
-        result = controller.create(account_name='test_account', account_id='RGW18661471562806836',
-                                   email='test@example.com')
+        result = controller.create(account_name='test_account', tenant='',
+                                   email='test@example.com', max_buckets=1000,
+                                   max_users=1000, max_roles=1000, max_group=1000,
+                                   max_access_keys=4)
 
         # Check if the account creation method was called with the correct parameters
-        mock_create_account.assert_called_with('test_account', 'RGW18661471562806836',
-                                               'test@example.com')
+        mock_create_account.assert_called_with('test_account', '', 'test@example.com',
+                                               1000, 1000, 1000, 1000, 4)
         # Check the returned result
         self.assertEqual(result, mockReturnVal)
 
@@ -207,10 +209,12 @@ class TestRgwUserAccountsController(TestCase):
 
         controller = RgwUserAccountsController()
         result = controller.set(account_id='RGW59378973811515857', account_name='new_account_name',
-                                email='new_email@example.com')
+                                email='new_email@example.com', tenant='', max_buckets=1000,
+                                max_users=1000, max_roles=1000, max_group=1000, max_access_keys=4)
 
         mock_modify_account.assert_called_with('RGW59378973811515857', 'new_account_name',
-                                               'new_email@example.com')
+                                               'new_email@example.com', '', 1000, 1000, 1000,
+                                               1000, 4)
 
         self.assertEqual(result, mock_return_value)
 
@@ -246,9 +250,9 @@ class TestRgwUserAccountsController(TestCase):
 
         controller = RgwUserAccountsController()
         result = controller.set_quota(quota_type='account', account_id='RGW11111111111111111',
-                                      max_size='10GB', max_objects='1000')
+                                      max_size='10GB', max_objects='1000', enabled=True)
 
-        mock_set_quota.assert_called_with('account', 'RGW11111111111111111', '10GB', '1000')
+        mock_set_quota.assert_called_with('account', 'RGW11111111111111111', '10GB', '1000', True)
 
         self.assertEqual(result, mock_return_value)