]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add RGW bucket notification listing in dashboard
authorpujaoshahu <pshahu@redhat.com>
Thu, 10 Apr 2025 17:29:06 +0000 (22:59 +0530)
committerpujashahu <pshahu@redhat.com>
Thu, 31 Jul 2025 14:23:09 +0000 (19:53 +0530)
Fixes: https://tracker.ceph.com/issues/70880
Signed-off-by: pujaoshahu <pshahu@redhat.com>
(cherry picked from commit 92fb5863767913a1ea7bdb03788ee21778fcabc7)

 Conflicts:
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-topic-list/rgw-topic-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-topic.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/notification-configuration.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/services/rgw_client.py

index 49d4f4255c7afbb838f4bd465c0422db1a2dfc9d..538436fb3df7b6b4707b4c096af618a48ddb1edd 100644 (file)
                                       (updateBucketDetails)="updateBucketDetails(extractLifecycleDetails.bind(this))"></cd-rgw-bucket-lifecycle-list>
       </ng-template>
     </ng-container>
+    <ng-container ngbNavItem="notification">
+      <a ngbNavLink
+         i18n>Notification</a>
+      <ng-template ngbNavContent>
+        <cd-rgw-bucket-notification-list [bucket]="selection"></cd-rgw-bucket-notification-list>
+      </ng-template>
+    </ng-container>
   </nav>
 
   <div [ngbNavOutlet]="nav"></div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.html
