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
);
66 resetSelectionState
= () => {
67 this.selectedFormat
= defaults
.catalogItemExportFormats
[0];
68 this.selectedGrammar
= defaults
.catalogItemExportGrammars
[0];
72 return this.catalogs
|| (this.catalogs
= []);
75 getAllSelectedCatalogItems() {
76 return this.getCatalogs().reduce((r
, d
) => {
77 d
.descriptors
.forEach(d
=> {
78 if (SelectionManager
.isSelected(d
) /*d.uiState.selected*/) {
86 getFirstSelectedCatalogItem() {
87 return this.getCatalogs().reduce((r
, catalog
) => {
88 return r
.concat(catalog
.descriptors
.filter(d
=> SelectionManager
.isSelected(d
) /*d.uiState.selected*/));
92 getCatalogItemById(id
) {
93 return this.getCatalogs().reduce((r
, catalog
) => {
94 return r
.concat(catalog
.descriptors
.filter(d
=> d
.id
=== id
));
98 getCatalogItemByUid(uid
) {
99 return this.getCatalogs().reduce((r
, catalog
) => {
100 return r
.concat(catalog
.descriptors
.filter(d
=> UID
.from(d
) === uid
));
104 removeCatalogItem(deleteItem
= {}) {
105 this.getCatalogs().map(catalog
=> {
106 catalog
.descriptors
= catalog
.descriptors
.filter(d
=> d
.id
!== deleteItem
.id
);
111 addNewItemToCatalog(newItem
) {
113 const type
= newItem
.uiState
.type
;
115 UID
.assignUniqueId(newItem
.uiState
);
116 this.getCatalogs().filter(d
=> d
.type
=== type
).forEach(catalog
=> {
117 catalog
.descriptors
.push(newItem
);
119 // update indexes and integrate new model into catalog
120 this.updateCatalogIndexes(this.getCatalogs());
121 return this.getCatalogItemById(id
);
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() !== '') {
133 descriptor
.uiState
= JSON
.parse(descriptor
.meta
);
135 console
.warn('Unable to deserialize the uiState property.');
137 } else if (typeof descriptor
.meta
=== 'object') {
138 descriptor
.uiState
= descriptor
.meta
;
139 descriptor
.meta
= JSON
.stringify(descriptor
.meta
);
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
);
149 if (catalog
.type
=== 'vnfd') {
150 vnfdLookup
[descriptor
.id
] = descriptor
;
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
];
164 throw new ReferenceError('no VNFD found in the VNFD Catalog for the constituent-vnfd: ' + d
);
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
;
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'];
181 return updatedCatalogs
;
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
) {
197 this.setState({catalogs
: catalogs
});
200 mergeEditsIntoLatestFromServer(catalogsFromServer
= []) {
202 // if the UI has modified items use them instead of the server items
204 const currentData
= this.getCatalogs();
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
;
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
;
220 const newItemsMap
= currentData
.reduce((result
, catalog
) => {
221 result
[catalog
.id
] = catalog
.descriptors
.filter(d
=> d
.uiState
.isNew
);
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
];
232 if (itemMetaMap
[key
]) {
233 Object
.assign(d
.uiState
, itemMetaMap
[key
]);
237 if (newItemsMap
[catalog
.id
]) {
238 catalog
.descriptors
= catalog
.descriptors
.concat(newItemsMap
[catalog
.id
]);
242 return catalogsFromServer
;
246 loadCatalogsSuccess(context
) {
247 const fromServer
= this.updateCatalogIndexes(context
.data
);
248 const catalogs
= this.mergeEditsIntoLatestFromServer(fromServer
);
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
);
265 this.setState({catalogs
: catalogs
});
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'
275 selectCatalogItem(item
= {}) {
276 SelectionManager
.select(item
);
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
);
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'
293 return _
.cloneDeep(d
);
295 item
.uiState
.modified
= modified
;
297 this.addSnapshot(item
);
308 this.setState({catalogs
: catalogs
, requiresSave
: true});
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
) {
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'
327 return _
.cloneDeep(d
);
329 itemDescriptor
.model
.uiState
.modified
= true;
330 this.addSnapshot(itemDescriptor
.model
);
331 return itemDescriptor
.model
;
339 this.setState({catalogs
: catalogs
, requiresSave
: true})
342 deleteSelectedCatalogItem() {
343 SelectionManager
.getSelections().forEach(selectedId
=> {
344 const item
= this.getCatalogItemByUid(selectedId
);
346 this.deleteCatalogItem(item
);
349 SelectionManager
.clearSelectionAndRemoveOutline();
352 deleteCatalogItem(item
) {
353 const snapshot
= JSON
.stringify(item
);
354 function confirmDeleteCancel(event
) {
356 event
.preventDefault();
357 ModalOverlayActions
.hideModalOverlay();
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);
367 ModalOverlayActions
.hideModalOverlay();
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();
381 if (item
.uiState
.isNew
) {
382 CatalogDataStore
.confirmDelete(remove
, confirmDeleteCancel
);
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'
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
)
398 .then(ModalOverlayActions
.hideModalOverlay
, ModalOverlayActions
.hideModalOverlay
)
400 console
.log('overcoming ES6 unhandled rejection red herring');
403 CatalogDataStore
.confirmDelete(confirmDeleteOK
, confirmDeleteCancel
);
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
>
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
>
423 createCatalogItem(type
= 'nsd') {
424 const model
= DescriptorModelMetaFactory
.createModelInstanceForType(type
);
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
432 this.selectCatalogItem(newItem
);
433 CatalogItemsActions
.editCatalogItem
.defer(newItem
);
438 duplicateSelectedCatalogItem() {
439 const item
= this.getFirstSelectedCatalogItem();
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
];
455 this.selectCatalogItem(nsd
);
456 CatalogItemsActions
.editCatalogItem
.defer(nsd
);
463 if (!this.snapshots
[item
.id
]) {
464 this.snapshots
[item
.id
] = [];
466 this.snapshots
[item
.id
].push(JSON
.stringify(item
));
470 resetSnapshots(item
) {
472 this.snapshots
[item
.id
] = [];
473 this.addSnapshot(item
);
477 editCatalogItem(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
) {
493 this.setState({catalogs
: catalogs
});
494 this.catalogItemMetaDataChanged(item
);
498 cancelCatalogItemChanges() {
499 const activeItem
= ComposerAppStore
.getState().item
;
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
);
514 const activeItem
= ComposerAppStore
.getState().item
;
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'
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
);
532 const failure
= () => {
533 ModalOverlayActions
.hideModalOverlay
.defer();
534 CatalogItemsActions
.editCatalogItem
.defer(activeItem
);
536 const exception
= () => {
537 console
.warn('unable to save catalog item', activeItem
);
538 ModalOverlayActions
.hideModalOverlay
.defer();
539 CatalogItemsActions
.editCatalogItem
.defer(activeItem
);
541 ModalOverlayActions
.showModalOverlay
.defer();
542 this.getInstance().saveCatalogItem(activeItem
).then(success
, failure
).catch(exception
);
546 exportSelectedCatalogItems(draggedItem
) {
547 const onSelectFormat
= (selectedFormat
, event
) => {
549 selectedFormat
: selectedFormat
553 const onSelectGrammar
= (selectedGrammar
, event
) => {
555 selectedGrammar
: selectedGrammar
560 const onCancel
= () => {
561 this.resetSelectionState();
562 ModalOverlayActions
.hideModalOverlay();
565 const onDownload
= (event
) => {
566 CatalogPackageManagerActions
.downloadCatalogPackage
.defer({
567 selectedItems
: selectedItems
,
568 selectedFormat
: this.selectedFormat
,
569 selectedGrammar
: this.selectedGrammar
571 this.resetSelectionState();
572 ModalOverlayActions
.hideModalOverlay();
577 // if item is given make sure it is also selected
578 //draggedItem.uiState.selected = true;
579 SelectionManager
.addSelection(draggedItem
);
580 this.updateCatalogItem(draggedItem
);
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
);
589 static chooseExportFormat(onSelectFormat
, onSelectGrammar
, onDownload
, onCancel
) {
590 ModalOverlayActions
.showModalOverlay
.defer(
591 <ExportSelectorDialog
592 onSelectFormat
={onSelectFormat
}
593 onSelectGrammar
={onSelectGrammar
}
595 onDownload
={onDownload
}
596 currentlySelectedFormat
='mano'
597 currentlySelectedGrammar
='osm'
604 export default alt
.createStore(CatalogDataStore
, 'CatalogDataStore');