]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: migrated accounts table tabs to resource pages 69807/head
authorSyed Ali Ul Hasan <syedaliulhasan19@gmail.com>
Thu, 16 Jul 2026 19:24:16 +0000 (00:54 +0530)
committerSyed Ali Ul Hasan <syedaliulhasan19@gmail.com>
Thu, 16 Jul 2026 19:24:16 +0000 (00:54 +0530)
- Fixed E2E tests for user, roles, accounts

- Fixes: https://tracker.ceph.com/issues/77778

- Fixes: https://tracker.ceph.com/issues/78254

Signed-off-by: Syed Ali Ul Hasan <syedaliulhasan19@gmail.com>
23 files changed:
src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/accounts.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details-breadcrumb.resolver.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details-breadcrumb.resolver.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details.resolver.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details.resolver.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.ts [new file with mode: 0644]
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/frontend/src/app/ceph/rgw/rgw.module.ts

index d1ad20b7a47fce8c4ccaec9c7c0589aa025e4c7b..4a26f780ee0a81c43d99c27e42278032059cb1ff 100644 (file)
@@ -270,6 +270,17 @@ export abstract class PageHelper {
     return cy.get('.cds--table-expand__button').first();
   }
 
+  getResourcePage(content?: string) {
+    this.waitDataTableToLoad();
+    if (content) {
+      return cy
+        .contains('[cdstablerow] [cdstabledata]', content)
+        .parent('[cdstablerow]')
+        .contains('[cdstabledata] a', new RegExp(`^${content}$`));
+    }
+    return cy.get('[cdstablerow] [cdstabledata] a').first();
+  }
+
   /**
    * Gets column headers of table
    */
index f172c6265cd5dbe8b1dc6fce7fdc51bdab6b4c76..fd6c4d013e7a79f6d3514dc9a05db816f1472eed 100644 (file)
@@ -9,14 +9,14 @@ export class AccountsPageHelper extends PageHelper {
   pages = pages;
 
   columnIndex = {
-    tenant: 2,
-    account_id: 3,
-    email: 4,
-    max_users: 5,
-    max_roles: 6,
-    max_groups: 7,
-    max_buckets: 8,
-    max_access_keys: 9
+    tenant: 1,
+    account_id: 2,
+    email: 3,
+    max_users: 4,
+    max_roles: 5,
+    max_groups: 6,
+    max_buckets: 7,
+    max_access_keys: 8
   };
 
   @PageHelper.restrictTo(pages.create.url)
@@ -67,12 +67,14 @@ export class AccountsPageHelper extends PageHelper {
       cy.get('td').eq(this.columnIndex.max_access_keys).should('have.text', 4);
     });
 
-    this.getExpandCollapseElement(account.name).click().wait(1000);
+    this.getResourcePage(account.name).click();
 
-    cy.get('[data-testid="datatable-row-detail"]').first().as('accountDetailsTable');
-    cy.get('@accountDetailsTable').find('legend').its(0).as('accountQuota');
-    cy.get('@accountQuota').should('have.text', 'Account quota');
-    cy.get('@accountDetailsTable').find('cd-table-key-value').its(0).as('accountQuotaTable');
+    cy.contains('section h2.user-account-resource-section__title', /Account\s+Quota/i)
+      .should('be.visible')
+      .closest('section')
+      .find('cd-table-key-value')
+      .first()
+      .as('accountQuotaTable');
     cy.get('@accountQuotaTable')
       .find('tbody tr')
       .first()
@@ -80,15 +82,20 @@ export class AccountsPageHelper extends PageHelper {
       .last()
       .should('have.text', 'Yes');
 
-    cy.get('@accountDetailsTable').find('legend').its(1).as('bucketQuota');
-    cy.get('@bucketQuota').should('have.text', 'Bucket quota');
-    cy.get('@accountDetailsTable').find('cd-table-key-value').its(1).as('bucketQuotaTable');
+    cy.contains('section h2.user-account-resource-section__title', /Bucket\s+Quota/i)
+      .should('be.visible')
+      .closest('section')
+      .find('cd-table-key-value')
+      .first()
+      .as('bucketQuotaTable');
     cy.get('@bucketQuotaTable')
       .find('tbody tr')
       .first()
       .find('td')
       .last()
       .should('have.text', 'Yes');
+
+    this.navigateTo();
   }
 
   @PageHelper.restrictTo(pages.create.url)
