index: { url: '#/logs', id: 'cd-logs' }
};
+ private setTimepickerValue(index: number, value: number) {
+ cy.get('.ngb-tp-input')
+ .eq(index)
+ .then(($input) => {
+ const input = $input[0] as HTMLInputElement;
+ input.value = String(value).padStart(2, '0');
+ input.dispatchEvent(new Event('input', { bubbles: true }));
+ input.dispatchEvent(new Event('change', { bubbles: true }));
+ });
+ }
+
checkAuditForPoolFunction(poolname: string, poolfunction: string, hour: number, minute: number) {
this.navigateTo();
cy.contains('.nav-link', 'Audit Logs').click();
// Enter an earliest time so that no old messages with the same pool name show up
- cy.get('.ngb-tp-input')
- .its(0)
- .then((input) => {
- cy.wrap(input).clear();
-
- if (hour < 10) cy.wrap(input).type(`${hour}`);
- });
-
- cy.get('.ngb-tp-input')
- .its(1)
- .then((input) => {
- cy.wrap(input).clear();
-
- if (minute < 10) cy.wrap(input).type(`${minute}`);
- });
+ this.setTimepickerValue(0, hour);
+ this.setTimepickerValue(1, minute);
// Enter the pool name into the filter box
- cy.get('input.form-control.ng-valid').first().clear().type(poolname);
+ cy.get('#logs-keyword').clear();
+ cy.get('#logs-keyword').type(poolname);
cy.get('.tab-pane.active')
.get('.log-viewer')
cy.contains('.nav-link', 'Audit Logs').click();
// Enter an earliest time so that no old messages with the same config name show up
- cy.get('.ngb-tp-input')
- .its(0)
- .then((input) => {
- cy.wrap(input).clear();
-
- if (hour < 10) cy.wrap(input).type(`${hour}`);
- });
-
- cy.get('.ngb-tp-input')
- .its(1)
- .then((input) => {
- cy.wrap(input).clear();
-
- if (minute < 10) cy.wrap(input).type(`${minute}`);
- });
+ this.setTimepickerValue(0, hour);
+ this.setTimepickerValue(1, minute);
// Enter the config name into the filter box
- cy.get('input.form-control.ng-valid').first().clear().type(configname);
+ cy.get('#logs-keyword').clear();
+ cy.get('#logs-keyword').type(configname);
cy.get('.tab-pane.active')
.get('.log-viewer')
getResourcePage(content?: string) {
this.waitDataTableToLoad();
if (content) {
+ const escapedContent = content.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return cy
.contains('[cdstablerow] [cdstabledata]', content)
.parent('[cdstablerow]')
- .contains('[cdstabledata] a', new RegExp(`^${content}$`));
+ .contains(
+ '[cdstabledata] a, [cdstabledata] [cdslink]',
+ new RegExp(`^\\s*${escapedContent}\\s*$`)
+ );
}
- return cy.get('[cdstablerow] [cdstabledata] a').first();
+ return cy.get('[cdstablerow] [cdstabledata] a, [cdstablerow] [cdstabledata] [cdslink]').first();
}
/**
cy.contains('button', 'Edit User').click();
- // Click the user and check its details table for updated content
- this.getExpandCollapseElement(name).click();
- cy.get('[data-testid="datatable-row-detail"]')
- .should('contain.text', new_fullname)
- .and('contain.text', new_email)
- .and('contain.text', new_maxbuckets);
+ // Check that the new values are reflected in the table
+ this.getTableCell(4, new_fullname, true).should('exist');
+ this.getTableCell(5, new_email, true).should('exist');
+ this.getTableCell(7, new_maxbuckets, true).should('exist');
}
invalidCreate() {
}
checkUserKeys(user_name: string) {
- this.getExpandCollapseElement(user_name).should('be.visible').click();
- cy.get('cd-table').contains('td', user_name).click();
- cy.get('cd-rgw-user-details cd-table [cdstablerow]').first().click();
- cy.get("[aria-label='Show']").should('exist').click({ force: true });
+ this.searchTable(user_name);
+ this.getResourcePage(user_name).click();
+ cy.get('cd-table').contains('td', user_name).should('exist');
+ cy.contains('a', 'Show').should('exist').click({ force: true });
cy.get('input#user').should('exist');
cy.get('input#access_key').should('exist');
cy.get('input#secret_key').should('exist');
cy.contains('button', 'Edit User').click();
this.getTableRow(tenant + '$' + user_id).as('AccountUser');
- cy.get('@AccountUser').find('td').eq(3).should('contain.text', `${account_name}`);
-
- // check table details if we have all the details there
- this.getExpandCollapseElement(username).should('be.visible').click();
- // check the Account Details section
- cy.get('legend').should('contain.text', 'Account Details');
- cy.get('table#accountsDetails').scrollIntoView();
- cy.wait(500);
- cy.get('table#accountsDetails').find('tbody tr').should('have.length', 4);
- cy.get('table#accountsDetails').within(() => {
- cy.get('tr')
- .eq(0)
- .within(() => {
- cy.wait(500);
- cy.get('td').eq(0).should('contain.text', 'Account ID');
- cy.get('td').eq(1).should('contain.text', account_id);
- });
- cy.get('tr')
- .eq(1)
- .within(() => {
- cy.wait(500);
- cy.get('td').eq(0).should('contain.text', 'Name');
- cy.get('td').eq(1).should('contain.text', account_name);
- });
- cy.get('tr')
- .eq(2)
- .within(() => {
- cy.wait(500);
- cy.get('td').eq(0).should('contain.text', 'Tenant');
- cy.get('td').eq(1).should('contain.text', tenant);
- });
- cy.get('tr')
- .eq(3)
- .within(() => {
- cy.wait(500);
- cy.get('td').eq(0).should('contain.text', 'User type');
- cy.get('td').eq(1).should('contain.text', 'rgw user');
- });
- });
+ cy.get('@AccountUser').find('td').eq(2).should('contain.text', `${account_name}`);
+
+ // Check account details rendered in the resource overview card.
+ this.getResourcePage(username).should('be.visible').click();
+ this.assertOverviewFieldValue('Account ID', account_id);
+ this.assertOverviewFieldValue('Name', account_name);
+ this.assertOverviewFieldValue('Tenant', tenant);
+ this.assertOverviewFieldValue('User type', 'rgw user');
}
makeRootAccount(account_name: string, user_id: string, tenant: string) {
cy.contains('button', 'Edit User').click();
- // check table details if we have all the details there
- this.getExpandCollapseElement(username).should('be.visible').click();
- // check the Account Details section
- cy.get('legend').should('contain.text', 'Account Details');
- cy.get('table#accountsDetails').scrollIntoView();
- cy.wait(500);
- cy.get('table#accountsDetails').find('tbody tr').should('have.length', 4);
- cy.get('table#accountsDetails').within(() => {
- cy.get('tr')
- .eq(3)
- .within(() => {
- cy.wait(500);
- cy.get('td').eq(0).should('contain.text', 'User type');
- cy.get('td').eq(1).should('contain.text', 'Account root user');
- });
- });
+ // Check account details rendered in the resource overview card.
+ this.getResourcePage(username).should('be.visible').click();
+ this.assertOverviewFieldValue('User type', 'Account root user');
+ }
+
+ private assertOverviewFieldValue(label: string, value: string) {
+ cy.contains('cd-resource-overview-card h3', 'User details').should('be.visible');
+ cy.contains('cd-resource-overview-card .cd-overview-label', label)
+ .parent('.cd-overview-item')
+ .find('.cd-overview-value')
+ .should('contain.text', value);
}
}
-interface Key {
+export interface Key {
access_key: string;
active: boolean;
secret_key: string;
user: string;
}
-interface SwiftKey {
+export interface SwiftKey {
active: boolean;
secret_key: string;
user: string;
permissions: string;
}
-interface BucketQuota {
+export interface BucketQuota {
check_on_raw: boolean;
enabled: boolean;
max_objects: number;
max_size_kb: number;
}
-interface UserQuota {
+export interface UserQuota {
check_on_raw: boolean;
enabled: boolean;
max_objects: number;
user_id: string;
user_quota: UserQuota;
}
+
+export interface KeyRow {
+ id: number;
+ type: 'S3' | 'Swift';
+ username: string;
+ ref: Key | SwiftKey;
+}
+
+export type ExtendedRgwUser = RgwUser & {
+ account?: { id?: string; name?: string; tenant?: string };
+ managed_user_policies?: string[];
+};
+
+export const RGW_MAX_BUCKETS_MAP: Record<string, string> = {
+ '-1': $localize`Disabled`,
+ '0': $localize`Unlimited`
+};
-<legend i18n>{{ type === 'user' ? 'User Rate Limit' : 'Bucket Rate Limit' }}</legend>
+@if (showHeading) {
+ <legend i18n>
+ {{ type === 'user' ? 'User Rate Limit' : 'Bucket Rate Limit' }}
+ </legend>
+}
<table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
<tbody>
<tr>
})
export class RgwRateLimitDetailsComponent {
@Input() rateLimitConfig: RgwRateLimitConfig;
- @Input() type: string;
+ @Input() type!: string;
+ @Input() showHeading = true;
}
+++ /dev/null
-<ng-container *ngIf="selection">
- <div *ngIf="user">
- <div *ngIf="keys.length">
- <legend i18n>Keys</legend>
- <cd-table
- [data]="keys"
- [columns]="keysColumns"
- columnMode="flex"
- selectionType="single"
- forceIdentifier="true"
- (updateSelection)="updateKeysSelection($event)"
- >
- <cd-table-actions
- class="table-actions"
- [permission]="{ read: true }"
- [selection]="selection"
- [tableActions]="tableAction"
- ></cd-table-actions>
- </cd-table>
- </div>
-
- <legend i18n>Details</legend>
- <table
- class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md"
- >
- <tbody>
- <tr>
- <td
- i18n
- class="bold w-25"
- >
- Tenant
- </td>
- <td class="w-75">{{ user.tenant }}</td>
- </tr>
- <tr>
- <td
- i18n
- class="bold w-25"
- >
- User ID
- </td>
- <td class="w-75">{{ user.uid }}</td>
- </tr>
- <tr>
- <td
- i18n
- class="bold w-25"
- >
- Username
- </td>
- <td class="w-75">{{ user.uid }}</td>
- </tr>
- <tr>
- <td
- i18n
- class="bold"
- >
- Full name
- </td>
- <td>{{ user.display_name }}</td>
- </tr>
- <tr *ngIf="user.email?.length">
- <td
- i18n
- class="bold"
- >
- Email address
- </td>
- <td>{{ user.email }}</td>
- </tr>
- <tr>
- <td
- i18n
- class="bold"
- >
- Suspended
- </td>
- <td>{{ user.suspended | booleanText }}</td>
- </tr>
- <tr>
- <td
- i18n
- class="bold"
- >
- System user
- </td>
- <td>{{ user.system | booleanText }}</td>
- </tr>
- <tr>
- <td
- i18n
- class="bold"
- >
- Maximum buckets
- </td>
- <td>{{ user.max_buckets | map: maxBucketsMap }}</td>
- </tr>
- @if (user.type === 'rgw' && selection.account?.id) {
- <tr>
- <td
- i18n
- class="bold"
- >
- Managed policies
- </td>
- <td i18n>{{ extractPolicyNamesFromArns(user.managed_user_policies) }}</td>
- </tr>
- }
- <tr *ngIf="user.subusers && user.subusers.length">
- <td
- i18n
- class="bold"
- >
- Subusers
- </td>
- <td>
- <div *ngFor="let subuser of user.subusers">
- {{ subuser.id }} ({{ subuser.permissions }})
- </div>
- </td>
- </tr>
- <tr *ngIf="user.caps && user.caps.length">
- <td
- i18n
- class="bold"
- >
- Capabilities
- </td>
- <td>
- <div *ngFor="let cap of user.caps">{{ cap.type }} ({{ cap.perm }})</div>
- </td>
- </tr>
- <tr *ngIf="user.mfa_ids?.length">
- <td
- i18n
- class="bold"
- >
- MFAs(Id)
- </td>
- <td>{{ user.mfa_ids | join }}</td>
- </tr>
- </tbody>
- </table>
-
- <ng-container *ngIf="selection.account && selection.account?.id">
- <legend i18n>Account Details</legend>
- <table
- class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md"
- id="accountsDetails"
- >
- <tbody>
- <tr>
- <td
- i18n
- class="bold w-25"
- >
- Account ID
- </td>
- <td class="w-75">{{ selection.account?.id }}</td>
- </tr>
- <tr>
- <td
- i18n
- class="bold w-25"
- >
- Name
- </td>
- <td class="w-75">{{ selection.account?.name }}</td>
- </tr>
- <tr>
- <td
- i18n
- class="bold w-25"
- >
- Tenant
- </td>
- <td class="w-75">{{ selection.account?.tenant || '-' }}</td>
- </tr>
- <tr>
- <td
- i18n
- class="bold w-25"
- >
- User type
- </td>
- <td
- class="w-75"
- i18n
- >
- {{ user?.type === 'root' ? 'Account root user' : 'rgw user' }}
- </td>
- </tr>
- </tbody>
- </table>
- </ng-container>
-
- <!-- User quota -->
- <div *ngIf="user.user_quota">
- <legend i18n>User quota</legend>
- <table
- class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md"
- >
- <tbody>
- <tr>
- <td
- i18n
- class="bold w-25"
- >
- Enabled
- </td>
- <td class="w-75">{{ user.user_quota.enabled | booleanText }}</td>
- </tr>
- <tr>
- <td
- i18n
- class="bold"
- >
- Maximum size
- </td>
- <td *ngIf="!user.user_quota.enabled">-</td>
- <td
- *ngIf="user.user_quota.enabled && user.user_quota.max_size <= -1"
- i18n
- >
- Unlimited
- </td>
- <td *ngIf="user.user_quota.enabled && user.user_quota.max_size > -1">
- {{ user.user_quota.max_size | dimlessBinary }}
- </td>
- </tr>
- <tr>
- <td
- i18n
- class="bold"
- >
- Maximum objects
- </td>
- <td *ngIf="!user.user_quota.enabled">-</td>
- <td
- *ngIf="user.user_quota.enabled && user.user_quota.max_objects <= -1"
- i18n
- >
- Unlimited
- </td>
- <td *ngIf="user.user_quota.enabled && user.user_quota.max_objects > -1">
- {{ user.user_quota.max_objects }}
- </td>
- </tr>
- </tbody>
- </table>
- </div>
-
- <!-- Bucket quota -->
- <div *ngIf="user.bucket_quota">
- <legend i18n>Bucket quota</legend>
- <table
- class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md"
- >
- <tbody>
- <tr>
- <td
- i18n
- class="bold w-25"
- >
- Enabled
- </td>
- <td class="w-75">{{ user.bucket_quota.enabled | booleanText }}</td>
- </tr>
- <tr>
- <td
- i18n
- class="bold"
- >
- Maximum size
- </td>
- <td *ngIf="!user.bucket_quota.enabled">-</td>
- <td
- *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_size <= -1"
- i18n
- >
- Unlimited
- </td>
- <td *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_size > -1">
- {{ user.bucket_quota.max_size | dimlessBinary }}
- </td>
- </tr>
- <tr>
- <td
- i18n
- class="bold"
- >
- Maximum objects
- </td>
- <td *ngIf="!user.bucket_quota.enabled">-</td>
- <td
- *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_objects <= -1"
- i18n
- >
- Unlimited
- </td>
- <td *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_objects > -1">
- {{ user.bucket_quota.max_objects }}
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!-- User Rate Limit -->
- <div *ngIf="user.user_ratelimit">
- <cd-rgw-rate-limit-details
- [rateLimitConfig]="user.user_ratelimit"
- [type]="'user'"
- ></cd-rgw-rate-limit-details>
- </div>
- </div>
-</ng-container>
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-
-import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
-
-import { SharedModule } from '~/app/shared/shared.module';
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { RgwUserDetailsComponent } from './rgw-user-details.component';
-import { ModalService } from 'carbon-components-angular';
-
-describe('RgwUserDetailsComponent', () => {
- let component: RgwUserDetailsComponent;
- let fixture: ComponentFixture<RgwUserDetailsComponent>;
- let modalRef: any;
- configureTestBed({
- declarations: [RgwUserDetailsComponent],
- imports: [BrowserAnimationsModule, HttpClientTestingModule, SharedModule, NgbNavModule],
- provider: [ModalService]
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(RgwUserDetailsComponent);
- component = fixture.componentInstance;
- component.selection = {};
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('should show correct "System" info', () => {
- component.selection = { uid: '', email: '', system: true, keys: [], swift_keys: [] };
-
- component.ngOnChanges();
- fixture.detectChanges();
-
- const detailsTab = fixture.debugElement.nativeElement.querySelectorAll(
- '.cds--data-table--sort.cds--data-table--no-border tr td'
- );
- expect(detailsTab[10].textContent.trim()).toEqual('System user');
- expect(detailsTab[11].textContent.trim()).toEqual('Yes');
-
- component.selection.system = false;
- component.ngOnChanges();
- fixture.detectChanges();
-
- expect(detailsTab[11].textContent.trim()).toEqual('No');
- });
-
- it('should show mfa ids only if length > 0', () => {
- component.selection = {
- uid: 'dashboard',
- email: '',
- system: 'true',
- keys: [],
- swift_keys: [],
- mfa_ids: ['testMFA1', 'testMFA2'],
- type: 'rgw',
- account: { id: 'RGW12345678901234567' }
- };
-
- component.ngOnChanges();
- fixture.detectChanges();
-
- const detailsTab = fixture.debugElement.nativeElement.querySelectorAll(
- '.cds--data-table--sort.cds--data-table--no-border tr td'
- );
- expect(detailsTab[16].textContent.trim()).toEqual('MFAs(Id)');
- expect(detailsTab[17].textContent.trim()).toEqual('testMFA1, testMFA2');
- });
- it('should test updateKeysSelection', () => {
- component.selection = {
- hasMultiSelection: false,
- hasSelection: false,
- hasSingleSelection: false,
- _selected: []
- };
- component.updateKeysSelection(component.selection);
- expect(component.keysSelection).toEqual(component.selection);
- });
- it('should call showKeyModal when key selection is of type S3', () => {
- component.keysSelection.first = () => {
- return { type: 'S3', ref: { user: '', access_key: '', secret_key: '' } };
- };
- const modalShowSpy = spyOn(component['cdsModalService'], 'show').and.callFake(() => {
- modalRef = {
- setValues: jest.fn(),
- setViewing: jest.fn()
- };
- return modalRef;
- });
- component.showKeyModal();
- expect(modalShowSpy).toHaveBeenCalled();
- // expect(s).toHaveBeenCalledWith( modalRef.componentInstance.setViewing);
- });
- it('should call showKeyModal when key selection is of type Swift', () => {
- component.keysSelection.first = () => {
- return { type: 'Swift', ref: { user: '', access_key: '', secret_key: '' } };
- };
- const modalShowSpy = spyOn(component['cdsModalService'], 'show').and.callFake(() => {
- modalRef = {
- setValues: jest.fn(),
- setViewing: jest.fn()
- };
- return modalRef;
- });
- component.showKeyModal();
- expect(modalShowSpy).toHaveBeenCalled();
- // expect(s).toHaveBeenCalledWith( modalRef.componentInstance.setViewing);
- });
-});
+++ /dev/null
-import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
-
-import _ from 'lodash';
-
-import { RgwUserService } from '~/app/shared/api/rgw-user.service';
-import { Icons } from '~/app/shared/enum/icons.enum';
-import { CdTableColumn } from '~/app/shared/models/cd-table-column';
-import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
-import { RgwUserS3Key } from '../models/rgw-user-s3-key';
-import { RgwUserSwiftKey } from '../models/rgw-user-swift-key';
-import { RgwUserS3KeyModalComponent } from '../rgw-user-s3-key-modal/rgw-user-s3-key-modal.component';
-import { RgwUserSwiftKeyModalComponent } from '../rgw-user-swift-key-modal/rgw-user-swift-key-modal.component';
-import { CdTableAction } from '~/app/shared/models/cd-table-action';
-import { Permissions } from '~/app/shared/models/permissions';
-import { RgwRateLimitConfig } from '../models/rgw-rate-limit';
-import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
-import { USER } from '~/app/shared/constants/app.constants';
-
-@Component({
- selector: 'cd-rgw-user-details',
- templateUrl: './rgw-user-details.component.html',
- styleUrls: ['./rgw-user-details.component.scss'],
- standalone: false
-})
-export class RgwUserDetailsComponent implements OnChanges, OnInit {
- @ViewChild('accessKeyTpl')
- public accessKeyTpl: TemplateRef<any>;
- @ViewChild('secretKeyTpl')
- public secretKeyTpl: TemplateRef<any>;
-
- @Input()
- selection: any;
-
- // Details tab
- user: any;
- maxBucketsMap: {};
-
- // Keys tab
- keys: any = [];
- keysColumns: CdTableColumn[] = [];
- keysSelection: CdTableSelection = new CdTableSelection();
- tableAction: CdTableAction[] = [];
- permissions: Permissions;
-
- icons = Icons;
-
- constructor(
- private rgwUserService: RgwUserService,
- private cdsModalService: ModalCdsService
- ) {}
-
- ngOnInit() {
- this.keysColumns = [
- {
- name: $localize`Username`,
- prop: 'username',
- flexGrow: 1
- },
- {
- name: $localize`Type`,
- prop: 'type',
- flexGrow: 1
- }
- ];
- this.maxBucketsMap = {
- '-1': $localize`Disabled`,
- 0: $localize`Unlimited`
- };
- }
-
- ngOnChanges() {
- this.tableAction = [
- {
- name: $localize`Show`,
- permission: 'read',
- click: () => this.showKeyModal(),
- icon: Icons.show
- }
- ];
-
- if (this.selection) {
- this.user = this.selection;
-
- // Sort subusers and capabilities.
- this.user.subusers = _.sortBy(this.user.subusers, 'id');
- this.user.caps = _.sortBy(this.user.caps, 'type');
-
- // Load the user/bucket quota of the selected user.
- this.rgwUserService.getQuota(this.user.uid).subscribe((resp: object) => {
- _.extend(this.user, resp);
- });
-
- // Load the user rate limit of the selected user.
- this.rgwUserService.getUserRateLimit(this.user.uid).subscribe((resp: RgwRateLimitConfig) => {
- _.extend(this.user, resp);
- });
-
- // Process the keys.
- this.keys = [];
- if (this.user.keys) {
- this.user.keys.forEach((key: RgwUserS3Key) => {
- this.keys.push({
- id: this.keys.length + 1, // Create an unique identifier
- type: 'S3',
- username: key.user,
- ref: key
- });
- });
- }
- if (this.user.swift_keys) {
- this.user.swift_keys.forEach((key: RgwUserSwiftKey) => {
- this.keys.push({
- id: this.keys.length + 1, // Create an unique identifier
- type: 'Swift',
- username: key.user,
- ref: key
- });
- });
- }
-
- this.keys = _.sortBy(this.keys, USER);
- }
- }
-
- updateKeysSelection(selection: CdTableSelection) {
- this.keysSelection = selection;
- }
-
- showKeyModal() {
- const key = this.keysSelection.first();
- const modalRef = this.cdsModalService.show(
- key.type === 'S3' ? RgwUserS3KeyModalComponent : RgwUserSwiftKeyModalComponent
- );
- switch (key.type) {
- case 'S3':
- modalRef.setViewing();
- modalRef.setValues(key.ref.user, key.ref.access_key, key.ref.secret_key);
- break;
- case 'Swift':
- modalRef.setValues(key.ref.user, key.ref.secret_key);
- break;
- }
- }
-
- extractPolicyNamesFromArns(arnList: string[]) {
- if (!arnList || arnList.length === 0) {
- return '-';
- }
- return arnList
- .map((arn) => arn.trim().split('/').pop())
- .filter(Boolean)
- .join(', ');
- }
-}
[columns]="columns"
columnMode="flex"
selectionType="single"
- [hasDetails]="true"
- (setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)"
identifier="uid"
(fetchData)="getUserList($event)"
[tableActions]="tableActions"
>
</cd-table-actions>
- <cd-rgw-user-details
- *cdTableDetail
- [selection]="expandedRow"
- >
- </cd-rgw-user-details>
</cd-table>
<ng-template
#usernameTpl
let-row="data.row"
>
- <div cdsRow>
- <span i18n>{{ row.uid }}</span>
+ <div class="username-cell">
+ <a
+ cdsLink
+ [routerLink]="[viewUrl, row.uid | encodeUri, 'overview']"
+ (click)="$event.stopPropagation()"
+ >
+ {{ row.uid }}
+ </a>
@if (row.type === 'root') {
<cds-tooltip
[description]="'Account root user'"
@use '@carbon/layout';
+.username-cell {
+ display: flex;
+ align-items: center;
+ min-height: layout.$spacing-07;
+}
+
.account-root-icon {
margin-left: layout.$spacing-03;
}
import { URLBuilderService } from '~/app/shared/services/url-builder.service';
import { Account } from '../models/rgw-user-accounts';
import { catchError, map, switchMap } from 'rxjs/operators';
-import { RgwUser } from '../models/rgw-user';
+import { RgwUser, RGW_MAX_BUCKETS_MAP } from '../models/rgw-user';
const BASE_URL = 'rgw/user';
users: RgwUser[] = [];
userAccounts: Account[];
selection: CdTableSelection = new CdTableSelection();
- userDataSubject = new Subject();
+ userDataSubject: Subject<RgwUser[]> = new Subject<RgwUser[]>();
declare staleTimeout: number;
icons = Icons;
+ viewUrl = '/rgw/user';
constructor(
private authStorageService: AuthStorageService,
ngOnInit() {
this.permission = this.authStorageService.getPermissions().rgw;
+ this.userAccounts = [];
+ this.tableActions = [];
this.columns = [
{
name: $localize`Username`,
prop: 'max_buckets',
flexGrow: 1,
cellTransformation: CellTemplate.map,
- customTemplateConfig: {
- '-1': $localize`Disabled`,
- 0: $localize`Unlimited`
- }
+ customTemplateConfig: RGW_MAX_BUCKETS_MAP
},
{
name: $localize`Capacity Limit %`,
mapUsersWithAccount(users: RgwUser[]): RgwUser[] {
return users.map((user: RgwUser) => {
- const account: Account = this.userAccounts.find((acc: Account) => acc.id === user.account_id);
+ const account: Account | undefined = this.userAccounts.find(
+ (acc: Account) => acc.id === user.account_id
+ );
return {
account: account ? account : { name: '' }, // adding {name: ''} for sorting account name in user list to work
...user
--- /dev/null
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
+import { Observable, of } from 'rxjs';
+import { catchError, map, switchMap } from 'rxjs/operators';
+
+import _ from 'lodash';
+import { ExtendedRgwUser, RgwUser } from '~/app/ceph/rgw/models/rgw-user';
+
+import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service';
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+import { Account } from '../models/rgw-user-accounts';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RgwUserDetailsResolver implements Resolve<ExtendedRgwUser | null> {
+ constructor(
+ private rgwUserService: RgwUserService,
+ private rgwUserAccountsService: RgwUserAccountsService
+ ) {}
+
+ resolve(route: ActivatedRouteSnapshot): Observable<ExtendedRgwUser | null> {
+ const uid = route.paramMap.get('uid') ?? '';
+ if (!uid) {
+ return of(null);
+ }
+
+ return this.rgwUserService.get(uid).pipe(
+ switchMap((user: RgwUser) =>
+ this.rgwUserService.getQuota(uid).pipe(
+ catchError(() => of({})),
+ map((quotaResp: Partial<RgwUser>) => ({ user, quotaResp }))
+ )
+ ),
+ switchMap(({ user, quotaResp }) =>
+ this.rgwUserService.getUserRateLimit(uid).pipe(
+ catchError(() => of({})),
+ map((rateLimitResp: Record<string, number | boolean>) => ({
+ user,
+ quotaResp,
+ rateLimitResp
+ }))
+ )
+ ),
+ switchMap(({ user, quotaResp, rateLimitResp }) => {
+ if (!user?.account_id) {
+ return of({
+ ...user,
+ ...quotaResp,
+ ...rateLimitResp,
+ subusers: _.sortBy(user?.subusers, 'id'),
+ caps: _.sortBy(user?.caps, 'type')
+ });
+ }
+
+ return this.rgwUserAccountsService.get(user.account_id).pipe(
+ catchError(() => of(null)),
+ map((account: Account | null) => ({
+ ...user,
+ ...quotaResp,
+ ...rateLimitResp,
+ account: account ?? undefined,
+ subusers: _.sortBy(user?.subusers, 'id'),
+ caps: _.sortBy(user?.caps, 'type')
+ }))
+ );
+ }),
+ catchError(() => of(null))
+ );
+ }
+}
--- /dev/null
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot } from '@angular/router';
+
+import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RgwUserResourceBreadcrumbResolver extends BreadcrumbsResolver {
+ resolve(route: ActivatedRouteSnapshot): IBreadcrumb[] {
+ const uid = route.params?.uid || route.parent?.params?.uid || '';
+ const section =
+ route.firstChild?.url?.[0]?.path ||
+ route.params?.section ||
+ route.queryParams?.section ||
+ 'overview';
+ const sectionLabel = section
+ .split('-')
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(' ');
+ return [
+ { text: uid, path: `/rgw/user/${uid}/overview` },
+ { text: sectionLabel, path: this.getFullPath(route) }
+ ];
+ }
+}
--- /dev/null
+@if (user) {
+ @switch (section) {
+ @case ('overview') {
+ <cd-resource-overview-card
+ title="User details"
+ [columns]="4"
+ [fields]="overviewFields"
+ i18n-title
+ >
+ </cd-resource-overview-card>
+
+ @if (keys.length) {
+ <div class="cds-mt-6 cds-mb-6">
+ <h2
+ class="cds--type-heading-03"
+ i18n
+ >
+ Keys
+ </h2>
+ <p
+ class="cds--type-body-compact-01"
+ i18n
+ >
+ View the user key details, including access and secret keys.
+ </p>
+ <cd-table
+ [data]="keys"
+ [columns]="keysColumns"
+ columnMode="flex"
+ forceIdentifier="true"
+ >
+ </cd-table>
+ </div>
+ }
+
+ @if (user.user_quota) {
+ <section class="cds-mt-6 cds-mb-6">
+ <h2
+ class="cds--type-heading-03"
+ i18n
+ >
+ User Quota
+ </h2>
+ <p
+ class="cds--type-body-compact-01"
+ i18n
+ >
+ View the user quota details, including maximum object and size.
+ </p>
+ <cd-table-key-value [data]="userQuota"></cd-table-key-value>
+ </section>
+ }
+
+ @if (user.bucket_quota) {
+ <section class="cds-mb-6">
+ <h2
+ class="cds--type-heading-03"
+ i18n
+ >
+ Bucket Quota
+ </h2>
+ <p
+ class="cds--type-body-compact-01"
+ i18n
+ >
+ View the bucket quota details, including maximum object and size.
+ </p>
+ <cd-table-key-value [data]="bucketQuota"></cd-table-key-value>
+ </section>
+ }
+
+ @if (user.user_ratelimit) {
+ <h2
+ class="user-resource-section__title cds--type-heading-03"
+ i18n
+ >
+ User Rate Limit
+ </h2>
+ <p
+ class="user-resource-section__description cds--type-body-compact-01"
+ i18n
+ >
+ View the user rate limit details.
+ </p>
+ <cd-rgw-rate-limit-details
+ [rateLimitConfig]="user.user_ratelimit"
+ [type]="'user'"
+ [showHeading]="false"
+ >
+ </cd-rgw-rate-limit-details>
+ }
+ }
+ }
+} @else if (notFound) {
+ <cd-alert-panel
+ type="error"
+ i18n
+ >No user found.</cd-alert-panel
+ >
+}
+
+<ng-template
+ #viewKeyTpl
+ let-row="data.row"
+>
+ <a
+ cdsLink
+ tabindex="0"
+ (click)="showKeyModal(row); $event.stopPropagation()"
+ (keydown.enter)="showKeyModal(row); $event.stopPropagation()"
+ i18n
+ >
+ Show
+ </a>
+</ng-template>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute } from '@angular/router';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { of } from 'rxjs';
+
+import { RgwUserResourcePageComponent } from './rgw-user-resource-page.component';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
+import { RgwUserS3KeyModalComponent } from '../rgw-user-s3-key-modal/rgw-user-s3-key-modal.component';
+import { RgwUserSwiftKeyModalComponent } from '../rgw-user-swift-key-modal/rgw-user-swift-key-modal.component';
+import { RgwUser } from '~/app/ceph/rgw/models/rgw-user';
+
+describe('RgwUserResourcePageComponent', () => {
+ let component: RgwUserResourcePageComponent;
+ let fixture: ComponentFixture<RgwUserResourcePageComponent>;
+ let modalCdsServiceMock: any;
+
+ const mockUser: Partial<RgwUser> & { account?: any; managed_user_policies?: any } = {
+ uid: 'test-user',
+ tenant: 'test-tenant',
+ display_name: 'Test User',
+ email: 'test@example.com',
+ suspended: 0,
+ system: false,
+ max_buckets: 0,
+ caps: [{ type: 'users', perm: '*' }],
+ subusers: [{ id: 'sub1', permissions: 'read' }],
+ mfa_ids: ['mfa1', 'mfa2'],
+ managed_user_policies: ['arn:aws:iam::123:policy/Pol1'],
+ user_quota: {
+ enabled: true,
+ max_size: 1024,
+ max_objects: 100,
+ max_size_kb: 1,
+ check_on_raw: false
+ },
+ bucket_quota: {
+ enabled: false,
+ max_size: -1,
+ max_objects: -1,
+ max_size_kb: -1,
+ check_on_raw: false
+ },
+ stats: {
+ size_actual: 512,
+ num_objects: 25,
+ size: 0,
+ size_utilized: 0,
+ size_kb: 0,
+ size_kb_actual: 0,
+ size_kb_utilized: 0
+ },
+ keys: [{ user: 'test-user', access_key: 'A1', secret_key: 'S1', active: true }],
+ swift_keys: [{ user: 'test-user:swift', secret_key: 'S2', active: true }],
+ account: { id: 'acc1', name: 'acc-name', tenant: 'acc-tenant' },
+ type: 'rgw'
+ };
+
+ const activatedRouteMock = {
+ snapshot: { data: { section: 'overview' } },
+ parent: {
+ data: of({ user: mockUser })
+ }
+ };
+
+ class MockDimlessBinaryPipe {
+ transform(value: any): string {
+ return `${value} B`;
+ }
+ }
+
+ beforeEach(async () => {
+ // Create a Jest mock object instead of a Jasmine SpyObj
+ modalCdsServiceMock = {
+ show: jest.fn()
+ };
+
+ await TestBed.configureTestingModule({
+ declarations: [RgwUserResourcePageComponent],
+ providers: [
+ { provide: ActivatedRoute, useValue: activatedRouteMock },
+ { provide: ModalCdsService, useValue: modalCdsServiceMock },
+ { provide: DimlessBinaryPipe, useClass: MockDimlessBinaryPipe }
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwUserResourcePageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should set section from route snapshot on init', () => {
+ expect(component.section).toBe('overview');
+ });
+
+ it('should set notFound to true if user is null', () => {
+ component['applyUser'](null);
+ expect(component.notFound).toBe(true);
+ expect(component.user).toBeUndefined();
+ expect(component.overviewFields).toEqual([]);
+ expect(component.keys).toEqual([]);
+ });
+
+ it('should populate overviewFields correctly based on user data', () => {
+ expect(component.overviewFields).toBeDefined();
+ expect(component.overviewFields.length).toBeGreaterThan(0);
+
+ const mfaField = component.overviewFields.find((f) => f.label === 'MFAs (Id)');
+ expect(mfaField?.value).toBe('mfa1, mfa2');
+
+ const subuserField = component.overviewFields.find((f) => f.label === 'Subusers');
+ expect(subuserField?.value).toBe('sub1 (read)');
+
+ const capsField = component.overviewFields.find((f) => f.label === 'Capabilities');
+ expect(capsField?.value).toBe('users (*)');
+
+ const maxBucketsField = component.overviewFields.find((f) => f.label === 'Max buckets');
+ expect(maxBucketsField?.value).toBe('Unlimited');
+ });
+
+ it('should calculate quota usage text correctly', () => {
+ const sizeLimitField = component.overviewFields.find((f) => f.label === 'Capacity limit');
+ expect(sizeLimitField?.value).toBe('50.0%');
+
+ const objectLimitField = component.overviewFields.find((f) => f.label === 'Object limit');
+ expect(objectLimitField?.value).toBe('25.0%');
+ });
+
+ it('should return null for quota usage if disabled', () => {
+ const quotaText = component['getQuotaUsageText'].call(
+ { user: { user_quota: mockUser.bucket_quota, stats: mockUser.stats } },
+ 'size'
+ );
+ expect(quotaText).toBeNull();
+ });
+
+ it('should build user and bucket quota display values properly', () => {
+ expect(component.userQuota['Enabled']).toBe('Yes');
+ expect(component.userQuota['Maximum size']).toBe('1024 B');
+ expect(component.userQuota['Maximum objects']).toBe(100);
+
+ expect(component.bucketQuota['Enabled']).toBe('No');
+ expect(component.bucketQuota['Maximum size']).toBe('-');
+ expect(component.bucketQuota['Maximum objects']).toBe('-');
+ });
+
+ it('should process S3 and Swift keys correctly', () => {
+ expect(component.keys.length).toBe(2);
+
+ expect(component.keys[0].type).toBe('S3');
+ expect(component.keys[0].username).toBe('test-user');
+
+ expect(component.keys[1].type).toBe('Swift');
+ expect(component.keys[1].username).toBe('test-user:swift');
+ });
+
+ it('should show Key Modal for S3 keys', () => {
+ const s3KeyRow: any = { type: 'S3', ref: { user: 'u1', access_key: 'a1', secret_key: 's1' } };
+
+ // Create Jest mock functions for the modal reference
+ const modalRefMock = {
+ setViewing: jest.fn(),
+ setValues: jest.fn()
+ };
+ modalCdsServiceMock.show.mockReturnValue(modalRefMock);
+
+ component.showKeyModal(s3KeyRow);
+
+ expect(modalCdsServiceMock.show).toHaveBeenCalledWith(RgwUserS3KeyModalComponent);
+ expect(modalRefMock.setViewing).toHaveBeenCalled();
+ expect(modalRefMock.setValues).toHaveBeenCalledWith('u1', 'a1', 's1');
+ });
+
+ it('should show Key Modal for Swift keys', () => {
+ const swiftKeyRow: any = { type: 'Swift', ref: { user: 'u2', secret_key: 's2' } };
+
+ // Create Jest mock functions for the modal reference
+ const modalRefMock = {
+ setViewing: jest.fn(),
+ setValues: jest.fn()
+ };
+ modalCdsServiceMock.show.mockReturnValue(modalRefMock);
+
+ component.showKeyModal(swiftKeyRow);
+
+ expect(modalCdsServiceMock.show).toHaveBeenCalledWith(RgwUserSwiftKeyModalComponent);
+ expect(modalRefMock.setViewing).not.toHaveBeenCalled();
+ expect(modalRefMock.setValues).toHaveBeenCalledWith('u2', 's2');
+ });
+
+ it('should not throw error if showKeyModal is called without a key', () => {
+ expect(() => component.showKeyModal(undefined as any)).not.toThrow();
+ expect(modalCdsServiceMock.show).not.toHaveBeenCalled();
+ });
+
+ it('should unsubscribe on destroy', () => {
+ // Use jest.spyOn instead of spyOn
+ const subSpy = jest.spyOn(component['sub'], 'unsubscribe');
+ component.ngOnDestroy();
+ expect(subSpy).toHaveBeenCalled();
+ });
+});
--- /dev/null
+import {
+ Component,
+ OnDestroy,
+ OnInit,
+ TemplateRef,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { Subscription } from 'rxjs';
+
+import _ from 'lodash';
+import {
+ KeyRow,
+ ExtendedRgwUser,
+ UserQuota,
+ BucketQuota,
+ Key,
+ SwiftKey,
+ RGW_MAX_BUCKETS_MAP
+} from '~/app/ceph/rgw/models/rgw-user';
+import { RgwUserS3KeyModalComponent } from '../rgw-user-s3-key-modal/rgw-user-s3-key-modal.component';
+import { RgwUserSwiftKeyModalComponent } from '../rgw-user-swift-key-modal/rgw-user-swift-key-modal.component';
+import { USER } from '~/app/shared/constants/app.constants';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { OverviewField } from '~/app/shared/components/resource-overview-card/resource-overview-card.component';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
+@Component({
+ selector: 'cd-rgw-user-resource-page',
+ templateUrl: './rgw-user-resource-page.component.html',
+ styleUrls: ['./rgw-user-resource-page.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ standalone: false
+})
+export class RgwUserResourcePageComponent implements OnInit, OnDestroy {
+ private sub = new Subscription();
+
+ @ViewChild('viewKeyTpl', { static: true })
+ declare viewKeyTpl: TemplateRef<any>;
+
+ section = 'overview';
+ user?: ExtendedRgwUser;
+ selection?: ExtendedRgwUser;
+ notFound = false;
+ keys: KeyRow[] = [];
+ keysColumns: CdTableColumn[] = [];
+ userQuota: Record<string, string | number> = {};
+ bucketQuota: Record<string, string | number> = {};
+ overviewFields: OverviewField[] = [];
+
+ constructor(
+ private route: ActivatedRoute,
+ private cdsModalService: ModalCdsService,
+ private dimlessBinary: DimlessBinaryPipe
+ ) {}
+
+ ngOnInit(): void {
+ this.section = this.route.snapshot.data['section'] ?? 'overview';
+ this.keysColumns = [
+ {
+ name: $localize`Username`,
+ prop: 'username',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Type`,
+ prop: 'type',
+ flexGrow: 1
+ },
+ {
+ name: $localize`View`,
+ prop: 'view',
+ flexGrow: 1,
+ cellTemplate: this.viewKeyTpl
+ }
+ ];
+
+ this.sub.add(
+ this.route.parent?.data.subscribe((data) => {
+ this.applyUser(data?.user ?? null);
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ this.sub.unsubscribe();
+ }
+
+ private applyUser(user: ExtendedRgwUser | null): void {
+ this.notFound = !user;
+
+ if (!user) {
+ this.user = undefined;
+ this.selection = undefined;
+ this.overviewFields = [];
+ this.userQuota = {};
+ this.bucketQuota = {};
+ this.keys = [];
+ return;
+ }
+
+ this.user = user;
+ this.selection = user;
+ this.overviewFields = this.buildOverviewFields(this.user, this.selection);
+ this.userQuota = this.createDisplayValues(this.user?.user_quota);
+ this.bucketQuota = this.createDisplayValues(this.user?.bucket_quota);
+ this.processKeys();
+ }
+
+ private buildOverviewFields(user: ExtendedRgwUser, selection: ExtendedRgwUser): OverviewField[] {
+ const fields: OverviewField[] = [
+ {
+ label: $localize`Username`,
+ value: user?.uid
+ },
+ {
+ label: $localize`Tenant`,
+ value: user?.tenant
+ },
+ {
+ label: $localize`Account name`,
+ value: selection?.account?.name
+ },
+ {
+ label: $localize`Full name`,
+ value: user?.display_name
+ },
+ {
+ label: $localize`Email`,
+ value: user?.email
+ },
+ {
+ label: $localize`Suspended`,
+ value: user?.suspended ? $localize`Yes` : $localize`No`
+ },
+ {
+ label: $localize`System user`,
+ value: user?.system ? $localize`Yes` : $localize`No`
+ },
+ {
+ label: $localize`Max buckets`,
+ value: RGW_MAX_BUCKETS_MAP[`${user.max_buckets}`] || `${user.max_buckets}`
+ },
+ {
+ label: $localize`Capacity limit`,
+ value: this.getQuotaUsageText('size'),
+ emptyText: $localize`No Limit`
+ },
+ {
+ label: $localize`Object limit`,
+ value: this.getQuotaUsageText('object'),
+ emptyText: $localize`No Limit`
+ },
+ {
+ label: $localize`Managed policies`,
+ value: user?.managed_user_policies
+ ?.map((arn) => arn?.trim()?.split('/').pop())
+ .filter(Boolean)
+ .join(', ')
+ },
+ {
+ label: $localize`Subusers`,
+ value: user?.subusers?.map((subuser) => `${subuser.id} (${subuser.permissions})`).join(', ')
+ },
+ {
+ label: $localize`Capabilities`,
+ value: user?.caps?.map((cap) => `${cap.type} (${cap.perm})`).join(', ')
+ },
+ {
+ label: $localize`MFAs (Id)`,
+ value: user?.mfa_ids?.join(', ')
+ }
+ ];
+
+ if (selection?.account?.id) {
+ fields.push(
+ ...[
+ {
+ label: $localize`Account ID`,
+ value: selection?.account?.id
+ },
+ {
+ label: $localize`Name`,
+ value: selection?.account?.name
+ },
+ {
+ label: $localize`Tenant`,
+ value: selection?.account?.tenant
+ },
+ {
+ label: $localize`User type`,
+ value: user?.type === 'root' ? $localize`Account root user` : $localize`rgw user`
+ }
+ ]
+ );
+ }
+
+ return fields;
+ }
+
+ private getQuotaUsageText(kind: 'size' | 'object'): string | null {
+ const quota = this.user?.user_quota;
+ const stats = this.user?.stats;
+
+ if (!quota?.enabled) return null;
+
+ if (kind === 'size' && quota.max_size > 0) {
+ const used = Number(stats?.size_actual ?? 0);
+ return `${((used / quota.max_size) * 100).toFixed(1)}%`;
+ }
+
+ if (kind === 'object' && quota.max_objects > 0) {
+ const used = Number(stats?.num_objects ?? 0);
+ return `${((used / quota.max_objects) * 100).toFixed(1)}%`;
+ }
+
+ return null;
+ }
+
+ private createDisplayValues(quota?: UserQuota | BucketQuota): Record<string, string | number> {
+ if (!quota) {
+ return {};
+ }
+
+ 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
+ : '-'
+ };
+ }
+
+ private processKeys(): void {
+ this.keys = [];
+ if (this.user?.keys) {
+ this.user.keys.forEach((key: Key) => {
+ this.keys.push({
+ id: this.keys.length + 1,
+ type: 'S3',
+ username: key.user,
+ ref: key
+ });
+ });
+ }
+
+ if (this.user?.swift_keys) {
+ this.user.swift_keys.forEach((key: SwiftKey) => {
+ this.keys.push({
+ id: this.keys.length + 1,
+ type: 'Swift',
+ username: key.user,
+ ref: key
+ });
+ });
+ }
+
+ this.keys = _.sortBy(this.keys, USER);
+ }
+
+ showKeyModal(key: KeyRow): void {
+ if (!key) {
+ return;
+ }
+
+ const modalRef = this.cdsModalService.show(
+ key.type === 'S3' ? RgwUserS3KeyModalComponent : RgwUserSwiftKeyModalComponent
+ );
+
+ switch (key.type) {
+ case 'S3':
+ const s3Ref = key.ref as Key;
+ modalRef.setViewing();
+ modalRef.setValues(s3Ref.user, s3Ref.access_key, s3Ref.secret_key);
+ break;
+ case 'Swift':
+ const swiftRef = key.ref as SwiftKey;
+ modalRef.setValues(swiftRef.user, swiftRef.secret_key);
+ break;
+ }
+ }
+}
--- /dev/null
+<cd-sidebar-layout
+ class="rgw-user-details-layout"
+ [title]="user?.uid || uid"
+ [items]="sidebarItems"
+>
+ <router-outlet></router-outlet>
+</cd-sidebar-layout>
--- /dev/null
+.rgw-user-details-layout .sidebar-layout-container {
+ min-height: auto;
+ padding-right: 0;
+}
+
+.rgw-user-details-layout .sidebar-layout-main {
+ padding: var(--cds-spacing-05);
+}
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute } from '@angular/router';
+import { of } from 'rxjs';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwUserResourceSidebarComponent } from './rgw-user-resource-sidebar.component';
+import { RgwUser } from '../models/rgw-user';
+
+describe('RgwUserResourceSidebarComponent', () => {
+ let component: RgwUserResourceSidebarComponent;
+ let fixture: ComponentFixture<RgwUserResourceSidebarComponent>;
+
+ const mockActivatedRoute = {
+ paramMap: of({ get: (key: string) => (key === 'uid' ? 'test-user-id' : null) }),
+ data: of({ user: { uid: 'test-user-id' } as RgwUser })
+ };
+
+ configureTestBed({
+ declarations: [RgwUserResourceSidebarComponent],
+ providers: [{ provide: ActivatedRoute, useValue: mockActivatedRoute }]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwUserResourceSidebarComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should set uid and build sidebar items on init', () => {
+ expect(component.uid).toBe('test-user-id');
+ expect(component.sidebarItems.length).toBe(1);
+ expect(component.sidebarItems[0].label).toBe('Overview');
+ expect(component.sidebarItems[0].route).toEqual(['/rgw/user', 'test-user-id', 'overview']);
+ });
+
+ it('should set user from route data on init', () => {
+ expect(component.user).toBeDefined();
+ expect(component.user?.uid).toBe('test-user-id');
+ });
+});
--- /dev/null
+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 { RgwUser } from '../models/rgw-user';
+
+@Component({
+ selector: 'cd-rgw-user-resource-sidebar',
+ templateUrl: './rgw-user-resource-sidebar.component.html',
+ styleUrls: ['./rgw-user-resource-sidebar.component.scss'],
+ standalone: false
+})
+export class RgwUserResourceSidebarComponent implements OnInit, OnDestroy {
+ private sub = new Subscription();
+
+ uid = '';
+ user: RgwUser;
+ sidebarItems: SidebarItem[] = [];
+ readonly basePath = '/rgw/user';
+
+ constructor(private route: ActivatedRoute) {}
+
+ ngOnInit() {
+ this.sub.add(
+ this.route.paramMap.subscribe((pm: ParamMap) => {
+ this.uid = pm.get('uid') ?? '';
+ this.buildSidebarItems();
+ })
+ );
+
+ this.sub.add(
+ this.route.data.subscribe((data) => {
+ this.user = data?.user ?? null;
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ this.sub.unsubscribe();
+ }
+
+ private buildSidebarItems(): void {
+ this.sidebarItems = [
+ {
+ label: $localize`Overview`,
+ route: [this.basePath, this.uid, 'overview'],
+ routerLinkActiveOptions: { exact: true }
+ }
+ ];
+ }
+}
import { RgwDaemonDetailsComponent } from './rgw-daemon-details/rgw-daemon-details.component';
import { RgwDaemonListComponent } from './rgw-daemon-list/rgw-daemon-list.component';
import { RgwUserCapabilityModalComponent } from './rgw-user-capability-modal/rgw-user-capability-modal.component';
-import { RgwUserDetailsComponent } from './rgw-user-details/rgw-user-details.component';
+import { RgwUserResourceSidebarComponent } from './rgw-user-resource-sidebar/rgw-user-resource-sidebar.component';
+import { RgwUserResourcePageComponent } from './rgw-user-resource-page/rgw-user-resource-page.component';
+import { RgwUserResourceBreadcrumbResolver } from './rgw-user-resource-page/rgw-user-resource-breadcrumb.resolver';
+import { RgwUserDetailsResolver } from './rgw-user-resource-page/rgw-user-details.resolver';
import { RgwUserFormComponent } from './rgw-user-form/rgw-user-form.component';
import { RgwUserListComponent } from './rgw-user-list/rgw-user-list.component';
import { RgwUserS3KeyModalComponent } from './rgw-user-s3-key-modal/rgw-user-s3-key-modal.component';
RgwBucketListComponent,
RgwBucketDetailsComponent,
RgwUserListComponent,
- RgwUserDetailsComponent,
+ RgwUserResourceSidebarComponent,
+ RgwUserResourcePageComponent,
RgwStorageClassListComponent
],
declarations: [
RgwBucketListComponent,
RgwBucketDetailsComponent,
RgwUserListComponent,
- RgwUserDetailsComponent,
+ RgwUserResourceSidebarComponent,
+ RgwUserResourcePageComponent,
RgwUserFormComponent,
RgwUserSwiftKeyModalComponent,
RgwUserS3KeyModalComponent,
path: `${URLVerbs.EDIT}/:uid`,
component: RgwUserFormComponent,
data: { breadcrumbs: ActionLabels.EDIT }
+ },
+ {
+ path: ':uid',
+ component: RgwUserResourceSidebarComponent,
+ data: { breadcrumbs: RgwUserResourceBreadcrumbResolver },
+ resolve: {
+ user: RgwUserDetailsResolver
+ },
+ children: [
+ { path: '', redirectTo: 'overview', pathMatch: 'full' },
+ {
+ path: 'overview',
+ component: RgwUserResourcePageComponent,
+ data: { breadcrumbs: 'Overview', section: 'overview' }
+ }
+ ]
}
]
},
}
.sidebar-layout-container {
- min-height: calc(100vh - (vv.$navbar-height + layout.rem(55px)));
padding-right: var(--cds-spacing-07);
background-color: var(--cds-background);
}
.sidebar-layout-shell {
- transform: translate(0);
position: relative;
- height: 100vh;
+ min-height: 100vh;
}
.sidebar-layout-nav {
background-color: var(--cds-layer-03);
+ position: sticky !important;
+ top: 0;
+ height: calc(100vh - vv.$navbar-height);
+ width: layout.rem(272px);
+ float: left;
+ z-index: 10;
+ overflow-y: auto;
.cds--side-nav__icon:not(.cds--side-nav__submenu-chevron) {
display: none;