Skip to content

Commit 8f65f0e

Browse files
author
pujaoshahu
committed
mgr/dashboard: Add RGW topic listing in dashboard
Fixes: https://tracker.ceph.com/issues/69143 Signed-off-by: pujaoshahu <[email protected]>
1 parent 5104574 commit 8f65f0e

16 files changed

+530
-34
lines changed

src/pybind/mgr/dashboard/controllers/rgw.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1506,7 +1506,7 @@ def create(
15061506
def list(self, uid: Optional[str] = None, tenant: Optional[str] = None):
15071507
rgw_topic_instance = RgwTopicmanagement()
15081508
result = rgw_topic_instance.list_topics(uid, tenant)
1509-
return result
1509+
return result['topics'] if 'topics' in result else []
15101510

15111511
@EndpointDoc(
15121512
"Get RGW Topic",
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<ng-container *ngIf="!!selection">
2+
<cds-tabs type="contained"
3+
theme="light">
4+
<cds-tab heading="Details"
5+
i18n-heading>
6+
<table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md"
7+
data-testid="rgw-topic-details">
8+
<tbody>
9+
<tr>
10+
<td i18n
11+
class="bold">Push endpoint arguments</td>
12+
<td>{{ selection?.dest?.push_endpoint_args }}</td>
13+
</tr>
14+
<tr>
15+
<td i18n
16+
class="bold w-25">Push endpoint topic</td>
17+
<td class="w-75">{{ selection?.dest?.push_endpoint_topic}}</td>
18+
</tr>
19+
<tr>
20+
<td i18n
21+
class="bold w-25">Push endpoint</td>
22+
<td class="w-75">{{ selection?.dest?.push_endpoint }}</td>
23+
</tr>
24+
<tr>
25+
<td i18n
26+
class="bold w-25">Stored secret</td>
27+
<td class="w-75">{{ selection?.dest?.stored_secret}}</td>
28+
</tr>
29+
<tr>
30+
<td i18n
31+
class="bold">Persistent</td>
32+
<td>{{ selection?.dest?.persistent }}</td>
33+
</tr>
34+
<tr>
35+
<td i18n
36+
class="bold">Persistent queue</td>
37+
<td>{{ selection?.dest?.persistent_queue }}</td>
38+
</tr>
39+
<tr>
40+
<td i18n
41+
class="bold">Time to live</td>
42+
<td>{{ selection?.dest?.time_to_live }}</td>
43+
</tr>
44+
<tr>
45+
<td i18n
46+
class="bold">Max retries</td>
47+
<td>{{ selection?.dest?.max_retries }}</td>
48+
</tr>
49+
<tr>
50+
<td i18n
51+
class="bold">Retry sleep duration</td>
52+
<td>{{ selection?.dest?.retry_sleep_duration }}</td>
53+
</tr>
54+
<tr>
55+
<td i18n
56+
class="bold">Opaque data</td>
57+
<td>{{ selection?.opaqueData }}</td>
58+
</tr>
59+
</tbody>
60+
</table>
61+
</cds-tab>
62+
<cds-tab heading="Policies"
63+
i18n-heading>
64+
<div class="table-scroller">
65+
<table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
66+
<tbody>
67+
<tr>
68+
<td i18n
69+
class="bold w-25 ">Policy</td>
70+
<td><pre>{{ policy | json }}</pre></td>
71+
</tr>
72+
</tbody>
73+
</table>
74+
</div>
75+
</cds-tab>
76+
<cds-tab heading="Subscribed buckets"
77+
i18n-heading>
78+
<table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
79+
<tbody>
80+
<tr>
81+
<td i18n
82+
class="bold w-25">Subscribed buckets</td>
83+
<td><pre>{{ selection.subscribed_buckets | json}}</pre></td>
84+
</tr>
85+
</tbody>
86+
</table>
87+
</cds-tab>
88+
</cds-tabs>
89+
</ng-container>

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.scss

Whitespace-only changes.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { RgwTopicDetailsComponent } from './rgw-topic-details.component';
3+
import { TopicDetails } from '~/app/shared/models/topic.model';
4+
5+
interface Destination {
6+
push_endpoint: string;
7+
push_endpoint_args: string;
8+
push_endpoint_topic: string;
9+
stored_secret: string;
10+
persistent: boolean;
11+
persistent_queue: string;
12+
time_to_live: number;
13+
max_retries: number;
14+
retry_sleep_duration: number;
15+
}
16+
17+
const mockDestination: Destination = {
18+
push_endpoint: 'http://localhost:8080',
19+
push_endpoint_args: 'args',
20+
push_endpoint_topic: 'topic',
21+
stored_secret: 'secret',
22+
persistent: true,
23+
persistent_queue: 'queue',
24+
time_to_live: 3600,
25+
max_retries: 5,
26+
retry_sleep_duration: 10
27+
};
28+
29+
describe('RgwTopicDetailsComponent', () => {
30+
let component: RgwTopicDetailsComponent;
31+
let fixture: ComponentFixture<RgwTopicDetailsComponent>;
32+
33+
beforeEach(async () => {
34+
await TestBed.configureTestingModule({
35+
declarations: [RgwTopicDetailsComponent]
36+
}).compileComponents();
37+
38+
fixture = TestBed.createComponent(RgwTopicDetailsComponent);
39+
component = fixture.componentInstance;
40+
fixture.detectChanges();
41+
});
42+
43+
it('should create', () => {
44+
expect(component).toBeTruthy();
45+
});
46+
47+
it('should parse policy string correctly', () => {
48+
const mockSelection: TopicDetails = {
49+
name: 'testHttp',
50+
owner: 'ownerName',
51+
arn: 'arnValue',
52+
dest: mockDestination,
53+
policy: '{"key": "value"}',
54+
opaqueData: 'test@12345',
55+
subscribed_buckets: []
56+
};
57+
58+
component.selection = mockSelection;
59+
component.ngOnChanges({
60+
selection: {
61+
currentValue: mockSelection,
62+
previousValue: null,
63+
firstChange: true,
64+
isFirstChange: () => true
65+
}
66+
});
67+
68+
expect(component.policy).toEqual({ key: 'value' });
69+
});
70+
71+
it('should set policy to empty object if policy is not a string', () => {
72+
const mockSelection: TopicDetails = {
73+
name: 'testHttp',
74+
owner: 'ownerName',
75+
arn: 'arnValue',
76+
dest: mockDestination,
77+
policy: '{}',
78+
subscribed_buckets: [],
79+
opaqueData: ''
80+
};
81+
82+
component.selection = mockSelection;
83+
component.ngOnChanges({
84+
selection: {
85+
currentValue: mockSelection,
86+
previousValue: null,
87+
firstChange: true,
88+
isFirstChange: () => true
89+
}
90+
});
91+
92+
expect(component.policy).toEqual({});
93+
});
94+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Component, Input, SimpleChanges, OnChanges } from '@angular/core';
2+
3+
import { TopicDetails } from '~/app/shared/models/topic.model';
4+
import * as _ from 'lodash';
5+
6+
@Component({
7+
selector: 'cd-rgw-topic-details',
8+
templateUrl: './rgw-topic-details.component.html',
9+
styleUrls: ['./rgw-topic-details.component.scss']
10+
})
11+
export class RgwTopicDetailsComponent implements OnChanges {
12+
@Input()
13+
selection: TopicDetails;
14+
policy: string;
15+
constructor() {}
16+
ngOnChanges(changes: SimpleChanges): void {
17+
if (changes['selection'] && this.selection) {
18+
if (_.isString(this.selection.policy)) {
19+
try {
20+
this.policy = JSON.parse(this.selection.policy);
21+
} catch (e) {
22+
this.policy = '{}';
23+
}
24+
} else {
25+
this.policy = this.selection.policy || {};
26+
}
27+
}
28+
}
29+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<ng-container *ngIf="topic$ | async as topics">
2+
<cd-table #table
3+
[autoReload]="false"
4+
[data]="topics"
5+
[columns]="columns"
6+
columnMode="flex"
7+
selectionType="single"
8+
[hasDetails]="true"
9+
(setExpandedRow)="setExpandedRow($event)"
10+
(updateSelection)="updateSelection($event)"
11+
(fetchData)="fetchData($event)">
12+
<cd-rgw-topic-details *cdTableDetail
13+
[selection]="expandedRow"></cd-rgw-topic-details>
14+
</cd-table>
15+
</ng-container>

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.scss

Whitespace-only changes.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { RgwTopicListComponent } from './rgw-topic-list.component';
4+
import { RgwTopicService } from '~/app/shared/api/rgw-topic.service';
5+
import { SharedModule } from '~/app/shared/shared.module';
6+
import { configureTestBed } from '~/testing/unit-test-helper';
7+
import { RgwTopicDetailsComponent } from '../rgw-topic-details/rgw-topic-details.component';
8+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
9+
import { RouterTestingModule } from '@angular/router/testing';
10+
import { HttpClientTestingModule } from '@angular/common/http/testing';
11+
import { ToastrModule } from 'ngx-toastr';
12+
describe('RgwTopicListComponent', () => {
13+
let component: RgwTopicListComponent;
14+
let fixture: ComponentFixture<RgwTopicListComponent>;
15+
let rgwtTopicService: RgwTopicService;
16+
let rgwTopicServiceListSpy: jasmine.Spy;
17+
18+
configureTestBed({
19+
declarations: [RgwTopicListComponent, RgwTopicDetailsComponent],
20+
imports: [BrowserAnimationsModule, RouterTestingModule, HttpClientTestingModule, SharedModule]
21+
});
22+
23+
beforeEach(async () => {
24+
await TestBed.configureTestingModule({
25+
imports: [
26+
BrowserAnimationsModule,
27+
SharedModule,
28+
HttpClientTestingModule,
29+
ToastrModule.forRoot(),
30+
RouterTestingModule
31+
],
32+
33+
declarations: [RgwTopicListComponent]
34+
}).compileComponents();
35+
36+
fixture = TestBed.createComponent(RgwTopicListComponent);
37+
component = fixture.componentInstance;
38+
rgwtTopicService = TestBed.inject(RgwTopicService);
39+
rgwTopicServiceListSpy = spyOn(rgwtTopicService, 'listTopic').and.callThrough();
40+
fixture = TestBed.createComponent(RgwTopicListComponent);
41+
component = fixture.componentInstance;
42+
spyOn(component, 'setTableRefreshTimeout').and.stub();
43+
fixture.detectChanges();
44+
});
45+
46+
it('should create', () => {
47+
expect(component).toBeTruthy();
48+
expect(rgwTopicServiceListSpy).toHaveBeenCalledTimes(1);
49+
});
50+
51+
it('should call listTopic on ngOnInit', () => {
52+
component.ngOnInit();
53+
expect(rgwTopicServiceListSpy).toHaveBeenCalled();
54+
});
55+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Component, OnInit, ViewChild } from '@angular/core';
2+
import _ from 'lodash';
3+
4+
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
5+
import { TableComponent } from '~/app/shared/datatable/table/table.component';
6+
import { CdTableAction } from '~/app/shared/models/cd-table-action';
7+
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
8+
import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
9+
import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
10+
import { Permission } from '~/app/shared/models/permissions';
11+
12+
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
13+
import { RgwTopicService } from '~/app/shared/api/rgw-topic.service';
14+
15+
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
16+
import { BehaviorSubject, Observable, of } from 'rxjs';
17+
import { Topic } from '~/app/shared/models/topic.model';
18+
import { catchError, shareReplay, switchMap } from 'rxjs/operators';
19+
20+
@Component({
21+
selector: 'cd-rgw-topic-list',
22+
templateUrl: './rgw-topic-list.component.html',
23+
styleUrls: ['./rgw-topic-list.component.scss']
24+
})
25+
export class RgwTopicListComponent extends ListWithDetails implements OnInit {
26+
@ViewChild('table', { static: true })
27+
table: TableComponent;
28+
columns: CdTableColumn[];
29+
permission: Permission;
30+
tableActions: CdTableAction[];
31+
context: CdTableFetchDataContext;
32+
errorMessage: string;
33+
selection: CdTableSelection = new CdTableSelection();
34+
topic$: Observable<Topic[]>;
35+
subject = new BehaviorSubject<Topic[]>([]);
36+
name: string;
37+
constructor(
38+
private authStorageService: AuthStorageService,
39+
public actionLabels: ActionLabelsI18n,
40+
private rgwTopicService: RgwTopicService
41+
) {
42+
super();
43+
this.permission = this.authStorageService.getPermissions().rgw;
44+
}
45+
46+
ngOnInit() {
47+
this.columns = [
48+
{
49+
name: $localize`Name`,
50+
prop: 'name',
51+
flexGrow: 2
52+
},
53+
{
54+
name: $localize`Owner`,
55+
prop: 'owner',
56+
flexGrow: 2
57+
},
58+
{
59+
name: $localize`Amazon resource name`,
60+
prop: 'arn',
61+
flexGrow: 2
62+
},
63+
{
64+
name: $localize`Push endpoint`,
65+
prop: 'dest.push_endpoint',
66+
flexGrow: 2
67+
}
68+
];
69+
this.topic$ = this.subject.pipe(
70+
switchMap(() =>
71+
this.rgwTopicService.listTopic().pipe(
72+
catchError(() => {
73+
this.context.error();
74+
return of(null);
75+
})
76+
)
77+
),
78+
shareReplay(1)
79+
);
80+
}
81+
82+
fetchData() {
83+
this.subject.next([]);
84+
}
85+
86+
updateSelection(selection: CdTableSelection) {
87+
this.selection = selection;
88+
}
89+
}

0 commit comments

Comments
 (0)