]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts
df66b0e8842ae01dd96e61d657e635cbbc8626c4
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / rbd-snapshot-list / rbd-snapshot-list.component.ts
1 import {
2 ChangeDetectionStrategy,
3 ChangeDetectorRef,
4 Component,
5 Input,
6 OnChanges,
7 OnInit,
8 TemplateRef,
9 ViewChild
10 } from '@angular/core';
11
12 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
13 import moment from 'moment';
14 import { of } from 'rxjs';
15
16 import { RbdService } from '~/app/shared/api/rbd.service';
17 import { CdHelperClass } from '~/app/shared/classes/cd-helper.class';
18 import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
19 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
20 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
21 import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
22 import { CdTableAction } from '~/app/shared/models/cd-table-action';
23 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
24 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
25 import { ExecutingTask } from '~/app/shared/models/executing-task';
26 import { FinishedTask } from '~/app/shared/models/finished-task';
27 import { ImageSpec } from '~/app/shared/models/image-spec';
28 import { Permission } from '~/app/shared/models/permissions';
29 import { Task } from '~/app/shared/models/task';
30 import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
31 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
32 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
33 import { ModalService } from '~/app/shared/services/modal.service';
34 import { NotificationService } from '~/app/shared/services/notification.service';
35 import { SummaryService } from '~/app/shared/services/summary.service';
36 import { TaskListService } from '~/app/shared/services/task-list.service';
37 import { TaskManagerService } from '~/app/shared/services/task-manager.service';
38 import { RbdSnapshotFormModalComponent } from '../rbd-snapshot-form/rbd-snapshot-form-modal.component';
39 import { RbdSnapshotActionsModel } from './rbd-snapshot-actions.model';
40 import { RbdSnapshotModel } from './rbd-snapshot.model';
41
42 @Component({
43 selector: 'cd-rbd-snapshot-list',
44 templateUrl: './rbd-snapshot-list.component.html',
45 styleUrls: ['./rbd-snapshot-list.component.scss'],
46 providers: [TaskListService],
47 changeDetection: ChangeDetectionStrategy.OnPush
48 })
49 export class RbdSnapshotListComponent implements OnInit, OnChanges {
50 @Input()
51 snapshots: RbdSnapshotModel[] = [];
52 @Input()
53 featuresName: string[];
54 @Input()
55 poolName: string;
56 @Input()
57 namespace: string;
58 @Input()
59 mirroring: string;
60 @Input()
61 rbdName: string;
62 @ViewChild('nameTpl')
63 nameTpl: TemplateRef<any>;
64 @ViewChild('rollbackTpl', { static: true })
65 rollbackTpl: TemplateRef<any>;
66
67 permission: Permission;
68 selection = new CdTableSelection();
69 tableActions: CdTableAction[];
70 rbdTableActions: RbdSnapshotActionsModel;
71 imageSpec: ImageSpec;
72
73 data: RbdSnapshotModel[];
74
75 columns: CdTableColumn[];
76
77 modalRef: NgbModalRef;
78
79 builders = {
80 'rbd/snap/create': (metadata: any) => {
81 const model = new RbdSnapshotModel();
82 model.name = metadata['snapshot_name'];
83 return model;
84 }
85 };
86
87 constructor(
88 private authStorageService: AuthStorageService,
89 private modalService: ModalService,
90 private dimlessBinaryPipe: DimlessBinaryPipe,
91 private cdDatePipe: CdDatePipe,
92 private rbdService: RbdService,
93 private taskManagerService: TaskManagerService,
94 private notificationService: NotificationService,
95 private summaryService: SummaryService,
96 private taskListService: TaskListService,
97 private actionLabels: ActionLabelsI18n,
98 private cdr: ChangeDetectorRef
99 ) {
100 this.permission = this.authStorageService.getPermissions().rbdImage;
101 }
102
103 ngOnInit() {
104 this.columns = [
105 {
106 name: $localize`Name`,
107 prop: 'name',
108 cellTransformation: CellTemplate.executing,
109 flexGrow: 2
110 },
111 {
112 name: $localize`Size`,
113 prop: 'size',
114 flexGrow: 1,
115 cellClass: 'text-right',
116 pipe: this.dimlessBinaryPipe
117 },
118 {
119 name: $localize`Provisioned`,
120 prop: 'disk_usage',
121 flexGrow: 1,
122 cellClass: 'text-right',
123 pipe: this.dimlessBinaryPipe
124 },
125 {
126 name: $localize`State`,
127 prop: 'is_protected',
128 flexGrow: 1,
129 cellTransformation: CellTemplate.badge,
130 customTemplateConfig: {
131 map: {
132 true: { value: $localize`PROTECTED`, class: 'badge-success' },
133 false: { value: $localize`UNPROTECTED`, class: 'badge-info' }
134 }
135 }
136 },
137 {
138 name: $localize`Created`,
139 prop: 'timestamp',
140 flexGrow: 1,
141 pipe: this.cdDatePipe
142 }
143 ];
144
145 this.imageSpec = new ImageSpec(this.poolName, this.namespace, this.rbdName);
146 this.rbdTableActions = new RbdSnapshotActionsModel(
147 this.actionLabels,
148 this.featuresName,
149 this.rbdService
150 );
151 this.rbdTableActions.create.click = () => this.openCreateSnapshotModal();
152 this.rbdTableActions.rename.click = () => this.openEditSnapshotModal();
153 this.rbdTableActions.protect.click = () => this.toggleProtection();
154 this.rbdTableActions.unprotect.click = () => this.toggleProtection();
155 const getImageUri = () =>
156 this.selection.first() &&
157 `${this.imageSpec.toStringEncoded()}/${encodeURIComponent(this.selection.first().name)}`;
158 this.rbdTableActions.clone.routerLink = () => `/block/rbd/clone/${getImageUri()}`;
159 this.rbdTableActions.copy.routerLink = () => `/block/rbd/copy/${getImageUri()}`;
160 this.rbdTableActions.rollback.click = () => this.rollbackModal();
161 this.rbdTableActions.deleteSnap.click = () => this.deleteSnapshotModal();
162
163 this.tableActions = this.rbdTableActions.ordering;
164
165 const itemFilter = (entry: any, task: Task) => {
166 return entry.name === task.metadata['snapshot_name'];
167 };
168
169 const taskFilter = (task: Task) => {
170 return (
171 ['rbd/snap/create', 'rbd/snap/delete', 'rbd/snap/edit', 'rbd/snap/rollback'].includes(
172 task.name
173 ) && this.imageSpec.toString() === task.metadata['image_spec']
174 );
175 };
176
177 this.taskListService.init(
178 () => of(this.snapshots),
179 null,
180 (items) => {
181 const hasChanges = CdHelperClass.updateChanged(this, {
182 data: items
183 });
184 if (hasChanges) {
185 this.cdr.detectChanges();
186 this.data = [...this.data];
187 }
188 },
189 () => {
190 const hasChanges = CdHelperClass.updateChanged(this, {
191 data: this.snapshots
192 });
193 if (hasChanges) {
194 this.cdr.detectChanges();
195 this.data = [...this.data];
196 }
197 },
198 taskFilter,
199 itemFilter,
200 this.builders
201 );
202 }
203
204 ngOnChanges() {
205 if (this.columns) {
206 this.imageSpec = new ImageSpec(this.poolName, this.namespace, this.rbdName);
207 if (this.rbdTableActions) {
208 this.rbdTableActions.featuresName = this.featuresName;
209 }
210 this.taskListService.fetch();
211 }
212 }
213
214 private openSnapshotModal(taskName: string, snapName: string = null) {
215 const modalVariables = {
216 mirroring: this.mirroring
217 };
218 this.modalRef = this.modalService.show(RbdSnapshotFormModalComponent, modalVariables);
219 this.modalRef.componentInstance.poolName = this.poolName;
220 this.modalRef.componentInstance.imageName = this.rbdName;
221 this.modalRef.componentInstance.namespace = this.namespace;
222 if (snapName) {
223 this.modalRef.componentInstance.setEditing();
224 } else {
225 // Auto-create a name for the snapshot: <image_name>_<timestamp_ISO_8601>
226 // https://en.wikipedia.org/wiki/ISO_8601
227 snapName = `${this.rbdName}_${moment().toISOString(true)}`;
228 }
229 this.modalRef.componentInstance.setSnapName(snapName);
230 this.modalRef.componentInstance.onSubmit.subscribe((snapshotName: string) => {
231 const executingTask = new ExecutingTask();
232 executingTask.name = taskName;
233 executingTask.metadata = {
234 image_spec: this.imageSpec.toString(),
235 snapshot_name: snapshotName
236 };
237 this.summaryService.addRunningTask(executingTask);
238 });
239 }
240
241 openCreateSnapshotModal() {
242 this.openSnapshotModal('rbd/snap/create');
243 }
244
245 openEditSnapshotModal() {
246 this.openSnapshotModal('rbd/snap/edit', this.selection.first().name);
247 }
248
249 toggleProtection() {
250 const snapshotName = this.selection.first().name;
251 const isProtected = this.selection.first().is_protected;
252 const finishedTask = new FinishedTask();
253 finishedTask.name = 'rbd/snap/edit';
254 const imageSpec = new ImageSpec(this.poolName, this.namespace, this.rbdName);
255 finishedTask.metadata = {
256 image_spec: imageSpec.toString(),
257 snapshot_name: snapshotName
258 };
259 this.rbdService
260 .protectSnapshot(imageSpec, snapshotName, !isProtected)
261 .toPromise()
262 .then(() => {
263 const executingTask = new ExecutingTask();
264 executingTask.name = finishedTask.name;
265 executingTask.metadata = finishedTask.metadata;
266 this.summaryService.addRunningTask(executingTask);
267 this.taskManagerService.subscribe(
268 finishedTask.name,
269 finishedTask.metadata,
270 (asyncFinishedTask: FinishedTask) => {
271 this.notificationService.notifyTask(asyncFinishedTask);
272 }
273 );
274 });
275 }
276
277 _asyncTask(task: string, taskName: string, snapshotName: string) {
278 const finishedTask = new FinishedTask();
279 finishedTask.name = taskName;
280 finishedTask.metadata = {
281 image_spec: new ImageSpec(this.poolName, this.namespace, this.rbdName).toString(),
282 snapshot_name: snapshotName
283 };
284 const imageSpec = new ImageSpec(this.poolName, this.namespace, this.rbdName);
285 this.rbdService[task](imageSpec, snapshotName)
286 .toPromise()
287 .then(() => {
288 const executingTask = new ExecutingTask();
289 executingTask.name = finishedTask.name;
290 executingTask.metadata = finishedTask.metadata;
291 this.summaryService.addRunningTask(executingTask);
292 this.modalRef.close();
293 this.taskManagerService.subscribe(
294 executingTask.name,
295 executingTask.metadata,
296 (asyncFinishedTask: FinishedTask) => {
297 this.notificationService.notifyTask(asyncFinishedTask);
298 }
299 );
300 })
301 .catch(() => {
302 this.modalRef.componentInstance.stopLoadingSpinner();
303 });
304 }
305
306 rollbackModal() {
307 const snapshotName = this.selection.selected[0].name;
308 const imageSpec = new ImageSpec(this.poolName, this.namespace, this.rbdName).toString();
309 const initialState = {
310 titleText: $localize`RBD snapshot rollback`,
311 buttonText: $localize`Rollback`,
312 bodyTpl: this.rollbackTpl,
313 bodyData: {
314 snapName: `${imageSpec}@${snapshotName}`
315 },
316 onSubmit: () => {
317 this._asyncTask('rollbackSnapshot', 'rbd/snap/rollback', snapshotName);
318 }
319 };
320
321 this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState);
322 }
323
324 deleteSnapshotModal() {
325 const snapshotName = this.selection.selected[0].name;
326 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
327 itemDescription: $localize`RBD snapshot`,
328 itemNames: [snapshotName],
329 submitAction: () => this._asyncTask('deleteSnapshot', 'rbd/snap/delete', snapshotName)
330 });
331 }
332
333 updateSelection(selection: CdTableSelection) {
334 this.selection = selection;
335 }
336 }