@@ -147,12 +154,14 @@ export class AccountsPageHelper extends PageHelper {
         .eq(this.columnIndex.max_access_keys)
         .should('have.text', account.max_access_keys);
     });
-    this.getExpandCollapseElement(account.name).click().wait(1000);
+    this.getResourcePage(account.name).click();
 
-    cy.get('[data-testid="datatable-row-detail"]').first().as('accountDetailsTable');
-    cy.get('@accountDetailsTable').find('legend').eq(0).as('accountQuota');
-    cy.get('@accountQuota').should('have.text', 'Account quota');
-    cy.get('@accountDetailsTable').find('cd-table-key-value').eq(0).as('accountQuotaTable');
+    cy.contains('section h2.user-account-resource-section__title', /Account\s+Quota/i)
+      .should('be.visible')
+      .closest('section')
+      .find('cd-table-key-value')
+      .first()
+      .as('accountQuotaTable');
     cy.get('@accountQuotaTable').find('tbody tr').should('have.length', 3);
     cy.get('@accountQuotaTable')
       .find('tbody tr')
@@ -177,9 +186,12 @@ export class AccountsPageHelper extends PageHelper {
         cy.get('td').last().should('have.text', '1.2 GiB');
       });
 
-    cy.get('@accountDetailsTable').find('legend').eq(1).as('bucketQuota');
-    cy.get('@bucketQuota').should('have.text', 'Bucket quota');
-    cy.get('@accountDetailsTable').find('cd-table-key-value').eq(1).as('bucketQuotaTable');
+    cy.contains('section h2.user-account-resource-section__title', /Bucket\s+Quota/i)
+      .should('be.visible')
+      .closest('section')
+      .find('cd-table-key-value')
+      .first()
+      .as('bucketQuotaTable');
     cy.get('@bucketQuotaTable')
       .find('tbody tr')
       .first()
@@ -202,6 +214,8 @@ export class AccountsPageHelper extends PageHelper {
         cy.get('td').first().should('have.text', 'Maximum size');
         cy.get('td').last().should('have.text', '1.2 GiB');
       });
