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'
42 catalogItemExportFormats
: ['mano', 'rift'],
43 catalogItemExportGrammars
: ['osm', 'tosca']
46 const areCatalogItemsMetaDataEqual = function (a
, b
) {
47 const metaProps
= ['id', 'name', 'short-name', 'description', 'vendor', 'version'];
48 const aMetaData
= _pick(a
, metaProps
);
49 const bMetaData
= _pick(b
, metaProps
);
50 return _isEqual(aMetaData
, bMetaData
);
53 function createItem (type
) {
54 let newItem
= DescriptorModelMetaFactory
.createModelInstanceForType(type
);
57 UID
.assignUniqueId(newItem
.uiState
);
58 newItem
.uiState
.isNew
= true;
59 newItem
.uiState
.modified
= true;
60 newItem
.uiState
['instance-ref-count'] = 0;
65 class CatalogDataStore
{
68 this.catalogs
= defaults
.catalogs
;
69 this.isLoading
= true;
70 this.requiresSave
= false;
72 this.selectedFormat
= defaults
.catalogItemExportFormats
[0];
73 this.selectedGrammar
= defaults
.catalogItemExportGrammars
[0];
74 this.registerAsync(CatalogDataSource
);
75 this.bindActions(CatalogDataSourceActions
);
76 this.bindActions(CatalogItemsActions
);
77 this.exportPublicMethods({
78 getCatalogs
: this.getCatalogs
,
79 getCatalogItemById
: this.getCatalogItemById
,
80 getCatalogItemByUid
: this.getCatalogItemByUid
,
81 getTransientCatalogs
: this.getTransientCatalogs
,
82 getTransientCatalogItemById
: this.getTransientCatalogItemById
,
83 getTransientCatalogItemByUid
: this.getTransientCatalogItemByUid
87 resetSelectionState
= () => {
88 this.selectedFormat
= defaults
.catalogItemExportFormats
[0];
89 this.selectedGrammar
= defaults
.catalogItemExportGrammars
[0];
93 return this.catalogs
|| (this.catalogs
= []);
96 getTransientCatalogs() {
97 return this.state
.catalogs
|| (this.state
.catalogs
= []);
100 getAllSelectedCatalogItems() {
101 return this.getCatalogs().reduce((r
, d
) => {
102 d
.descriptors
.forEach(d
=> {
103 if (SelectionManager
.isSelected(d
) /*d.uiState.selected*/) {
111 getFirstSelectedCatalogItem() {
112 return this.getCatalogs().reduce((r
, catalog
) => {
113 return r
.concat(catalog
.descriptors
.filter(d
=> SelectionManager
.isSelected(d
) /*d.uiState.selected*/));
117 getCatalogItemById(id
) {
118 return this.getCatalogs().reduce((r
, catalog
) => {
119 return r
.concat(catalog
.descriptors
.filter(d
=> d
.id
=== id
));
123 getTransientCatalogItemById(id
) {
124 return this.getTransientCatalogs().reduce((r
, catalog
) => {
125 return r
.concat(catalog
.descriptors
.filter(d
=> d
.id
=== id
));
129 getCatalogItemByUid(uid
) {
130 return this.getCatalogs().reduce((r
, catalog
) => {
131 return r
.concat(catalog
.descriptors
.filter(d
=> UID
.from(d
) === uid
));
135 getTransientCatalogItemByUid(uid
) {
136 return this.getTransientCatalogs().reduce((r
, catalog
) => {
137 return r
.concat(catalog
.descriptors
.filter(d
=> UID
.from(d
) === uid
));
141 removeCatalogItem(deleteItem
= {}) {
142 this.getCatalogs().map(catalog
=> {
143 catalog
.descriptors
= catalog
.descriptors
.filter(d
=> d
.id
!== deleteItem
.id
);
148 addNewItemToCatalog(newItem
) {
149 const type
= newItem
.uiState
.type
;
150 this.getCatalogs().filter(d
=> d
.type
=== type
).forEach(catalog
=> {
151 catalog
.descriptors
.push(newItem
);
153 // update indexes and integrate new model into catalog
154 this.updateCatalogIndexes(this.getCatalogs());
155 return this.getCatalogItemById(newItem
.id
);
158 updateCatalogIndexes(catalogs
) {
159 // associate catalog identity with individual descriptors so we can
160 // update the catalog when any given descriptor is updated also add
161 // vnfd model to the nsd object to make it easier to render an nsd
162 const vnfdLookup
= {};
163 const updatedCatalogs
= catalogs
.map(catalog
=> {
164 catalog
.descriptors
.map(descriptor
=> {
165 if (typeof descriptor
.meta
=== 'string' && descriptor
.meta
.trim() !== '') {
167 descriptor
.uiState
= JSON
.parse(descriptor
.meta
);
169 console
.warn('Unable to deserialize the uiState property.');
171 } else if (typeof descriptor
.meta
=== 'object') {
172 descriptor
.uiState
= descriptor
.meta
;
173 descriptor
.meta
= JSON
.stringify(descriptor
.meta
);
176 const uiState
= descriptor
.uiState
|| (descriptor
.uiState
= {});
177 uiState
.catalogId
= catalog
.id
;
178 uiState
.catalogName
= catalog
.name
;
179 uiState
.type
= catalog
.type
;
180 if (!UID
.hasUniqueId(uiState
)) {
181 UID
.assignUniqueId(uiState
);
183 if (catalog
.type
=== 'vnfd') {
184 vnfdLookup
[descriptor
.id
] = descriptor
;
190 updatedCatalogs
.filter(d
=> d
.type
=== 'nsd').forEach(catalog
=> {
191 catalog
.descriptors
= catalog
.descriptors
.map(descriptor
=> {
192 const instanceRefCount
= parseInt(descriptor
.uiState
['instance-ref-count'], 10);
193 if (descriptor
['constituent-vnfd']) {
194 descriptor
.vnfd
= descriptor
['constituent-vnfd'].map(d
=> {
195 const vnfdId
= d
['vnfd-id-ref'];
196 const vnfd
= vnfdLookup
[vnfdId
];
198 throw new ReferenceError('no VNFD found in the VNFD Catalog for the constituent-vnfd: ' + d
);
200 if (!isNaN(instanceRefCount
) && instanceRefCount
> 0) {
201 // this will notify user that this item cannot be updated when/if they make a change to it
202 vnfd
.uiState
['instance-ref-count'] = instanceRefCount
;
204 // create an instance of this vnfd to carry transient ui state properties
205 const instance
= _cloneDeep(vnfd
);
206 instance
.uiState
['member-vnf-index'] = d
['member-vnf-index'];
207 instance
['vnf-configuration'] = d
['vnf-configuration'];
208 instance
['start-by-default'] = d
['start-by-default'];
215 return updatedCatalogs
;
218 updateCatalogItem(item
) {
219 // replace the given item in the catalog
220 const catalogs
= this.getCatalogs().map(catalog
=> {
221 if (catalog
.id
=== item
.uiState
.catalogId
) {
222 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
223 if (d
.id
=== item
.id
) {
231 this.setState({catalogs
: catalogs
});
234 mergeEditsIntoLatestFromServer(catalogsFromServer
= []) {
236 // if the UI has modified items use them instead of the server items
238 const currentData
= this.getCatalogs();
240 const modifiedItemsMap
= currentData
.reduce((result
, catalog
) => {
241 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
.modified
));
242 }, []).reduce((r
, d
) => {
243 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
;
247 const itemMetaMap
= currentData
.reduce((result
, catalog
) => {
248 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
));
249 }, []).reduce((r
, d
) => {
250 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
.uiState
;
254 const newItemsMap
= currentData
.reduce((result
, catalog
) => {
255 result
[catalog
.id
] = catalog
.descriptors
.filter(d
=> d
.uiState
.isNew
);
259 catalogsFromServer
.forEach(catalog
=> {
260 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
261 const key
= d
.uiState
.catalogId
+ '/' + d
.id
;
262 if (modifiedItemsMap
[key
]) {
263 // use local modified item instead of the server item
264 return modifiedItemsMap
[key
];
266 if (itemMetaMap
[key
]) {
267 Object
.assign(d
.uiState
, itemMetaMap
[key
]);
271 if (newItemsMap
[catalog
.id
]) {
272 catalog
.descriptors
= catalog
.descriptors
.concat(newItemsMap
[catalog
.id
]);
276 return catalogsFromServer
;
280 loadCatalogsSuccess(context
) {
281 const fromServer
= this.updateCatalogIndexes(context
.data
);
282 const catalogs
= this.mergeEditsIntoLatestFromServer(fromServer
);
289 deleteCatalogItemSuccess (response
) {
290 let catalogType
= response
.catalogType
;
291 let itemId
= response
.itemId
;
292 const catalogs
= this.getCatalogs().map(catalog
=> {
293 if (catalog
.type
=== catalogType
) {
294 catalog
.descriptors
= catalog
.descriptors
.filter(d
=> d
.id
!== itemId
);
299 this.setState({catalogs
: catalogs
});
302 deleteCatalogItemError (data
) {
303 console
.log('Unable to delete', data
.catalogType
, 'id:', data
.itemId
, 'Error:', data
.error
.responseText
);
304 ComposerAppActions
.showError
.defer({
305 errorMessage
: 'Unable to delete ' + data
.catalogType
+ ' id: ' + data
.itemId
+ '. Check if it is in use'
309 selectCatalogItem(item
= {}) {
310 SelectionManager
.select(item
);
313 catalogItemMetaDataChanged(item
) {
314 let requiresSave
= false;
315 const catalogs
= this.getCatalogs().map(catalog
=> {
316 if (catalog
.id
=== item
.uiState
.catalogId
) {
317 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
318 if (d
.id
=== item
.id
) {
319 // compare just the catalog uiState data (id, name, short-name, description, etc.)
320 const modified
= !areCatalogItemsMetaDataEqual(d
, item
);
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 item
.uiState
.modified
= modified
;
331 this.addSnapshot(item
);
342 this.setState({catalogs
: catalogs
, requiresSave
: true});
346 catalogItemDescriptorChanged(itemDescriptor
) {
347 // when a descriptor object is modified in the canvas we have to update the catalog
348 const catalogId
= itemDescriptor
.uiState
.catalogId
;
349 const catalogs
= this.getCatalogs().map(catalog
=> {
350 if (catalog
.id
=== catalogId
) {
352 const descriptorId
= itemDescriptor
.id
;
353 // replace the old descriptor with the updated one
354 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
355 if (d
.id
=== descriptorId
) {
356 if (d
.uiState
['instance-ref-count'] > 0) {
357 console
.log('cannot edit NSD/VNFD with references to instantiated Network Services');
358 ComposerAppActions
.showError
.defer({
359 errorMessage
: 'Cannot edit NSD/VNFD with references to instantiated Network Services'
361 return _cloneDeep(d
);
363 itemDescriptor
.model
.uiState
.modified
= true;
364 this.addSnapshot(itemDescriptor
.model
);
365 return itemDescriptor
.model
;
373 this.setState({catalogs
: catalogs
, requiresSave
: true})
376 deleteSelectedCatalogItem() {
377 SelectionManager
.getSelections().forEach(selectedId
=> {
378 const item
= this.getCatalogItemByUid(selectedId
);
380 this.deleteCatalogItem(item
);
383 SelectionManager
.clearSelectionAndRemoveOutline();
386 deleteCatalogItem(item
) {
387 const snapshot
= JSON
.stringify(item
);
388 function confirmDeleteCancel(event
) {
390 event
.preventDefault();
391 ModalOverlayActions
.hideModalOverlay();
393 const remove
= () => {
394 // item is deleted or does not exist on server, so remove from ui
395 this.removeCatalogItem(item
);
396 this.setState({catalogs
: this.getCatalogs()});
397 const activeItem
= ComposerAppStore
.getState().item
;
398 if (activeItem
&& activeItem
.id
=== item
.id
) {
399 CatalogItemsActions
.editCatalogItem
.defer(null);
401 ModalOverlayActions
.hideModalOverlay();
404 // item failed to delete on server so revert ui
405 const revertTo
= JSON
.parse(snapshot
);
406 this.updateCatalogItem(revertTo
);
407 const activeItem
= ComposerAppStore
.getState().item
;
408 if (activeItem
&& activeItem
.id
=== revertTo
.id
) {
409 SelectionManager
.select(activeItem
);
410 CatalogItemsActions
.editCatalogItem
.defer(revertTo
);
411 SelectionManager
.refreshOutline();
415 if (item
.uiState
.isNew
) {
416 CatalogDataStore
.confirmDelete(remove
, confirmDeleteCancel
);
418 if (item
.uiState
['instance-ref-count'] > 0) {
419 console
.log('cannot delete NSD/VNFD with references to instantiated Network Services');
420 ComposerAppActions
.showError
.defer({
421 errorMessage
: 'Cannot delete NSD/VNFD with references to instantiated Network Services'
425 const confirmDeleteOK
= event
=> {
426 event
.preventDefault();
427 item
.uiState
.deleted
= true;
428 this.setState({catalogs
: this.getCatalogs()});
429 ModalOverlayActions
.showModalOverlay
.defer();
430 this.getInstance().deleteCatalogItem(item
.uiState
.type
, item
.id
)
432 .then(ModalOverlayActions
.hideModalOverlay
, ModalOverlayActions
.hideModalOverlay
)
434 console
.log('overcoming ES6 unhandled rejection red herring');
437 CatalogDataStore
.confirmDelete(confirmDeleteOK
, confirmDeleteCancel
);
443 static confirmDelete(onClickYes
, onClickCancel
) {
444 ModalOverlayActions
.showModalOverlay
.defer((
445 <div className
="actions panel">
446 <div className
="panel-header">
447 <h1
>Delete the selected catalog item
?</h1
>
449 <div className
="panel-body">
450 <a className
="action confirm-yes primary-action Button" onClick
={onClickYes
}>Yes
, delete selected catalog item
</a
>
451 <a className
="action cancel secondary-action Button" onClick
={onClickCancel
}>No
, cancel
</a
>
457 createCatalogItem(type
= 'nsd') {
458 const newItem
= createItem(type
);
459 this.saveItem(newItem
)
462 duplicateSelectedCatalogItem() {
463 // make request to backend to duplicate an item
464 const srcItem
= this.getFirstSelectedCatalogItem();
466 CatalogPackageManagerActions
.copyCatalogPackage
.defer(srcItem
);
472 if (!this.snapshots
[item
.id
]) {
473 this.snapshots
[item
.id
] = [];
475 this.snapshots
[item
.id
].push(JSON
.stringify(item
));
479 resetSnapshots(item
) {
481 this.snapshots
[item
.id
] = [];
482 this.addSnapshot(item
);
486 editCatalogItem(item
) {
488 this.addSnapshot(item
);
489 // replace the given item in the catalog
490 const catalogs
= this.getCatalogs().map(catalog
=> {
491 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
492 // note only one item can be "open" at a time
493 // so remove the flag from all the other items
494 d
.uiState
.isOpenForEdit
= (d
.id
=== item
.id
);
495 if (d
.uiState
.isOpenForEdit
) {
502 this.setState({catalogs
: catalogs
});
503 this.catalogItemMetaDataChanged(item
);
507 cancelCatalogItemChanges() {
508 const activeItem
= ComposerAppStore
.getState().item
;
510 const snapshots
= this.snapshots
[activeItem
.id
];
511 if (snapshots
.length
) {
512 const revertTo
= JSON
.parse(snapshots
[0]);
513 this.updateCatalogItem(revertTo
);
514 // TODO should the cancel action clear the undo/redo stack back to the beginning?
515 this.resetSnapshots(revertTo
);
516 this.setState({requiresSave
: false});
517 CatalogItemsActions
.editCatalogItem
.defer(revertTo
);
523 const activeItem
= ComposerAppStore
.getState().item
;
525 this.saveItem(activeItem
);
531 if (item
.uiState
['instance-ref-count'] > 0) {
532 console
.log('cannot save NSD/VNFD with references to instantiated Network Services');
533 ComposerAppActions
.showError
.defer({
534 errorMessage
: 'Cannot save NSD/VNFD with references to instantiated Network Services'
538 const success
= () => {
539 delete item
.uiState
.modified
;
540 if (item
.uiState
.isNew
) {
541 this.addNewItemToCatalog(item
);
542 delete item
.uiState
.isNew
;
544 this.updateCatalogItem(item
);
546 // TODO should the save action clear the undo/redo stack back to the beginning?
547 this.resetSnapshots(item
);
548 ModalOverlayActions
.hideModalOverlay
.defer();
549 CatalogItemsActions
.editCatalogItem
.defer(item
);
551 const failure
= () => {
552 ModalOverlayActions
.hideModalOverlay
.defer();
553 CatalogItemsActions
.editCatalogItem
.defer(item
);
555 const exception
= () => {
556 console
.warn('unable to save catalog item', item
);
557 ModalOverlayActions
.hideModalOverlay
.defer();
558 CatalogItemsActions
.editCatalogItem
.defer(item
);
560 ModalOverlayActions
.showModalOverlay
.defer();
561 this.getInstance().saveCatalogItem(item
).then(success
, failure
).catch(exception
);
565 exportSelectedCatalogItems(draggedItem
) {
566 // collect the selected items and delegate to the catalog package manager action creator
567 const selectedItems
= this.getAllSelectedCatalogItems();
568 if (selectedItems
.length
) {
569 CatalogPackageManagerActions
.downloadCatalogPackage
.defer({
570 selectedItems
: selectedItems
,
571 selectedFormat
: 'mano',
572 selectedGrammar
: 'osm'
574 this.resetSelectionState();
577 saveCatalogItemError(data
){
578 let error
= JSON
.parse(data
.error
.responseText
);
579 const errorMsg
= error
&& error
.body
&& error
.body
['rpc-reply'] && JSON
.stringify(error
.body
['rpc-reply']['rpc-error'], null, ' ')
580 ComposerAppActions
.showError
.defer({
581 errorMessage
: 'Unable to save the descriptor.\n' + errorMsg
586 export default alt
.createStore(CatalogDataStore
, 'CatalogDataStore');