Skip to content

Commit 8d7d132

Browse files
authored
Merge pull request ceph#58555 from rhcs-dashboard/rgw-multisite-sync-pipe
mgr/dashboard: RGW multisite sync pipe Reviewed-by: Pedro Gonzalez Gomez <[email protected]> Reviewed-by: Ankush Behl <[email protected]>
2 parents 5b6f1ee + d451b4d commit 8d7d132

18 files changed

+704
-50
lines changed

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -222,26 +222,27 @@ def remove_sync_flow(self, flow_id: str, flow_type: str, group_id: str,
222222
@EndpointDoc("Create or update the sync pipe")
223223
@CreatePermission
224224
def create_sync_pipe(self, group_id: str, pipe_id: str,
225+
source_bucket: str = '',
225226
source_zones: Optional[List[str]] = None,
226227
destination_zones: Optional[List[str]] = None,
227-
destination_buckets: Optional[List[str]] = None,
228+
destination_bucket: str = '',
228229
bucket_name: str = ''):
229230
multisite_instance = RgwMultisite()
230231
return multisite_instance.create_sync_pipe(group_id, pipe_id, source_zones,
231-
destination_zones, destination_buckets,
232-
bucket_name)
232+
destination_zones, source_bucket,
233+
destination_bucket, bucket_name)
233234

234235
@Endpoint(method='DELETE', path='/sync-pipe')
235236
@EndpointDoc("Remove the sync pipe")
236237
@DeletePermission
237238
def remove_sync_pipe(self, group_id: str, pipe_id: str,
238239
source_zones: Optional[List[str]] = None,
239240
destination_zones: Optional[List[str]] = None,
240-
destination_buckets: Optional[List[str]] = None,
241+
destination_bucket: str = '',
241242
bucket_name: str = ''):
242243
multisite_instance = RgwMultisite()
243244
return multisite_instance.remove_sync_pipe(group_id, pipe_id, source_zones,
244-
destination_zones, destination_buckets,
245+
destination_zones, destination_bucket,
245246
bucket_name)
246247

247248

src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -308,31 +308,31 @@ export abstract class PageHelper {
308308
}
309309

