]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add RGW topic listing in dashboard 61273/head
authorpujaoshahu <pshahu@redhat.com>
Thu, 9 Jan 2025 05:14:43 +0000 (10:44 +0530)
committerpujaoshahu <pshahu@redhat.com>
Wed, 9 Apr 2025 06:30:01 +0000 (12:00 +0530)
Fixes: https://tracker.ceph.com/issues/69143
Signed-off-by: pujaoshahu <pshahu@redhat.com>
16 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-topic.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-topic.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/topic.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/tests/test_rgw.py

index 4c8d0b5e1eb93f27142b846b07611471c51e7bea..10e6d0dbbc678d39411cb01739b625d66b0d035c 100755 (executable)
@@ -1506,7 +1506,7 @@ class RgwTopic(RESTController):
     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",
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.html
new file mode 100644 (file)
index 0000000..dec2843
--- /dev/null
@@ -0,0 +1,89 @@
+<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>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.spec.ts
new file mode 100644 (file)
index 0000000..85b1b1b
--- /dev/null
@@ -0,0 +1,94 @@
+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({});
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-details/rgw-topic-details.component.ts
new file mode 100644 (file)
index 0000000..359c927
--- /dev/null
@@ -0,0 +1,29 @@
+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 || {};
+      }
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.html
new file mode 100644 (file)
index 0000000..22fce6e
--- /dev/null
@@ -0,0 +1,15 @@
+  <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>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.spec.ts
new file mode 100644 (file)
index 0000000..c874278
--- /dev/null
@@ -0,0 +1,55 @@
+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();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.ts
new file mode 100644 (file)
index 0000000..4c3dace
--- /dev/null
@@ -0,0 +1,89 @@
+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;
+  }
+}
index c5aeef5e91fb7f30db4946da5341afef430077e2..83a0414b000b1543e08abd971797460e96b6e9e3 100644 (file)
@@ -96,6 +96,8 @@ import { RgwBucketLifecycleListComponent } from './rgw-bucket-lifecycle-list/rgw
 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: [
@@ -128,11 +130,11 @@ import { NfsClusterComponent } from '../nfs/nfs-cluster/nfs-cluster.component';
     SelectModule,
     NumberModule,
     TabsModule,
-    RadioModule,
     TagModule,
     TooltipModule,
     ComboBoxModule,
-    ToggletipModule
+    ToggletipModule,
+    RadioModule
   ],
   exports: [
     RgwDaemonDetailsComponent,
@@ -193,7 +195,9 @@ import { NfsClusterComponent } from '../nfs/nfs-cluster/nfs-cluster.component';
     RgwStorageClassFormComponent,
     RgwBucketTieringFormComponent,
     RgwBucketLifecycleListComponent,
-    RgwRateLimitDetailsComponent
+    RgwRateLimitDetailsComponent,
+    RgwTopicListComponent,
+    RgwTopicDetailsComponent
   ],
   providers: [TitleCasePipe]
 })
@@ -392,6 +396,11 @@ const routes: Routes = [
     path: 'configuration',
     data: { breadcrumbs: 'Configuration' },
     children: [{ path: '', component: RgwConfigurationPageComponent }]
+  },
+  {
+    path: 'topic',
+    data: { breadcrumbs: 'Topic' },
+    children: [{ path: '', component: RgwTopicListComponent }]
   }
 ];
 
index 53d36bd1c849e0f0c71565f711398a9d2d7fedd8..8bd4895eb646ff4d67616c9ce5d64ddf34b8bf80 100644 (file)
                             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
index 86bc4610acc7d50010567f099824710898cfc8a4..a9d512d9b31dd7f1400228de761226ec37e1cf49 100644 (file)
@@ -137,8 +137,9 @@ describe('NavigationComponent', () => {
         [
           '.tc_menuitem_rgw',
           '.tc_submenuitem_rgw_daemons',
+          '.tc_submenuitem_rgw_users',
           '.tc_submenuitem_rgw_buckets',
-          '.tc_submenuitem_rgw_users'
+          '.tc_submenuitem_rgw_topics'
         ]
       ]
     ];
@@ -185,8 +186,9 @@ describe('NavigationComponent', () => {
         [
           '.tc_menuitem_rgw',
           '.tc_submenuitem_rgw_daemons',
+          '.tc_submenuitem_rgw_users',
           '.tc_submenuitem_rgw_buckets',
-          '.tc_submenuitem_rgw_users'
+          '.tc_submenuitem_rgw_topics'
         ]
       ]
     ];
@@ -246,8 +248,9 @@ describe('NavigationComponent', () => {
         '.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',
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-topic.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-topic.service.spec.ts
new file mode 100644 (file)
index 0000000..b30cf04
--- /dev/null
@@ -0,0 +1,56 @@
+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');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-topic.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-topic.service.ts
new file mode 100644 (file)
index 0000000..8de7b32
--- /dev/null
@@ -0,0 +1,24 @@
+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}`);
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/topic.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/topic.model.ts
new file mode 100644 (file)
index 0000000..31aa7b6
--- /dev/null
@@ -0,0 +1,31 @@
+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[];
+}
index 298b6a4c21361eb04652a55d6b8022d9b42124ac..00b2f5aff88183d726727de09a9cd05e7afbf5e2 100644 (file)
@@ -535,28 +535,25 @@ class TestRgwTopicController(ControllerTestCase):
     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)
@@ -565,8 +562,8 @@ class TestRgwTopicController(ControllerTestCase):
 
     @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": {
@@ -585,7 +582,7 @@ class TestRgwTopicController(ControllerTestCase):
                 "policy": "{}",
                 "subscribed_buckets": []
             }
-        }
+        ]
         mock_get_topic.return_value = mock_return_value
 
         controller = RgwTopic()