From 67eef860b4f67090aba67105e92a1cd78e7fbe9b Mon Sep 17 00:00:00 2001 From: pujaoshahu Date: Thu, 10 Apr 2025 22:59:06 +0530 Subject: [PATCH] mgr/dashboard: Add RGW bucket notification listing in dashboard Fixes: https://tracker.ceph.com/issues/70880 Signed-off-by: pujaoshahu (cherry picked from commit 92fb5863767913a1ea7bdb03788ee21778fcabc7) Conflicts: src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts --- .../rgw-bucket-details.component.html | 7 ++ ...gw-bucket-notification-list.component.html | 46 ++++++++++ ...gw-bucket-notification-list.component.scss | 5 + ...bucket-notification-list.component.spec.ts | 47 ++++++++++ .../rgw-bucket-notification-list.component.ts | 92 +++++++++++++++++++ .../rgw-topic-list.component.spec.ts | 2 +- .../frontend/src/app/ceph/rgw/rgw.module.ts | 10 +- .../src/app/shared/api/rgw-bucket.service.ts | 20 ++++ .../src/app/shared/api/rgw-topic.service.ts | 4 +- .../notification-configuration.model.ts | 30 ++++++ .../mgr/dashboard/services/rgw_client.py | 2 +- 11 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/models/notification-configuration.model.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html index 49d4f4255c7..538436fb3df 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html @@ -238,6 +238,13 @@ (updateBucketDetails)="updateBucketDetails(extractLifecycleDetails.bind(this))"> + + Notification + + + +
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 index 00000000000..b035a1a22aa --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.html @@ -0,0 +1,46 @@ +
+ + Notification Configuration + + Configure bucket notification to trigger alerts for specific events, such as object creation or transitions, based on prefixes or tags. + + +
+ + + + + + + + +
+ {{ item.key }}: +
+
+ {{ rule.Name }}: {{ rule.Value }} +
+
+
+
+
+
+ + + + {{ event }} + + + 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 index 00000000000..79de9ddd49e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.scss @@ -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 index 00000000000..bd82bb0e6a0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.spec.ts @@ -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; + 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 index 00000000000..5e926af2ac5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.ts @@ -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; + subject = new BehaviorSubject([]); + context: CdTableFetchDataContext; + @ViewChild('filterTpl', { static: true }) + filterTpl: TemplateRef; + @ViewChild('eventTpl', { static: true }) + eventTpl: TemplateRef; + + 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([]); + } +} 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 index b4e7d6653ee..f7ddf1593ea 100644 --- 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 @@ -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; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index adb0f0d43fa..8bce2c66af6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -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] }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts index 67e26757a40..b47b551feb3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts @@ -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 }); + }); + } } 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 index 6dcc48882f4..f8975ddbb61 100644 --- 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 @@ -17,8 +17,8 @@ export class RgwTopicService extends ApiClient { super(); } - listTopic(): Observable { - return this.http.get(this.baseURL); + listTopic(): Observable { + return this.http.get(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 index 00000000000..9fa51a420a1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/notification-configuration.model.ts @@ -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; +} diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index 43cbae0f21e..0fd6f419d84 100755 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -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): -- 2.39.5