]>
Commit | Line | Data |
---|---|---|
1 | import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; | |
2 | ||
3 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; | |
4 | import _ from 'lodash'; | |
5 | import { Observable, Subscriber } from 'rxjs'; | |
6 | ||
7 | import { RbdService } from '~/app/shared/api/rbd.service'; | |
8 | import { ListWithDetails } from '~/app/shared/classes/list-with-details.class'; | |
9 | import { TableStatus } from '~/app/shared/classes/table-status'; | |
10 | import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component'; | |
11 | import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; | |
12 | import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; | |
13 | import { TableComponent } from '~/app/shared/datatable/table/table.component'; | |
14 | import { Icons } from '~/app/shared/enum/icons.enum'; | |
15 | import { CdTableAction } from '~/app/shared/models/cd-table-action'; | |
16 | import { CdTableColumn } from '~/app/shared/models/cd-table-column'; | |
17 | import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; | |
18 | import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; | |
19 | import { FinishedTask } from '~/app/shared/models/finished-task'; | |
20 | import { ImageSpec } from '~/app/shared/models/image-spec'; | |
21 | import { Permission } from '~/app/shared/models/permissions'; | |
22 | import { Task } from '~/app/shared/models/task'; | |
23 | import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; | |
24 | import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe'; | |
25 | import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; | |
26 | import { CdTableServerSideService } from '~/app/shared/services/cd-table-server-side.service'; | |
27 | import { ModalService } from '~/app/shared/services/modal.service'; | |
28 | import { TaskListService } from '~/app/shared/services/task-list.service'; | |
29 | import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; | |
30 | import { URLBuilderService } from '~/app/shared/services/url-builder.service'; | |
31 | import { RbdFormEditRequestModel } from '../rbd-form/rbd-form-edit-request.model'; | |
32 | import { RbdParentModel } from '../rbd-form/rbd-parent.model'; | |
33 | import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-move-modal.component'; | |
34 | import { RBDImageFormat, RbdModel } from './rbd-model'; | |
35 | ||
36 | const BASE_URL = 'block/rbd'; | |
37 | ||
38 | @Component({ | |
39 | selector: 'cd-rbd-list', | |
40 | templateUrl: './rbd-list.component.html', | |
41 | styleUrls: ['./rbd-list.component.scss'], | |
42 | providers: [ | |
43 | TaskListService, | |
44 | { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) } | |
45 | ] | |
46 | }) | |
47 | export class RbdListComponent extends ListWithDetails implements OnInit { | |
48 | @ViewChild(TableComponent, { static: true }) | |
49 | table: TableComponent; | |
50 | @ViewChild('usageTpl') | |
51 | usageTpl: TemplateRef<any>; | |
52 | @ViewChild('parentTpl', { static: true }) | |
53 | parentTpl: TemplateRef<any>; | |
54 | @ViewChild('nameTpl') | |
55 | nameTpl: TemplateRef<any>; | |
56 | @ViewChild('scheduleTpl', { static: true }) | |
57 | scheduleTpl: TemplateRef<any>; | |
58 | @ViewChild('mirroringTpl', { static: true }) | |
59 | mirroringTpl: TemplateRef<any>; | |
60 | @ViewChild('flattenTpl', { static: true }) | |
61 | flattenTpl: TemplateRef<any>; | |
62 | @ViewChild('deleteTpl', { static: true }) | |
63 | deleteTpl: TemplateRef<any>; | |
64 | @ViewChild('removingStatTpl', { static: true }) | |
65 | removingStatTpl: TemplateRef<any>; | |
66 | @ViewChild('provisionedNotAvailableTooltipTpl', { static: true }) | |
67 | provisionedNotAvailableTooltipTpl: TemplateRef<any>; | |
68 | @ViewChild('totalProvisionedNotAvailableTooltipTpl', { static: true }) | |
69 | totalProvisionedNotAvailableTooltipTpl: TemplateRef<any>; | |
70 | ||
71 | permission: Permission; | |
72 | tableActions: CdTableAction[]; | |
73 | images: any; | |
74 | columns: CdTableColumn[]; | |
75 | retries: number; | |
76 | tableStatus = new TableStatus('light'); | |
77 | selection = new CdTableSelection(); | |
78 | icons = Icons; | |
79 | count = 0; | |
80 | private tableContext: CdTableFetchDataContext = null; | |
81 | modalRef: NgbModalRef; | |
82 | ||
83 | builders = { | |
84 | 'rbd/create': (metadata: object) => | |
85 | this.createRbdFromTask(metadata['pool_name'], metadata['namespace'], metadata['image_name']), | |
86 | 'rbd/delete': (metadata: object) => this.createRbdFromTaskImageSpec(metadata['image_spec']), | |
87 | 'rbd/clone': (metadata: object) => | |
88 | this.createRbdFromTask( | |
89 | metadata['child_pool_name'], | |
90 | metadata['child_namespace'], | |
91 | metadata['child_image_name'] | |
92 | ), | |
93 | 'rbd/copy': (metadata: object) => | |
94 | this.createRbdFromTask( | |
95 | metadata['dest_pool_name'], | |
96 | metadata['dest_namespace'], | |
97 | metadata['dest_image_name'] | |
98 | ) | |
99 | }; | |
100 | remove_scheduling: boolean; | |
101 | ||
102 | private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel { | |
103 | const imageSpec = ImageSpec.fromString(imageSpecStr); | |
104 | return this.createRbdFromTask(imageSpec.poolName, imageSpec.namespace, imageSpec.imageName); | |
105 | } | |
106 | ||
107 | private createRbdFromTask(pool: string, namespace: string, name: string): RbdModel { | |
108 | const model = new RbdModel(); | |
109 | model.id = '-1'; | |
110 | model.unique_id = '-1'; | |
111 | model.name = name; | |
112 | model.namespace = namespace; | |
113 | model.pool_name = pool; | |
114 | model.image_format = RBDImageFormat.V2; | |
115 | return model; | |
116 | } | |
117 | ||
118 | constructor( | |
119 | private authStorageService: AuthStorageService, | |
120 | private rbdService: RbdService, | |
121 | private dimlessBinaryPipe: DimlessBinaryPipe, | |
122 | private dimlessPipe: DimlessPipe, | |
123 | private modalService: ModalService, | |
124 | private taskWrapper: TaskWrapperService, | |
125 | public taskListService: TaskListService, | |
126 | private urlBuilder: URLBuilderService, | |
127 | public actionLabels: ActionLabelsI18n | |
128 | ) { | |
129 | super(); | |
130 | this.permission = this.authStorageService.getPermissions().rbdImage; | |
131 | const getImageUri = () => | |
132 | this.selection.first() && | |
133 | new ImageSpec( | |
134 | this.selection.first().pool_name, | |
135 | this.selection.first().namespace, | |
136 | this.selection.first().name | |
137 | ).toStringEncoded(); | |
138 | const addAction: CdTableAction = { | |
139 | permission: 'create', | |
140 | icon: Icons.add, | |
141 | routerLink: () => this.urlBuilder.getCreate(), | |
142 | canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection, | |
143 | name: this.actionLabels.CREATE | |
144 | }; | |
145 | const editAction: CdTableAction = { | |
146 | permission: 'update', | |
147 | icon: Icons.edit, | |
148 | routerLink: () => this.urlBuilder.getEdit(getImageUri()), | |
149 | name: this.actionLabels.EDIT, | |
150 | disable: (selection: CdTableSelection) => | |
151 | this.getRemovingStatusDesc(selection) || this.getInvalidNameDisable(selection) | |
152 | }; | |
153 | const deleteAction: CdTableAction = { | |
154 | permission: 'delete', | |
155 | icon: Icons.destroy, | |
156 | click: () => this.deleteRbdModal(), | |
157 | name: this.actionLabels.DELETE, | |
158 | disable: (selection: CdTableSelection) => this.getDeleteDisableDesc(selection) | |
159 | }; | |
160 | const resyncAction: CdTableAction = { | |
161 | permission: 'update', | |
162 | icon: Icons.refresh, | |
163 | click: () => this.resyncRbdModal(), | |
164 | name: this.actionLabels.RESYNC, | |
165 | disable: (selection: CdTableSelection) => this.getResyncDisableDesc(selection) | |
166 | }; | |
167 | const copyAction: CdTableAction = { | |
168 | permission: 'create', | |
169 | canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection, | |
170 | disable: (selection: CdTableSelection) => | |
171 | this.getRemovingStatusDesc(selection) || | |
172 | this.getInvalidNameDisable(selection) || | |
173 | !!selection.first().cdExecuting, | |
174 | icon: Icons.copy, | |
175 | routerLink: () => `/block/rbd/copy/${getImageUri()}`, | |
176 | name: this.actionLabels.COPY | |
177 | }; | |
178 | const flattenAction: CdTableAction = { | |
179 | permission: 'update', | |
180 | disable: (selection: CdTableSelection) => | |
181 | this.getRemovingStatusDesc(selection) || | |
182 | this.getInvalidNameDisable(selection) || | |
183 | selection.first().cdExecuting || | |
184 | !selection.first().parent, | |
185 | icon: Icons.flatten, | |
186 | click: () => this.flattenRbdModal(), | |
187 | name: this.actionLabels.FLATTEN | |
188 | }; | |
189 | const moveAction: CdTableAction = { | |
190 | permission: 'delete', | |
191 | icon: Icons.trash, | |
192 | click: () => this.trashRbdModal(), | |
193 | name: this.actionLabels.TRASH, | |
194 | disable: (selection: CdTableSelection) => | |
195 | this.getRemovingStatusDesc(selection) || | |
196 | this.getInvalidNameDisable(selection) || | |
197 | selection.first().image_format === RBDImageFormat.V1 | |
198 | }; | |
199 | const removeSchedulingAction: CdTableAction = { | |
200 | permission: 'update', | |
201 | icon: Icons.edit, | |
202 | click: () => this.removeSchedulingModal(), | |
203 | name: this.actionLabels.REMOVE_SCHEDULING, | |
204 | disable: (selection: CdTableSelection) => | |
205 | this.getRemovingStatusDesc(selection) || | |
206 | this.getInvalidNameDisable(selection) || | |
207 | selection.first().schedule_info === undefined | |
208 | }; | |
209 | const promoteAction: CdTableAction = { | |
210 | permission: 'update', | |
211 | icon: Icons.edit, | |
212 | click: () => this.actionPrimary(true), | |
213 | name: this.actionLabels.PROMOTE, | |
214 | visible: () => this.selection.first() != null && !this.selection.first().primary | |
215 | }; | |
216 | const demoteAction: CdTableAction = { | |
217 | permission: 'update', | |
218 | icon: Icons.edit, | |
219 | click: () => this.actionPrimary(false), | |
220 | name: this.actionLabels.DEMOTE, | |
221 | visible: () => this.selection.first() != null && this.selection.first().primary | |
222 | }; | |
223 | this.tableActions = [ | |
224 | addAction, | |
225 | editAction, | |
226 | copyAction, | |
227 | flattenAction, | |
228 | resyncAction, | |
229 | deleteAction, | |
230 | moveAction, | |
231 | removeSchedulingAction, | |
232 | promoteAction, | |
233 | demoteAction | |
234 | ]; | |
235 | } | |
236 | ||
237 | ngOnInit() { | |
238 | this.columns = [ | |
239 | { | |
240 | name: $localize`Name`, | |
241 | prop: 'name', | |
242 | flexGrow: 2, | |
243 | cellTemplate: this.removingStatTpl | |
244 | }, | |
245 | { | |
246 | name: $localize`Pool`, | |
247 | prop: 'pool_name', | |
248 | flexGrow: 2 | |
249 | }, | |
250 | { | |
251 | name: $localize`Namespace`, | |
252 | prop: 'namespace', | |
253 | flexGrow: 2 | |
254 | }, | |
255 | { | |
256 | name: $localize`Size`, | |
257 | prop: 'size', | |
258 | flexGrow: 1, | |
259 | cellClass: 'text-right', | |
260 | sortable: false, | |
261 | pipe: this.dimlessBinaryPipe | |
262 | }, | |
263 | { | |
264 | name: $localize`Objects`, | |
265 | prop: 'num_objs', | |
266 | flexGrow: 1, | |
267 | cellClass: 'text-right', | |
268 | sortable: false, | |
269 | pipe: this.dimlessPipe | |
270 | }, | |
271 | { | |
272 | name: $localize`Object size`, | |
273 | prop: 'obj_size', | |
274 | flexGrow: 1, | |
275 | cellClass: 'text-right', | |
276 | sortable: false, | |
277 | pipe: this.dimlessBinaryPipe | |
278 | }, | |
279 | { | |
280 | name: $localize`Provisioned`, | |
281 | prop: 'disk_usage', | |
282 | cellClass: 'text-center', | |
283 | flexGrow: 1, | |
284 | pipe: this.dimlessBinaryPipe, | |
285 | sortable: false, | |
286 | cellTemplate: this.provisionedNotAvailableTooltipTpl | |
287 | }, | |
288 | { | |
289 | name: $localize`Total provisioned`, | |
290 | prop: 'total_disk_usage', | |
291 | cellClass: 'text-center', | |
292 | flexGrow: 1, | |
293 | pipe: this.dimlessBinaryPipe, | |
294 | sortable: false, | |
295 | cellTemplate: this.totalProvisionedNotAvailableTooltipTpl | |
296 | }, | |
297 | { | |
298 | name: $localize`Parent`, | |
299 | prop: 'parent', | |
300 | flexGrow: 2, | |
301 | sortable: false, | |
302 | cellTemplate: this.parentTpl | |
303 | }, | |
304 | { | |
305 | name: $localize`Mirroring`, | |
306 | prop: 'mirror_mode', | |
307 | flexGrow: 3, | |
308 | sortable: false, | |
309 | cellTemplate: this.mirroringTpl | |
310 | }, | |
311 | { | |
312 | name: $localize`Next Scheduled Snapshot`, | |
313 | prop: 'mirror_mode', | |
314 | flexGrow: 3, | |
315 | sortable: false, | |
316 | cellTemplate: this.scheduleTpl | |
317 | } | |
318 | ]; | |
319 | ||
320 | const itemFilter = (entry: Record<string, any>, task: Task) => { | |
321 | let taskImageSpec: string; | |
322 | switch (task.name) { | |
323 | case 'rbd/copy': | |
324 | taskImageSpec = new ImageSpec( | |
325 | task.metadata['dest_pool_name'], | |
326 | task.metadata['dest_namespace'], | |
327 | task.metadata['dest_image_name'] | |
328 | ).toString(); | |
329 | break; | |
330 | case 'rbd/clone': | |
331 | taskImageSpec = new ImageSpec( | |
332 | task.metadata['child_pool_name'], | |
333 | task.metadata['child_namespace'], | |
334 | task.metadata['child_image_name'] | |
335 | ).toString(); | |
336 | break; | |
337 | case 'rbd/create': | |
338 | taskImageSpec = new ImageSpec( | |
339 | task.metadata['pool_name'], | |
340 | task.metadata['namespace'], | |
341 | task.metadata['image_name'] | |
342 | ).toString(); | |
343 | break; | |
344 | default: | |
345 | taskImageSpec = task.metadata['image_spec']; | |
346 | break; | |
347 | } | |
348 | return ( | |
349 | taskImageSpec === new ImageSpec(entry.pool_name, entry.namespace, entry.name).toString() | |
350 | ); | |
351 | }; | |
352 | ||
353 | const taskFilter = (task: Task) => { | |
354 | return [ | |
355 | 'rbd/clone', | |
356 | 'rbd/copy', | |
357 | 'rbd/create', | |
358 | 'rbd/delete', | |
359 | 'rbd/edit', | |
360 | 'rbd/flatten', | |
361 | 'rbd/trash/move' | |
362 | ].includes(task.name); | |
363 | }; | |
364 | ||
365 | this.taskListService.init( | |
366 | (context) => this.getRbdImages(context), | |
367 | (resp) => this.prepareResponse(resp), | |
368 | (images) => (this.images = images), | |
369 | () => this.onFetchError(), | |
370 | taskFilter, | |
371 | itemFilter, | |
372 | this.builders | |
373 | ); | |
374 | } | |
375 | ||
376 | onFetchError() { | |
377 | this.table.reset(); // Disable loading indicator. | |
378 | this.tableStatus = new TableStatus('danger'); | |
379 | } | |
380 | ||
381 | getRbdImages(context: CdTableFetchDataContext) { | |
382 | if (context !== null) { | |
383 | this.tableContext = context; | |
384 | } | |
385 | if (this.tableContext == null) { | |
386 | this.tableContext = new CdTableFetchDataContext(() => undefined); | |
387 | } | |
388 | return this.rbdService.list(this.tableContext?.toParams()); | |
389 | } | |
390 | ||
391 | prepareResponse(resp: any[]): any[] { | |
392 | let images: any[] = []; | |
393 | ||
394 | resp.forEach((pool) => { | |
395 | images = images.concat(pool.value); | |
396 | }); | |
397 | ||
398 | images.forEach((image) => { | |
399 | if (image.schedule_info !== undefined) { | |
400 | let scheduling: any[] = []; | |
401 | const scheduleStatus = 'scheduled'; | |
402 | let nextSnapshotDate = +new Date(image.schedule_info.schedule_time); | |
403 | const offset = new Date().getTimezoneOffset(); | |
404 | nextSnapshotDate = nextSnapshotDate + Math.abs(offset) * 60000; | |
405 | scheduling.push(image.mirror_mode, scheduleStatus, nextSnapshotDate); | |
406 | image.mirror_mode = scheduling; | |
407 | scheduling = []; | |
408 | } | |
409 | }); | |
410 | ||
411 | if (images.length > 0) { | |
412 | this.count = CdTableServerSideService.getCount(resp[0]); | |
413 | } else { | |
414 | this.count = 0; | |
415 | } | |
416 | return images; | |
417 | } | |
418 | ||
419 | updateSelection(selection: CdTableSelection) { | |
420 | this.selection = selection; | |
421 | } | |
422 | ||
423 | deleteRbdModal() { | |
424 | const poolName = this.selection.first().pool_name; | |
425 | const namespace = this.selection.first().namespace; | |
426 | const imageName = this.selection.first().name; | |
427 | const imageSpec = new ImageSpec(poolName, namespace, imageName); | |
428 | ||
429 | this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { | |
430 | itemDescription: 'RBD', | |
431 | itemNames: [imageSpec], | |
432 | bodyTemplate: this.deleteTpl, | |
433 | bodyContext: { | |
434 | hasSnapshots: this.hasSnapshots(), | |
435 | snapshots: this.listProtectedSnapshots() | |
436 | }, | |
437 | submitActionObservable: () => | |
438 | this.taskWrapper.wrapTaskAroundCall({ | |
439 | task: new FinishedTask('rbd/delete', { | |
440 | image_spec: imageSpec.toString() | |
441 | }), | |
442 | call: this.rbdService.delete(imageSpec) | |
443 | }) | |
444 | }); | |
445 | } | |
446 | ||
447 | resyncRbdModal() { | |
448 | const poolName = this.selection.first().pool_name; | |
449 | const namespace = this.selection.first().namespace; | |
450 | const imageName = this.selection.first().name; | |
451 | const imageSpec = new ImageSpec(poolName, namespace, imageName); | |
452 | ||
453 | this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { | |
454 | itemDescription: 'RBD', | |
455 | itemNames: [imageSpec], | |
456 | actionDescription: 'resync', | |
457 | submitActionObservable: () => | |
458 | this.taskWrapper.wrapTaskAroundCall({ | |
459 | task: new FinishedTask('rbd/edit', { | |
460 | image_spec: imageSpec.toString() | |
461 | }), | |
462 | call: this.rbdService.update(imageSpec, { resync: true }) | |
463 | }) | |
464 | }); | |
465 | } | |
466 | ||
467 | trashRbdModal() { | |
468 | const initialState = { | |
469 | poolName: this.selection.first().pool_name, | |
470 | namespace: this.selection.first().namespace, | |
471 | imageName: this.selection.first().name, | |
472 | hasSnapshots: this.hasSnapshots() | |
473 | }; | |
474 | this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, initialState); | |
475 | } | |
476 | ||
477 | flattenRbd(imageSpec: ImageSpec) { | |
478 | this.taskWrapper | |
479 | .wrapTaskAroundCall({ | |
480 | task: new FinishedTask('rbd/flatten', { | |
481 | image_spec: imageSpec.toString() | |
482 | }), | |
483 | call: this.rbdService.flatten(imageSpec) | |
484 | }) | |
485 | .subscribe({ | |
486 | complete: () => { | |
487 | this.modalRef.close(); | |
488 | } | |
489 | }); | |
490 | } | |
491 | ||
492 | flattenRbdModal() { | |
493 | const poolName = this.selection.first().pool_name; | |
494 | const namespace = this.selection.first().namespace; | |
495 | const imageName = this.selection.first().name; | |
496 | const parent: RbdParentModel = this.selection.first().parent; | |
497 | const parentImageSpec = new ImageSpec( | |
498 | parent.pool_name, | |
499 | parent.pool_namespace, | |
500 | parent.image_name | |
501 | ); | |
502 | const childImageSpec = new ImageSpec(poolName, namespace, imageName); | |
503 | ||
504 | const initialState = { | |
505 | titleText: 'RBD flatten', | |
506 | buttonText: 'Flatten', | |
507 | bodyTpl: this.flattenTpl, | |
508 | bodyData: { | |
509 | parent: `${parentImageSpec}@${parent.snap_name}`, | |
510 | child: childImageSpec.toString() | |
511 | }, | |
512 | onSubmit: () => { | |
513 | this.flattenRbd(childImageSpec); | |
514 | } | |
515 | }; | |
516 | ||
517 | this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState); | |
518 | } | |
519 | ||
520 | editRequest() { | |
521 | const request = new RbdFormEditRequestModel(); | |
522 | request.remove_scheduling = !request.remove_scheduling; | |
523 | return request; | |
524 | } | |
525 | ||
526 | removeSchedulingModal() { | |
527 | const imageName = this.selection.first().name; | |
528 | ||
529 | const imageSpec = new ImageSpec( | |
530 | this.selection.first().pool_name, | |
531 | this.selection.first().namespace, | |
532 | this.selection.first().name | |
533 | ); | |
534 | ||
535 | this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { | |
536 | actionDescription: 'remove scheduling on', | |
537 | itemDescription: $localize`image`, | |
538 | itemNames: [`${imageName}`], | |
539 | submitActionObservable: () => | |
540 | new Observable((observer: Subscriber<any>) => { | |
541 | this.taskWrapper | |
542 | .wrapTaskAroundCall({ | |
543 | task: new FinishedTask('rbd/edit', { | |
544 | image_spec: imageSpec.toString() | |
545 | }), | |
546 | call: this.rbdService.update(imageSpec, this.editRequest()) | |
547 | }) | |
548 | .subscribe({ | |
549 | error: (resp) => observer.error(resp), | |
550 | complete: () => { | |
551 | this.modalRef.close(); | |
552 | } | |
553 | }); | |
554 | }) | |
555 | }); | |
556 | } | |
557 | ||
558 | actionPrimary(primary: boolean) { | |
559 | const request = new RbdFormEditRequestModel(); | |
560 | request.primary = primary; | |
561 | request.features = null; | |
562 | const imageSpec = new ImageSpec( | |
563 | this.selection.first().pool_name, | |
564 | this.selection.first().namespace, | |
565 | this.selection.first().name | |
566 | ); | |
567 | this.taskWrapper | |
568 | .wrapTaskAroundCall({ | |
569 | task: new FinishedTask('rbd/edit', { | |
570 | image_spec: imageSpec.toString() | |
571 | }), | |
572 | call: this.rbdService.update(imageSpec, request) | |
573 | }) | |
574 | .subscribe(); | |
575 | } | |
576 | ||
577 | hasSnapshots() { | |
578 | const snapshots = this.selection.first()['snapshots'] || []; | |
579 | return snapshots.length > 0; | |
580 | } | |
581 | ||
582 | hasClonedSnapshots(image: object) { | |
583 | const snapshots = image['snapshots'] || []; | |
584 | return snapshots.some((snap: object) => snap['children'] && snap['children'].length > 0); | |
585 | } | |
586 | ||
587 | listProtectedSnapshots() { | |
588 | const first = this.selection.first(); | |
589 | const snapshots = first['snapshots']; | |
590 | return snapshots.reduce((accumulator: string[], snap: object) => { | |
591 | if (snap['is_protected']) { | |
592 | accumulator.push(snap['name']); | |
593 | } | |
594 | return accumulator; | |
595 | }, []); | |
596 | } | |
597 | ||
598 | getDeleteDisableDesc(selection: CdTableSelection): string | boolean { | |
599 | const first = selection.first(); | |
600 | ||
601 | if (first && this.hasClonedSnapshots(first)) { | |
602 | return $localize`This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.`; | |
603 | } | |
604 | ||
605 | return this.getInvalidNameDisable(selection) || this.hasClonedSnapshots(selection.first()); | |
606 | } | |
607 | ||
608 | getResyncDisableDesc(selection: CdTableSelection): string | boolean { | |
609 | const first = selection.first(); | |
610 | ||
611 | if (first && this.imageIsPrimary(first)) { | |
612 | return $localize`Primary RBD images cannot be resynced`; | |
613 | } | |
614 | ||
615 | return this.getInvalidNameDisable(selection); | |
616 | } | |
617 | ||
618 | imageIsPrimary(image: object) { | |
619 | return image['primary']; | |
620 | } | |
621 | getInvalidNameDisable(selection: CdTableSelection): string | boolean { | |
622 | const first = selection.first(); | |
623 | ||
624 | if (first?.name?.match(/[@/]/)) { | |
625 | return $localize`This RBD image has an invalid name and can't be managed by ceph.`; | |
626 | } | |
627 | ||
628 | return !selection.first() || !selection.hasSingleSelection; | |
629 | } | |
630 | ||
631 | getRemovingStatusDesc(selection: CdTableSelection): string | boolean { | |
632 | const first = selection.first(); | |
633 | if (first?.source === 'REMOVING') { | |
634 | return $localize`Action not possible for an RBD in status 'Removing'`; | |
635 | } | |
636 | return false; | |
637 | } | |
638 | } |