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 _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 import ExportSelectorDialog
from '../components/ExportSelectorDialog'
43 catalogItemExportFormats
: ['mano', 'rift'],
44 catalogItemExportGrammars
: ['osm', 'tosca']
47 const areCatalogItemsMetaDataEqual = function (a
, b
) {
48 const metaProps
= ['id', 'name', 'short-name', 'description', 'vendor', 'version'];
49 const aMetaData
= _pick(a
, metaProps
);
50 const bMetaData
= _pick(b
, metaProps
);
51 return _isEqual(aMetaData
, bMetaData
);
54 function createItem (type
) {
55 let newItem
= DescriptorModelMetaFactory
.createModelInstanceForType(type
);
58 UID
.assignUniqueId(newItem
.uiState
);
59 newItem
.uiState
.isNew
= true;
60 newItem
.uiState
.modified
= true;
61 newItem
.uiState
['instance-ref-count'] = 0;
66 class CatalogDataStore
{
69 this.catalogs
= defaults
.catalogs
;
70 this.isLoading
= true;
71 this.requiresSave
= false;
73 this.selectedFormat
= defaults
.catalogItemExportFormats
[0];
74 this.selectedGrammar
= defaults
.catalogItemExportGrammars
[0];
75 this.registerAsync(CatalogDataSource
);
76 this.bindActions(CatalogDataSourceActions
);
77 this.bindActions(CatalogItemsActions
);
78 this.exportPublicMethods({
79 getCatalogs
: this.getCatalogs
,
80 getCatalogItemById
: this.getCatalogItemById
,
81 getCatalogItemByUid
: this.getCatalogItemByUid
,
82 getTransientCatalogs
: this.getTransientCatalogs
,
83 getTransientCatalogItemById
: this.getTransientCatalogItemById
,
84 getTransientCatalogItemByUid
: this.getTransientCatalogItemByUid
88 resetSelectionState
= () => {
89 this.selectedFormat
= defaults
.catalogItemExportFormats
[0];
90 this.selectedGrammar
= defaults
.catalogItemExportGrammars
[0];
94 return this.catalogs
|| (this.catalogs
= []);
97 getTransientCatalogs() {
98 return this.state
.catalogs
|| (this.state
.catalogs
= []);
101 getAllSelectedCatalogItems() {
102 return this.getCatalogs().reduce((r
, d
) => {
103 d
.descriptors
.forEach(d
=> {
104 if (SelectionManager
.isSelected(d
) /*d.uiState.selected*/) {
112 getFirstSelectedCatalogItem() {
113 return this.getCatalogs().reduce((r
, catalog
) => {
114 return r
.concat(catalog
.descriptors
.filter(d
=> SelectionManager
.isSelected(d
) /*d.uiState.selected*/));
118 getCatalogItemById(id
) {
119 return this.getCatalogs().reduce((r
, catalog
) => {
120 return r
.concat(catalog
.descriptors
.filter(d
=> d
.id
=== id
));
124 getTransientCatalogItemById(id
) {
125 return this.getTransientCatalogs().reduce((r
, catalog
) => {
126 return r
.concat(catalog
.descriptors
.filter(d
=> d
.id
=== id
));
130 getCatalogItemByUid(uid
) {
131 return this.getCatalogs().reduce((r
, catalog
) => {
132 return r
.concat(catalog
.descriptors
.filter(d
=> UID
.from(d
) === uid
));
136 getTransientCatalogItemByUid(uid
) {
137 return this.getTransientCatalogs().reduce((r
, catalog
) => {
138 return r
.concat(catalog
.descriptors
.filter(d
=> UID
.from(d
) === uid
));
142 removeCatalogItem(deleteItem
= {}) {
143 this.getCatalogs().map(catalog
=> {
144 catalog
.descriptors
= catalog
.descriptors
.filter(d
=> d
.id
!== deleteItem
.id
);
149 addNewItemToCatalog(newItem
) {
150 const type
= newItem
.uiState
.type
;
151 this.getCatalogs().filter(d
=> d
.type
=== type
).forEach(catalog
=> {
152 catalog
.descriptors
.push(newItem
);
154 // update indexes and integrate new model into catalog
155 this.updateCatalogIndexes(this.getCatalogs());
156 return this.getCatalogItemById(newItem
.id
);
159 updateCatalogIndexes(catalogs
) {
160 // associate catalog identity with individual descriptors so we can
161 // update the catalog when any given descriptor is updated also add
162 // vnfd model to the nsd object to make it easier to render an nsd
163 const vnfdLookup
= {};
164 const updatedCatalogs
= catalogs
.map(catalog
=> {
165 catalog
.descriptors
.map(descriptor
=> {
166 if (typeof descriptor
.meta
=== 'string' && descriptor
.meta
.trim() !== '') {
168 descriptor
.uiState
= JSON
.parse(descriptor
.meta
);
170 console
.warn('Unable to deserialize the uiState property.');
172 } else if (typeof descriptor
.meta
=== 'object') {
173 descriptor
.uiState
= descriptor
.meta
;
174 descriptor
.meta
= JSON
.stringify(descriptor
.meta
);
177 const uiState
= descriptor
.uiState
|| (descriptor
.uiState
= {});
178 uiState
.catalogId
= catalog
.id
;
179 uiState
.catalogName
= catalog
.name
;
180 uiState
.type
= catalog
.type
;
181 if (!UID
.hasUniqueId(uiState
)) {
182 UID
.assignUniqueId(uiState
);
184 if (catalog
.type
=== 'vnfd') {
185 vnfdLookup
[descriptor
.id
] = descriptor
;
191 updatedCatalogs
.filter(d
=> d
.type
=== 'nsd').forEach(catalog
=> {
192 catalog
.descriptors
= catalog
.descriptors
.map(descriptor
=> {
193 const instanceRefCount
= parseInt(descriptor
.uiState
['instance-ref-count'], 10);
194 if (descriptor
['constituent-vnfd']) {
195 descriptor
.vnfd
= descriptor
['constituent-vnfd'].map(d
=> {
196 const vnfdId
= d
['vnfd-id-ref'];
197 const vnfd
= vnfdLookup
[vnfdId
];
199 throw new ReferenceError('no VNFD found in the VNFD Catalog for the constituent-vnfd: ' + d
);
201 if (!isNaN(instanceRefCount
) && instanceRefCount
> 0) {
202 // this will notify user that this item cannot be updated when/if they make a change to it
203 vnfd
.uiState
['instance-ref-count'] = instanceRefCount
;
205 // create an instance of this vnfd to carry transient ui state properties
206 const instance
= _cloneDeep(vnfd
);
207 instance
.uiState
['member-vnf-index'] = d
['member-vnf-index'];
208 instance
['vnf-configuration'] = d
['vnf-configuration'];
209 instance
['start-by-default'] = d
['start-by-default'];
216 return updatedCatalogs
;
219 updateCatalogItem(item
) {
220 // replace the given item in the catalog
221 const catalogs
= this.getCatalogs().map(catalog
=> {
222 if (catalog
.id
=== item
.uiState
.catalogId
) {
223 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
224 if (d
.id
=== item
.id
) {
232 this.setState({catalogs
: catalogs
});
235 mergeEditsIntoLatestFromServer(catalogsFromServer
= []) {
237 // if the UI has modified items use them instead of the server items
239 const currentData
= this.getCatalogs();
241 const modifiedItemsMap
= currentData
.reduce((result
, catalog
) => {
242 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
.modified
));
243 }, []).reduce((r
, d
) => {
244 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
;
248 const itemMetaMap
= currentData
.reduce((result
, catalog
) => {
249 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
));
250 }, []).reduce((r
, d
) => {
251 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
.uiState
;
255 const newItemsMap
= currentData
.reduce((result
, catalog
) => {
256 result
[catalog
.id
] = catalog
.descriptors
.filter(d
=> d
.uiState
.isNew
);
260 catalogsFromServer
.forEach(catalog
=> {
261 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
262 const key
= d
.uiState
.catalogId
+ '/' + d
.id
;
263 if (modifiedItemsMap
[key
]) {
264 // use local modified item instead of the server item
265 return modifiedItemsMap
[key
];
267 if (itemMetaMap
[key
]) {
268 Object
.assign(d
.uiState
, itemMetaMap
[key
]);
272 if (newItemsMap
[catalog
.id
]) {
273 catalog
.descriptors
= catalog
.descriptors
.concat(newItemsMap
[catalog
.id
]);
277 return catalogsFromServer
;
281 loadCatalogsSuccess(context
) {
282 const fromServer
= this.updateCatalogIndexes(context
.data
);
283 const catalogs
= this.mergeEditsIntoLatestFromServer(fromServer
);
290 deleteCatalogItemSuccess (response
) {
291 let catalogType
= response
.catalogType
;
292 let itemId
= response
.itemId
;
293 const catalogs
= this.getCatalogs().map(catalog
=> {
294 if (catalog
.type
=== catalogType
) {
295 catalog
.descriptors
= catalog
.descriptors
.filter(d
=> d
.id
!== itemId
);
300 this.setState({catalogs
: catalogs
});
303 deleteCatalogItemError (data
) {
304 console
.log('Unable to delete', data
.catalogType
, 'id:', data
.itemId
, 'Error:', data
.error
.responseText
);
305 ComposerAppActions
.showError
.defer({
306 errorMessage
: 'Unable to delete ' + data
.catalogType
+ ' id: ' + data
.itemId
+ '. Check if it is in use'
310 selectCatalogItem(item
= {}) {
311 SelectionManager
.select(item
);
314 catalogItemMetaDataChanged(item
) {
315 let requiresSave
= false;
316 const catalogs
= this.getCatalogs().map(catalog
=> {
317 if (catalog
.id
=== item
.uiState
.catalogId
) {
318 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
319 if (d
.id
=== item
.id
) {
320 // compare just the catalog uiState data (id, name, short-name, description, etc.)
321 const modified
= !areCatalogItemsMetaDataEqual(d
, item
);
323 if (d
.uiState
['instance-ref-count'] > 0) {
324 console
.log('cannot edit NSD/VNFD with references to instantiated Network Services');
325 ComposerAppActions
.showError
.defer({
326 errorMessage
: 'Cannot edit NSD/VNFD with references to instantiated Network Services'
328 return _cloneDeep(d
);
330 item
.uiState
.modified
= modified
;
332 this.addSnapshot(item
);
343 this.setState({catalogs
: catalogs
, requiresSave
: true});
347 catalogItemDescriptorChanged(itemDescriptor
) {
348 // when a descriptor object is modified in the canvas we have to update the catalog
349 const catalogId
= itemDescriptor
.uiState
.catalogId
;
350 const catalogs
= this.getCatalogs().map(catalog
=> {
351 if (catalog
.id
=== catalogId
) {
353 const descriptorId
= itemDescriptor
.id
;
354 // replace the old descriptor with the updated one
355 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
356 if (d
.id
=== descriptorId
) {
357 if (d
.uiState
['instance-ref-count'] > 0) {
358 console
.log('cannot edit NSD/VNFD with references to instantiated Network Services');
359 ComposerAppActions
.showError
.defer({
360 errorMessage
: 'Cannot edit NSD/VNFD with references to instantiated Network Services'
362 return _cloneDeep(d
);
364 itemDescriptor
.model
.uiState
.modified
= true;
365 this.addSnapshot(itemDescriptor
.model
);
366 return itemDescriptor
.model
;
374 this.setState({catalogs
: catalogs
, requiresSave
: true})
377 deleteSelectedCatalogItem() {
378 SelectionManager
.getSelections().forEach(selectedId
=> {
379 const item
= this.getCatalogItemByUid(selectedId
);
381 this.deleteCatalogItem(item
);
384 SelectionManager
.clearSelectionAndRemoveOutline();
387 deleteCatalogItem(item
) {
388 const snapshot
= JSON
.stringify(item
);
389 function confirmDeleteCancel(event
) {
391 event
.preventDefault();
392 ModalOverlayActions
.hideModalOverlay();
394 const remove
= () => {
395 // item is deleted or does not exist on server, so remove from ui
396 this.removeCatalogItem(item
);
397 this.setState({catalogs
: this.getCatalogs()});
398 const activeItem
= ComposerAppStore
.getState().item
;
399 if (activeItem
&& activeItem
.id
=== item
.id
) {
400 CatalogItemsActions
.editCatalogItem
.defer(null);
402 ModalOverlayActions
.hideModalOverlay();
405 // item failed to delete on server so revert ui
406 const revertTo
= JSON
.parse(snapshot
);
407 this.updateCatalogItem(revertTo
);
408 const activeItem
= ComposerAppStore
.getState().item
;
409 if (activeItem
&& activeItem
.id
=== revertTo
.id
) {
410 SelectionManager
.select(activeItem
);
411 CatalogItemsActions
.editCatalogItem
.defer(revertTo
);
412 SelectionManager
.refreshOutline();
416 if (item
.uiState
.isNew
) {
417 CatalogDataStore
.confirmDelete(remove
, confirmDeleteCancel
);
419 if (item
.uiState
['instance-ref-count'] > 0) {
420 console
.log('cannot delete NSD/VNFD with references to instantiated Network Services');
421 ComposerAppActions
.showError
.defer({
422 errorMessage
: 'Cannot delete NSD/VNFD with references to instantiated Network Services'
426 const confirmDeleteOK
= event
=> {
427 event
.preventDefault();
428 item
.uiState
.deleted
= true;
429 this.setState({catalogs
: this.getCatalogs()});
430 ModalOverlayActions
.showModalOverlay
.defer();
431 this.getInstance().deleteCatalogItem(item
.uiState
.type
, item
.id
)
433 .then(ModalOverlayActions
.hideModalOverlay
, ModalOverlayActions
.hideModalOverlay
)
435 console
.log('overcoming ES6 unhandled rejection red herring');
438 CatalogDataStore
.confirmDelete(confirmDeleteOK
, confirmDeleteCancel
);
444 static confirmDelete(onClickYes
, onClickCancel
) {
445 ModalOverlayActions
.showModalOverlay
.defer((
446 <div className
="actions panel">
447 <div className
="panel-header">
448 <h1
>Delete the selected catalog item
?</h1
>
450 <div className
="panel-body">
451 <a className
="action confirm-yes primary-action Button" onClick
={onClickYes
}>Yes
, delete selected catalog item
</a
>
452 <a className
="action cancel secondary-action Button" onClick
={onClickCancel
}>No
, cancel
</a
>
458 createCatalogItem(type
= 'nsd') {
459 const newItem
= createItem(type
);
460 this.saveItem(newItem
)
463 duplicateSelectedCatalogItem() {
464 const item
= this.getFirstSelectedCatalogItem();
466 const newItem
= _cloneDeep(item
);
467 newItem
.name
= newItem
.name
+ ' Copy';
469 UID
.assignUniqueId(newItem
.uiState
);
470 const nsd
= this.addNewItemToCatalog(newItem
);
471 this.selectCatalogItem(nsd
);
472 nsd
.uiState
.isNew
= true;
473 nsd
.uiState
.modified
= true;
474 nsd
.uiState
['instance-ref-count'] = 0;
475 // note duplicated items get a new id, map the layout position
476 // of the old id to the new id in order to preserve the layout
477 if (nsd
.uiState
.containerPositionMap
) {
478 nsd
.uiState
.containerPositionMap
[nsd
.id
] = nsd
.uiState
.containerPositionMap
[item
.id
];
479 delete nsd
.uiState
.containerPositionMap
[item
.id
];
482 this.selectCatalogItem(nsd
);
483 CatalogItemsActions
.editCatalogItem
.defer(nsd
);
490 if (!this.snapshots
[item
.id
]) {
491 this.snapshots
[item
.id
] = [];
493 this.snapshots
[item
.id
].push(JSON
.stringify(item
));
497 resetSnapshots(item
) {
499 this.snapshots
[item
.id
] = [];
500 this.addSnapshot(item
);
504 editCatalogItem(item
) {
506 this.addSnapshot(item
);
507 // replace the given item in the catalog
508 const catalogs
= this.getCatalogs().map(catalog
=> {
509 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
510 // note only one item can be "open" at a time
511 // so remove the flag from all the other items
512 d
.uiState
.isOpenForEdit
= (d
.id
=== item
.id
);
513 if (d
.uiState
.isOpenForEdit
) {
520 this.setState({catalogs
: catalogs
});
521 this.catalogItemMetaDataChanged(item
);
525 cancelCatalogItemChanges() {
526 const activeItem
= ComposerAppStore
.getState().item
;
528 const snapshots
= this.snapshots
[activeItem
.id
];
529 if (snapshots
.length
) {
530 const revertTo
= JSON
.parse(snapshots
[0]);
531 this.updateCatalogItem(revertTo
);
532 // TODO should the cancel action clear the undo/redo stack back to the beginning?
533 this.resetSnapshots(revertTo
);
534 this.setState({requiresSave
: false});
535 CatalogItemsActions
.editCatalogItem
.defer(revertTo
);
541 const activeItem
= ComposerAppStore
.getState().item
;
543 this.saveItem(activeItem
);
549 if (item
.uiState
['instance-ref-count'] > 0) {
550 console
.log('cannot save NSD/VNFD with references to instantiated Network Services');
551 ComposerAppActions
.showError
.defer({
552 errorMessage
: 'Cannot save NSD/VNFD with references to instantiated Network Services'
556 const success
= () => {
557 delete item
.uiState
.modified
;
558 if (item
.uiState
.isNew
) {
559 this.addNewItemToCatalog(item
);
560 delete item
.uiState
.isNew
;
562 this.updateCatalogItem(item
);
564 // TODO should the save action clear the undo/redo stack back to the beginning?
565 this.resetSnapshots(item
);
566 ModalOverlayActions
.hideModalOverlay
.defer();
567 CatalogItemsActions
.editCatalogItem
.defer(item
);
569 const failure
= () => {
570 ModalOverlayActions
.hideModalOverlay
.defer();
571 CatalogItemsActions
.editCatalogItem
.defer(item
);
573 const exception
= () => {
574 console
.warn('unable to save catalog item', item
);
575 ModalOverlayActions
.hideModalOverlay
.defer();
576 CatalogItemsActions
.editCatalogItem
.defer(item
);
578 ModalOverlayActions
.showModalOverlay
.defer();
579 this.getInstance().saveCatalogItem(item
).then(success
, failure
).catch(exception
);
583 exportSelectedCatalogItems(draggedItem
) {
584 const onSelectFormat
= (selectedFormat
, event
) => {
586 selectedFormat
: selectedFormat
590 const onSelectGrammar
= (selectedGrammar
, event
) => {
592 selectedGrammar
: selectedGrammar
597 const onCancel
= () => {
598 this.resetSelectionState();
599 ModalOverlayActions
.hideModalOverlay();
602 const onDownload
= (event
) => {
603 CatalogPackageManagerActions
.downloadCatalogPackage
.defer({
604 selectedItems
: selectedItems
,
605 selectedFormat
: this.selectedFormat
,
606 selectedGrammar
: this.selectedGrammar
608 this.resetSelectionState();
609 ModalOverlayActions
.hideModalOverlay();
614 // if item is given make sure it is also selected
615 //draggedItem.uiState.selected = true;
616 SelectionManager
.addSelection(draggedItem
);
617 this.updateCatalogItem(draggedItem
);
619 // collect the selected items and delegate to the catalog package manager action creator
620 const selectedItems
= this.getAllSelectedCatalogItems();
621 if (selectedItems
.length
) {
622 CatalogDataStore
.chooseExportFormat(onSelectFormat
, onSelectGrammar
, onDownload
, onCancel
);
626 static chooseExportFormat(onSelectFormat
, onSelectGrammar
, onDownload
, onCancel
) {
627 ModalOverlayActions
.showModalOverlay
.defer(
628 <ExportSelectorDialog
629 onSelectFormat
={onSelectFormat
}
630 onSelectGrammar
={onSelectGrammar
}
632 onDownload
={onDownload
}
633 currentlySelectedFormat
='mano'
634 currentlySelectedGrammar
='osm'
641 export default alt
.createStore(CatalogDataStore
, 'CatalogDataStore');