4 * Copyright 2016 RIFT.IO Inc
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
10 * http://www.apache.org/licenses/LICENSE-2.0
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.
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'
41 catalogItemExportFormats
: ['mano', 'rift'],
42 catalogItemExportGrammars
: ['osm', 'tosca']
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
);
52 function createItem (type
) {
53 let newItem
= DescriptorModelMetaFactory
.createModelInstanceForType(type
);
56 UID
.assignUniqueId(newItem
.uiState
);
57 newItem
.uiState
.isNew
= true;
58 newItem
.uiState
.modified
= true;
59 newItem
.uiState
['instance-ref-count'] = 0;
64 class CatalogDataStore
{
67 this.catalogs
= defaults
.catalogs
;
68 this.isLoading
= true;
69 this.requiresSave
= false;
71 this.selectedFormat
= defaults
.catalogItemExportFormats
[0];
72 this.selectedGrammar
= defaults
.catalogItemExportGrammars
[0];
73 this.registerAsync(CatalogDataSource
);
74 this.bindActions(CatalogDataSourceActions
);
75 this.bindActions(CatalogItemsActions
);
76 this.exportPublicMethods({
77 getCatalogs
: this.getCatalogs
,
78 getCatalogItemById
: this.getCatalogItemById
,
79 getCatalogItemByUid
: this.getCatalogItemByUid
,
80 getTransientCatalogs
: this.getTransientCatalogs
,
81 getTransientCatalogItemById
: this.getTransientCatalogItemById
,
82 getTransientCatalogItemByUid
: this.getTransientCatalogItemByUid
86 resetSelectionState
= () => {
87 this.selectedFormat
= defaults
.catalogItemExportFormats
[0];
88 this.selectedGrammar
= defaults
.catalogItemExportGrammars
[0];
92 return this.catalogs
|| (this.catalogs
= []);
95 getTransientCatalogs() {
96 return this.state
.catalogs
|| (this.state
.catalogs
= []);
99 getAllSelectedCatalogItems() {
100 return this.getCatalogs().reduce((r
, d
) => {
101 d
.descriptors
.forEach(d
=> {
102 if (SelectionManager
.isSelected(d
) /*d.uiState.selected*/) {
110 getFirstSelectedCatalogItem() {
111 return this.getCatalogs().reduce((r
, catalog
) => {
112 return r
.concat(catalog
.descriptors
.filter(d
=> SelectionManager
.isSelected(d
) /*d.uiState.selected*/));
116 getCatalogItemById(id
) {
117 return this.getCatalogs().reduce((r
, catalog
) => {
118 return r
.concat(catalog
.descriptors
.filter(d
=> d
.id
=== id
));
122 getTransientCatalogItemById(id
) {
123 return this.getTransientCatalogs().reduce((r
, catalog
) => {
124 return r
.concat(catalog
.descriptors
.filter(d
=> d
.id
=== id
));
128 getCatalogItemByUid(uid
) {
129 return this.getCatalogs().reduce((r
, catalog
) => {
130 return r
.concat(catalog
.descriptors
.filter(d
=> UID
.from(d
) === uid
));
134 getTransientCatalogItemByUid(uid
) {
135 return this.getTransientCatalogs().reduce((r
, catalog
) => {
136 return r
.concat(catalog
.descriptors
.filter(d
=> UID
.from(d
) === uid
));
140 removeCatalogItem(deleteItem
= {}) {
141 this.getCatalogs().map(catalog
=> {
142 catalog
.descriptors
= catalog
.descriptors
.filter(d
=> d
.id
!== deleteItem
.id
);
147 addNewItemToCatalog(newItem
) {
148 const type
= newItem
.uiState
.type
;
149 this.getCatalogs().filter(d
=> d
.type
=== type
).forEach(catalog
=> {
150 catalog
.descriptors
.push(newItem
);
152 // update indexes and integrate new model into catalog
153 this.updateCatalogIndexes(this.getCatalogs());
154 return this.getCatalogItemById(newItem
.id
);
157 updateCatalogIndexes(catalogs
) {
158 // associate catalog identity with individual descriptors so we can
159 // update the catalog when any given descriptor is updated also add
160 // vnfd model to the nsd object to make it easier to render an nsd
161 const vnfdLookup
= {};
162 const updatedCatalogs
= catalogs
.map(catalog
=> {
163 catalog
.descriptors
.map(descriptor
=> {
164 if (typeof descriptor
.meta
=== 'string' && descriptor
.meta
.trim() !== '') {
166 descriptor
.uiState
= JSON
.parse(descriptor
.meta
);
168 console
.warn('Unable to deserialize the uiState property.');
170 } else if (typeof descriptor
.meta
=== 'object') {
171 descriptor
.uiState
= descriptor
.meta
;
172 descriptor
.meta
= JSON
.stringify(descriptor
.meta
);
175 const uiState
= descriptor
.uiState
|| (descriptor
.uiState
= {});
176 uiState
.catalogId
= catalog
.id
;
177 uiState
.catalogName
= catalog
.name
;
178 uiState
.type
= catalog
.type
;
179 if (!UID
.hasUniqueId(uiState
)) {
180 UID
.assignUniqueId(uiState
);
182 if (catalog
.type
=== 'vnfd') {
183 vnfdLookup
[descriptor
.id
] = descriptor
;
189 updatedCatalogs
.filter(d
=> d
.type
=== 'nsd').forEach(catalog
=> {
190 catalog
.descriptors
= catalog
.descriptors
.map(descriptor
=> {
191 const instanceRefCount
= parseInt(descriptor
.uiState
['instance-ref-count'], 10);
192 if (descriptor
['constituent-vnfd']) {
193 descriptor
.vnfd
= descriptor
['constituent-vnfd'].map(d
=> {
194 const vnfdId
= d
['vnfd-id-ref'];
195 const vnfd
= vnfdLookup
[vnfdId
];
197 throw new ReferenceError('no VNFD found in the VNFD Catalog for the constituent-vnfd: ' + d
);
199 if (!isNaN(instanceRefCount
) && instanceRefCount
> 0) {
200 // this will notify user that this item cannot be updated when/if they make a change to it
201 vnfd
.uiState
['instance-ref-count'] = instanceRefCount
;
203 // create an instance of this vnfd to carry transient ui state properties
204 const instance
= _
.cloneDeep(vnfd
);
205 instance
.uiState
['member-vnf-index'] = d
['member-vnf-index'];
206 instance
['vnf-configuration'] = d
['vnf-configuration'];
207 instance
['start-by-default'] = d
['start-by-default'];
214 return updatedCatalogs
;
217 updateCatalogItem(item
) {
218 // replace the given item in the catalog
219 const catalogs
= this.getCatalogs().map(catalog
=> {
220 if (catalog
.id
=== item
.uiState
.catalogId
) {
221 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
222 if (d
.id
=== item
.id
) {
230 this.setState({catalogs
: catalogs
});
233 mergeEditsIntoLatestFromServer(catalogsFromServer
= []) {
235 // if the UI has modified items use them instead of the server items
237 const currentData
= this.getCatalogs();
239 const modifiedItemsMap
= currentData
.reduce((result
, catalog
) => {
240 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
.modified
));
241 }, []).reduce((r
, d
) => {
242 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
;
246 const itemMetaMap
= currentData
.reduce((result
, catalog
) => {
247 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
));
248 }, []).reduce((r
, d
) => {
249 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
.uiState
;
253 const newItemsMap
= currentData
.reduce((result
, catalog
) => {
254 result
[catalog
.id
] = catalog
.descriptors
.filter(d
=> d
.uiState
.isNew
);
258 catalogsFromServer
.forEach(catalog
=> {
259 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
260 const key
= d
.uiState
.catalogId
+ '/' + d
.id
;
261 if (modifiedItemsMap
[key
]) {
262 // use local modified item instead of the server item
263 return modifiedItemsMap
[key
];
265 if (itemMetaMap
[key
]) {
266 Object
.assign(d
.uiState
, itemMetaMap
[key
]);
270 if (newItemsMap
[catalog
.id
]) {
271 catalog
.descriptors
= catalog
.descriptors
.concat(newItemsMap
[catalog
.id
]);
275 return catalogsFromServer
;
279 loadCatalogsSuccess(context
) {
280 const fromServer
= this.updateCatalogIndexes(context
.data
);
281 const catalogs
= this.mergeEditsIntoLatestFromServer(fromServer
);
288 deleteCatalogItemSuccess (response
) {
289 let catalogType
= response
.catalogType
;
290 let itemId
= response
.itemId
;
291 const catalogs
= this.getCatalogs().map(catalog
=> {
292 if (catalog
.type
=== catalogType
) {
293 catalog
.descriptors
= catalog
.descriptors
.filter(d
=> d
.id
!== itemId
);
298 this.setState({catalogs
: catalogs
});
301 deleteCatalogItemError (data
) {
302 console
.log('Unable to delete', data
.catalogType
, 'id:', data
.itemId
, 'Error:', data
.error
.responseText
);
303 ComposerAppActions
.showError
.defer({
304 errorMessage
: 'Unable to delete ' + data
.catalogType
+ ' id: ' + data
.itemId
+ '. Check if it is in use'
308 selectCatalogItem(item
= {}) {
309 SelectionManager
.select(item
);
312 catalogItemMetaDataChanged(item
) {
313 let requiresSave
= false;
314 const catalogs
= this.getCatalogs().map(catalog
=> {
315 if (catalog
.id
=== item
.uiState
.catalogId
) {
316 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
317 if (d
.id
=== item
.id
) {
318 // compare just the catalog uiState data (id, name, short-name, description, etc.)
319 const modified
= !areCatalogItemsMetaDataEqual(d
, item
);
321 if (d
.uiState
['instance-ref-count'] > 0) {
322 console
.log('cannot edit NSD/VNFD with references to instantiated Network Services');
323 ComposerAppActions
.showError
.defer({
324 errorMessage
: 'Cannot edit NSD/VNFD with references to instantiated Network Services'
326 return _
.cloneDeep(d
);
328 item
.uiState
.modified
= modified
;
330 this.addSnapshot(item
);
341 this.setState({catalogs
: catalogs
, requiresSave
: true});
345 catalogItemDescriptorChanged(itemDescriptor
) {
346 // when a descriptor object is modified in the canvas we have to update the catalog
347 const catalogId
= itemDescriptor
.uiState
.catalogId
;
348 const catalogs
= this.getCatalogs().map(catalog
=> {
349 if (catalog
.id
=== catalogId
) {
351 const descriptorId
= itemDescriptor
.id
;
352 // replace the old descriptor with the updated one
353 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
354 if (d
.id
=== descriptorId
) {
355 if (d
.uiState
['instance-ref-count'] > 0) {
356 console
.log('cannot edit NSD/VNFD with references to instantiated Network Services');
357 ComposerAppActions
.showError
.defer({
358 errorMessage
: 'Cannot edit NSD/VNFD with references to instantiated Network Services'
360 return _
.cloneDeep(d
);
362 itemDescriptor
.model
.uiState
.modified
= true;
363 this.addSnapshot(itemDescriptor
.model
);
364 return itemDescriptor
.model
;
372 this.setState({catalogs
: catalogs
, requiresSave
: true})
375 deleteSelectedCatalogItem() {
376 SelectionManager
.getSelections().forEach(selectedId
=> {
377 const item
= this.getCatalogItemByUid(selectedId
);
379 this.deleteCatalogItem(item
);
382 SelectionManager
.clearSelectionAndRemoveOutline();
385 deleteCatalogItem(item
) {
386 const snapshot
= JSON
.stringify(item
);
387 function confirmDeleteCancel(event
) {
389 event
.preventDefault();
390 ModalOverlayActions
.hideModalOverlay();
392 const remove
= () => {
393 // item is deleted or does not exist on server, so remove from ui
394 this.removeCatalogItem(item
);
395 this.setState({catalogs
: this.getCatalogs()});
396 const activeItem
= ComposerAppStore
.getState().item
;
397 if (activeItem
&& activeItem
.id
=== item
.id
) {
398 CatalogItemsActions
.editCatalogItem
.defer(null);
400 ModalOverlayActions
.hideModalOverlay();
403 // item failed to delete on server so revert ui
404 const revertTo
= JSON
.parse(snapshot
);
405 this.updateCatalogItem(revertTo
);
406 const activeItem
= ComposerAppStore
.getState().item
;
407 if (activeItem
&& activeItem
.id
=== revertTo
.id
) {
408 SelectionManager
.select(activeItem
);
409 CatalogItemsActions
.editCatalogItem
.defer(revertTo
);
410 SelectionManager
.refreshOutline();
414 if (item
.uiState
.isNew
) {
415 CatalogDataStore
.confirmDelete(remove
, confirmDeleteCancel
);
417 if (item
.uiState
['instance-ref-count'] > 0) {
418 console
.log('cannot delete NSD/VNFD with references to instantiated Network Services');
419 ComposerAppActions
.showError
.defer({
420 errorMessage
: 'Cannot delete NSD/VNFD with references to instantiated Network Services'
424 const confirmDeleteOK
= event
=> {
425 event
.preventDefault();
426 item
.uiState
.deleted
= true;
427 this.setState({catalogs
: this.getCatalogs()});
428 ModalOverlayActions
.showModalOverlay
.defer();
429 this.getInstance().deleteCatalogItem(item
.uiState
.type
, item
.id
)
431 .then(ModalOverlayActions
.hideModalOverlay
, ModalOverlayActions
.hideModalOverlay
)
433 console
.log('overcoming ES6 unhandled rejection red herring');
436 CatalogDataStore
.confirmDelete(confirmDeleteOK
, confirmDeleteCancel
);
442 static confirmDelete(onClickYes
, onClickCancel
) {
443 ModalOverlayActions
.showModalOverlay
.defer((
444 <div className
="actions panel">
445 <div className
="panel-header">
446 <h1
>Delete the selected catalog item
?</h1
>
448 <div className
="panel-body">
449 <a className
="action confirm-yes primary-action Button" onClick
={onClickYes
}>Yes
, delete selected catalog item
</a
>
450 <a className
="action cancel secondary-action Button" onClick
={onClickCancel
}>No
, cancel
</a
>
456 createCatalogItem(type
= 'nsd') {
457 const newItem
= createItem(type
);
458 this.saveItem(newItem
)
461 duplicateSelectedCatalogItem() {
462 const item
= this.getFirstSelectedCatalogItem();
464 const newItem
= _
.cloneDeep(item
);
465 newItem
.name
= newItem
.name
+ ' Copy';
467 UID
.assignUniqueId(newItem
.uiState
);
468 const nsd
= this.addNewItemToCatalog(newItem
);
469 this.selectCatalogItem(nsd
);
470 nsd
.uiState
.isNew
= true;
471 nsd
.uiState
.modified
= true;
472 nsd
.uiState
['instance-ref-count'] = 0;
473 // note duplicated items get a new id, map the layout position
474 // of the old id to the new id in order to preserve the layout
475 if (nsd
.uiState
.containerPositionMap
) {
476 nsd
.uiState
.containerPositionMap
[nsd
.id
] = nsd
.uiState
.containerPositionMap
[item
.id
];
477 delete nsd
.uiState
.containerPositionMap
[item
.id
];
480 this.selectCatalogItem(nsd
);
481 CatalogItemsActions
.editCatalogItem
.defer(nsd
);
488 if (!this.snapshots
[item
.id
]) {
489 this.snapshots
[item
.id
] = [];
491 this.snapshots
[item
.id
].push(JSON
.stringify(item
));
495 resetSnapshots(item
) {
497 this.snapshots
[item
.id
] = [];
498 this.addSnapshot(item
);
502 editCatalogItem(item
) {
504 this.addSnapshot(item
);
505 // replace the given item in the catalog
506 const catalogs
= this.getCatalogs().map(catalog
=> {
507 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
508 // note only one item can be "open" at a time
509 // so remove the flag from all the other items
510 d
.uiState
.isOpenForEdit
= (d
.id
=== item
.id
);
511 if (d
.uiState
.isOpenForEdit
) {
518 this.setState({catalogs
: catalogs
});
519 this.catalogItemMetaDataChanged(item
);
523 cancelCatalogItemChanges() {
524 const activeItem
= ComposerAppStore
.getState().item
;
526 const snapshots
= this.snapshots
[activeItem
.id
];
527 if (snapshots
.length
) {
528 const revertTo
= JSON
.parse(snapshots
[0]);
529 this.updateCatalogItem(revertTo
);
530 // TODO should the cancel action clear the undo/redo stack back to the beginning?
531 this.resetSnapshots(revertTo
);
532 this.setState({requiresSave
: false});
533 CatalogItemsActions
.editCatalogItem
.defer(revertTo
);
539 const activeItem
= ComposerAppStore
.getState().item
;
541 this.saveItem(activeItem
);
547 if (item
.uiState
['instance-ref-count'] > 0) {
548 console
.log('cannot save NSD/VNFD with references to instantiated Network Services');
549 ComposerAppActions
.showError
.defer({
550 errorMessage
: 'Cannot save NSD/VNFD with references to instantiated Network Services'
554 const success
= () => {
555 delete item
.uiState
.modified
;
556 if (item
.uiState
.isNew
) {
557 this.addNewItemToCatalog(item
);
558 delete item
.uiState
.isNew
;
560 this.updateCatalogItem(item
);
562 // TODO should the save action clear the undo/redo stack back to the beginning?
563 this.resetSnapshots(item
);
564 ModalOverlayActions
.hideModalOverlay
.defer();
565 CatalogItemsActions
.editCatalogItem
.defer(item
);
567 const failure
= () => {
568 ModalOverlayActions
.hideModalOverlay
.defer();
569 CatalogItemsActions
.editCatalogItem
.defer(item
);
571 const exception
= () => {
572 console
.warn('unable to save catalog item', item
);
573 ModalOverlayActions
.hideModalOverlay
.defer();
574 CatalogItemsActions
.editCatalogItem
.defer(item
);
576 ModalOverlayActions
.showModalOverlay
.defer();
577 this.getInstance().saveCatalogItem(item
).then(success
, failure
).catch(exception
);
581 exportSelectedCatalogItems(draggedItem
) {
582 const onSelectFormat
= (selectedFormat
, event
) => {
584 selectedFormat
: selectedFormat
588 const onSelectGrammar
= (selectedGrammar
, event
) => {
590 selectedGrammar
: selectedGrammar
595 const onCancel
= () => {
596 this.resetSelectionState();
597 ModalOverlayActions
.hideModalOverlay();
600 const onDownload
= (event
) => {
601 CatalogPackageManagerActions
.downloadCatalogPackage
.defer({
602 selectedItems
: selectedItems
,
603 selectedFormat
: this.selectedFormat
,
604 selectedGrammar
: this.selectedGrammar
606 this.resetSelectionState();
607 ModalOverlayActions
.hideModalOverlay();
612 // if item is given make sure it is also selected
613 //draggedItem.uiState.selected = true;
614 SelectionManager
.addSelection(draggedItem
);
615 this.updateCatalogItem(draggedItem
);
617 // collect the selected items and delegate to the catalog package manager action creator
618 const selectedItems
= this.getAllSelectedCatalogItems();
619 if (selectedItems
.length
) {
620 CatalogDataStore
.chooseExportFormat(onSelectFormat
, onSelectGrammar
, onDownload
, onCancel
);
624 static chooseExportFormat(onSelectFormat
, onSelectGrammar
, onDownload
, onCancel
) {
625 ModalOverlayActions
.showModalOverlay
.defer(
626 <ExportSelectorDialog
627 onSelectFormat
={onSelectFormat
}
628 onSelectGrammar
={onSelectGrammar
}
630 onDownload
={onDownload
}
631 currentlySelectedFormat
='mano'
632 currentlySelectedGrammar
='osm'
639 export default alt
.createStore(CatalogDataStore
, 'CatalogDataStore');