From: Syed Ali Ul Hasan Date: Thu, 16 Jul 2026 19:24:16 +0000 (+0530) Subject: mgr/dashboard: migrated accounts table tabs to resource pages X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F69807%2Fhead;p=ceph.git mgr/dashboard: migrated accounts table tabs to resource pages - 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 --- diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts index d1ad20b7a47..4a26f780ee0 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts @@ -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 */ diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/accounts.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/accounts.po.ts index f172c6265cd..fd6c4d013e7 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/accounts.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/accounts.po.ts @@ -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() { diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts index 2026d0dc589..a660be5550a 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts @@ -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'); }); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts index e42abd40d71..04bae24ff19 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts @@ -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 index 97c6a623e33..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.html +++ /dev/null @@ -1,34 +0,0 @@ -@if (selection) { - - -
- @if (selection.quota) { -
- Account quota - -
- } - @if (selection.bucket_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 deleted file mode 100644 index e69de29bb2d..00000000000 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 index 1aad6167fa2..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.spec.ts +++ /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; - - 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 index 2e15a701540..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.ts +++ /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 index 00000000000..2477cf06c40 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details-breadcrumb.resolver.spec.ts @@ -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 index 00000000000..029e4a19499 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details-breadcrumb.resolver.ts @@ -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 index 00000000000..cb1b36c3e25 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details.resolver.spec.ts @@ -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 index 00000000000..a46e1793160 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-account-details.resolver.ts @@ -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 { + constructor(private rgwUserAccountsService: RgwUserAccountsService) {} + + resolve(route: ActivatedRouteSnapshot): Observable { + 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 index 00000000000..cbad58d691d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component.html @@ -0,0 +1,61 @@ +@if (selection) { + @switch (section) { + @case ('overview') { + + + + @if (selection.quota) { +
+ + + +
+ } + + @if (selection.bucket_quota) { +
+ + + +
+ } + } + @case ('roles') { + + + } + } +} @else if (notFound) { + No account found. +} 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 index 00000000000..e69de29bb2d 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 index 00000000000..9942e6dd33a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.component.ts @@ -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 = {}; + bucket_quota: Record = {}; + + 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 { + 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 index 00000000000..493ebda0b46 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-page/rgw-user-accounts-resource-page.spec.ts @@ -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; + + 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 index 00000000000..c6ad2f17587 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.html @@ -0,0 +1,6 @@ + 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 index 00000000000..e69de29bb2d 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 index 00000000000..70d13a40b60 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.spec.ts @@ -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; + + 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 index 00000000000..a5e35cc5700 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-resource-sidebar/rgw-user-accounts-resource-sidebar.component.ts @@ -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 } + } + ]; + } +} 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 7e1b5acf9ef..2c06e66aaa9 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 @@ -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" > - - 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 e31187dd0e5..4e87b97107e 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 @@ -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) { 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 86ee46d60fc..cd112ad8eda 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 @@ -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' } + } + ] } ] },