mirror of
https://github.com/ceph/ceph
synced 2026-08-02 07:03:18 +00:00
Merge pull request #69924 from syedali237/dashboard/notif-dest
mgr/dashboard : migrated notification destination tabs to resource pages Reviewed-by: Afreen Misbah <afreen@ibm.com> Reviewed-by: Abhishek Desai <abhishek.desai1@ibm.com>
This commit is contained in:
commit
64e8dc794d
@ -1,137 +0,0 @@
|
||||
<ng-container *ngIf="!!selection">
|
||||
<cds-tabs
|
||||
type="contained"
|
||||
theme="light"
|
||||
>
|
||||
<cds-tab
|
||||
heading="Details"
|
||||
i18n-heading
|
||||
>
|
||||
<table
|
||||
class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md"
|
||||
data-testid="rgw-topic-details"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
i18n
|
||||
class="bold"
|
||||
>
|
||||
Push endpoint arguments
|
||||
</td>
|
||||
<td>{{ selection?.dest?.push_endpoint_args }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
i18n
|
||||
class="bold w-25"
|
||||
>
|
||||
Stored secret
|
||||
</td>
|
||||
<td class="w-75">{{ selection?.dest?.stored_secret }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
i18n
|
||||
class="bold"
|
||||
>
|
||||
Persistent
|
||||
</td>
|
||||
<td>{{ selection?.dest?.persistent }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
i18n
|
||||
class="bold"
|
||||
>
|
||||
Persistent queue
|
||||
</td>
|
||||
<td>{{ selection?.dest?.persistent_queue }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
i18n
|
||||
class="bold"
|
||||
>
|
||||
Time to live
|
||||
</td>
|
||||
<td>{{ selection?.dest?.time_to_live }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
i18n
|
||||
class="bold"
|
||||
>
|
||||
Max retries
|
||||
</td>
|
||||
<td>{{ selection?.dest?.max_retries }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
i18n
|
||||
class="bold"
|
||||
>
|
||||
Retry sleep duration
|
||||
</td>
|
||||
<td>{{ selection?.dest?.retry_sleep_duration }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
i18n
|
||||
class="bold"
|
||||
>
|
||||
Opaque data
|
||||
</td>
|
||||
<td>{{ selection?.opaqueData }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</cds-tab>
|
||||
<cds-tab
|
||||
heading="Policies"
|
||||
i18n-heading
|
||||
>
|
||||
<div>
|
||||
<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"
|
||||
>
|
||||
Policy
|
||||
</td>
|
||||
<td>
|
||||
<pre>{{ policy | json }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</cds-tab>
|
||||
<cds-tab
|
||||
heading="Subscribed buckets"
|
||||
i18n-heading
|
||||
>
|
||||
<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"
|
||||
>
|
||||
Subscribed buckets
|
||||
</td>
|
||||
<td>
|
||||
<pre>{{ selection.subscribed_buckets | json }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</cds-tab>
|
||||
</cds-tabs>
|
||||
</ng-container>
|
||||
@ -1,96 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RgwTopicDetailsComponent } from './rgw-topic-details.component';
|
||||
import { Topic } from '~/app/shared/models/topic.model';
|
||||
|
||||
interface Destination {
|
||||
push_endpoint: string;
|
||||
push_endpoint_args: string;
|
||||
push_endpoint_topic: string;
|
||||
stored_secret: boolean;
|
||||
persistent: boolean;
|
||||
persistent_queue: string;
|
||||
time_to_live: number;
|
||||
max_retries: number;
|
||||
retry_sleep_duration: number;
|
||||
}
|
||||
|
||||
const mockDestination: Destination = {
|
||||
push_endpoint: 'http://localhost:8080',
|
||||
push_endpoint_args: 'args',
|
||||
push_endpoint_topic: 'topic',
|
||||
stored_secret: false,
|
||||
persistent: true,
|
||||
persistent_queue: 'queue',
|
||||
time_to_live: 3600,
|
||||
max_retries: 5,
|
||||
retry_sleep_duration: 10
|
||||
};
|
||||
|
||||
describe('RgwTopicDetailsComponent', () => {
|
||||
let component: RgwTopicDetailsComponent;
|
||||
let fixture: ComponentFixture<RgwTopicDetailsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RgwTopicDetailsComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RgwTopicDetailsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should parse policy string correctly', () => {
|
||||
const mockSelection: Topic = {
|
||||
name: 'testHttp',
|
||||
owner: 'ownerName',
|
||||
arn: 'arnValue',
|
||||
dest: mockDestination,
|
||||
policy: '{"key": "value"}',
|
||||
key: 'topic:ownerName:testHttp',
|
||||
opaqueData: 'test@12345',
|
||||
subscribed_buckets: []
|
||||
};
|
||||
|
||||
component.selection = mockSelection;
|
||||
component.ngOnChanges({
|
||||
selection: {
|
||||
currentValue: mockSelection,
|
||||
previousValue: null,
|
||||
firstChange: true,
|
||||
isFirstChange: () => true
|
||||
}
|
||||
});
|
||||
|
||||
expect(component.policy).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
it('should set policy to empty object if policy is not a string', () => {
|
||||
const mockSelection: Topic = {
|
||||
name: 'testHttp',
|
||||
owner: 'ownerName',
|
||||
arn: 'arnValue',
|
||||
dest: mockDestination,
|
||||
policy: '{}',
|
||||
key: 'topic:ownerName:testHttp',
|
||||
subscribed_buckets: [],
|
||||
opaqueData: ''
|
||||
};
|
||||
|
||||
component.selection = mockSelection;
|
||||
component.ngOnChanges({
|
||||
selection: {
|
||||
currentValue: mockSelection,
|
||||
previousValue: null,
|
||||
firstChange: true,
|
||||
isFirstChange: () => true
|
||||
}
|
||||
});
|
||||
|
||||
expect(component.policy).toEqual({});
|
||||
});
|
||||
});
|
||||
@ -1,30 +0,0 @@
|
||||
import { Component, Input, SimpleChanges, OnChanges } from '@angular/core';
|
||||
|
||||
import { Topic } from '~/app/shared/models/topic.model';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
@Component({
|
||||
selector: 'cd-rgw-topic-details',
|
||||
templateUrl: './rgw-topic-details.component.html',
|
||||
styleUrls: ['./rgw-topic-details.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class RgwTopicDetailsComponent implements OnChanges {
|
||||
@Input()
|
||||
selection: Topic;
|
||||
policy: string | object = '{}';
|
||||
constructor() {}
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['selection'] && this.selection) {
|
||||
if (_.isString(this.selection.policy)) {
|
||||
try {
|
||||
this.policy = JSON.parse(this.selection.policy);
|
||||
} catch (e) {
|
||||
this.policy = '{}';
|
||||
}
|
||||
} else {
|
||||
this.policy = this.selection.policy || {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,9 +14,7 @@
|
||||
[columns]="columns"
|
||||
columnMode="flex"
|
||||
selectionType="single"
|
||||
[hasDetails]="true"
|
||||
id="key"
|
||||
(setExpandedRow)="setExpandedRow($event)"
|
||||
(updateSelection)="updateSelection($event)"
|
||||
(fetchData)="fetchData()"
|
||||
>
|
||||
@ -27,9 +25,5 @@
|
||||
[tableActions]="tableActions"
|
||||
>
|
||||
</cd-table-actions>
|
||||
<cd-rgw-topic-details
|
||||
*cdTableDetail
|
||||
[selection]="expandedRow"
|
||||
></cd-rgw-topic-details>
|
||||
</cd-table>
|
||||
</ng-container>
|
||||
|
||||
@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RgwTopicListComponent } from './rgw-topic-list.component';
|
||||
import { SharedModule } from '~/app/shared/shared.module';
|
||||
import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper';
|
||||
import { RgwTopicDetailsComponent } from '../rgw-topic-details/rgw-topic-details.component';
|
||||
import { RgwTopicResourceSidebarComponent } from '../rgw-topic-resource-sidebar/rgw-topic-resource-sidebar.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
@ -17,7 +17,7 @@ describe('RgwTopicListComponent', () => {
|
||||
let rgwTopicServiceListSpy: jasmine.Spy;
|
||||
|
||||
configureTestBed({
|
||||
declarations: [RgwTopicListComponent, RgwTopicDetailsComponent],
|
||||
declarations: [RgwTopicListComponent, RgwTopicResourceSidebarComponent],
|
||||
imports: [BrowserAnimationsModule, RouterTestingModule, HttpClientTestingModule, SharedModule]
|
||||
});
|
||||
|
||||
@ -29,7 +29,7 @@ describe('RgwTopicListComponent', () => {
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [RgwTopicListComponent]
|
||||
declarations: [RgwTopicListComponent, RgwTopicResourceSidebarComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RgwTopicListComponent);
|
||||
|
||||
@ -14,13 +14,14 @@ import { RgwTopicService } from '~/app/shared/api/rgw-topic.service';
|
||||
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
|
||||
import { URLBuilderService } from '~/app/shared/services/url-builder.service';
|
||||
import { Icons } from '~/app/shared/enum/icons.enum';
|
||||
import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
|
||||
import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
|
||||
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
|
||||
import { FinishedTask } from '~/app/shared/models/finished-task';
|
||||
import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
|
||||
import { Topic } from '~/app/shared/models/topic.model';
|
||||
import { BehaviorSubject, Observable, of, Subscriber } from 'rxjs';
|
||||
import { catchError, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { catchError, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
|
||||
const BASE_URL = 'rgw/destination';
|
||||
@Component({
|
||||
@ -32,16 +33,16 @@ const BASE_URL = 'rgw/destination';
|
||||
})
|
||||
export class RgwTopicListComponent extends ListWithDetails implements OnInit {
|
||||
@ViewChild('table', { static: true })
|
||||
table: TableComponent;
|
||||
columns: CdTableColumn[];
|
||||
table!: TableComponent;
|
||||
columns!: CdTableColumn[];
|
||||
permission: Permission;
|
||||
tableActions: CdTableAction[];
|
||||
context: CdTableFetchDataContext;
|
||||
errorMessage: string;
|
||||
tableActions!: CdTableAction[];
|
||||
context!: CdTableFetchDataContext;
|
||||
errorMessage!: string;
|
||||
selection: CdTableSelection = new CdTableSelection();
|
||||
topicsSubject = new BehaviorSubject<Topic[]>([]);
|
||||
topics$ = this.topicsSubject.asObservable();
|
||||
name: string;
|
||||
name!: string;
|
||||
constructor(
|
||||
private authStorageService: AuthStorageService,
|
||||
public actionLabels: ActionLabelsI18n,
|
||||
@ -60,7 +61,8 @@ export class RgwTopicListComponent extends ListWithDetails implements OnInit {
|
||||
{
|
||||
name: $localize`Name`,
|
||||
prop: 'name',
|
||||
flexGrow: 2
|
||||
flexGrow: 2,
|
||||
cellTransformation: CellTemplate.routerLink
|
||||
},
|
||||
{
|
||||
name: $localize`Owner`,
|
||||
@ -79,7 +81,7 @@ export class RgwTopicListComponent extends ListWithDetails implements OnInit {
|
||||
}
|
||||
];
|
||||
|
||||
const getBucketUri = () =>
|
||||
const getTopicUri = () =>
|
||||
this.selection.first() && `${encodeURIComponent(this.selection.first().key)}`;
|
||||
const addAction: CdTableAction = {
|
||||
permission: 'create',
|
||||
@ -92,7 +94,7 @@ export class RgwTopicListComponent extends ListWithDetails implements OnInit {
|
||||
const editAction: CdTableAction = {
|
||||
permission: 'update',
|
||||
icon: Icons.edit,
|
||||
routerLink: () => this.urlBuilder.getEdit(getBucketUri()),
|
||||
routerLink: () => this.urlBuilder.getEdit(getTopicUri()),
|
||||
name: this.actionLabels.EDIT
|
||||
};
|
||||
|
||||
@ -111,10 +113,16 @@ export class RgwTopicListComponent extends ListWithDetails implements OnInit {
|
||||
this.rgwTopicService.listTopic().pipe(
|
||||
catchError(() => {
|
||||
this.context.error();
|
||||
return of(null);
|
||||
return of([]);
|
||||
})
|
||||
)
|
||||
),
|
||||
map((topics: Topic[]) =>
|
||||
topics.map((topic: Topic) => ({
|
||||
...topic,
|
||||
cdLink: `/rgw/destination/${encodeURIComponent(topic.key)}/overview`
|
||||
}))
|
||||
),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||
|
||||
import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs';
|
||||
import { RgwTopicKeyService } from '~/app/shared/services/rgw-topic-key.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RgwTopicResourceBreadcrumbResolver extends BreadcrumbsResolver {
|
||||
constructor(private rgwTopicKeyService: RgwTopicKeyService) {
|
||||
super();
|
||||
}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot): IBreadcrumb[] {
|
||||
const topicKey = route.params?.name || '';
|
||||
const topicName = this.rgwTopicKeyService.extractTopicName(topicKey);
|
||||
|
||||
return [{ text: topicName, path: this.getFullPath(route) }];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
@if (hasTopic) {
|
||||
@switch (section) {
|
||||
@case ('overview') {
|
||||
<cd-resource-overview-card
|
||||
title="Notification destination details"
|
||||
[columns]="4"
|
||||
[fields]="topicOverviewFields"
|
||||
>
|
||||
</cd-resource-overview-card>
|
||||
}
|
||||
@case ('policies') {
|
||||
<cd-table-key-value
|
||||
[data]="[{ key: 'Policy', value: (policy | json) }]"
|
||||
[autoReload]="false"
|
||||
[showMultiLineText]="true"
|
||||
[multilineTextKeys]="['Policy']"
|
||||
></cd-table-key-value>
|
||||
}
|
||||
@case ('subscribed-buckets') {
|
||||
<cd-table-key-value
|
||||
[data]="[{ key: 'Subscribed buckets', value: (topic.subscribed_buckets | json) }]"
|
||||
[autoReload]="false"
|
||||
[showMultiLineText]="true"
|
||||
[multilineTextKeys]="['Subscribed buckets']"
|
||||
></cd-table-key-value>
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ActivatedRoute, convertToParamMap } from '@angular/router';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { ComponentsModule } from '~/app/shared/components/components.module';
|
||||
import { PipesModule } from '~/app/shared/pipes/pipes.module';
|
||||
import { RgwTopicResourcePageComponent } from './rgw-topic-resource-page.component';
|
||||
import { RgwTopicService } from '~/app/shared/api/rgw-topic.service';
|
||||
import { Topic } from '~/app/shared/models/topic.model';
|
||||
|
||||
describe('RgwTopicResourcePageComponent', () => {
|
||||
let component: RgwTopicResourcePageComponent;
|
||||
let fixture: ComponentFixture<RgwTopicResourcePageComponent>;
|
||||
let rgwTopicServiceSpy: { getTopic: jest.Mock };
|
||||
|
||||
const mockTopic: Topic = {
|
||||
name: 'test-topic',
|
||||
owner: 'test-user',
|
||||
arn: 'arn:aws:sns:us-east-1:123456789012:test-topic',
|
||||
policy: '{"Version":"2012-10-17"}',
|
||||
subscribed_buckets: [],
|
||||
dest: {
|
||||
push_endpoint: 'http://localhost:8080',
|
||||
push_endpoint_args: '',
|
||||
push_endpoint_topic: 'test-topic',
|
||||
stored_secret: false,
|
||||
persistent: false,
|
||||
persistent_queue: 'false',
|
||||
time_to_live: 60,
|
||||
max_retries: 3,
|
||||
retry_sleep_duration: 10
|
||||
},
|
||||
key: 'test-key',
|
||||
opaqueData: ''
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
rgwTopicServiceSpy = {
|
||||
getTopic: jest.fn().mockReturnValue(of(mockTopic))
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RgwTopicResourcePageComponent],
|
||||
imports: [ComponentsModule, HttpClientTestingModule, PipesModule, RouterTestingModule],
|
||||
providers: [
|
||||
{ provide: RgwTopicService, useValue: rgwTopicServiceSpy },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
data: of({ section: 'overview' }),
|
||||
parent: {
|
||||
paramMap: of(convertToParamMap({ name: 'test-topic' }))
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RgwTopicResourcePageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set section from route data', () => {
|
||||
expect(component.section).toBe('overview');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,152 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import _ from 'lodash';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { RgwTopicService } from '~/app/shared/api/rgw-topic.service';
|
||||
import { OverviewField } from '~/app/shared/components/resource-overview-card/resource-overview-card.component';
|
||||
import { Topic } from '~/app/shared/models/topic.model';
|
||||
import { RgwTopicKeyService } from '~/app/shared/services/rgw-topic-key.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cd-rgw-topic-resource-page',
|
||||
templateUrl: './rgw-topic-resource-page.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RgwTopicResourcePageComponent implements OnInit, OnDestroy {
|
||||
private sub = new Subscription();
|
||||
|
||||
section = '';
|
||||
topic!: Topic;
|
||||
hasTopic = false;
|
||||
loading = false;
|
||||
loadError = false;
|
||||
topicOverviewFields: OverviewField[] = [];
|
||||
policy: string | object = '{}';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private rgwTopicService: RgwTopicService,
|
||||
private rgwTopicKeyService: RgwTopicKeyService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sub.add(
|
||||
this.route.data.subscribe((data) => {
|
||||
this.section = data['section'] ?? '';
|
||||
})
|
||||
);
|
||||
|
||||
this.sub.add(
|
||||
this.route.parent?.paramMap.subscribe((pm: ParamMap) => {
|
||||
this.loadTopic(pm.get('name') ?? '');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
|
||||
private loadTopic(topicKeyParam: string): void {
|
||||
if (!topicKeyParam) {
|
||||
this.hasTopic = false;
|
||||
this.loading = false;
|
||||
this.loadError = false;
|
||||
this.topicOverviewFields = [];
|
||||
this.policy = {};
|
||||
return;
|
||||
}
|
||||
|
||||
const topicKey = this.rgwTopicKeyService.decodeTopicKey(topicKeyParam);
|
||||
|
||||
this.loading = true;
|
||||
this.loadError = false;
|
||||
|
||||
this.sub.add(
|
||||
this.rgwTopicService.getTopic(topicKey).subscribe({
|
||||
next: (topic: Topic) => {
|
||||
this.topic = topic;
|
||||
this.hasTopic = true;
|
||||
this.topicOverviewFields = this.buildOverviewSections(topic);
|
||||
this.policy = this.parsePolicy(topic?.policy);
|
||||
this.loading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.hasTopic = false;
|
||||
this.topicOverviewFields = [];
|
||||
this.policy = {};
|
||||
this.loading = false;
|
||||
this.loadError = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private buildOverviewSections(topic?: Topic): OverviewField[] {
|
||||
const details = topic ?? ({} as Topic);
|
||||
|
||||
return [
|
||||
{
|
||||
label: $localize`Name`,
|
||||
value: details?.name
|
||||
},
|
||||
{
|
||||
label: $localize`Owner`,
|
||||
value: details?.owner
|
||||
},
|
||||
{
|
||||
label: $localize`Amazon resource name`,
|
||||
value: details?.arn
|
||||
},
|
||||
{
|
||||
label: $localize`Push endpoint`,
|
||||
value: details?.dest?.push_endpoint
|
||||
},
|
||||
{
|
||||
label: $localize`Push endpoint arguments`,
|
||||
value: details?.dest?.push_endpoint_args
|
||||
},
|
||||
{
|
||||
label: $localize`Stored secret`,
|
||||
value: details?.dest?.stored_secret
|
||||
},
|
||||
{
|
||||
label: $localize`Persistent`,
|
||||
value: details?.dest?.persistent
|
||||
},
|
||||
{
|
||||
label: $localize`Persistent queue`,
|
||||
value: details?.dest?.persistent_queue
|
||||
},
|
||||
{
|
||||
label: $localize`Time to live`,
|
||||
value: details?.dest?.time_to_live
|
||||
},
|
||||
{
|
||||
label: $localize`Max retries`,
|
||||
value: details?.dest?.max_retries
|
||||
},
|
||||
{
|
||||
label: $localize`Retry sleep duration`,
|
||||
value: details?.dest?.retry_sleep_duration
|
||||
},
|
||||
{
|
||||
label: $localize`Opaque data`,
|
||||
value: details?.opaqueData
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private parsePolicy(policy: string | object): string | object {
|
||||
if (_.isString(policy)) {
|
||||
try {
|
||||
return JSON.parse(policy);
|
||||
} catch {
|
||||
return '{}';
|
||||
}
|
||||
}
|
||||
|
||||
return policy || {};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
<cd-sidebar-layout
|
||||
class="rgw-topic-details-layout"
|
||||
[title]="topicName"
|
||||
[items]="sidebarItems"
|
||||
>
|
||||
</cd-sidebar-layout>
|
||||
@ -0,0 +1,3 @@
|
||||
.rgw-topic-details-layout .sidebar-header {
|
||||
padding-right: var(--cds-spacing-07);
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, convertToParamMap } from '@angular/router';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { RgwTopicResourceSidebarComponent } from './rgw-topic-resource-sidebar.component';
|
||||
import { RgwTopicService } from '~/app/shared/api/rgw-topic.service';
|
||||
import { Topic } from '~/app/shared/models/topic.model';
|
||||
|
||||
interface Destination {
|
||||
push_endpoint: string;
|
||||
push_endpoint_args: string;
|
||||
push_endpoint_topic: string;
|
||||
stored_secret: boolean;
|
||||
persistent: boolean;
|
||||
persistent_queue: string;
|
||||
time_to_live: number;
|
||||
max_retries: number;
|
||||
retry_sleep_duration: number;
|
||||
}
|
||||
|
||||
const mockDestination: Destination = {
|
||||
push_endpoint: 'http://localhost:8080',
|
||||
push_endpoint_args: 'args',
|
||||
push_endpoint_topic: 'topic',
|
||||
stored_secret: false,
|
||||
persistent: true,
|
||||
persistent_queue: 'queue',
|
||||
time_to_live: 3600,
|
||||
max_retries: 5,
|
||||
retry_sleep_duration: 10
|
||||
};
|
||||
|
||||
describe('RgwTopicResourceSidebarComponent', () => {
|
||||
let component: RgwTopicResourceSidebarComponent;
|
||||
let fixture: ComponentFixture<RgwTopicResourceSidebarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RgwTopicResourceSidebarComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
paramMap: of(convertToParamMap({ name: 'topic:ownerName:testHttp' }))
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: RgwTopicService,
|
||||
useValue: {
|
||||
getTopic: () =>
|
||||
of({
|
||||
name: 'testHttp',
|
||||
owner: 'ownerName',
|
||||
arn: 'arnValue',
|
||||
dest: mockDestination,
|
||||
policy: '{}',
|
||||
key: 'topic:ownerName:testHttp',
|
||||
opaqueData: 'test@12345',
|
||||
subscribed_buckets: []
|
||||
} as Topic)
|
||||
}
|
||||
}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RgwTopicResourceSidebarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set topic title and sidebar items', () => {
|
||||
expect(component.topicName).toBe('testHttp');
|
||||
expect(component.sidebarItems.length).toBe(3);
|
||||
expect(component.sidebarItems[0].label).toBe('Overview');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,87 @@
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { RgwTopicService } from '~/app/shared/api/rgw-topic.service';
|
||||
import { SidebarItem } from '~/app/shared/components/sidebar-layout/sidebar-layout.component';
|
||||
import { Topic } from '~/app/shared/models/topic.model';
|
||||
import { RgwTopicKeyService } from '~/app/shared/services/rgw-topic-key.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cd-rgw-topic-resource-sidebar',
|
||||
templateUrl: './rgw-topic-resource-sidebar.component.html',
|
||||
styleUrls: ['./rgw-topic-resource-sidebar.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false
|
||||
})
|
||||
export class RgwTopicResourceSidebarComponent implements OnInit, OnDestroy {
|
||||
private sub = new Subscription();
|
||||
|
||||
topicKey = '';
|
||||
topicName = '';
|
||||
selection: Topic | undefined;
|
||||
sidebarItems: SidebarItem[] = [];
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private rgwTopicService: RgwTopicService,
|
||||
private rgwTopicKeyService: RgwTopicKeyService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sub.add(
|
||||
this.route.paramMap.subscribe((pm: ParamMap) => {
|
||||
this.topicKey = pm.get('name') ?? '';
|
||||
this.buildSidebarItems();
|
||||
this.loadTopic();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
|
||||
private buildSidebarItems(): void {
|
||||
this.sidebarItems = [
|
||||
{
|
||||
label: $localize`Overview`,
|
||||
route: ['/rgw/destination', this.topicKey, 'overview'],
|
||||
routerLinkActiveOptions: { exact: true }
|
||||
},
|
||||
{
|
||||
label: $localize`Policies`,
|
||||
route: ['/rgw/destination', this.topicKey, 'policies'],
|
||||
routerLinkActiveOptions: { exact: true }
|
||||
},
|
||||
{
|
||||
label: $localize`Subscribed buckets`,
|
||||
route: ['/rgw/destination', this.topicKey, 'subscribed-buckets'],
|
||||
routerLinkActiveOptions: { exact: true }
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private loadTopic(): void {
|
||||
if (!this.topicKey) {
|
||||
this.selection = undefined;
|
||||
this.topicName = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const key = this.rgwTopicKeyService.decodeTopicKey(this.topicKey);
|
||||
|
||||
this.sub.add(
|
||||
this.rgwTopicService.getTopic(key).subscribe({
|
||||
next: (topic: Topic) => {
|
||||
this.selection = topic;
|
||||
this.topicName = topic?.name || this.rgwTopicKeyService.extractTopicName(key);
|
||||
},
|
||||
error: () => {
|
||||
this.selection = undefined;
|
||||
this.topicName = this.rgwTopicKeyService.extractTopicName(key);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -120,7 +120,9 @@ import { RgwRateLimitComponent } from './rgw-rate-limit/rgw-rate-limit.component
|
||||
import { RgwRateLimitDetailsComponent } from './rgw-rate-limit-details/rgw-rate-limit-details.component';
|
||||
import { NfsClusterComponent } from '../nfs/nfs-cluster/nfs-cluster.component';
|
||||
import { RgwTopicListComponent } from './rgw-topic-list/rgw-topic-list.component';
|
||||
import { RgwTopicDetailsComponent } from './rgw-topic-details/rgw-topic-details.component';
|
||||
import { RgwTopicResourceSidebarComponent } from './rgw-topic-resource-sidebar/rgw-topic-resource-sidebar.component';
|
||||
import { RgwTopicResourcePageComponent } from './rgw-topic-resource-page/rgw-topic-resource-page.component';
|
||||
import { RgwTopicResourceBreadcrumbResolver } from './rgw-topic-resource-page/rgw-topic-resource-breadcrumb.resolver';
|
||||
import { RgwTopicFormComponent } from './rgw-topic-form/rgw-topic-form.component';
|
||||
import { RgwBucketNotificationListComponent } from './rgw-bucket-notification-list/rgw-bucket-notification-list.component';
|
||||
import { RgwNotificationFormComponent } from './rgw-notification-form/rgw-notification-form.component';
|
||||
@ -233,7 +235,8 @@ import { RgwAccountRoleFormComponent } from './rgw-account-role-form/rgw-account
|
||||
RgwBucketLifecycleListComponent,
|
||||
RgwRateLimitDetailsComponent,
|
||||
RgwTopicListComponent,
|
||||
RgwTopicDetailsComponent,
|
||||
RgwTopicResourceSidebarComponent,
|
||||
RgwTopicResourcePageComponent,
|
||||
RgwTopicFormComponent,
|
||||
RgwBucketNotificationListComponent,
|
||||
RgwNotificationFormComponent,
|
||||
@ -451,6 +454,29 @@ const routes: Routes = [
|
||||
path: `${URLVerbs.EDIT}/:name`,
|
||||
component: RgwTopicFormComponent,
|
||||
data: { breadcrumbs: ActionLabels.EDIT }
|
||||
},
|
||||
{
|
||||
path: ':name',
|
||||
component: RgwTopicResourceSidebarComponent,
|
||||
data: { breadcrumbs: RgwTopicResourceBreadcrumbResolver },
|
||||
children: [
|
||||
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
|
||||
{
|
||||
path: 'overview',
|
||||
component: RgwTopicResourcePageComponent,
|
||||
data: { breadcrumbs: 'Overview', section: 'overview' }
|
||||
},
|
||||
{
|
||||
path: 'policies',
|
||||
component: RgwTopicResourcePageComponent,
|
||||
data: { breadcrumbs: 'Policies', section: 'policies' }
|
||||
},
|
||||
{
|
||||
path: 'subscribed-buckets',
|
||||
component: RgwTopicResourcePageComponent,
|
||||
data: { breadcrumbs: 'Subscribed buckets', section: 'subscribed-buckets' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import _ from 'lodash';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { ApiClient } from './api-client';
|
||||
import { Topic, TopicRequest } from '~/app/shared/models/topic.model';
|
||||
import { catchError, mapTo } from 'rxjs/operators';
|
||||
import { catchError, mapTo, shareReplay, tap } from 'rxjs/operators';
|
||||
import { RgwDaemonService } from './rgw-daemon.service';
|
||||
|
||||
@Injectable({
|
||||
@ -13,6 +13,8 @@ import { RgwDaemonService } from './rgw-daemon.service';
|
||||
export class RgwTopicService extends ApiClient {
|
||||
baseURL = 'api/rgw/topic';
|
||||
|
||||
private topicCache = new Map<string, Observable<Topic>>();
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private rgwDaemonService: RgwDaemonService
|
||||
@ -24,20 +26,30 @@ export class RgwTopicService extends ApiClient {
|
||||
return this.http.get<Topic[]>(this.baseURL);
|
||||
}
|
||||
|
||||
getTopic(key: string) {
|
||||
return this.http.get<Topic>(`${this.baseURL}/${encodeURIComponent(key)}`);
|
||||
getTopic(key: string): Observable<Topic> {
|
||||
if (!this.topicCache.has(key)) {
|
||||
const request$ = this.http
|
||||
.get<Topic>(`${this.baseURL}/${encodeURIComponent(key)}`)
|
||||
.pipe(tap({ error: () => this.topicCache.delete(key) }), shareReplay(1));
|
||||
this.topicCache.set(key, request$);
|
||||
}
|
||||
return this.topicCache.get(key)!;
|
||||
}
|
||||
|
||||
create(createParam: TopicRequest) {
|
||||
return this.rgwDaemonService.request((params: HttpParams) => {
|
||||
return this.http.post(`${this.baseURL}`, createParam, { params: params });
|
||||
return this.http
|
||||
.post(`${this.baseURL}`, createParam, { params: params })
|
||||
.pipe(tap(() => this.clearCache()));
|
||||
});
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
return this.http.delete(`${this.baseURL}/${key}`, {
|
||||
observe: 'response'
|
||||
});
|
||||
return this.http
|
||||
.delete(`${this.baseURL}/${key}`, {
|
||||
observe: 'response'
|
||||
})
|
||||
.pipe(tap(() => this.clearCache(key)));
|
||||
}
|
||||
|
||||
exists(key: string): Observable<boolean> {
|
||||
@ -52,4 +64,16 @@ export class RgwTopicService extends ApiClient {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to manually invalidate cache.
|
||||
* If a key is provided, clears just that topic. Otherwise, clears all.
|
||||
*/
|
||||
private clearCache(key?: string): void {
|
||||
if (key) {
|
||||
this.topicCache.delete(key);
|
||||
} else {
|
||||
this.topicCache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { configureTestBed } from '~/testing/unit-test-helper';
|
||||
import { RgwTopicKeyService } from './rgw-topic-key.service';
|
||||
|
||||
describe('RgwTopicKeyService', () => {
|
||||
let service: RgwTopicKeyService;
|
||||
|
||||
configureTestBed({
|
||||
providers: [RgwTopicKeyService]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(RgwTopicKeyService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('decodeTopicKey', () => {
|
||||
it('should decode a valid URL-encoded topic key', () => {
|
||||
const input = 'arn%3Aaws%3Asns%3Aus-east-1%3A12345%3Amy-topic';
|
||||
const expected = 'arn:aws:sns:us-east-1:12345:my-topic';
|
||||
expect(service.decodeTopicKey(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return the original string if no encoding is present', () => {
|
||||
const input = 'arn:aws:sns:us-east-1:12345:my-topic';
|
||||
expect(service.decodeTopicKey(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should catch errors and return original string for a malformed URI', () => {
|
||||
// '%1' is a malformed URI component that triggers decodeURIComponent to throw
|
||||
const malformedInput = 'arn%3Aaws%3Asns%3A%1';
|
||||
expect(service.decodeTopicKey(malformedInput)).toBe(malformedInput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractTopicName', () => {
|
||||
it('should extract the last segment of a decoded topic key', () => {
|
||||
const input = 'arn:aws:sns:us-east-1:12345:my-topic';
|
||||
expect(service.extractTopicName(input)).toBe('my-topic');
|
||||
});
|
||||
|
||||
it('should extract the last segment of an encoded topic key', () => {
|
||||
const input = 'arn%3Aaws%3Asns%3Aus-east-1%3A12345%3Amy-topic';
|
||||
expect(service.extractTopicName(input)).toBe('my-topic');
|
||||
});
|
||||
|
||||
it('should return the whole string if there are no colons', () => {
|
||||
const input = 'my-topic-only';
|
||||
expect(service.extractTopicName(input)).toBe('my-topic-only');
|
||||
});
|
||||
|
||||
it('should return the full decoded string if the last segment is empty', () => {
|
||||
// Since split(':') creates an empty string at the end,
|
||||
// the || operator in your service falls back to the full decoded string.
|
||||
const input = 'arn:aws:sns:us-east-1:12345:';
|
||||
expect(service.extractTopicName(input)).toBe(input);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RgwTopicKeyService {
|
||||
decodeTopicKey(topicKey: string): string {
|
||||
try {
|
||||
return decodeURIComponent(topicKey);
|
||||
} catch {
|
||||
return topicKey;
|
||||
}
|
||||
}
|
||||
|
||||
extractTopicName(topicKey: string): string {
|
||||
const decoded = this.decodeTopicKey(topicKey);
|
||||
const segments = decoded.split(':');
|
||||
return segments[segments.length - 1] || decoded;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user