+
+    this.navigateTo();
   }
 
   invalidCreate() {
index 2026d0dc589bc0dd1b653d682832516f246c740d..a660be5550a03c0a53ace485a3651e7ae05be01b 100644 (file)
@@ -22,8 +22,9 @@ describe('RGW roles page', () => {
   beforeEach(() => {
     cy.login();
     accounts.navigateTo();
-    accounts.getExpandCollapseElement(accountName).click();
-    cy.contains('cds-tab-headers button[role="tab"]', 'Roles').click();
+    accounts.getResourcePage(accountName).click();
+    cy.contains('cds-sidenav-item a', /^Roles$/).click();
+    cy.location('hash').should('include', '/roles');
     // Wait for the roles list to render
     cy.get('cd-rgw-account-roles-list').should('exist');
   });
index e42abd40d715f839c176052bef6f9334669e18c3..04bae24ff19886bd1575c69ea40ee164e46946e5 100644 (file)
@@ -65,7 +65,7 @@ describe('RGW users page', () => {
       accounts
         .getTableRow(account.name)
         .find('td')
-        .eq(3)
+        .eq(2)
         .invoke('text')
         .then((acc_id: string) => {
           cy.log(acc_id);
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
deleted file mode 100644 (file)
index 97c6a62..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-@if (selection) {
-  <cds-tabs>
-    <cds-tab
-      heading="Quotas"
-      i18n-heading
-    >
-      <div class="row">
-        @if (selection.quota) {
-          <div class="col-sm-6">
-            <legend i18n>Account quota</legend>
-            <cd-table-key-value [data]="quota"> </cd-table-key-value>
-          </div>
-        }
-        @if (selection.bucket_quota) {
-          <div class="col-sm-6">
-            <legend i18n>Bucket quota</legend>
-            <cd-table-key-value [data]="bucket_quota"> </cd-table-key-value>
-          </div>
-        }
-      </div>
-    </cds-tab>
-    <cds-tab
-      heading="Roles"
-      i18n-heading
-    >
-      <div class="mt-4">
-        <cd-rgw-account-roles-list
-          [accountId]="selection.id"
-          [accountName]="selection.name"
-        ></cd-rgw-account-roles-list>
-      </div>
-    </cds-tab>
-  </cds-tabs>
-}
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
deleted file mode 100644 (file)
index e69de29..0000000
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
deleted file mode 100644 (file)
index 1aad616..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-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;
-  let fixture: ComponentFixture<RgwUserAccountsDetailsComponent>;
-
-  beforeEach(async () => {
-    await TestBed.configureTestingModule({
-      declarations: [RgwUserAccountsDetailsComponent, TableKeyValueComponent],
-      providers: [DimlessBinaryPipe, CdDatePipe]
-    }).compileComponents();
-
-    fixture = TestBed.createComponent(RgwUserAccountsDetailsComponent);
-    component = fixture.componentInstance;
-    component.selection = { quota: {}, bucket_quota: {} };
-    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
deleted file mode 100644 (file)
index 2e15a70..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-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'],
-  standalone: false
-})
-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].enabled
-        ? this.selection[quota_type].max_size <= -1
-          ? 'Unlimited'
-          : 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
-        : '-'
-    };
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details-breadcrumb.resolver.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details-breadcrumb.resolver.spec.ts
new file mode 100644 (file)
index 0000000..2477cf0
--- /dev/null
@@ -0,0 +1,31 @@
+import { convertToParamMap } from '@angular/router';
+import { expect } from '@jest/globals';
+
+import { RgwAccountDetailsBreadcrumbResolver } from './rgw-account-details-breadcrumb.resolver';
+
+describe('RgwAccountDetailsBreadcrumbResolver', () => {
+  it('should include account name in breadcrumb', () => {
+    const resolver = new RgwAccountDetailsBreadcrumbResolver();
+    const route: any = {
+      url: [{ path: 'account1' }],
+      pathFromRoot: [
+        { url: [] },
+        { url: [{ path: 'rgw' }] },
+        { url: [{ path: 'accounts' }] },
+        { url: [{ path: 'account1' }] }
+      ],
+      paramMap: convertToParamMap({ accountName: 'account1' }),
+      data: {
+        account: {
+          name: 'account1'
+        }
+      }
+    };
+
+    const result = resolver.resolve(route);
+
+    expect(result.length).toBe(1);
+    expect(result[0].text).toBe('account1');
+    expect(result[0].path).toBe('/rgw/accounts/account1');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details-breadcrumb.resolver.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details-breadcrumb.resolver.ts
new file mode 100644 (file)
index 0000000..029e4a1
--- /dev/null
@@ -0,0 +1,17 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot } from '@angular/router';
+
+import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs';
+import { Account } from '../models/rgw-user-accounts';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class RgwAccountDetailsBreadcrumbResolver extends BreadcrumbsResolver {
+  resolve(route: ActivatedRouteSnapshot): IBreadcrumb[] {
+    const account = route.data?.account as Account | null;
+    const accountName = account?.name || route.paramMap.get('accountName') || '';
+
+    return [{ text: accountName, path: this.getFullPath(route) }];
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details.resolver.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details.resolver.spec.ts
new file mode 100644 (file)
index 0000000..cb1b36c
--- /dev/null
@@ -0,0 +1,51 @@
+import { TestBed } from '@angular/core/testing';
+import { convertToParamMap } from '@angular/router';
+import { of } from 'rxjs';
+
+import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service';
+import { RgwAccountDetailsResolver } from './rgw-account-details.resolver';
+
+describe('RgwAccountDetailsResolver', () => {
+  let resolver: RgwAccountDetailsResolver;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [
+        RgwAccountDetailsResolver,
+        {
+          provide: RgwUserAccountsService,
+          useValue: {
+            list: () =>
+              of([
+                {
+                  id: 'RGW11111111111111111',
+                  name: 'account1'
+                },
+                {
+                  id: 'RGW22222222222222222',
+                  name: 'account2'
+                }
+              ])
+          }
+        }
+      ]
+    });
+
+    resolver = TestBed.inject(RgwAccountDetailsResolver);
+  });
+
+  it('should resolve account by account name', (done) => {
+    const route: any = {
+      paramMap: convertToParamMap({ accountName: 'account2' })
+    };
+
+    resolver.resolve(route).subscribe((account) => {
+      if (account?.id !== 'RGW22222222222222222') {
+        done(new Error('Failed to resolve account by account name'));
+        return;
+      }
+
+      done();
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details.resolver.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details.resolver.ts
new file mode 100644 (file)
index 0000000..a46e179
--- /dev/null
@@ -0,0 +1,29 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
+import { Observable, of } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
+
+import { Account } from '../models/rgw-user-accounts';
+import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class RgwAccountDetailsResolver implements Resolve<Account | null> {
+  constructor(private rgwUserAccountsService: RgwUserAccountsService) {}
+
+  resolve(route: ActivatedRouteSnapshot): Observable<Account | null> {
+    const accountName = route.paramMap.get('accountName') ?? '';
+    if (!accountName) {
+      return of(null);
+    }
+
+    return this.rgwUserAccountsService.list(true).pipe(
+      map((accounts: Account[]) => {
+        const account = accounts.find((item) => item.name === accountName);
+        return account ?? null;
+      }),
+      catchError(() => of(null))
+    );
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component.html
new file mode 100644 (file)
index 0000000..cbad58d
--- /dev/null
@@ -0,0 +1,61 @@
+@if (selection) {
+  @switch (section) {
+    @case ('overview') {
+      <cd-resource-overview-card
+        title="Account details"
+        [columns]="4"
+        [fields]="overviewField"
+      >
+      </cd-resource-overview-card>
+
+      @if (selection.quota) {
+        <section class="cds-mt-6 cds-mb-6">
+          <h2
+            class="user-account-resource-section__title cds--type-heading-03"
+            i18n
+          >
+            Account Quota
+          </h2>
+          <p
+            class="user-account-resource-section__description cds--type-body-compact-01"
+            i18n
+          >
+            View the account quota details, including maximum object and size.
+          </p>
+          <cd-table-key-value [data]="quota"></cd-table-key-value>
+        </section>
+      }
+
+      @if (selection.bucket_quota) {
+        <section class="cds-mb-6">
+          <h2
+            class="user-account-resource-section__title cds--type-heading-03"
+            i18n
+          >
+            Bucket Quota
+          </h2>
+          <p
+            class="user-account-resource-section__description cds--type-body-compact-01"
+            i18n
+          >
+            View the bucket quota details, including maximum object and size.
+          </p>
+          <cd-table-key-value [data]="bucket_quota"></cd-table-key-value>
+        </section>
+      }
+    }
+    @case ('roles') {
+      <cd-rgw-account-roles-list
+        [accountId]="selection.id"
+        [accountName]="selection.name"
+      >
+      </cd-rgw-account-roles-list>
+    }
+  }
+} @else if (notFound) {
+  <cd-alert-panel
+    type="error"
+    i18n
+    >No account found.</cd-alert-panel
+  >
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component.ts
new file mode 100644 (file)
index 0000000..9942e6d
--- /dev/null
@@ -0,0 +1,123 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { Subscription } from 'rxjs';
+import { Account } from '../models/rgw-user-accounts';
+import { OverviewField } from '~/app/shared/components/resource-overview-card/resource-overview-card.component';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+
+@Component({
+  selector: 'cd-rgw-user-accounts-resource-page',
+  templateUrl: './rgw-user-accounts-resource-page.component.html',
+  styleUrls: ['./rgw-user-accounts-resource-page.component.scss'],
+  standalone: false
+})
+export class RgwUserAccountsResourcePageComponent implements OnInit, OnDestroy {
+  private sub = new Subscription();
+
+  section = '';
+  selection?: Account;
+  notFound = false;
+  overviewField: OverviewField[] = [];
+  quota: Record<string, string | number> = {};
+  bucket_quota: Record<string, string | number> = {};
+
+  constructor(
+    private route: ActivatedRoute,
+    private dimlessBinary: DimlessBinaryPipe
+  ) {}
+
+  ngOnInit(): void {
+    this.section = this.route.snapshot.data['section'] ?? 'overview';
+
+    this.sub.add(
+      this.route.parent?.data.subscribe((data) => {
+        const account = (data?.account ?? null) as Account | null;
+        this.applyAccount(account);
+      })
+    );
+  }
+
+  ngOnDestroy(): void {
+    this.sub.unsubscribe();
+  }
+
+  private applyAccount(account: Account | null): void {
+    this.notFound = !account;
+    if (!account) {
+      this.selection = undefined;
+      this.overviewField = [];
+      this.quota = {};
+      this.bucket_quota = {};
+      return;
+    }
+
+    this.selection = account;
+    this.overviewField = this.buildOverviewFields(account);
+    this.quota = this.createDisplayValues('quota');
+    this.bucket_quota = this.createDisplayValues('bucket_quota');
+  }
+
+  private buildOverviewFields(account: Account): OverviewField[] {
+    return [
+      {
+        label: $localize`Name`,
+        value: account.name
+      },
+      {
+        label: $localize`Tenant`,
+        value: account.tenant
+      },
+      {
+        label: $localize`Account id`,
+        value: account.id
+      },
+      {
+        label: $localize`Email address`,
+        value: account.email
+      },
+      {
+        label: $localize`Max users`,
+        value: account.max_users
+      },
+      {
+        label: $localize`Max roles`,
+        value: account.max_roles
+      },
+      {
+        label: $localize`Max groups`,
+        value: account.max_groups
+      },
+      {
+        label: $localize`Max. buckets`,
+        value: account.max_buckets
+      },
+      {
+        label: $localize`Max access keys`,
+        value: account.max_access_keys
+      }
+    ];
+  }
+
+  private createDisplayValues(
+    quotaType: 'quota' | 'bucket_quota'
+  ): Record<string, string | number> {
+    if (!this.selection?.[quotaType]) {
+      return {};
+    }
+
+    const quota = this.selection[quotaType];
+    return {
+      [$localize`Enabled`]: quota.enabled ? $localize`Yes` : $localize`No`,
+      [$localize`Maximum size`]: quota.enabled
+        ? quota.max_size <= -1
+          ? $localize`Unlimited`
+          : this.dimlessBinary.transform(quota.max_size)
+        : '-',
+      [$localize`Maximum objects`]: quota.enabled
+        ? quota.max_objects <= -1
+          ? $localize`Unlimited`
+          : quota.max_objects
+        : '-'
+    };
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.spec.ts
new file mode 100644 (file)
index 0000000..493ebda
--- /dev/null
@@ -0,0 +1,75 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute } from '@angular/router';
+import { of } from 'rxjs';
+
+import { RgwUserAccountsResourcePageComponent } from './rgw-user-accounts-resource-page.component';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+
+describe('RgwUserAccountsResourcePageComponent', () => {
+  let component: RgwUserAccountsResourcePageComponent;
+  let fixture: ComponentFixture<RgwUserAccountsResourcePageComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [RgwUserAccountsResourcePageComponent],
+      providers: [
+        DimlessBinaryPipe,
+        {
+          provide: ActivatedRoute,
+          useValue: {
+            parent: {
+              data: of({
+                account: {
+                  id: 'RGW11111111111111111',
+                  tenant: '',
+                  name: 'account1',
+                  email: 'account1@ceph.com',
+                  quota: {
+                    enabled: false,
+                    check_on_raw: false,
+                    max_size: -1,
+                    max_size_kb: -1,
+                    max_objects: -1
+                  },
+                  bucket_quota: {
+                    enabled: false,
+                    check_on_raw: false,
+                    max_size: -1,
+                    max_size_kb: -1,
+                    max_objects: -1
+                  },
+                  max_users: 1000,
+                  max_roles: 1000,
+                  max_groups: 1000,
+                  max_buckets: 1000,
+                  max_access_keys: 4
+                }
+              })
+            },
+            snapshot: {
+              data: {
+                section: 'overview'
+              }
+            }
+          }
+        }
+      ],
+      schemas: [NO_ERRORS_SCHEMA]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(RgwUserAccountsResourcePageComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should load account overview data', () => {
+    expect(component.selection?.name).toBe('account1');
+    expect(component.overviewField.length).toBeGreaterThan(0);
+    expect(component.overviewField.find((field) => field.label === 'Max users')?.value).toBe(1000);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.html
new file mode 100644 (file)
index 0000000..c6ad2f1
--- /dev/null
@@ -0,0 +1,6 @@
+<cd-sidebar-layout
+  class="rgw-account-details-layout"
+  [title]="accountName || accountId"
+  [items]="sidebarItems"
+>
+</cd-sidebar-layout>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.spec.ts
new file mode 100644 (file)
index 0000000..70d13a4
--- /dev/null
@@ -0,0 +1,48 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, convertToParamMap } from '@angular/router';
+import { of } from 'rxjs';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+
+import { RgwUserAccountsResourceSidebarComponent } from './rgw-user-accounts-resource-sidebar.component';
+
+describe('RgwUserAccountsResourceSidebarComponent', () => {
+  let component: RgwUserAccountsResourceSidebarComponent;
+  let fixture: ComponentFixture<RgwUserAccountsResourceSidebarComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [RgwUserAccountsResourceSidebarComponent],
+      providers: [
+        {
+          provide: ActivatedRoute,
+          useValue: {
+            paramMap: of(convertToParamMap({ accountName: 'account1' })),
+            data: of({
+              account: {
+                id: 'RGW11111111111111111',
+                name: 'account1'
+              }
+            })
+          }
+        }
+      ],
+      schemas: [NO_ERRORS_SCHEMA]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(RgwUserAccountsResourceSidebarComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should build sidebar items', () => {
+    expect(component.sidebarItems.length).toBe(2);
+  });
+
+  it('should expose resolved account name for header', () => {
+    expect(component.accountName).toBe('account1');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.ts
new file mode 100644 (file)
index 0000000..a5e35cc
--- /dev/null
@@ -0,0 +1,58 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute, ParamMap } from '@angular/router';
+import { Subscription } from 'rxjs';
+import { SidebarItem } from '~/app/shared/components/sidebar-layout/sidebar-layout.component';
+import { Account } from '../models/rgw-user-accounts';
+
+@Component({
+  selector: 'cd-rgw-user-accounts-resource-sidebar',
+  templateUrl: './rgw-user-accounts-resource-sidebar.component.html',
+  styleUrls: ['./rgw-user-accounts-resource-sidebar.component.scss'],
+  standalone: false
+})
+export class RgwUserAccountsResourceSidebarComponent implements OnInit, OnDestroy {
+  private sub = new Subscription();
+  readonly basePath = '/rgw/accounts';
+  accountNameRoute = '';
+  accountId = '';
+  accountName = '';
+  sidebarItems: SidebarItem[] = [];
+
+  constructor(private route: ActivatedRoute) {}
+
+  ngOnInit(): void {
+    this.sub.add(
+      this.route.paramMap.subscribe((pm: ParamMap) => {
+        this.accountNameRoute = pm.get('accountName') ?? '';
+        this.buildSidebarItems();
+      })
+    );
+
+    this.sub.add(
+      this.route.data.subscribe((data) => {
+        const account = (data?.account ?? null) as Account | null;
+        this.accountId = account?.id ?? '';
+        this.accountName = account?.name ?? this.accountNameRoute;
+      })
+    );
+  }
+
+  ngOnDestroy(): void {
+    this.sub.unsubscribe();
+  }
+
+  private buildSidebarItems(): void {
+    this.sidebarItems = [
+      {
+        label: $localize`Overview`,
+        route: [this.basePath, this.accountNameRoute, 'overview'],
+        routerLinkActiveOptions: { exact: true }
+      },
+      {
+        label: $localize`Roles`,
+        route: [this.basePath, this.accountNameRoute, 'roles'],
+        routerLinkActiveOptions: { exact: true }
+      }
+    ];
+  }
+}
index 7e1b5acf9ef42641fdb056266a58250f54c5af13..2c06e66aaa933739c30e13438566b563ca6bc087 100644 (file)
@@ -10,8 +10,7 @@
   [columns]="columns"
   selectionType="single"
   columnMode="flex"
-  [hasDetails]="true"
-  (setExpandedRow)="setExpandedRow($event)"
+  [hasDetails]="false"
   (updateSelection)="updateSelection($event)"
   identifier="id"
   (fetchData)="getAccountsList($event)"
@@ -24,9 +23,4 @@
     [tableActions]="tableActions"
   >
   </cd-table-actions>
-  <cd-rgw-user-accounts-details
-    *cdTableDetail
-    [selection]="expandedRow"
-  >
-  </cd-rgw-user-accounts-details>
 </cd-table>
index e31187dd0e5fbaaf441a3b05282d2cd92e980a82..4e87b97107ee8d2487fcc21872398bbecc891907 100644 (file)
@@ -36,7 +36,7 @@ export class RgwUserAccountsComponent extends ListWithDetails implements OnInit
   permission: Permission;
   tableActions: CdTableAction[] = [];
   columns: CdTableColumn[] = [];
-  accounts: Account[] = [];
+  accounts: (Account & { cdLink?: string })[] = [];
   selection: CdTableSelection = new CdTableSelection();
   declare staleTimeout: number;
 
@@ -58,7 +58,8 @@ export class RgwUserAccountsComponent extends ListWithDetails implements OnInit
       {
         name: $localize`Name`,
         prop: 'name',
-        flexGrow: 1
+        flexGrow: 1,
+        cellTransformation: CellTemplate.routerLink
       },
       {
         name: $localize`Tenant`,
@@ -160,7 +161,10 @@ export class RgwUserAccountsComponent extends ListWithDetails implements OnInit
     this.setTableRefreshTimeout();
     this.rgwUserAccountsService.list(true).subscribe({
       next: (accounts: Account[]) => {
-        this.accounts = accounts;
+        this.accounts = accounts.map((account) => ({
+          ...account,
+          cdLink: `/rgw/accounts/${encodeURIComponent(account.name)}/overview`
+        }));
       },
       error: () => {
         if (context) {
index 86ee46d60fc57ecc37f507288a163f9228ca00f7..cd112ad8edab3fcceab993b07283eccad886b8ca 100644 (file)
@@ -108,7 +108,10 @@ import { AreaChartComponent } from '~/app/shared/components/area-chart/area-char
 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';
+import { RgwUserAccountsResourceSidebarComponent } from './rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component';
+import { RgwUserAccountsResourcePageComponent } from './rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component';
+import { RgwAccountDetailsResolver } from './rgw-user-accounts-resource-page/rgw-account-details.resolver';
+import { RgwAccountDetailsBreadcrumbResolver } from './rgw-user-accounts-resource-page/rgw-account-details-breadcrumb.resolver';
 import { RgwStorageClassDetailsComponent } from './rgw-storage-class-details/rgw-storage-class-details.component';
 import { RgwStorageClassFormComponent } from './rgw-storage-class-form/rgw-storage-class-form.component';
 import { RgwBucketTieringFormComponent } from './rgw-bucket-tiering-form/rgw-bucket-tiering-form.component';
@@ -187,7 +190,6 @@ import { RgwAccountRoleFormComponent } from './rgw-account-role-form/rgw-account
     RgwBucketDetailsComponent,
     RgwUserListComponent,
     RgwUserDetailsComponent,
-    RgwBucketFormComponent,
     RgwUserFormComponent,
     RgwUserSwiftKeyModalComponent,
     RgwUserS3KeyModalComponent,
@@ -222,7 +224,8 @@ import { RgwAccountRoleFormComponent } from './rgw-account-role-form/rgw-account
     RgwMultisiteTabsComponent,
     RgwUserAccountsComponent,
     RgwUserAccountsFormComponent,
-    RgwUserAccountsDetailsComponent,
+    RgwUserAccountsResourceSidebarComponent,
+    RgwUserAccountsResourcePageComponent,
     RgwStorageClassListComponent,
     RgwStorageClassDetailsComponent,
     RgwStorageClassFormComponent,
@@ -296,7 +299,28 @@ const routes: Routes = [
       {
         path: `${URLVerbs.EDIT}/:id`,
         component: RgwUserAccountsFormComponent,
-        data: { breadcrumbs: $localize`Edit account` }
+        data: { breadcrumbs: ActionLabels.EDIT }
+      },
+      {
+        path: ':accountName',
+        component: RgwUserAccountsResourceSidebarComponent,
+        data: { breadcrumbs: RgwAccountDetailsBreadcrumbResolver },
+        resolve: {
+          account: RgwAccountDetailsResolver
+        },
+        children: [
+          { path: '', redirectTo: 'overview', pathMatch: 'full' },
+          {
+            path: 'overview',
+            component: RgwUserAccountsResourcePageComponent,
+            data: { breadcrumbs: 'Overview', section: 'overview' }
+          },
+          {
+            path: 'roles',
+            component: RgwUserAccountsResourcePageComponent,
+            data: { breadcrumbs: 'Roles', section: 'roles' }
+          }
+        ]
       }
     ]
   },