]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; |
2 | ||
f67539c2 TL |
3 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; |
4 | import _ from 'lodash'; | |
5 | ||
6 | import { RbdService } from '~/app/shared/api/rbd.service'; | |
7 | import { ListWithDetails } from '~/app/shared/classes/list-with-details.class'; | |
8 | import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache'; | |
9 | import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component'; | |
10 | import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; | |
11 | import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; | |
12 | import { TableComponent } from '~/app/shared/datatable/table/table.component'; | |
f67539c2 TL |
13 | import { Icons } from '~/app/shared/enum/icons.enum'; |
14 | import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum'; | |
15 | import { CdTableAction } from '~/app/shared/models/cd-table-action'; | |
16 | import { CdTableColumn } from '~/app/shared/models/cd-table-column'; | |
17 | import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; | |
18 | import { FinishedTask } from '~/app/shared/models/finished-task'; | |
19 | import { ImageSpec } from '~/app/shared/models/image-spec'; | |
20 | import { Permission } from '~/app/shared/models/permissions'; | |
21 | import { Task } from '~/app/shared/models/task'; | |
22 | import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; | |
23 | import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe'; | |
24 | import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; | |
25 | import { ModalService } from '~/app/shared/services/modal.service'; | |
26 | import { TaskListService } from '~/app/shared/services/task-list.service'; | |
27 | import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; | |
28 | import { URLBuilderService } from '~/app/shared/services/url-builder.service'; | |
11fdf7f2 TL |
29 | import { RbdParentModel } from '../rbd-form/rbd-parent.model'; |
30 | import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-move-modal.component'; | |
f6b5b4d7 | 31 | import { RBDImageFormat, RbdModel } from './rbd-model'; |
11fdf7f2 TL |
32 | |
33 | const BASE_URL = 'block/rbd'; | |
34 | ||
35 | @Component({ | |
36 | selector: 'cd-rbd-list', | |
37 | templateUrl: './rbd-list.component.html', | |
38 | styleUrls: ['./rbd-list.component.scss'], | |
39 | providers: [ | |
40 | TaskListService, | |
41 | { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) } | |
42 | ] | |
43 | }) | |
e306af50 | 44 | export class RbdListComponent extends ListWithDetails implements OnInit { |
9f95a23c | 45 | @ViewChild(TableComponent, { static: true }) |
11fdf7f2 | 46 | table: TableComponent; |
f67539c2 | 47 | @ViewChild('usageTpl') |
11fdf7f2 | 48 | usageTpl: TemplateRef<any>; |
9f95a23c | 49 | @ViewChild('parentTpl', { static: true }) |
11fdf7f2 | 50 | parentTpl: TemplateRef<any>; |
f67539c2 | 51 | @ViewChild('nameTpl') |
11fdf7f2 | 52 | nameTpl: TemplateRef<any>; |
9f95a23c | 53 | @ViewChild('flattenTpl', { static: true }) |
11fdf7f2 | 54 | flattenTpl: TemplateRef<any>; |
9f95a23c TL |
55 | @ViewChild('deleteTpl', { static: true }) |
56 | deleteTpl: TemplateRef<any>; | |
b3b6e05e TL |
57 | @ViewChild('removingStatTpl', { static: true }) |
58 | removingStatTpl: TemplateRef<any>; | |
11fdf7f2 TL |
59 | |
60 | permission: Permission; | |
61 | tableActions: CdTableAction[]; | |
62 | images: any; | |
63 | columns: CdTableColumn[]; | |
64 | retries: number; | |
f67539c2 | 65 | tableStatus = new TableStatusViewCache(); |
11fdf7f2 | 66 | selection = new CdTableSelection(); |
b3b6e05e | 67 | icons = Icons; |
11fdf7f2 | 68 | |
f67539c2 | 69 | modalRef: NgbModalRef; |
11fdf7f2 TL |
70 | |
71 | builders = { | |
9f95a23c TL |
72 | 'rbd/create': (metadata: object) => |
73 | this.createRbdFromTask(metadata['pool_name'], metadata['namespace'], metadata['image_name']), | |
74 | 'rbd/delete': (metadata: object) => this.createRbdFromTaskImageSpec(metadata['image_spec']), | |
75 | 'rbd/clone': (metadata: object) => | |
76 | this.createRbdFromTask( | |
77 | metadata['child_pool_name'], | |
78 | metadata['child_namespace'], | |
79 | metadata['child_image_name'] | |
80 | ), | |
81 | 'rbd/copy': (metadata: object) => | |
82 | this.createRbdFromTask( | |
83 | metadata['dest_pool_name'], | |
84 | metadata['dest_namespace'], | |
85 | metadata['dest_image_name'] | |
86 | ) | |
11fdf7f2 TL |
87 | }; |
88 | ||
9f95a23c TL |
89 | private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel { |
90 | const imageSpec = ImageSpec.fromString(imageSpecStr); | |
91 | return this.createRbdFromTask(imageSpec.poolName, imageSpec.namespace, imageSpec.imageName); | |
92 | } | |
93 | ||
94 | private createRbdFromTask(pool: string, namespace: string, name: string): RbdModel { | |
11fdf7f2 TL |
95 | const model = new RbdModel(); |
96 | model.id = '-1'; | |
f6b5b4d7 | 97 | model.unique_id = '-1'; |
11fdf7f2 | 98 | model.name = name; |
9f95a23c | 99 | model.namespace = namespace; |
11fdf7f2 | 100 | model.pool_name = pool; |
f6b5b4d7 | 101 | model.image_format = RBDImageFormat.V2; |
11fdf7f2 TL |
102 | return model; |
103 | } | |
104 | ||
105 | constructor( | |
106 | private authStorageService: AuthStorageService, | |
107 | private rbdService: RbdService, | |
108 | private dimlessBinaryPipe: DimlessBinaryPipe, | |
109 | private dimlessPipe: DimlessPipe, | |
f67539c2 | 110 | private modalService: ModalService, |
11fdf7f2 | 111 | private taskWrapper: TaskWrapperService, |
f67539c2 | 112 | public taskListService: TaskListService, |
11fdf7f2 TL |
113 | private urlBuilder: URLBuilderService, |
114 | public actionLabels: ActionLabelsI18n | |
115 | ) { | |
e306af50 | 116 | super(); |
11fdf7f2 TL |
117 | this.permission = this.authStorageService.getPermissions().rbdImage; |
118 | const getImageUri = () => | |
119 | this.selection.first() && | |
9f95a23c TL |
120 | new ImageSpec( |
121 | this.selection.first().pool_name, | |
122 | this.selection.first().namespace, | |
11fdf7f2 | 123 | this.selection.first().name |
9f95a23c | 124 | ).toStringEncoded(); |
11fdf7f2 TL |
125 | const addAction: CdTableAction = { |
126 | permission: 'create', | |
9f95a23c | 127 | icon: Icons.add, |
11fdf7f2 TL |
128 | routerLink: () => this.urlBuilder.getCreate(), |
129 | canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection, | |
130 | name: this.actionLabels.CREATE | |
131 | }; | |
132 | const editAction: CdTableAction = { | |
133 | permission: 'update', | |
9f95a23c | 134 | icon: Icons.edit, |
11fdf7f2 | 135 | routerLink: () => this.urlBuilder.getEdit(getImageUri()), |
f67539c2 | 136 | name: this.actionLabels.EDIT, |
b3b6e05e TL |
137 | disable: (selection: CdTableSelection) => |
138 | this.getRemovingStatusDesc(selection) || this.getInvalidNameDisable(selection) | |
11fdf7f2 TL |
139 | }; |
140 | const deleteAction: CdTableAction = { | |
141 | permission: 'delete', | |
9f95a23c | 142 | icon: Icons.destroy, |
11fdf7f2 | 143 | click: () => this.deleteRbdModal(), |
9f95a23c | 144 | name: this.actionLabels.DELETE, |
f91f0fd5 | 145 | disable: (selection: CdTableSelection) => this.getDeleteDisableDesc(selection) |
11fdf7f2 TL |
146 | }; |
147 | const copyAction: CdTableAction = { | |
148 | permission: 'create', | |
149 | canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection, | |
150 | disable: (selection: CdTableSelection) => | |
b3b6e05e TL |
151 | this.getRemovingStatusDesc(selection) || |
152 | this.getInvalidNameDisable(selection) || | |
153 | !!selection.first().cdExecuting, | |
9f95a23c | 154 | icon: Icons.copy, |
11fdf7f2 | 155 | routerLink: () => `/block/rbd/copy/${getImageUri()}`, |
eafe8130 | 156 | name: this.actionLabels.COPY |
11fdf7f2 TL |
157 | }; |
158 | const flattenAction: CdTableAction = { | |
159 | permission: 'update', | |
160 | disable: (selection: CdTableSelection) => | |
b3b6e05e | 161 | this.getRemovingStatusDesc(selection) || |
f67539c2 TL |
162 | this.getInvalidNameDisable(selection) || |
163 | selection.first().cdExecuting || | |
164 | !selection.first().parent, | |
9f95a23c | 165 | icon: Icons.flatten, |
11fdf7f2 | 166 | click: () => this.flattenRbdModal(), |
eafe8130 | 167 | name: this.actionLabels.FLATTEN |
11fdf7f2 TL |
168 | }; |
169 | const moveAction: CdTableAction = { | |
170 | permission: 'delete', | |
9f95a23c | 171 | icon: Icons.trash, |
11fdf7f2 | 172 | click: () => this.trashRbdModal(), |
f6b5b4d7 TL |
173 | name: this.actionLabels.TRASH, |
174 | disable: (selection: CdTableSelection) => | |
b3b6e05e | 175 | this.getRemovingStatusDesc(selection) || |
f67539c2 | 176 | this.getInvalidNameDisable(selection) || |
f6b5b4d7 | 177 | selection.first().image_format === RBDImageFormat.V1 |
11fdf7f2 TL |
178 | }; |
179 | this.tableActions = [ | |
180 | addAction, | |
181 | editAction, | |
182 | copyAction, | |
183 | flattenAction, | |
184 | deleteAction, | |
185 | moveAction | |
186 | ]; | |
187 | } | |
188 | ||
189 | ngOnInit() { | |
190 | this.columns = [ | |
191 | { | |
f67539c2 | 192 | name: $localize`Name`, |
11fdf7f2 TL |
193 | prop: 'name', |
194 | flexGrow: 2, | |
b3b6e05e | 195 | cellTemplate: this.removingStatTpl |
11fdf7f2 TL |
196 | }, |
197 | { | |
f67539c2 | 198 | name: $localize`Pool`, |
11fdf7f2 TL |
199 | prop: 'pool_name', |
200 | flexGrow: 2 | |
201 | }, | |
9f95a23c | 202 | { |
f67539c2 | 203 | name: $localize`Namespace`, |
9f95a23c TL |
204 | prop: 'namespace', |
205 | flexGrow: 2 | |
206 | }, | |
11fdf7f2 | 207 | { |
f67539c2 | 208 | name: $localize`Size`, |
11fdf7f2 TL |
209 | prop: 'size', |
210 | flexGrow: 1, | |
211 | cellClass: 'text-right', | |
212 | pipe: this.dimlessBinaryPipe | |
213 | }, | |
214 | { | |
f67539c2 | 215 | name: $localize`Objects`, |
11fdf7f2 TL |
216 | prop: 'num_objs', |
217 | flexGrow: 1, | |
218 | cellClass: 'text-right', | |
219 | pipe: this.dimlessPipe | |
220 | }, | |
221 | { | |
f67539c2 | 222 | name: $localize`Object size`, |
11fdf7f2 TL |
223 | prop: 'obj_size', |
224 | flexGrow: 1, | |
225 | cellClass: 'text-right', | |
226 | pipe: this.dimlessBinaryPipe | |
227 | }, | |
228 | { | |
f67539c2 | 229 | name: $localize`Provisioned`, |
11fdf7f2 TL |
230 | prop: 'disk_usage', |
231 | cellClass: 'text-center', | |
232 | flexGrow: 1, | |
233 | pipe: this.dimlessBinaryPipe | |
234 | }, | |
235 | { | |
f67539c2 | 236 | name: $localize`Total provisioned`, |
11fdf7f2 TL |
237 | prop: 'total_disk_usage', |
238 | cellClass: 'text-center', | |
239 | flexGrow: 1, | |
240 | pipe: this.dimlessBinaryPipe | |
241 | }, | |
242 | { | |
f67539c2 | 243 | name: $localize`Parent`, |
11fdf7f2 TL |
244 | prop: 'parent', |
245 | flexGrow: 2, | |
246 | cellTemplate: this.parentTpl | |
247 | } | |
248 | ]; | |
249 | ||
9f95a23c TL |
250 | const itemFilter = (entry: Record<string, any>, task: Task) => { |
251 | let taskImageSpec: string; | |
252 | switch (task.name) { | |
253 | case 'rbd/copy': | |
254 | taskImageSpec = new ImageSpec( | |
255 | task.metadata['dest_pool_name'], | |
256 | task.metadata['dest_namespace'], | |
257 | task.metadata['dest_image_name'] | |
258 | ).toString(); | |
259 | break; | |
260 | case 'rbd/clone': | |
261 | taskImageSpec = new ImageSpec( | |
262 | task.metadata['child_pool_name'], | |
263 | task.metadata['child_namespace'], | |
264 | task.metadata['child_image_name'] | |
265 | ).toString(); | |
266 | break; | |
267 | case 'rbd/create': | |
268 | taskImageSpec = new ImageSpec( | |
269 | task.metadata['pool_name'], | |
270 | task.metadata['namespace'], | |
271 | task.metadata['image_name'] | |
272 | ).toString(); | |
273 | break; | |
274 | default: | |
275 | taskImageSpec = task.metadata['image_spec']; | |
276 | break; | |
277 | } | |
278 | return ( | |
279 | taskImageSpec === new ImageSpec(entry.pool_name, entry.namespace, entry.name).toString() | |
280 | ); | |
281 | }; | |
282 | ||
283 | const taskFilter = (task: Task) => { | |
284 | return [ | |
285 | 'rbd/clone', | |
286 | 'rbd/copy', | |
287 | 'rbd/create', | |
288 | 'rbd/delete', | |
289 | 'rbd/edit', | |
290 | 'rbd/flatten', | |
291 | 'rbd/trash/move' | |
292 | ].includes(task.name); | |
293 | }; | |
294 | ||
11fdf7f2 TL |
295 | this.taskListService.init( |
296 | () => this.rbdService.list(), | |
297 | (resp) => this.prepareResponse(resp), | |
298 | (images) => (this.images = images), | |
299 | () => this.onFetchError(), | |
9f95a23c TL |
300 | taskFilter, |
301 | itemFilter, | |
11fdf7f2 TL |
302 | this.builders |
303 | ); | |
304 | } | |
305 | ||
306 | onFetchError() { | |
307 | this.table.reset(); // Disable loading indicator. | |
f67539c2 | 308 | this.tableStatus = new TableStatusViewCache(ViewCacheStatus.ValueException); |
11fdf7f2 TL |
309 | } |
310 | ||
311 | prepareResponse(resp: any[]): any[] { | |
9f95a23c | 312 | let images: any[] = []; |
11fdf7f2 | 313 | const viewCacheStatusMap = {}; |
f67539c2 | 314 | |
11fdf7f2 TL |
315 | resp.forEach((pool) => { |
316 | if (_.isUndefined(viewCacheStatusMap[pool.status])) { | |
317 | viewCacheStatusMap[pool.status] = []; | |
318 | } | |
319 | viewCacheStatusMap[pool.status].push(pool.pool_name); | |
320 | images = images.concat(pool.value); | |
321 | }); | |
f67539c2 TL |
322 | |
323 | let status: number; | |
324 | if (viewCacheStatusMap[ViewCacheStatus.ValueException]) { | |
325 | status = ViewCacheStatus.ValueException; | |
326 | } else if (viewCacheStatusMap[ViewCacheStatus.ValueStale]) { | |
327 | status = ViewCacheStatus.ValueStale; | |
328 | } else if (viewCacheStatusMap[ViewCacheStatus.ValueNone]) { | |
329 | status = ViewCacheStatus.ValueNone; | |
330 | } | |
331 | ||
332 | if (status) { | |
333 | const statusFor = | |
334 | (viewCacheStatusMap[status].length > 1 ? 'pools ' : 'pool ') + | |
335 | viewCacheStatusMap[status].join(); | |
336 | ||
337 | this.tableStatus = new TableStatusViewCache(status, statusFor); | |
338 | } else { | |
339 | this.tableStatus = new TableStatusViewCache(); | |
340 | } | |
341 | ||
11fdf7f2 TL |
342 | return images; |
343 | } | |
344 | ||
11fdf7f2 TL |
345 | updateSelection(selection: CdTableSelection) { |
346 | this.selection = selection; | |
347 | } | |
348 | ||
349 | deleteRbdModal() { | |
350 | const poolName = this.selection.first().pool_name; | |
9f95a23c | 351 | const namespace = this.selection.first().namespace; |
11fdf7f2 | 352 | const imageName = this.selection.first().name; |
9f95a23c | 353 | const imageSpec = new ImageSpec(poolName, namespace, imageName); |
11fdf7f2 TL |
354 | |
355 | this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { | |
f67539c2 TL |
356 | itemDescription: 'RBD', |
357 | itemNames: [imageSpec], | |
358 | bodyTemplate: this.deleteTpl, | |
359 | bodyContext: { | |
360 | hasSnapshots: this.hasSnapshots(), | |
361 | snapshots: this.listProtectedSnapshots() | |
362 | }, | |
363 | submitActionObservable: () => | |
364 | this.taskWrapper.wrapTaskAroundCall({ | |
365 | task: new FinishedTask('rbd/delete', { | |
366 | image_spec: imageSpec.toString() | |
367 | }), | |
368 | call: this.rbdService.delete(imageSpec) | |
369 | }) | |
11fdf7f2 TL |
370 | }); |
371 | } | |
372 | ||
373 | trashRbdModal() { | |
374 | const initialState = { | |
11fdf7f2 | 375 | poolName: this.selection.first().pool_name, |
9f95a23c TL |
376 | namespace: this.selection.first().namespace, |
377 | imageName: this.selection.first().name, | |
378 | hasSnapshots: this.hasSnapshots() | |
11fdf7f2 | 379 | }; |
f67539c2 | 380 | this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, initialState); |
11fdf7f2 TL |
381 | } |
382 | ||
9f95a23c | 383 | flattenRbd(imageSpec: ImageSpec) { |
11fdf7f2 TL |
384 | this.taskWrapper |
385 | .wrapTaskAroundCall({ | |
386 | task: new FinishedTask('rbd/flatten', { | |
9f95a23c | 387 | image_spec: imageSpec.toString() |
11fdf7f2 | 388 | }), |
9f95a23c | 389 | call: this.rbdService.flatten(imageSpec) |
11fdf7f2 | 390 | }) |
f67539c2 TL |
391 | .subscribe({ |
392 | complete: () => { | |
393 | this.modalRef.close(); | |
394 | } | |
11fdf7f2 TL |
395 | }); |
396 | } | |
397 | ||
398 | flattenRbdModal() { | |
399 | const poolName = this.selection.first().pool_name; | |
9f95a23c | 400 | const namespace = this.selection.first().namespace; |
11fdf7f2 TL |
401 | const imageName = this.selection.first().name; |
402 | const parent: RbdParentModel = this.selection.first().parent; | |
9f95a23c TL |
403 | const parentImageSpec = new ImageSpec( |
404 | parent.pool_name, | |
405 | parent.pool_namespace, | |
406 | parent.image_name | |
407 | ); | |
408 | const childImageSpec = new ImageSpec(poolName, namespace, imageName); | |
11fdf7f2 TL |
409 | |
410 | const initialState = { | |
411 | titleText: 'RBD flatten', | |
412 | buttonText: 'Flatten', | |
413 | bodyTpl: this.flattenTpl, | |
414 | bodyData: { | |
9f95a23c TL |
415 | parent: `${parentImageSpec}@${parent.snap_name}`, |
416 | child: childImageSpec.toString() | |
11fdf7f2 TL |
417 | }, |
418 | onSubmit: () => { | |
9f95a23c | 419 | this.flattenRbd(childImageSpec); |
11fdf7f2 TL |
420 | } |
421 | }; | |
422 | ||
f67539c2 | 423 | this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState); |
11fdf7f2 | 424 | } |
9f95a23c TL |
425 | |
426 | hasSnapshots() { | |
427 | const snapshots = this.selection.first()['snapshots'] || []; | |
428 | return snapshots.length > 0; | |
429 | } | |
430 | ||
431 | hasClonedSnapshots(image: object) { | |
432 | const snapshots = image['snapshots'] || []; | |
433 | return snapshots.some((snap: object) => snap['children'] && snap['children'].length > 0); | |
434 | } | |
435 | ||
436 | listProtectedSnapshots() { | |
437 | const first = this.selection.first(); | |
438 | const snapshots = first['snapshots']; | |
439 | return snapshots.reduce((accumulator: string[], snap: object) => { | |
440 | if (snap['is_protected']) { | |
441 | accumulator.push(snap['name']); | |
442 | } | |
443 | return accumulator; | |
444 | }, []); | |
445 | } | |
446 | ||
f91f0fd5 TL |
447 | getDeleteDisableDesc(selection: CdTableSelection): string | boolean { |
448 | const first = selection.first(); | |
449 | ||
9f95a23c | 450 | if (first && this.hasClonedSnapshots(first)) { |
f67539c2 | 451 | return $localize`This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.`; |
9f95a23c TL |
452 | } |
453 | ||
f67539c2 TL |
454 | return this.getInvalidNameDisable(selection) || this.hasClonedSnapshots(selection.first()); |
455 | } | |
456 | ||
457 | getInvalidNameDisable(selection: CdTableSelection): string | boolean { | |
458 | const first = selection.first(); | |
459 | ||
460 | if (first?.name?.match(/[@/]/)) { | |
461 | return $localize`This RBD image has an invalid name and can't be managed by ceph.`; | |
462 | } | |
463 | ||
464 | return !selection.first() || !selection.hasSingleSelection; | |
9f95a23c | 465 | } |
b3b6e05e TL |
466 | |
467 | getRemovingStatusDesc(selection: CdTableSelection): string | boolean { | |
468 | const first = selection.first(); | |
469 | if (first?.source === 'REMOVING') { | |
470 | return $localize`Action not possible for an RBD in status 'Removing'`; | |
471 | } | |
472 | return false; | |
473 | } | |
11fdf7f2 | 474 | } |