def list(self, uid: Optional[str] = None, tenant: Optional[str] = None):
rgw_topic_instance = RgwTopicmanagement()
result = rgw_topic_instance.list_topics(uid, tenant)
- return result
+ return result['topics'] if 'topics' in result else []
@EndpointDoc(
"Get RGW Topic",
--- /dev/null
+<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">Push endpoint topic</td>
+ <td class="w-75">{{ selection?.dest?.push_endpoint_topic}}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold w-25">Push endpoint</td>
+ <td class="w-75">{{ selection?.dest?.push_endpoint }}</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 class="table-scroller">
+ <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>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RgwTopicDetailsComponent } from './rgw-topic-details.component';
+import { TopicDetails } from '~/app/shared/models/topic.model';
+
+interface Destination {
+ push_endpoint: string;
+ push_endpoint_args: string;
+ push_endpoint_topic: string;
+ stored_secret: string;
+ 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: 'secret',
+ 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: TopicDetails = {
+ name: 'testHttp',
+ owner: 'ownerName',
+ arn: 'arnValue',
+ dest: mockDestination,
+ policy: '{"key": "value"}',
+ 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: TopicDetails = {
+ name: 'testHttp',
+ owner: 'ownerName',
+ arn: 'arnValue',
+ dest: mockDestination,
+ policy: '{}',
+ subscribed_buckets: [],
+ opaqueData: ''
+ };
+
+ component.selection = mockSelection;
+ component.ngOnChanges({
+ selection: {
+ currentValue: mockSelection,
+ previousValue: null,
+ firstChange: true,
+ isFirstChange: () => true
+ }
+ });
+
+ expect(component.policy).toEqual({});
+ });
+});
--- /dev/null
+import { Component, Input, SimpleChanges, OnChanges } from '@angular/core';
+
+import { TopicDetails } 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']
+})
+export class RgwTopicDetailsComponent implements OnChanges {
+ @Input()
+ selection: TopicDetails;
+ policy: string;
+ 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 || {};
+ }
+ }
+ }
+}
--- /dev/null
+ <ng-container *ngIf="topic$ | async as topics">
+ <cd-table #table
+ [autoReload]="false"
+ [data]="topics"
+ [columns]="columns"
+ columnMode="flex"
+ selectionType="single"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)"
+ (fetchData)="fetchData($event)">
+ <cd-rgw-topic-details *cdTableDetail
+ [selection]="expandedRow"></cd-rgw-topic-details>
+ </cd-table>
+ </ng-container>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwTopicListComponent } from './rgw-topic-list.component';
+import { RgwTopicService } from '~/app/shared/api/rgw-topic.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwTopicDetailsComponent } from '../rgw-topic-details/rgw-topic-details.component';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule } from 'ngx-toastr';
+describe('RgwTopicListComponent', () => {
+ let component: RgwTopicListComponent;
+ let fixture: ComponentFixture<RgwTopicListComponent>;
+ let rgwtTopicService: RgwTopicService;
+ let rgwTopicServiceListSpy: jasmine.Spy;
+
+ configureTestBed({
+ declarations: [RgwTopicListComponent, RgwTopicDetailsComponent],
+ imports: [BrowserAnimationsModule, RouterTestingModule, HttpClientTestingModule, SharedModule]
+ });
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ HttpClientTestingModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule
+ ],
+
+ declarations: [RgwTopicListComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(RgwTopicListComponent);
+ component = fixture.componentInstance;
+ rgwtTopicService = TestBed.inject(RgwTopicService);
+ rgwTopicServiceListSpy = spyOn(rgwtTopicService, 'listTopic').and.callThrough();
+ fixture = TestBed.createComponent(RgwTopicListComponent);
+ component = fixture.componentInstance;
+ spyOn(component, 'setTableRefreshTimeout').and.stub();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(rgwTopicServiceListSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call listTopic on ngOnInit', () => {
+ component.ngOnInit();
+ expect(rgwTopicServiceListSpy).toHaveBeenCalled();
+ });
+});
--- /dev/null
+import { Component, OnInit, ViewChild } from '@angular/core';
+import _ from 'lodash';
+
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { Permission } from '~/app/shared/models/permissions';
+
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { RgwTopicService } from '~/app/shared/api/rgw-topic.service';
+
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { BehaviorSubject, Observable, of } from 'rxjs';
+import { Topic } from '~/app/shared/models/topic.model';
+import { catchError, shareReplay, switchMap } from 'rxjs/operators';
+
+@Component({
+ selector: 'cd-rgw-topic-list',
+ templateUrl: './rgw-topic-list.component.html',
+ styleUrls: ['./rgw-topic-list.component.scss']
+})
+export class RgwTopicListComponent extends ListWithDetails implements OnInit {
+ @ViewChild('table', { static: true })
+ table: TableComponent;
+ columns: CdTableColumn[];
+ permission: Permission;
+ tableActions: CdTableAction[];
+ context: CdTableFetchDataContext;
+ errorMessage: string;
+ selection: CdTableSelection = new CdTableSelection();
+ topic$: Observable<Topic[]>;
+ subject = new BehaviorSubject<Topic[]>([]);
+ name: string;
+ constructor(
+ private authStorageService: AuthStorageService,
+ public actionLabels: ActionLabelsI18n,
+ private rgwTopicService: RgwTopicService
+ ) {
+ super();
+ this.permission = this.authStorageService.getPermissions().rgw;
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Owner`,
+ prop: 'owner',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Amazon resource name`,
+ prop: 'arn',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Push endpoint`,
+ prop: 'dest.push_endpoint',
+ flexGrow: 2
+ }
+ ];
+ this.topic$ = this.subject.pipe(
+ switchMap(() =>
+ this.rgwTopicService.listTopic().pipe(
+ catchError(() => {
+ this.context.error();
+ return of(null);
+ })
+ )
+ ),
+ shareReplay(1)
+ );
+ }
+
+ fetchData() {
+ this.subject.next([]);
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+}
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';
@NgModule({
imports: [
SelectModule,
NumberModule,
TabsModule,
- RadioModule,
TagModule,
TooltipModule,
ComboBoxModule,
- ToggletipModule
+ ToggletipModule,
+ RadioModule
],
exports: [
RgwDaemonDetailsComponent,
RgwStorageClassFormComponent,
RgwBucketTieringFormComponent,
RgwBucketLifecycleListComponent,
- RgwRateLimitDetailsComponent
+ RgwRateLimitDetailsComponent,
+ RgwTopicListComponent,
+ RgwTopicDetailsComponent
],
providers: [TitleCasePipe]
})
path: 'configuration',
data: { breadcrumbs: 'Configuration' },
children: [{ path: '', component: RgwConfigurationPageComponent }]
+ },
+ {
+ path: 'topic',
+ data: { breadcrumbs: 'Topic' },
+ children: [{ path: '', component: RgwTopicListComponent }]
}
];
i18n-title
[useRouter]="true"
class="tc_submenuitem tc_submenuitem_rgw_overview"><span i18n>Overview</span></cds-sidenav-item>
+ <cds-sidenav-item route="/rgw/user"
+ title="Users"
+ i18n-title
+ [useRouter]="true"
+ class="tc_submenuitem tc_submenuitem_rgw_users"><span i18n>Users</span></cds-sidenav-item>
<cds-sidenav-item route="/rgw/bucket"
title="Buckets"
i18n-title
[useRouter]="true"
class="tc_submenuitem tc_submenuitem_rgw_buckets"><span i18n>Buckets</span></cds-sidenav-item>
- <cds-sidenav-item route="/rgw/user"
- title="Users"
- i18n-title
+ <cds-sidenav-item route="/rgw/topic"
[useRouter]="true"
- class="tc_submenuitem tc_submenuitem_rgw_users"><span i18n>Users</span></cds-sidenav-item>
+ title="Topics"
+ i18n-title
+ class="tc_submenuitem tc_submenuitem_rgw_topics"><span i18n>Topics</span></cds-sidenav-item>
<cds-sidenav-item route="/rgw/tiering"
title="Tiering"
i18n-title
[
'.tc_menuitem_rgw',
'.tc_submenuitem_rgw_daemons',
+ '.tc_submenuitem_rgw_users',
'.tc_submenuitem_rgw_buckets',
- '.tc_submenuitem_rgw_users'
+ '.tc_submenuitem_rgw_topics'
]
]
];
[
'.tc_menuitem_rgw',
'.tc_submenuitem_rgw_daemons',
+ '.tc_submenuitem_rgw_users',
'.tc_submenuitem_rgw_buckets',
- '.tc_submenuitem_rgw_users'
+ '.tc_submenuitem_rgw_topics'
]
]
];
'.tc_submenuitem_block_iscsi': 'iSCSI',
'.tc_submenuitem_block_nvme': 'NVMe/TCP',
'.tc_submenuitem_rgw_overview': 'Overview',
- '.tc_submenuitem_rgw_buckets': 'Buckets',
'.tc_submenuitem_rgw_users': 'Users',
+ '.tc_submenuitem_rgw_buckets': 'Buckets',
+ '.tc_submenuitem_rgw_topics': 'Topics',
'.tc_submenuitem_rgw_multi-site': 'Multi-site',
'.tc_submenuitem_rgw_daemons': 'Gateways',
'.tc_submenuitem_rgw_nfs': 'NFS',
--- /dev/null
+import { TestBed } from '@angular/core/testing';
+
+import { RgwTopicService } from './rgw-topic.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+
+describe('RgwTopicService', () => {
+ let service: RgwTopicService;
+ let httpTesting: HttpTestingController;
+ configureTestBed({
+ imports: [HttpClientTestingModule]
+ });
+ configureTestBed({
+ imports: [HttpClientTestingModule],
+ providers: [RgwTopicService]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(RgwTopicService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call list with empty result', () => {
+ let result;
+ service.listTopic().subscribe((resp) => {
+ result = resp;
+ });
+ const req = httpTesting.expectOne(`api/rgw/topic`);
+ expect(req.request.method).toBe('GET');
+ req.flush([]);
+ expect(result).toEqual([]);
+ });
+ it('should call list with result', () => {
+ service.listTopic().subscribe((resp) => {
+ let result = resp;
+ return result;
+ });
+ let req = httpTesting.expectOne(`api/rgw/topic`);
+ expect(req.request.method).toBe('GET');
+ req.flush(['foo', 'bar']);
+ });
+
+ it('should call get', () => {
+ service.getTopic('foo').subscribe();
+ const req = httpTesting.expectOne(`api/rgw/topic/foo`);
+ expect(req.request.method).toBe('GET');
+ });
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { ApiClient } from './api-client';
+import { Topic } from '~/app/shared/models/topic.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RgwTopicService extends ApiClient {
+ baseURL = 'api/rgw/topic';
+
+ constructor(private http: HttpClient) {
+ super();
+ }
+
+ listTopic(): Observable<Topic[]> {
+ return this.http.get<Topic[]>(this.baseURL);
+ }
+
+ getTopic(name: string) {
+ return this.http.get(`${this.baseURL}/${name}`);
+ }
+}
--- /dev/null
+interface Destination {
+ push_endpoint: string;
+ push_endpoint_args: string;
+ push_endpoint_topic: string;
+ stored_secret: string;
+ persistent: boolean;
+ persistent_queue: string;
+ time_to_live: number;
+ max_retries: number;
+ retry_sleep_duration: number;
+}
+
+export interface Topic {
+ owner: string;
+ name: string;
+ arn: string;
+ dest: Destination;
+ opaqueData: string;
+ policy: string | {};
+ subscribed_buckets: any[];
+}
+
+export interface TopicDetails {
+ owner: string;
+ name: string;
+ arn: string;
+ dest: Destination;
+ opaqueData: string;
+ policy: string;
+ subscribed_buckets: string[];
+}
def test_list_topic_with_details(self, mock_list_topics):
mock_return_value = [
{
- "topic": {
- "owner": "dashboard",
- "name": "HttpTest",
- "dest": {
- "push_endpoint": "https://10.0.66.13:443",
- "push_endpoint_args": "verify_ssl=true",
- "push_endpoint_topic": "HttpTest",
- "stored_secret": False,
- "persistent": True,
- "persistent_queue": ":HttpTest",
- "time_to_live": "5",
- "max_retries": "2",
- "retry_sleep_duration": "2"
- },
- "arn": "arn:aws:sns:zg1-realm1::HttpTest",
- "opaqueData": "test123",
- "policy": "{}",
- "subscribed_buckets": []
- }
+ "owner": "dashboard",
+ "name": "HttpTest",
+ "dest": {
+ "push_endpoint": "https://10.0.66.13:443",
+ "push_endpoint_args": "verify_ssl=true",
+ "push_endpoint_topic": "HttpTest",
+ "stored_secret": False,
+ "persistent": True,
+ "persistent_queue": ":HttpTest",
+ "time_to_live": "5",
+ "max_retries": "2",
+ "retry_sleep_duration": "2"
+ },
+ "arn": "arn:aws:sns:zg1-realm1::HttpTest",
+ "opaqueData": "test123",
+ "policy": "{}",
+ "subscribed_buckets": []
}
]
-
mock_list_topics.return_value = mock_return_value
controller = RgwTopic()
result = controller.list(True, None)
@patch('dashboard.controllers.rgw.RgwTopic.get')
def test_get_topic(self, mock_get_topic):
- mock_return_value = {
- "topic": {
+ mock_return_value = [
+ {
"owner": "dashboard",
"name": "HttpTest",
"dest": {
"policy": "{}",
"subscribed_buckets": []
}
- }
+ ]
mock_get_topic.return_value = mock_return_value
controller = RgwTopic()