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 class CatalogDataStore
{
55 this.catalogs
= defaults
.catalogs
;
56 this.isLoading
= true;
57 this.requiresSave
= false;
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 this.exportPublicMethods({
65 getCatalogs
: this.getCatalogs
,
66 getCatalogItemById
: this.getCatalogItemById
,
67 getCatalogItemByUid
: this.getCatalogItemByUid
,
68 getTransientCatalogs
: this.getTransientCatalogs
,
69 getTransientCatalogItemById
: this.getTransientCatalogItemById
,
70 getTransientCatalogItemByUid
: this.getTransientCatalogItemByUid
74 resetSelectionState
= () => {
75 this.selectedFormat
= defaults
.catalogItemExportFormats
[0];
76 this.selectedGrammar
= defaults
.catalogItemExportGrammars
[0];
80 return this.catalogs
|| (this.catalogs
= []);
83 getTransientCatalogs() {
84 return this.state
.catalogs
|| (this.state
.catalogs
= []);
87 getAllSelectedCatalogItems() {
88 return this.getCatalogs().reduce((r
, d
) => {
89 d
.descriptors
.forEach(d
=> {
90 if (SelectionManager
.isSelected(d
) /*d.uiState.selected*/) {
98 getFirstSelectedCatalogItem() {
99 return this.getCatalogs().reduce((r
, catalog
) => {
100 return r
.concat(catalog
.descriptors
.filter(d
=> SelectionManager
.isSelected(d
) /*d.uiState.selected*/));
104 getCatalogItemById(id
) {
105 return this.getCatalogs().reduce((r
, catalog
) => {
106 return r
.concat(catalog
.descriptors
.filter(d
=> d
.id
=== id
));
110 getTransientCatalogItemById(id
) {
111 return this.getTransientCatalogs().reduce((r
, catalog
) => {
112 return r
.concat(catalog
.descriptors
.filter(d
=> d
.id
=== id
));
116 getCatalogItemByUid(uid
) {
117 return this.getCatalogs().reduce((r
, catalog
) => {
118 return r
.concat(catalog
.descriptors
.filter(d
=> UID
.from(d
) === uid
));
122 getTransientCatalogItemByUid(uid
) {
123 return this.getTransientCatalogs().reduce((r
, catalog
) => {
124 return r
.concat(catalog
.descriptors
.filter(d
=> UID
.from(d
) === uid
));
128 removeCatalogItem(deleteItem
= {}) {
129 this.getCatalogs().map(catalog
=> {
130 catalog
.descriptors
= catalog
.descriptors
.filter(d
=> d
.id
!== deleteItem
.id
);
135 addNewItemToCatalog(newItem
) {
137 const type
= newItem
.uiState
.type
;
139 UID
.assignUniqueId(newItem
.uiState
);
140 this.getCatalogs().filter(d
=> d
.type
=== type
).forEach(catalog
=> {
141 catalog
.descriptors
.push(newItem
);
143 // update indexes and integrate new model into catalog
144 this.updateCatalogIndexes(this.getCatalogs());
145 return this.getCatalogItemById(id
);
148 updateCatalogIndexes(catalogs
) {
149 // associate catalog identity with individual descriptors so we can
150 // update the catalog when any given descriptor is updated also add
151 // vnfd model to the nsd object to make it easier to render an nsd
152 const vnfdLookup
= {};
153 const updatedCatalogs
= catalogs
.map(catalog
=> {
154 catalog
.descriptors
.map(descriptor
=> {
155 if (typeof descriptor
.meta
=== 'string' && descriptor
.meta
.trim() !== '') {
157 descriptor
.uiState
= JSON
.parse(descriptor
.meta
);
159 console
.warn('Unable to deserialize the uiState property.');
161 } else if (typeof descriptor
.meta
=== 'object') {
162 descriptor
.uiState
= descriptor
.meta
;
163 descriptor
.meta
= JSON
.stringify(descriptor
.meta
);
166 const uiState
= descriptor
.uiState
|| (descriptor
.uiState
= {});
167 uiState
.catalogId
= catalog
.id
;
168 uiState
.catalogName
= catalog
.name
;
169 uiState
.type
= catalog
.type
;
170 if (!UID
.hasUniqueId(uiState
)) {
171 UID
.assignUniqueId(uiState
);
173 if (catalog
.type
=== 'vnfd') {
174 vnfdLookup
[descriptor
.id
] = descriptor
;
180 updatedCatalogs
.filter(d
=> d
.type
=== 'nsd').forEach(catalog
=> {
181 catalog
.descriptors
= catalog
.descriptors
.map(descriptor
=> {
182 const instanceRefCount
= parseInt(descriptor
.uiState
['instance-ref-count'], 10);
183 if (descriptor
['constituent-vnfd']) {
184 descriptor
.vnfd
= descriptor
['constituent-vnfd'].map(d
=> {
185 const vnfdId
= d
['vnfd-id-ref'];
186 const vnfd
= vnfdLookup
[vnfdId
];
188 throw new ReferenceError('no VNFD found in the VNFD Catalog for the constituent-vnfd: ' + d
);
190 if (!isNaN(instanceRefCount
) && instanceRefCount
> 0) {
191 // this will notify user that this item cannot be updated when/if they make a change to it
192 vnfd
.uiState
['instance-ref-count'] = instanceRefCount
;
194 // create an instance of this vnfd to carry transient ui state properties
195 const instance
= _
.cloneDeep(vnfd
);
196 instance
.uiState
['member-vnf-index'] = d
['member-vnf-index'];
197 instance
['vnf-configuration'] = d
['vnf-configuration'];
198 instance
['start-by-default'] = d
['start-by-default'];
205 return updatedCatalogs
;
208 updateCatalogItem(item
) {
209 // replace the given item in the catalog
210 const catalogs
= this.getCatalogs().map(catalog
=> {
211 if (catalog
.id
=== item
.uiState
.catalogId
) {
212 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
213 if (d
.id
=== item
.id
) {
221 this.setState({catalogs
: catalogs
});
224 mergeEditsIntoLatestFromServer(catalogsFromServer
= []) {
226 // if the UI has modified items use them instead of the server items
228 const currentData
= this.getCatalogs();
230 const modifiedItemsMap
= currentData
.reduce((result
, catalog
) => {
231 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
.modified
));
232 }, []).reduce((r
, d
) => {
233 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
;
237 const itemMetaMap
= currentData
.reduce((result
, catalog
) => {
238 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
));
239 }, []).reduce((r
, d
) => {
240 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
.uiState
;
244 const newItemsMap
= currentData
.reduce((result
, catalog
) => {
245 result
[catalog
.id
] = catalog
.descriptors
.filter(d
=> d
.uiState
.isNew
);
249 catalogsFromServer
.forEach(catalog
=> {
250 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
251 const key
= d
.uiState
.catalogId
+ '/' + d
.id
;
252 if (modifiedItemsMap
[key
]) {
253 // use local modified item instead of the server item
254 return modifiedItemsMap
[key
];
256 if (itemMetaMap
[key
]) {
257 Object
.assign(d
.uiState
, itemMetaMap
[key
]);
261 if (newItemsMap
[catalog
.id
]) {
262 catalog
.descriptors
= catalog
.descriptors
.concat(newItemsMap
[catalog
.id
]);
266 return catalogsFromServer
;
270 loadCatalogsSuccess(context
) {
271 const fromServer
= this.updateCatalogIndexes(context
.data
);
272 const catalogs
= this.mergeEditsIntoLatestFromServer(fromServer
);
279 deleteCatalogItemSuccess (response
) {
280 let catalogType
= response
.catalogType
;
281 let itemId
= response
.itemId
;
282 const catalogs
= this.getCatalogs().map(catalog
=> {
283 if (catalog
.type
=== catalogType
) {
284 catalog
.descriptors
= catalog
.descriptors
.filter(d
=> d
.id
!== itemId
);
289 this.setState({catalogs
: catalogs
});
292 deleteCatalogItemError (data
) {
293 console
.log('Unable to delete', data
.catalogType
, 'id:', data
.itemId
, 'Error:', data
.error
.responseText
);
294 ComposerAppActions
.showError
.defer({
295 errorMessage
: 'Unable to delete ' + data
.catalogType
+ ' id: ' + data
.itemId
+ '. Check if it is in use'
299 selectCatalogItem(item
= {}) {
300 SelectionManager
.select(item
);
303 catalogItemMetaDataChanged(item
) {
304 let requiresSave
= false;
305 const catalogs
= this.getCatalogs().map(catalog
=> {
306 if (catalog
.id
=== item
.uiState
.catalogId
) {
307 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
308 if (d
.id
=== item
.id
) {
309 // compare just the catalog uiState data (id, name, short-name, description, etc.)
310 const modified
= !areCatalogItemsMetaDataEqual(d
, item
);
312 if (d
.uiState
['instance-ref-count'] > 0) {
313 console
.log('cannot edit NSD/VNFD with references to instantiated Network Services');
314 ComposerAppActions
.showError
.defer({
315 errorMessage
: 'Cannot edit NSD/VNFD with references to instantiated Network Services'
317 return _
.cloneDeep(d
);
319 item
.uiState
.modified
= modified
;
321 this.addSnapshot(item
);
332 this.setState({catalogs
: catalogs
, requiresSave
: true});
336 catalogItemDescriptorChanged(itemDescriptor
) {
337 // when a descriptor object is modified in the canvas we have to update the catalog
338 const catalogId
= itemDescriptor
.uiState
.catalogId
;
339 const catalogs
= this.getCatalogs().map(catalog
=> {
340 if (catalog
.id
=== catalogId
) {
342 const descriptorId
= itemDescriptor
.id
;
343 // replace the old descriptor with the updated one
344 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
345 if (d
.id
=== descriptorId
) {
346 if (d
.uiState
['instance-ref-count'] > 0) {
347 console
.log('cannot edit NSD/VNFD with references to instantiated Network Services');
348 ComposerAppActions
.showError
.defer({
349 errorMessage
: 'Cannot edit NSD/VNFD with references to instantiated Network Services'
351 return _
.cloneDeep(d
);
353 itemDescriptor
.model
.uiState
.modified
= true;
354 this.addSnapshot(itemDescriptor
.model
);
355 return itemDescriptor
.model
;
363 this.setState({catalogs
: catalogs
, requiresSave
: true})
366 deleteSelectedCatalogItem() {
367 SelectionManager
.getSelections().forEach(selectedId
=> {
368 const item
= this.getCatalogItemByUid(selectedId
);
370 this.deleteCatalogItem(item
);
373 SelectionManager
.clearSelectionAndRemoveOutline();
376 deleteCatalogItem(item
) {
377 const snapshot
= JSON
.stringify(item
);
378 function confirmDeleteCancel(event
) {
380 event
.preventDefault();
381 ModalOverlayActions
.hideModalOverlay();
383 const remove
= () => {
384 // item is deleted or does not exist on server, so remove from ui
385 this.removeCatalogItem(item
);
386 this.setState({catalogs
: this.getCatalogs()});
387 const activeItem
= ComposerAppStore
.getState().item
;
388 if (activeItem
&& activeItem
.id
=== item
.id
) {
389 CatalogItemsActions
.editCatalogItem
.defer(null);
391 ModalOverlayActions
.hideModalOverlay();
394 // item failed to delete on server so revert ui
395 const revertTo
= JSON
.parse(snapshot
);
396 this.updateCatalogItem(revertTo
);
397 const activeItem
= ComposerAppStore
.getState().item
;
398 if (activeItem
&& activeItem
.id
=== revertTo
.id
) {
399 SelectionManager
.select(activeItem
);
400 CatalogItemsActions
.editCatalogItem
.defer(revertTo
);
401 SelectionManager
.refreshOutline();
405 if (item
.uiState
.isNew
) {
406 CatalogDataStore
.confirmDelete(remove
, confirmDeleteCancel
);
408 if (item
.uiState
['instance-ref-count'] > 0) {
409 console
.log('cannot delete NSD/VNFD with references to instantiated Network Services');
410 ComposerAppActions
.showError
.defer({
411 errorMessage
: 'Cannot delete NSD/VNFD with references to instantiated Network Services'
415 const confirmDeleteOK
= event
=> {
416 event
.preventDefault();
417 item
.uiState
.deleted
= true;
418 this.setState({catalogs
: this.getCatalogs()});
419 ModalOverlayActions
.showModalOverlay
.defer();
420 this.getInstance().deleteCatalogItem(item
.uiState
.type
, item
.id
)
422 .then(ModalOverlayActions
.hideModalOverlay
, ModalOverlayActions
.hideModalOverlay
)
424 console
.log('overcoming ES6 unhandled rejection red herring');
427 CatalogDataStore
.confirmDelete(confirmDeleteOK
, confirmDeleteCancel
);
433 static confirmDelete(onClickYes
, onClickCancel
) {
434 ModalOverlayActions
.showModalOverlay
.defer((
435 <div className
="actions panel">
436 <div className
="panel-header">
437 <h1
>Delete the selected catalog item
?</h1
>
439 <div className
="panel-body">
440 <a className
="action confirm-yes primary-action Button" onClick
={onClickYes
}>Yes
, delete selected catalog item
</a
>
441 <a className
="action cancel secondary-action Button" onClick
={onClickCancel
}>No
, cancel
</a
>
447 createCatalogItem(type
= 'nsd') {
448 const model
= DescriptorModelMetaFactory
.createModelInstanceForType(type
);
450 const newItem
= this.addNewItemToCatalog(model
);
451 newItem
.uiState
.isNew
= true;
452 newItem
.uiState
.modified
= true;
453 newItem
.uiState
['instance-ref-count'] = 0;
454 // open the new model for editing in the canvas/details panels
456 this.selectCatalogItem(newItem
);
457 CatalogItemsActions
.editCatalogItem
.defer(newItem
);
462 duplicateSelectedCatalogItem() {
463 const item
= this.getFirstSelectedCatalogItem();
465 const newItem
= _
.cloneDeep(item
);
466 newItem
.name
= newItem
.name
+ ' Copy';
467 const nsd
= this.addNewItemToCatalog(newItem
);
468 this.selectCatalogItem(nsd
);
469 nsd
.uiState
.isNew
= true;
470 nsd
.uiState
.modified
= true;
471 nsd
.uiState
['instance-ref-count'] = 0;
472 // note duplicated items get a new id, map the layout position
473 // of the old id to the new id in order to preserve the layout
474 if (nsd
.uiState
.containerPositionMap
) {
475 nsd
.uiState
.containerPositionMap
[nsd
.id
] = nsd
.uiState
.containerPositionMap
[item
.id
];
476 delete nsd
.uiState
.containerPositionMap
[item
.id
];
479 this.selectCatalogItem(nsd
);
480 CatalogItemsActions
.editCatalogItem
.defer(nsd
);
487 if (!this.snapshots
[item
.id
]) {
488 this.snapshots
[item
.id
] = [];
490 this.snapshots
[item
.id
].push(JSON
.stringify(item
));
494 resetSnapshots(item
) {
496 this.snapshots
[item
.id
] = [];
497 this.addSnapshot(item
);
501 editCatalogItem(item
) {
503 this.addSnapshot(item
);
504 // replace the given item in the catalog
505 const catalogs
= this.getCatalogs().map(catalog
=> {
506 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
507 // note only one item can be "open" at a time
508 // so remove the flag from all the other items
509 d
.uiState
.isOpenForEdit
= (d
.id
=== item
.id
);
510 if (d
.uiState
.isOpenForEdit
) {
517 this.setState({catalogs
: catalogs
});
518 this.catalogItemMetaDataChanged(item
);
522 cancelCatalogItemChanges() {
523 const activeItem
= ComposerAppStore
.getState().item
;
525 const snapshots
= this.snapshots
[activeItem
.id
];
526 if (snapshots
.length
) {
527 const revertTo
= JSON
.parse(snapshots
[0]);
528 this.updateCatalogItem(revertTo
);
529 // TODO should the cancel action clear the undo/redo stack back to the beginning?
530 this.resetSnapshots(revertTo
);
531 this.setState({requiresSave
: false});
532 CatalogItemsActions
.editCatalogItem
.defer(revertTo
);
538 const activeItem
= ComposerAppStore
.getState().item
;
540 if (activeItem
.uiState
['instance-ref-count'] > 0) {
541 console
.log('cannot save NSD/VNFD with references to instantiated Network Services');
542 ComposerAppActions
.showError
.defer({
543 errorMessage
: 'Cannot save NSD/VNFD with references to instantiated Network Services'
547 const success
= () => {
548 delete activeItem
.uiState
.isNew
;
549 delete activeItem
.uiState
.modified
;
550 this.updateCatalogItem(activeItem
);
551 // TODO should the save action clear the undo/redo stack back to the beginning?
552 this.resetSnapshots(activeItem
);
553 ModalOverlayActions
.hideModalOverlay
.defer();
554 CatalogItemsActions
.editCatalogItem
.defer(activeItem
);
556 const failure
= () => {
557 ModalOverlayActions
.hideModalOverlay
.defer();
558 CatalogItemsActions
.editCatalogItem
.defer(activeItem
);
560 const exception
= () => {
561 console
.warn('unable to save catalog item', activeItem
);
562 ModalOverlayActions
.hideModalOverlay
.defer();
563 CatalogItemsActions
.editCatalogItem
.defer(activeItem
);
565 ModalOverlayActions
.showModalOverlay
.defer();
566 this.getInstance().saveCatalogItem(activeItem
).then(success
, failure
).catch(exception
);
570 exportSelectedCatalogItems(draggedItem
) {
571 const onSelectFormat
= (selectedFormat
, event
) => {
573 selectedFormat
: selectedFormat
577 const onSelectGrammar
= (selectedGrammar
, event
) => {
579 selectedGrammar
: selectedGrammar
584 const onCancel
= () => {
585 this.resetSelectionState();
586 ModalOverlayActions
.hideModalOverlay();
589 const onDownload
= (event
) => {
590 CatalogPackageManagerActions
.downloadCatalogPackage
.defer({
591 selectedItems
: selectedItems
,
592 selectedFormat
: this.selectedFormat
,
593 selectedGrammar
: this.selectedGrammar
595 this.resetSelectionState();
596 ModalOverlayActions
.hideModalOverlay();
601 // if item is given make sure it is also selected
602 //draggedItem.uiState.selected = true;
603 SelectionManager
.addSelection(draggedItem
);
604 this.updateCatalogItem(draggedItem
);
606 // collect the selected items and delegate to the catalog package manager action creator
607 const selectedItems
= this.getAllSelectedCatalogItems();
608 if (selectedItems
.length
) {
609 CatalogDataStore
.chooseExportFormat(onSelectFormat
, onSelectGrammar
, onDownload
, onCancel
);
613 static chooseExportFormat(onSelectFormat
, onSelectGrammar
, onDownload
, onCancel
) {
614 ModalOverlayActions
.showModalOverlay
.defer(
615 <ExportSelectorDialog
616 onSelectFormat
={onSelectFormat
}
617 onSelectGrammar
={onSelectGrammar
}
619 onDownload
={onDownload
}
620 currentlySelectedFormat
='mano'
621 currentlySelectedGrammar
='osm'
628 export default alt
.createStore(CatalogDataStore
, 'CatalogDataStore');