new file mode 100644 (file)
index 0000000..b035a1a
--- /dev/null
@@ -0,0 +1,46 @@
+  <fieldset>
+    <legend i18n
+            class="cd-header">
+    Notification Configuration
+    <cd-help-text>
+       Configure bucket notification to trigger alerts for specific events, such as object creation or transitions, based on prefixes or tags.
+    </cd-help-text>
+  </legend>
+  </fieldset>
+  <ng-container *ngIf="notification$ | async as notification">
+  <cd-table #table
+            [data]="notification"
+            [columns]="columns"
+            columnMode="flex"
+            selectionType="single"
+            identifier="Id"
+            (fetchData)="fetchData()">
+  </cd-table>
+  </ng-container>
+  <ng-template #filterTpl
+               let-config="data.value">
+    <ng-container *ngIf="config">
+      <ng-container *ngFor="let item of config | keyvalue">
+        <ng-container *ngIf="item.value?.FilterRule?.length">
+          <div class="cds--label">
+            {{ item.key }}:
+          </div>
+          <div [cdsStack]="'horizontal'"
+               *ngFor="let rule of item.value.FilterRule">
+            <cds-tag size="sm"
+                     class="badge-background-gray">{{ rule.Name }}: {{ rule.Value }}</cds-tag>
+          </div>
+          <br>
+        </ng-container>
+      </ng-container>
+    </ng-container>
+  </ng-template>
+  <ng-template #eventTpl
+               let-event="data.value">
+    <ng-container *ngIf="event">
+      <cds-tag size="sm"
+               class="badge-background-primary">
+      {{ event }}
+      </cds-tag>
+    </ng-container>
+  </ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.scss
new file mode 100644 (file)
index 0000000..79de9dd
--- /dev/null
@@ -0,0 +1,5 @@
+@use '@carbon/layout';
+
+::ng-deep.cds--type-mono {
+  margin-right: layout.$spacing-02;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.spec.ts
new file mode 100644 (file)
index 0000000..bd82bb0
--- /dev/null
@@ -0,0 +1,47 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { RgwBucketNotificationListComponent } from './rgw-bucket-notification-list.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { of } from 'rxjs';
+
+class MockRgwBucketService {
+  listNotification = jest.fn((bucket: string) => of([{ bucket, notifications: [] }]));
+}
+describe('RgwBucketNotificationListComponent', () => {
+  let component: RgwBucketNotificationListComponent;
+  let fixture: ComponentFixture<RgwBucketNotificationListComponent>;
+  let rgwtbucketService: RgwBucketService;
+  let rgwnotificationListSpy: jasmine.Spy;
+
+  configureTestBed({
+    declarations: [RgwBucketNotificationListComponent],
+    imports: [ComponentsModule, HttpClientTestingModule],
+    providers: [
+      { provide: 'bucket', useValue: { bucket: 'bucket1', owner: 'dashboard' } },
+      { provide: RgwBucketService, useClass: MockRgwBucketService }
+    ]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwBucketNotificationListComponent);
+    component = fixture.componentInstance;
+    rgwtbucketService = TestBed.inject(RgwBucketService);
+    rgwnotificationListSpy = spyOn(rgwtbucketService, 'listNotification').and.callThrough();
+
+    fixture = TestBed.createComponent(RgwBucketNotificationListComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+  it('should call list', () => {
+    rgwtbucketService.listNotification('testbucket').subscribe((response) => {
+      expect(response).toEqual([{ bucket: 'testbucket', notifications: [] }]);
+    });
+    expect(rgwnotificationListSpy).toHaveBeenCalledWith('testbucket');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.ts
new file mode 100644 (file)
index 0000000..5e926af
--- /dev/null
@@ -0,0 +1,92 @@
+import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { BehaviorSubject, Observable, of } from 'rxjs';
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+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 { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { Bucket } from '../models/rgw-bucket';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { catchError, switchMap } from 'rxjs/operators';
+import { TopicConfiguration } from '~/app/shared/models/notification-configuration.model';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+
+const BASE_URL = 'rgw/bucket';
+@Component({
+  selector: 'cd-rgw-bucket-notification-list',
+  templateUrl: './rgw-bucket-notification-list.component.html',
+  styleUrl: './rgw-bucket-notification-list.component.scss',
+  providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
+})
+export class RgwBucketNotificationListComponent extends ListWithDetails implements OnInit {
+  @Input() bucket: Bucket;
+  table: TableComponent;
+  permission: Permission;
+  tableActions: CdTableAction[];
+  columns: CdTableColumn[] = [];
+  selection: CdTableSelection = new CdTableSelection();
+  notification$: Observable<TopicConfiguration[]>;
+  subject = new BehaviorSubject<TopicConfiguration[]>([]);
+  context: CdTableFetchDataContext;
+  @ViewChild('filterTpl', { static: true })
+  filterTpl: TemplateRef<any>;
+  @ViewChild('eventTpl', { static: true })
+  eventTpl: TemplateRef<any>;
+
+  constructor(
+    private rgwBucketService: RgwBucketService,
+    private authStorageService: AuthStorageService,
+    public actionLabels: ActionLabelsI18n
+  ) {
+    super();
+  }
+
+  ngOnInit() {
+    this.permission = this.authStorageService.getPermissions().rgw;
+    this.columns = [
+      {
+        name: $localize`Name`,
+        prop: 'Id',
+        flexGrow: 2
+      },
+      {
+        name: $localize`Topic`,
+        prop: 'Topic',
+        flexGrow: 1,
+        cellTransformation: CellTemplate.copy
+      },
+      {
+        name: $localize`Event`,
+        prop: 'Event',
+        flexGrow: 1,
+        cellTemplate: this.eventTpl
+      },
+      {
+        name: $localize`Filter`,
+        prop: 'Filter',
+        flexGrow: 1,
+        cellTemplate: this.filterTpl
+      }
+    ];
+
+    this.notification$ = this.subject.pipe(
+      switchMap(() =>
+        this.rgwBucketService.listNotification(this.bucket.bucket).pipe(
+          catchError((error) => {
+            this.context.error(error);
+            return of(null);
+          })
+        )
+      )
+    );
+  }
+
+  fetchData() {
+    this.subject.next([]);
+  }
+}
index b4e7d6653ee6a6b28d851d757f4c33794c531fc7..f7ddf1593ea4783c9d3f3cb5de5d631fa7c943c3 100644 (file)
@@ -1,6 +1,5 @@
 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, PermissionHelper } from '~/testing/unit-test-helper';
 import { RgwTopicDetailsComponent } from '../rgw-topic-details/rgw-topic-details.component';
@@ -9,6 +8,7 @@ import { RouterTestingModule } from '@angular/router/testing';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { ToastrModule } from 'ngx-toastr';
 import { of } from 'rxjs';
+import { RgwTopicService } from '~/app/shared/api/rgw-topic.service';
 
 describe('RgwTopicListComponent', () => {
   let component: RgwTopicListComponent;
index adb0f0d43fabe0ad364e8afad32c5edcef2df933..8bce2c66af67b88a2a6671d8067215c36ce27be5 100644 (file)
@@ -83,7 +83,8 @@ import {
   TagModule,
   TooltipModule,
   ComboBoxModule,
-  ToggletipModule
+  ToggletipModule,
+  LayoutModule
 } from 'carbon-components-angular';
 import { CephSharedModule } from '../shared/ceph-shared.module';
 import { RgwUserAccountsComponent } from './rgw-user-accounts/rgw-user-accounts.component';
@@ -99,6 +100,7 @@ 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 { RgwTopicFormComponent } from './rgw-topic-form/rgw-topic-form.component';
+import { RgwBucketNotificationListComponent } from './rgw-bucket-notification-list/rgw-bucket-notification-list.component';
 
 @NgModule({
   imports: [
@@ -135,7 +137,8 @@ import { RgwTopicFormComponent } from './rgw-topic-form/rgw-topic-form.component
     ComboBoxModule,
     ToggletipModule,
     RadioModule,
-    SelectModule
+    SelectModule,
+    LayoutModule
   ],
   exports: [
     RgwDaemonDetailsComponent,
@@ -199,7 +202,8 @@ import { RgwTopicFormComponent } from './rgw-topic-form/rgw-topic-form.component
     RgwRateLimitDetailsComponent,
     RgwTopicListComponent,
     RgwTopicDetailsComponent,
-    RgwTopicFormComponent
+    RgwTopicFormComponent,
+    RgwBucketNotificationListComponent
   ],
   providers: [TitleCasePipe]
 })
index 67e26757a40c7867a15f215815e9f2679f2cc1f5..b47b551feb37d640e5831d067db92cbde3fd56c2 100644 (file)
@@ -281,4 +281,24 @@ export class RgwBucketService extends ApiClient {
   getGlobalBucketRateLimit() {
     return this.http.get(`${this.url}/ratelimit`);
   }
+
+  listNotification(bucket_name: string) {
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      params = params.appendAll({
+        bucket_name: bucket_name
+      });
+      return this.http.get(`${this.url}/notification`, { params: params });
+    });
+  }
+
+  setNotification(bucket_name: string, notification: string, owner: string) {
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      params = params.appendAll({
+        bucket_name: bucket_name,
+        notification: notification,
+        owner: owner
+      });
+      return this.http.put(`${this.url}/notification`, null, { params: params });
+    });
+  }
 }
index 6dcc48882f453d8075640a39729382e919b6e94a..f8975ddbb6156dc7c4eb05ca306992f4eb8c25ca 100644 (file)
@@ -17,8 +17,8 @@ export class RgwTopicService extends ApiClient {
     super();
   }
 
-  listTopic(): Observable<Topic[]> {
-    return this.http.get<Topic[]>(this.baseURL);
+  listTopic(): Observable<Topic> {
+    return this.http.get<Topic>(this.baseURL);
   }
 
   getTopic(key: string) {
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/notification-configuration.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/notification-configuration.model.ts
new file mode 100644 (file)
index 0000000..9fa51a4
--- /dev/null
@@ -0,0 +1,30 @@
+export interface NotificationConfiguration {
+  TopicConfiguration: TopicConfiguration[];
+}
+
+export interface TopicConfiguration {
+  Id: string;
+  Topic: string;
+  Event: string[];
+  Filter?: Filter;
+}
+
+export interface Filter {
+  Key: Key;
+  Metadata: Metadata;
+  Tags: Tags;
+}
+
+export interface Key {
+  FilterRules: FilterRules[];
+}
+export interface Metadata {
+  FilterRules: FilterRules[];
+}
+export interface Tags {
+  FilterRules: FilterRules[];
+}
+export interface FilterRules {
+  Name: string;
+  Value: string;
+}
index 43cbae0f21e2d39fd518779aa2d54f92b02bb91b..0fd6f419d84baa903f42ca387d7b949c3fb6d932 100755 (executable)
@@ -1240,7 +1240,7 @@ class RgwClient(RestClient):
             if topic_filter:
                 normalize_filter_rules(topic_filter)
 
-        return notification_configuration
+        return topic_configuration
 
     @RestClient.api_delete('/{bucket_name}?notification={notification_id}')
     def delete_notification(self, bucket_name, notification_id, request=None):