Skip to content

Commit 15601f0

Browse files
authored
Merge pull request ceph#62992 from rhcs-dashboard/notif-ui
mgr/dashboard: Create and delete and update s3 notification in dashboard
2 parents f5aafe0 + a4155c2 commit 15601f0

16 files changed

+919
-32
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,7 @@ def get_notification(self, bucket_name: str,
775775
return self._get_notification(bucket_name, daemon_name, owner)
776776

777777
@RESTController.Collection(method='PUT', path='/notification')
778+
@allow_empty_body
778779
@EndpointDoc("Create or update the bucket notification")
779780
def set_notification(self, bucket_name: str, notification: str = '', daemon_name=None,
780781
owner=None):

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.html

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414
columnMode="flex"
1515
selectionType="single"
1616
identifier="Id"
17+
(updateSelection)="updateSelection($event)"
1718
(fetchData)="fetchData()">
19+
<cd-table-actions class="table-actions"
20+
[permission]="permission"
21+
[selection]="selection"
22+
[tableActions]="tableActions">
23+
</cd-table-actions>
1824
</cd-table>
1925
</ng-container>
2026
<ng-template #filterTpl
@@ -36,11 +42,13 @@
3642
</ng-container>
3743
</ng-template>
3844
<ng-template #eventTpl
39-
let-event="data.value">
40-
<ng-container *ngIf="event">
41-
<cds-tag size="sm"
42-
class="badge-background-primary">
45+
let-events="data.value">
46+
<ng-container *ngIf="events && events.length">
47+
<cds-tag *ngFor="let event of events"
48+
size="sm"
49+
class="badge-background-primary">
4350
{{ event }}
44-
</cds-tag>
45-
</ng-container>
46-
</ng-template>
51+
</cds-tag>
52+
</ng-container>
53+
</ng-template>
54+

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.spec.ts

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
22
import { HttpClientTestingModule } from '@angular/common/http/testing';
33
import { RgwBucketNotificationListComponent } from './rgw-bucket-notification-list.component';
4-
import { configureTestBed } from '~/testing/unit-test-helper';
4+
import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper';
55
import { ComponentsModule } from '~/app/shared/components/components.module';
66
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
77
import { of } from 'rxjs';
8+
import { ToastrModule } from 'ngx-toastr';
89

910
class MockRgwBucketService {
1011
listNotification = jest.fn((bucket: string) => of([{ bucket, notifications: [] }]));
1112
}
13+
1214
describe('RgwBucketNotificationListComponent', () => {
1315
let component: RgwBucketNotificationListComponent;
1416
let fixture: ComponentFixture<RgwBucketNotificationListComponent>;
@@ -17,7 +19,7 @@ describe('RgwBucketNotificationListComponent', () => {
1719

1820
configureTestBed({
1921
declarations: [RgwBucketNotificationListComponent],
20-
imports: [ComponentsModule, HttpClientTestingModule],
22+
imports: [ComponentsModule, HttpClientTestingModule, ToastrModule.forRoot()],
2123
providers: [
2224
{ provide: 'bucket', useValue: { bucket: 'bucket1', owner: 'dashboard' } },
2325
{ provide: RgwBucketService, useClass: MockRgwBucketService }
@@ -30,18 +32,96 @@ describe('RgwBucketNotificationListComponent', () => {
3032
rgwtbucketService = TestBed.inject(RgwBucketService);
3133
rgwnotificationListSpy = spyOn(rgwtbucketService, 'listNotification').and.callThrough();
3234

33-
fixture = TestBed.createComponent(RgwBucketNotificationListComponent);
34-
component = fixture.componentInstance;
3535
fixture.detectChanges();
3636
});
3737

3838
it('should create', () => {
3939
expect(component).toBeTruthy();
4040
});
41+
4142
it('should call list', () => {
4243
rgwtbucketService.listNotification('testbucket').subscribe((response) => {
4344
expect(response).toEqual([{ bucket: 'testbucket', notifications: [] }]);
4445
});
4546
expect(rgwnotificationListSpy).toHaveBeenCalledWith('testbucket');
4647
});
48+
49+
it('should test all TableActions combinations', () => {
50+
const permissionHelper = new PermissionHelper(component.permission);
51+
const tableActions = permissionHelper.setPermissionsAndGetActions(component.tableActions);
52+
expect(tableActions).toEqual({
53+
'create,update,delete': {
54+
actions: ['Create', 'Edit', 'Delete'],
55+
primary: {
56+
multiple: 'Create',
57+
executing: 'Create',
58+
single: 'Create',
59+
no: 'Create'
60+
}
61+
},
62+
'create,update': {
63+
actions: ['Create', 'Edit'],
64+
primary: {
65+
multiple: 'Create',
66+
executing: 'Create',
67+
single: 'Create',
68+
no: 'Create'
69+
}
70+
},
71+
'create,delete': {
72+
actions: ['Create', 'Delete'],
73+
primary: {
74+
multiple: 'Create',
75+
executing: 'Create',
76+
single: 'Create',
77+
no: 'Create'
78+
}
79+
},
80+
create: {
81+
actions: ['Create'],
82+
primary: {
83+
multiple: 'Create',
84+
executing: 'Create',
85+
single: 'Create',
86+
no: 'Create'
87+
}
88+
},
89+
'update,delete': {
90+
actions: ['Edit', 'Delete'],
91+
primary: {
92+
multiple: '',
93+
executing: '',
94+
single: '',
95+
no: ''
96+
}
97+
},
98+
update: {
99+
actions: ['Edit'],
100+
primary: {
101+
multiple: 'Edit',
102+
executing: 'Edit',
103+
single: 'Edit',
104+
no: 'Edit'
105+
}
106+
},
107+
delete: {
108+
actions: ['Delete'],
109+
primary: {
110+
multiple: 'Delete',
111+
executing: 'Delete',
112+
single: 'Delete',
113+
no: 'Delete'
114+
}
115+
},
116+
'no-permissions': {
117+
actions: [],
118+
primary: {
119+
multiple: '',
120+
executing: '',
121+
single: '',
122+
no: ''
123+
}
124+
}
125+
});
126+
});
47127
});

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-notification-list/rgw-bucket-notification-list.component.ts

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
1+
import {
2+
Component,
3+
EventEmitter,
4+
Input,
5+
OnInit,
6+
Output,
7+
TemplateRef,
8+
ViewChild
9+
} from '@angular/core';
210
import { BehaviorSubject, Observable, of } from 'rxjs';
311
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
412
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
@@ -11,10 +19,16 @@ import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
1119
import { URLBuilderService } from '~/app/shared/services/url-builder.service';
1220
import { Bucket } from '../models/rgw-bucket';
1321
import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
22+
import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
1423
import { catchError, switchMap } from 'rxjs/operators';
15-
import { TopicConfiguration } from '~/app/shared/models/notification-configuration.model';
24+
import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
25+
import { NotificationService } from '~/app/shared/services/notification.service';
1626
import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
17-
import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
27+
import { TopicConfiguration } from '~/app/shared/models/notification-configuration.model';
28+
import { RgwNotificationFormComponent } from '../rgw-notification-form/rgw-notification-form.component';
29+
import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
30+
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
31+
import { Icons } from '~/app/shared/enum/icons.enum';
1832

1933
const BASE_URL = 'rgw/bucket';
2034
@Component({
@@ -25,6 +39,8 @@ const BASE_URL = 'rgw/bucket';
2539
})
2640
export class RgwBucketNotificationListComponent extends ListWithDetails implements OnInit {
2741
@Input() bucket: Bucket;
42+
@Output() updateBucketDetails = new EventEmitter();
43+
@ViewChild(TableComponent, { static: true })
2844
table: TableComponent;
2945
permission: Permission;
3046
tableActions: CdTableAction[];
@@ -37,11 +53,13 @@ export class RgwBucketNotificationListComponent extends ListWithDetails implemen
3753
filterTpl: TemplateRef<any>;
3854
@ViewChild('eventTpl', { static: true })
3955
eventTpl: TemplateRef<any>;
40-
56+
modalRef: any;
4157
constructor(
4258
private rgwBucketService: RgwBucketService,
4359
private authStorageService: AuthStorageService,
44-
public actionLabels: ActionLabelsI18n
60+
public actionLabels: ActionLabelsI18n,
61+
private modalService: ModalCdsService,
62+
private notificationService: NotificationService
4563
) {
4664
super();
4765
}
@@ -74,6 +92,28 @@ export class RgwBucketNotificationListComponent extends ListWithDetails implemen
7492
}
7593
];
7694

95+
const createAction: CdTableAction = {
96+
permission: 'create',
97+
icon: Icons.add,
98+
click: () => this.openNotificationModal(this.actionLabels.CREATE),
99+
name: this.actionLabels.CREATE
100+
};
101+
const editAction: CdTableAction = {
102+
permission: 'update',
103+
icon: Icons.edit,
104+
disable: () => this.selection.hasMultiSelection,
105+
click: () => this.openNotificationModal(this.actionLabels.EDIT),
106+
name: this.actionLabels.EDIT
107+
};
108+
const deleteAction: CdTableAction = {
109+
permission: 'delete',
110+
icon: Icons.destroy,
111+
click: () => this.deleteAction(),
112+
disable: () => !this.selection.hasSelection,
113+
name: this.actionLabels.DELETE,
114+
canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
115+
};
116+
this.tableActions = [createAction, editAction, deleteAction];
77117
this.notification$ = this.subject.pipe(
78118
switchMap(() =>
79119
this.rgwBucketService.listNotification(this.bucket.bucket).pipe(
@@ -89,4 +129,48 @@ export class RgwBucketNotificationListComponent extends ListWithDetails implemen
89129
fetchData() {
90130
this.subject.next([]);
91131
}
132+
133+
openNotificationModal(type: string) {
134+
const modalRef = this.modalService.show(RgwNotificationFormComponent, {
135+
bucket: this.bucket,
136+
selectedNotification: this.selection.first(),
137+
editing: type === this.actionLabels.EDIT ? true : false
138+
});
139+
modalRef?.close?.subscribe(() => this.updateBucketDetails.emit());
140+
}
141+
142+
updateSelection(selection: CdTableSelection) {
143+
this.selection = selection;
144+
}
145+
146+
deleteAction() {
147+
const selectedNotificationId = this.selection.selected.map((notification) => notification.Id);
148+
this.modalRef = this.modalService.show(DeleteConfirmationModalComponent, {
149+
itemDescription: $localize`Notification`,
150+
itemNames: selectedNotificationId,
151+
actionDescription: $localize`delete`,
152+
submitAction: () => this.submitDeleteNotifications(selectedNotificationId)
153+
});
154+
}
155+
156+
submitDeleteNotifications(notificationId: string[]) {
157+
this.rgwBucketService
158+
.deleteNotification(this.bucket.bucket, notificationId.join(','))
159+
.subscribe({
160+
next: () => {
161+
this.notificationService.show(
162+
NotificationType.success,
163+
$localize`Notifications deleted successfully.`
164+
);
165+
this.modalService.dismissAll();
166+
},
167+
error: () => {
168+
this.notificationService.show(
169+
NotificationType.success,
170+
$localize`Failed to delete notifications. Please try again.`
171+
);
172+
}
173+
});
174+
this.modalRef?.close?.subscribe(() => this.updateBucketDetails.emit());
175+
}
92176
}

0 commit comments

Comments
 (0)