310310
getNestedTableCell(
311-
tableSelector: string,
311+
selector: string,
312312
columnIndex: number,
313313
exactContent: string,
314314
partialMatch = false
315315
) {
316316
this.waitDataTableToLoad();
317317
this.clearTableSearchInput();
318-
this.searchNestedTable(tableSelector, exactContent);
318+
this.searchNestedTable(selector, exactContent);
319319
if (partialMatch) {
320320
return cy
321-
.get(`${tableSelector} datatable-body-row datatable-body-cell:nth-child(${columnIndex})`)
321+
.get(`${selector} datatable-body-row datatable-body-cell:nth-child(${columnIndex})`)
322322
.should('contain', exactContent);
323323
}
324324
return cy
325-
.get(`${tableSelector}`)
325+
.get(`${selector}`)
326326
.contains(
327327
`datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
328328
new RegExp(`^${exactContent}$`)
329329
);
330330
}
331331

332-
searchNestedTable(tableSelector: string, text: string) {
332+
searchNestedTable(selector: string, text: string) {
333333
this.waitDataTableToLoad();
334334

335335
this.setPageSize('10');
336-
cy.get(`${tableSelector} [aria-label=search]`).first().clear({ force: true }).type(text);
336+
cy.get(`${selector} [aria-label=search]`).first().clear({ force: true }).type(text);
337337
}
338338
}

src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.e2e.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,23 @@ describe('Multisite page', () => {
7979
);
8080
});
8181
});
82+
83+
describe('create, edit, delete pipe', () => {
84+
beforeEach(() => {
85+
multisite.getTab('Sync Policy').click();
86+
multisite.getExpandCollapseElement().click();
87+
});
88+
89+
it('should create pipe', () => {
90+
multisite.createPipe('new-pipe', ['zone1-zg1-realm1'], ['zone3-zg2-realm1']);
91+
});
92+
93+
it('should modify pipe zones', () => {
94+
multisite.editPipe('new-pipe', 'zone2-zg1-realm1');
95+
});
96+
97+
it('should delete pipe', () => {
98+
multisite.deletePipe('new-pipe');
99+
});
100+
});
82101
});

src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.po.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,108 @@ export class MultisitePageHelper extends PageHelper {
185185
.find('.datatable-body-cell-label')
186186
.should('contain', dest_zones[0]);
187187
}
188+
189+
@PageHelper.restrictTo(pages.index.url)
190+
createPipe(pipe_id: string, source_zones: string[], dest_zones: string[]) {
191+
cy.get('cd-rgw-multisite-sync-policy-details').should('exist');
192+
this.getTab('Pipe').should('exist');
193+
this.getTab('Pipe').click();
194+
cy.request({
195+
method: 'GET',
196+
url: '/api/rgw/daemon',
197+
headers: { Accept: 'application/vnd.ceph.api.v1.0+json' }
198+
});
199+
cy.get('cd-rgw-multisite-sync-policy-details .table-actions button').first().click();
200+
cy.get('cd-rgw-multisite-sync-pipe-modal').should('exist');
201+
202+
// Enter in pipe_id
203+
cy.get('#pipe_id').type(pipe_id);
204+
cy.wait(WAIT_TIMER);
205+
// Select zone
206+
cy.get('a[data-testid=select-menu-edit]').eq(0).click();
207+
for (const zone of source_zones) {
208+
cy.get('.popover-body div.select-menu-item-content').contains(zone).click();
209+
}
210+
cy.get('cd-rgw-multisite-sync-pipe-modal').click();
211+
cy.get('a[data-testid=select-menu-edit]').eq(1).click();
212+
for (const zone of dest_zones) {
213+
cy.get('.popover-body input').type(`${zone}{enter}`);
214+
}
215+
cy.get('button.tc_submitButton').click();
216+
217+
cy.get('cd-rgw-multisite-sync-policy-details .datatable-body-cell-label').should(
218+
'contain',
219+
pipe_id
220+
);
221+
222+
cy.get('cd-rgw-multisite-sync-policy-details')
223+
.first()
224+
.find('[aria-label=search]')
225+
.first()
226+
.clear({ force: true })
227+
.type(pipe_id);
228+
}
229+
230+
@PageHelper.restrictTo(pages.index.url)
231+
editPipe(pipe_id: string, zoneToAdd: string) {
232+
cy.get('cd-rgw-multisite-sync-policy-details').should('exist');
233+
this.getTab('Pipe').should('exist');
234+
this.getTab('Pipe').click();
235+
cy.request({
236+
method: 'GET',
237+
url: '/api/rgw/daemon',
238+
headers: { Accept: 'application/vnd.ceph.api.v1.0+json' }
239+
});
240+
241+
cy.get('cd-rgw-multisite-sync-policy-details').within(() => {
242+
cy.get('.datatable-body-cell-label').should('contain', pipe_id);
243+
cy.get('[aria-label=search]').first().clear({ force: true }).type(pipe_id);
244+
cy.get('input.cd-datatable-checkbox').first().check();
245+
cy.get('.table-actions button').first().click();
246+
});
247+
cy.get('cd-rgw-multisite-sync-pipe-modal').should('exist');
248+
249+
cy.wait(WAIT_TIMER);
250+
// Enter in pipe_id
251+
cy.get('#pipe_id').should('contain.value', pipe_id);
252+
// Select zone
253+
cy.get('a[data-testid=select-menu-edit]').eq(1).click();
254+
255+
cy.get('.popover-body input').type(`${zoneToAdd}{enter}`);
256+
257+
cy.get('button.tc_submitButton').click();
258+
259+
this.getNestedTableCell('cd-rgw-multisite-sync-policy-details', 4, zoneToAdd, true);
260+
}
261+
262+
@PageHelper.restrictTo(pages.index.url)
263+
deletePipe(pipe_id: string) {
264+
cy.get('cd-rgw-multisite-sync-policy-details').should('exist');
265+
this.getTab('Pipe').should('exist');
266+
this.getTab('Pipe').click();
267+
cy.get('cd-rgw-multisite-sync-policy-details').within(() => {
268+
cy.get('.datatable-body-cell-label').should('contain', pipe_id);
269+
cy.get('[aria-label=search]').first().clear({ force: true }).type(pipe_id);
270+
});
271+
272+
const getRow = this.getTableCellWithContent.bind(this);
273+
getRow('cd-rgw-multisite-sync-policy-details', pipe_id).click();
274+
275+
cy.get('cd-rgw-multisite-sync-policy-details').within(() => {
276+
cy.get('.table-actions button.dropdown-toggle').first().click(); // open submenu
277+
cy.get(`button.delete`).first().click();
278+
});
279+
280+
cy.get('cd-modal .custom-control-label').click();
281+
cy.get('[aria-label="Delete Pipe"]').click();
282+
cy.get('cd-modal').should('not.exist');
283+
284+
cy.get('cd-rgw-multisite-sync-policy-details')
285+
.first()
286+
.within(() => {
287+
cy.get('[aria-label=search]').first().clear({ force: true }).type(pipe_id);
288+
});
289+
// Waits for item to be removed from table
290+
getRow(pipe_id).should('not.exist');
291+
}
188292
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@
3838
type="text"
3939
i18n-placeholder
4040
placeholder="Bucket Name..."
41-
formControlName="bucket_name"
42-
[readonly]="true"/>
41+
formControlName="bucket_name"/>
4342
<span
4443
class="invalid-feedback"
4544
*ngIf="currentFormGroupContext.showError('bucket_name', frm, 'bucketNameNotAllowed')"

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class RgwMultisiteSyncFlowModalComponent implements OnInit {
5858
this.createDirectionalFlowForm();
5959
this.currentFormGroupContext = _.cloneDeep(this.syncPolicyDirectionalFlowForm);
6060
}
61-
61+
this.currentFormGroupContext.get('bucket_name').disable();
6262
if (this.editing) {
6363
this.currentFormGroupContext.patchValue({
6464
flow_id: this.flowSelectedRow.id,
@@ -161,19 +161,22 @@ export class RgwMultisiteSyncFlowModalComponent implements OnInit {
161161
this.currentFormGroupContext.setErrors({ cdSubmitButton: true });
162162
return;
163163
}
164-
this.rgwMultisiteService.createEditSyncFlow(this.currentFormGroupContext.value).subscribe(
165-
() => {
166-
this.notificationService.show(
167-
NotificationType.success,
168-
$localize`Created Sync Flow '${this.currentFormGroupContext.getValue('flow_id')}'`
169-
);
170-
this.activeModal.close('success');
171-
},
172-
() => {
173-
// Reset the 'Submit' button.
174-
this.currentFormGroupContext.setErrors({ cdSubmitButton: true });
175-
this.activeModal.dismiss();
176-
}
177-
);
164+
this.rgwMultisiteService
165+
.createEditSyncFlow(this.currentFormGroupContext.getRawValue())
166+
.subscribe(
167+
() => {
168+
const action = this.editing ? 'Modified' : 'Created';
169+
this.notificationService.show(
170+
NotificationType.success,
171+
$localize`${action} Sync Flow '${this.currentFormGroupContext.getValue('flow_id')}'`
172+
);
173+
this.activeModal.close('success');
174+
},
175+
() => {
176+
// Reset the 'Submit' button.
177+
this.currentFormGroupContext.setErrors({ cdSubmitButton: true });
178+
this.activeModal.dismiss();
179+
}
180+
);
178181
}
179182
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<cd-modal [modalRef]="activeModal">
2+
<ng-container
3+
i18n="form title"
4+
class="modal-title">{{ action | titlecase }} Pipe</ng-container>
5+
6+
<ng-container class="modal-content">
7+
<form
8+
name="pipeForm"
9+
#frm="ngForm"
10+
[formGroup]="pipeForm"
11+
novalidate>
12+
<div class="modal-body">
13+
<div class="form-group row">
14+
<label
15+
class="cd-col-form-label required"
16+
for="pipe_id"
17+
i18n>Name</label>
18+
<div class="cd-col-form-input">
19+
<input
20+
class="form-control"
21+
type="text"
22+
placeholder="Pipe Name..."
23+
id="pipe_id"
24+
name="pipe_id"
25+
formControlName="pipe_id"
26+
[readonly]="editing"/>
27+
</div>
28+
</div>
29+
<div class="form-group row">
30+
<label
31+
class="cd-col-form-label required"
32+
for="source_zone"
33+
i18n>Source Zone </label>
34+
<div class="cd-col-form-input">
35+
<ng-container *ngTemplateOutlet="zoneMultiSelect;context: { name: 'source_zones', zone: sourceZones }"></ng-container>
36+
</div>
37+
</div>
38+
<div class="form-group row">
39+
<label
40+
class="cd-col-form-label required"
41+
for="destination_zone"
42+
i18n>Destination Zone</label>
43+
<div class="cd-col-form-input">
44+
<ng-container *ngTemplateOutlet="zoneMultiSelect;context: { name: 'destination_zones', zone: destZones }"></ng-container>
45+
</div>
46+
</div>
47+
<div class="form-group row">
48+
<label
49+
class="cd-col-form-label"
50+
for="bucket"
51+
i18n>Bucket Name</label>
52+
<div class="cd-col-form-input">
53+
<input
54+
id="bucket"
55+
name="bucket"
56+
class="form-control"
57+
type="text"
58+
i18n-placeholder
59+
placeholder="Bucket Name..."
60+
formControlName="bucket_name"/>
61+
</div>
62+
</div>
63+
<div class="form-group row">
64+
<label
65+
class="cd-col-form-label"
66+
for="source_bucket"
67+
i18n>Source Bucket</label>
68+
<div class="cd-col-form-input">
69+
<input
70+
id="source_bucket"
71+
name="source_bucket"
72+
class="form-control"
73+
type="text"
74+
i18n-placeholder
75+
placeholder="Source Bucket Name..."
76+
formControlName="source_bucket"/>
77+
</div>
78+
</div>
79+
<div class="form-group row">
80+
<label
81+
class="cd-col-form-label"
82+
for="dest_bucket"
83+
i18n>Destination Bucket</label>
84+
<div class="cd-col-form-input">
85+
<input
86+
id="dest_bucket"
87+
name="dest_bucket"
88+
class="form-control"
89+
type="text"
90+
i18n-placeholder
91+
placeholder="Destination Bucket Name..."
92+
formControlName="destination_bucket"/>
93+
</div>
94+
</div>
95+
</div>
96+
<div class="modal-footer">
97+
<cd-form-button-panel
98+
(submitActionEvent)="submit()"
99+
[form]="pipeForm"
100+
[submitText]="(action | titlecase) + ' ' + 'Pipe'">
101+
</cd-form-button-panel>
102+
</div>
103+
</form>
104+
</ng-container>
105+
</cd-modal>
106+
107+
<ng-template
108+
#zoneMultiSelect
109+
let-name="name"
110+
let-zone="zone">
111+
<cd-select-badges
112+
id="{{ name }}"
113+
name="{{ name }}"
114+
[customBadges]="zone.customBadges"
115+
[customBadgeValidators]="zone.data.validators"
116+
[messages]="zone.data.messages"
117+
[data]="zone.data.selected"
118+
[options]="zone.data.available"
119+
(selection)="onZoneSelection(name)">
120+
</cd-select-badges>
121+
<i
122+
*ngIf="zone.data.selected.length <= 0"
123+
i18n-title
124+
title="Pipe should be associated with {{ name }}"
125+
class="{{ icons.warning }} icon-warning-color">
126+
</i>
127+
<span
128+
class="invalid-feedback"
129+
*ngIf="pipeForm.showError(name, frm, 'required')"
130+
i18n>{{ name }} selection is required!
131+
</span>
132+
</ng-template>

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.scss

Whitespace-only changes.

0 commit comments

Comments
 (0)