Merge "RIFT-15944 - editing an ‘id’ field is troublesome as focus is lost on each...
[osm/UI.git] / skyquake / plugins / composer / src / src / stores / CatalogDataStore.js
1
2 /*
3 *
4 * Copyright 2016 RIFT.IO Inc
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19 'use strict';
20
21 import _pick from 'lodash/pick'
22 import _isEqual from 'lodash/isEqual'
23 import _cloneDeep from 'lodash/cloneDeep'
24 import cc from 'change-case'
25 import alt from '../alt'
26 import UID from '../libraries/UniqueId'
27 import guid from '../libraries/guid'
28 import React from 'react'
29 import DescriptorModel from '../libraries/model/DescriptorModel'
30 import DescriptorModelMetaFactory from '../libraries/model/DescriptorModelMetaFactory'
31 import CatalogPackageManagerActions from '../actions/CatalogPackageManagerActions'
32 import CatalogDataSourceActions from '../actions/CatalogDataSourceActions'
33 import CatalogItemsActions from '../actions/CatalogItemsActions'
34 import ModalOverlayActions from '../actions/ModalOverlayActions'
35 import ComposerAppActions from '../actions/ComposerAppActions'
36 import CatalogDataSource from '../sources/CatalogDataSource'
37 import ComposerAppStore from '../stores/ComposerAppStore'
38 import SelectionManager from '../libraries/SelectionManager'
39
40 const defaults = {
41 catalogs: [],
42 catalogItemExportFormats: ['mano', 'rift'],
43 catalogItemExportGrammars: ['osm', 'tosca']
44 };
45
46 const areCatalogItemsMetaDataEqual = function (a, b) {
47 const metaProps = ['id', 'name', 'short-name', 'description', 'vendor', 'version'];
48 const aMetaData = _pick(a, metaProps);
49 const bMetaData = _pick(b, metaProps);
50 return _isEqual(aMetaData, bMetaData);
51 };
52
53 function createItem (type) {
54 let newItem = DescriptorModelMetaFactory.createModelInstanceForType(type);
55 if (newItem){
56 newItem.id = guid();
57 UID.assignUniqueId(newItem.uiState);
58 newItem.uiState.isNew = true;
59 newItem.uiState.modified = true;
60 newItem.uiState['instance-ref-count'] = 0;
61 }
62 return newItem;
63 }
64
65 class CatalogDataStore {
66
67 constructor() {
68 this.catalogs = defaults.catalogs;
69 this.isLoading = true;
70 this.requiresSave = false;
71 this.snapshots = {};
72 this.selectedFormat = defaults.catalogItemExportFormats[0];
73 this.selectedGrammar = defaults.catalogItemExportGrammars[0];
74 this.registerAsync(CatalogDataSource);
75 this.bindActions(CatalogDataSourceActions);
76 this.bindActions(CatalogItemsActions);
77 this.exportPublicMethods({
78 getCatalogs: this.getCatalogs,
79 getCatalogItemById: this.getCatalogItemById,
80 getCatalogItemByUid: this.getCatalogItemByUid,
81 getTransientCatalogs: this.getTransientCatalogs,
82 getTransientCatalogItemById: this.getTransientCatalogItemById,
83 getTransientCatalogItemByUid: this.getTransientCatalogItemByUid
84 });
85 }
86
87 resetSelectionState = () => {
88 this.selectedFormat = defaults.catalogItemExportFormats[0];
89 this.selectedGrammar = defaults.catalogItemExportGrammars[0];
90 }
91
92 getCatalogs() {
93 return this.catalogs || (this.catalogs = []);
94 }
95
96 getTransientCatalogs() {
97 return this.state.catalogs || (this.state.catalogs = []);
98 }
99
100 getAllSelectedCatalogItems() {
101 return this.getCatalogs().reduce((r, d) => {
102 d.descriptors.forEach(d => {
103 if (SelectionManager.isSelected(d) /*d.uiState.selected*/) {
104 r.push(d);
105 }
106 });
107 return r;
108 }, []);
109 }
110
111 getFirstSelectedCatalogItem() {
112 return this.getCatalogs().reduce((r, catalog) => {
113 return r.concat(catalog.descriptors.filter(d => SelectionManager.isSelected(d) /*d.uiState.selected*/));
114 }, [])[0];
115 }
116
117 getCatalogItemById(id) {
118 return this.getCatalogs().reduce((r, catalog) => {
119 return r.concat(catalog.descriptors.filter(d => d.id === id));
120 }, [])[0];
121 }
122
123 getTransientCatalogItemById(id) {
124 return this.getTransientCatalogs().reduce((r, catalog) => {
125 return r.concat(catalog.descriptors.filter(d => d.id === id));
126 }, [])[0];
127 }
128
129 getCatalogItemByUid(uid) {
130 return this.getCatalogs().reduce((r, catalog) => {
131 return r.concat(catalog.descriptors.filter(d => UID.from(d) === uid));
132 }, [])[0];
133 }
134
135 getTransientCatalogItemByUid(uid) {
136 return this.getTransientCatalogs().reduce((r, catalog) => {
137 return r.concat(catalog.descriptors.filter(d => UID.from(d) === uid));
138 }, [])[0];
139 }
140
141 removeCatalogItem(deleteItem = {}) {
142 this.getCatalogs().map(catalog => {
143 catalog.descriptors = catalog.descriptors.filter(d => d.id !== deleteItem.id);
144 return catalog;
145 });
146 }
147
148 addNewItemToCatalog(newItem) {
149 const type = newItem.uiState.type;
150 this.getCatalogs().filter(d => d.type === type).forEach(catalog => {
151 catalog.descriptors.push(newItem);
152 });
153 // update indexes and integrate new model into catalog
154 this.updateCatalogIndexes(this.getCatalogs());
155 return this.getCatalogItemById(newItem.id);
156 }
157
158 updateCatalogIndexes(catalogs) {
159 // associate catalog identity with individual descriptors so we can
160 // update the catalog when any given descriptor is updated also add
161 // vnfd model to the nsd object to make it easier to render an nsd
162 const vnfdLookup = {};
163 const updatedCatalogs = catalogs.map(catalog => {
164 catalog.descriptors.map(descriptor => {
165 if (typeof descriptor.meta === 'string' && descriptor.meta.trim() !== '') {
166 try {
167 descriptor.uiState = JSON.parse(descriptor.meta);
168 } catch (ignore) {
169 console.warn('Unable to deserialize the uiState property.');
170 }
171 } else if (typeof descriptor.meta === 'object') {
172 descriptor.uiState = descriptor.meta;
173 descriptor.meta = JSON.stringify(descriptor.meta);
174 }
175
176 const uiState = descriptor.uiState || (descriptor.uiState = {});
177 uiState.catalogId = catalog.id;
178 uiState.catalogName = catalog.name;
179 uiState.type = catalog.type;
180 if (!UID.hasUniqueId(uiState)) {
181 UID.assignUniqueId(uiState);
182 }
183 if (catalog.type === 'vnfd') {
184 vnfdLookup[descriptor.id] = descriptor;
185 }
186 return descriptor;
187 });
188 return catalog;
189 });
190 updatedCatalogs.filter(d => d.type === 'nsd').forEach(catalog => {
191 catalog.descriptors = catalog.descriptors.map(descriptor => {
192 const instanceRefCount = parseInt(descriptor.uiState['instance-ref-count'], 10);
193 if (descriptor['constituent-vnfd']) {
194 descriptor.vnfd = descriptor['constituent-vnfd'].map(d => {
195 const vnfdId = d['vnfd-id-ref'];
196 const vnfd = vnfdLookup[vnfdId];
197 if (!vnfd) {
198 throw new ReferenceError('no VNFD found in the VNFD Catalog for the constituent-vnfd: ' + d);
199 }
200 if (!isNaN(instanceRefCount) && instanceRefCount > 0) {
201 // this will notify user that this item cannot be updated when/if they make a change to it
202 vnfd.uiState['instance-ref-count'] = instanceRefCount;
203 }
204 // create an instance of this vnfd to carry transient ui state properties
205 const instance = _cloneDeep(vnfd);
206 instance.uiState['member-vnf-index'] = d['member-vnf-index'];
207 instance['vnf-configuration'] = d['vnf-configuration'];
208 instance['start-by-default'] = d['start-by-default'];
209 return instance;
210 });
211 }
212 return descriptor;
213 });
214 });
215 return updatedCatalogs;
216 }
217
218 updateCatalogItem(item) {
219 // replace the given item in the catalog
220 const catalogs = this.getCatalogs().map(catalog => {
221 if (catalog.id === item.uiState.catalogId) {
222 catalog.descriptors = catalog.descriptors.map(d => {
223 if (d.id === item.id) {
224 return item;
225 }
226 return d;
227 });
228 }
229 return catalog;
230 });
231 this.setState({catalogs: catalogs});
232 }
233
234 mergeEditsIntoLatestFromServer(catalogsFromServer = []) {
235
236 // if the UI has modified items use them instead of the server items
237
238 const currentData = this.getCatalogs();
239
240 const modifiedItemsMap = currentData.reduce((result, catalog) => {
241 return result.concat(catalog.descriptors.filter(d => d.uiState.modified));
242 }, []).reduce((r, d) => {
243 r[d.uiState.catalogId + '/' + d.id] = d;
244 return r;
245 }, {});
246
247 const itemMetaMap = currentData.reduce((result, catalog) => {
248 return result.concat(catalog.descriptors.filter(d => d.uiState));
249 }, []).reduce((r, d) => {
250 r[d.uiState.catalogId + '/' + d.id] = d.uiState;
251 return r;
252 }, {});
253
254 const newItemsMap = currentData.reduce((result, catalog) => {
255 result[catalog.id] = catalog.descriptors.filter(d => d.uiState.isNew);
256 return result;
257 }, {});
258
259 catalogsFromServer.forEach(catalog => {
260 catalog.descriptors = catalog.descriptors.map(d => {
261 const key = d.uiState.catalogId + '/' + d.id;
262 if (modifiedItemsMap[key]) {
263 // use local modified item instead of the server item
264 return modifiedItemsMap[key];
265 }
266 if (itemMetaMap[key]) {
267 Object.assign(d.uiState, itemMetaMap[key]);
268 }
269 return d;
270 });
271 if (newItemsMap[catalog.id]) {
272 catalog.descriptors = catalog.descriptors.concat(newItemsMap[catalog.id]);
273 }
274 });
275
276 return catalogsFromServer;
277
278 }
279
280 loadCatalogsSuccess(context) {
281 const fromServer = this.updateCatalogIndexes(context.data);
282 const catalogs = this.mergeEditsIntoLatestFromServer(fromServer);
283 this.setState({
284 catalogs: catalogs,
285 isLoading: false
286 });
287 }
288
289 deleteCatalogItemSuccess (response) {
290 let catalogType = response.catalogType;
291 let itemId = response.itemId;
292 const catalogs = this.getCatalogs().map(catalog => {
293 if (catalog.type === catalogType) {
294 catalog.descriptors = catalog.descriptors.filter(d => d.id !== itemId);
295 }
296 return catalog;
297 });
298
299 this.setState({catalogs: catalogs});
300 }
301
302 deleteCatalogItemError (data) {
303 console.log('Unable to delete', data.catalogType, 'id:', data.itemId, 'Error:', data.error.responseText);
304 ComposerAppActions.showError.defer({
305 errorMessage: 'Unable to delete ' + data.catalogType + ' id: ' + data.itemId + '. Check if it is in use'
306 });
307 }
308
309 selectCatalogItem(item = {}) {
310 SelectionManager.select(item);
311 }
312
313 catalogItemMetaDataChanged(item) {
314 let requiresSave = false;
315 const catalogs = this.getCatalogs().map(catalog => {
316 if (catalog.id === item.uiState.catalogId) {
317 catalog.descriptors = catalog.descriptors.map(d => {
318 if (d.id === item.id) {
319 // compare just the catalog uiState data (id, name, short-name, description, etc.)
320 const modified = !areCatalogItemsMetaDataEqual(d, item);
321 if (modified) {
322 if (d.uiState['instance-ref-count'] > 0) {
323 console.log('cannot edit NSD/VNFD with references to instantiated Network Services');
324 ComposerAppActions.showError.defer({
325 errorMessage: 'Cannot edit NSD/VNFD with references to instantiated Network Services'
326 });
327 return _cloneDeep(d);
328 } else {
329 item.uiState.modified = modified;
330 requiresSave = true;
331 this.addSnapshot(item);
332 }
333 }
334 return item;
335 }
336 return d;
337 });
338 }
339 return catalog;
340 });
341 if (requiresSave) {
342 this.setState({catalogs: catalogs, requiresSave: true});
343 }
344 }
345
346 catalogItemDescriptorChanged(itemDescriptor) {
347 // when a descriptor object is modified in the canvas we have to update the catalog
348 const catalogId = itemDescriptor.uiState.catalogId;
349 const catalogs = this.getCatalogs().map(catalog => {
350 if (catalog.id === catalogId) {
351 // find the catalog
352 const descriptorId = itemDescriptor.id;
353 // replace the old descriptor with the updated one
354 catalog.descriptors = catalog.descriptors.map(d => {
355 if (d.id === descriptorId) {
356 if (d.uiState['instance-ref-count'] > 0) {
357 console.log('cannot edit NSD/VNFD with references to instantiated Network Services');
358 ComposerAppActions.showError.defer({
359 errorMessage: 'Cannot edit NSD/VNFD with references to instantiated Network Services'
360 });
361 return _cloneDeep(d);
362 } else {
363 itemDescriptor.model.uiState.modified = true;
364 this.addSnapshot(itemDescriptor.model);
365 return itemDescriptor.model;
366 }
367 }
368 return d;
369 });
370 }
371 return catalog;
372 });
373 this.setState({catalogs: catalogs, requiresSave: true})
374 }
375
376 deleteSelectedCatalogItem() {
377 SelectionManager.getSelections().forEach(selectedId => {
378 const item = this.getCatalogItemByUid(selectedId);
379 if (item) {
380 this.deleteCatalogItem(item);
381 }
382 });
383 SelectionManager.clearSelectionAndRemoveOutline();
384 }
385
386 deleteCatalogItem(item) {
387 const snapshot = JSON.stringify(item);
388 function confirmDeleteCancel(event) {
389 undo();
390 event.preventDefault();
391 ModalOverlayActions.hideModalOverlay();
392 }
393 const remove = () => {
394 // item is deleted or does not exist on server, so remove from ui
395 this.removeCatalogItem(item);
396 this.setState({catalogs: this.getCatalogs()});
397 const activeItem = ComposerAppStore.getState().item;
398 if (activeItem && activeItem.id === item.id) {
399 CatalogItemsActions.editCatalogItem.defer(null);
400 }
401 ModalOverlayActions.hideModalOverlay();
402 };
403 const undo = () => {
404 // item failed to delete on server so revert ui
405 const revertTo = JSON.parse(snapshot);
406 this.updateCatalogItem(revertTo);
407 const activeItem = ComposerAppStore.getState().item;
408 if (activeItem && activeItem.id === revertTo.id) {
409 SelectionManager.select(activeItem);
410 CatalogItemsActions.editCatalogItem.defer(revertTo);
411 SelectionManager.refreshOutline();
412 }
413 };
414 if (item) {
415 if (item.uiState.isNew) {
416 CatalogDataStore.confirmDelete(remove, confirmDeleteCancel);
417 } else {
418 if (item.uiState['instance-ref-count'] > 0) {
419 console.log('cannot delete NSD/VNFD with references to instantiated Network Services');
420 ComposerAppActions.showError.defer({
421 errorMessage: 'Cannot delete NSD/VNFD with references to instantiated Network Services'
422 });
423 undo();
424 } else {
425 const confirmDeleteOK = event => {
426 event.preventDefault();
427 item.uiState.deleted = true;
428 this.setState({catalogs: this.getCatalogs()});
429 ModalOverlayActions.showModalOverlay.defer();
430 this.getInstance().deleteCatalogItem(item.uiState.type, item.id)
431 .then(remove, undo)
432 .then(ModalOverlayActions.hideModalOverlay, ModalOverlayActions.hideModalOverlay)
433 .catch(function() {
434 console.log('overcoming ES6 unhandled rejection red herring');
435 });
436 };
437 CatalogDataStore.confirmDelete(confirmDeleteOK, confirmDeleteCancel);
438 }
439 }
440 }
441 }
442
443 static confirmDelete(onClickYes, onClickCancel) {
444 ModalOverlayActions.showModalOverlay.defer((
445 <div className="actions panel">
446 <div className="panel-header">
447 <h1>Delete the selected catalog item?</h1>
448 </div>
449 <div className="panel-body">
450 <a className="action confirm-yes primary-action Button" onClick={onClickYes}>Yes, delete selected catalog item</a>
451 <a className="action cancel secondary-action Button" onClick={onClickCancel}>No, cancel</a>
452 </div>
453 </div>
454 ));
455 }
456
457 createCatalogItem(type = 'nsd') {
458 const newItem = createItem(type);
459 this.saveItem(newItem)
460 }
461
462 duplicateSelectedCatalogItem() {
463 const item = this.getFirstSelectedCatalogItem();
464 if (item) {
465 const newItem = _cloneDeep(item);
466 newItem.name = newItem.name + ' Copy';
467 newItem.id = guid();
468 UID.assignUniqueId(newItem.uiState);
469 const nsd = this.addNewItemToCatalog(newItem);
470 this.selectCatalogItem(nsd);
471 nsd.uiState.isNew = true;
472 nsd.uiState.modified = true;
473 nsd.uiState['instance-ref-count'] = 0;
474 // note duplicated items get a new id, map the layout position
475 // of the old id to the new id in order to preserve the layout
476 if (nsd.uiState.containerPositionMap) {
477 nsd.uiState.containerPositionMap[nsd.id] = nsd.uiState.containerPositionMap[item.id];
478 delete nsd.uiState.containerPositionMap[item.id];
479 }
480 setTimeout(() => {
481 this.selectCatalogItem(nsd);
482 CatalogItemsActions.editCatalogItem.defer(nsd);
483 }, 200);
484 }
485 }
486
487 addSnapshot(item) {
488 if (item) {
489 if (!this.snapshots[item.id]) {
490 this.snapshots[item.id] = [];
491 }
492 this.snapshots[item.id].push(JSON.stringify(item));
493 }
494 }
495
496 resetSnapshots(item) {
497 if (item) {
498 this.snapshots[item.id] = [];
499 this.addSnapshot(item);
500 }
501 }
502
503 editCatalogItem(item) {
504 if (item) {
505 this.addSnapshot(item);
506 // replace the given item in the catalog
507 const catalogs = this.getCatalogs().map(catalog => {
508 catalog.descriptors = catalog.descriptors.map(d => {
509 // note only one item can be "open" at a time
510 // so remove the flag from all the other items
511 d.uiState.isOpenForEdit = (d.id === item.id);
512 if (d.uiState.isOpenForEdit) {
513 return item;
514 }
515 return d;
516 });
517 return catalog;
518 });
519 this.setState({catalogs: catalogs});
520 this.catalogItemMetaDataChanged(item);
521 }
522 }
523
524 cancelCatalogItemChanges() {
525 const activeItem = ComposerAppStore.getState().item;
526 if (activeItem) {
527 const snapshots = this.snapshots[activeItem.id];
528 if (snapshots.length) {
529 const revertTo = JSON.parse(snapshots[0]);
530 this.updateCatalogItem(revertTo);
531 // TODO should the cancel action clear the undo/redo stack back to the beginning?
532 this.resetSnapshots(revertTo);
533 this.setState({requiresSave: false});
534 CatalogItemsActions.editCatalogItem.defer(revertTo);
535 }
536 }
537 }
538
539 saveCatalogItem() {
540 const activeItem = ComposerAppStore.getState().item;
541 if (activeItem) {
542 this.saveItem(activeItem);
543 }
544 }
545
546 saveItem(item) {
547 if (item) {
548 if (item.uiState['instance-ref-count'] > 0) {
549 console.log('cannot save NSD/VNFD with references to instantiated Network Services');
550 ComposerAppActions.showError.defer({
551 errorMessage: 'Cannot save NSD/VNFD with references to instantiated Network Services'
552 });
553 return;
554 }
555 const success = () => {
556 delete item.uiState.modified;
557 if (item.uiState.isNew) {
558 this.addNewItemToCatalog(item);
559 delete item.uiState.isNew;
560 } else {
561 this.updateCatalogItem(item);
562 }
563 // TODO should the save action clear the undo/redo stack back to the beginning?
564 this.resetSnapshots(item);
565 ModalOverlayActions.hideModalOverlay.defer();
566 CatalogItemsActions.editCatalogItem.defer(item);
567 };
568 const failure = () => {
569 ModalOverlayActions.hideModalOverlay.defer();
570 CatalogItemsActions.editCatalogItem.defer(item);
571 };
572 const exception = () => {
573 console.warn('unable to save catalog item', item);
574 ModalOverlayActions.hideModalOverlay.defer();
575 CatalogItemsActions.editCatalogItem.defer(item);
576 };
577 ModalOverlayActions.showModalOverlay.defer();
578 this.getInstance().saveCatalogItem(item).then(success, failure).catch(exception);
579 }
580 }
581
582 exportSelectedCatalogItems(draggedItem) {
583 // collect the selected items and delegate to the catalog package manager action creator
584 const selectedItems = this.getAllSelectedCatalogItems();
585 if (selectedItems.length) {
586 CatalogPackageManagerActions.downloadCatalogPackage.defer({
587 selectedItems: selectedItems,
588 selectedFormat: 'mano',
589 selectedGrammar: 'osm'
590 });
591 this.resetSelectionState();
592 }
593 }
594 saveCatalogItemError(data){
595 let error = JSON.parse(data.error.responseText);
596 const errorMsg = error && error.body && error.body['rpc-reply'] && JSON.stringify(error.body['rpc-reply']['rpc-error'], null, ' ')
597 ComposerAppActions.showError.defer({
598 errorMessage: 'Unable to save the descriptor.\n' + errorMsg
599 });
600 }
601 }
602
603 export default alt.createStore(CatalogDataStore, 'CatalogDataStore');