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