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;
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 if (descriptor
['constituent-vnfd']) {
192 descriptor
.vnfd
= descriptor
['constituent-vnfd'].map(d
=> {
193 const vnfdId
= d
['vnfd-id-ref'];
194 const vnfd
= vnfdLookup
[vnfdId
];
196 throw new ReferenceError('no VNFD found in the VNFD Catalog for the constituent-vnfd: ' + d
);
198 // create an instance of this vnfd to carry transient ui state properties
199 const instance
= _cloneDeep(vnfd
);
200 instance
.uiState
['member-vnf-index'] = d
['member-vnf-index'];
201 instance
['vnf-configuration'] = d
['vnf-configuration'];
202 instance
['start-by-default'] = d
['start-by-default'];
209 return updatedCatalogs
;
212 updateCatalogItem(item
) {
213 // replace the given item in the catalog
214 const catalogs
= this.getCatalogs().map(catalog
=> {
215 if (catalog
.id
=== item
.uiState
.catalogId
) {
216 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
217 if (d
.id
=== item
.id
) {
225 this.setState({catalogs
: catalogs
});
228 mergeEditsIntoLatestFromServer(catalogsFromServer
= []) {
230 // if the UI has modified items use them instead of the server items
232 const currentData
= this.getCatalogs();
234 const modifiedItemsMap
= currentData
.reduce((result
, catalog
) => {
235 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
.modified
));
236 }, []).reduce((r
, d
) => {
237 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
;
241 const itemMetaMap
= currentData
.reduce((result
, catalog
) => {
242 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
));
243 }, []).reduce((r
, d
) => {
244 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
.uiState
;
248 const newItemsMap
= currentData
.reduce((result
, catalog
) => {
249 result
[catalog
.id
] = catalog
.descriptors
.filter(d
=> d
.uiState
.isNew
);
253 catalogsFromServer
.forEach(catalog
=> {
254 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
255 const key
= d
.uiState
.catalogId
+ '/' + d
.id
;
256 if (modifiedItemsMap
[key
]) {
257 // use local modified item instead of the server item
258 return modifiedItemsMap
[key
];
260 if (itemMetaMap
[key
]) {
261 Object
.assign(d
.uiState
, itemMetaMap
[key
]);
265 if (newItemsMap
[catalog
.id
]) {
266 catalog
.descriptors
= catalog
.descriptors
.concat(newItemsMap
[catalog
.id
]);
270 return catalogsFromServer
;
274 loadCatalogsSuccess(context
) {
275 const fromServer
= this.updateCatalogIndexes(context
.data
);
276 const catalogs
= this.mergeEditsIntoLatestFromServer(fromServer
);
283 deleteCatalogItemSuccess (response
) {
284 let catalogType
= response
.catalogType
;
285 let itemId
= response
.itemId
;
286 const catalogs
= this.getCatalogs().map(catalog
=> {
287 if (catalog
.type
=== catalogType
) {
288 catalog
.descriptors
= catalog
.descriptors
.filter(d
=> d
.id
!== itemId
);
293 this.setState({catalogs
: catalogs
});
296 deleteCatalogItemError (data
) {
297 console
.log('Unable to delete', data
.catalogType
, 'id:', data
.itemId
, 'Error:', data
.error
.responseText
);
298 ComposerAppActions
.showError
.defer({
299 errorMessage
: 'Unable to delete ' + data
.catalogType
+ ' id: ' + data
.itemId
+ '. Check if it is in use'
303 selectCatalogItem(item
= {}) {
304 SelectionManager
.select(item
);
307 catalogItemMetaDataChanged(item
) {
308 let requiresSave
= false;
309 const catalogs
= this.getCatalogs().map(catalog
=> {
310 if (catalog
.id
=== item
.uiState
.catalogId
) {
311 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
312 if (d
.id
=== item
.id
) {
313 // compare just the catalog uiState data (id, name, short-name, description, etc.)
314 const modified
= !areCatalogItemsMetaDataEqual(d
, item
);
316 item
.uiState
.modified
= modified
;
318 this.addSnapshot(item
);
328 this.setState({catalogs
: catalogs
, requiresSave
: true});
332 catalogItemDescriptorChanged(itemDescriptor
) {
333 // when a descriptor object is modified in the canvas we have to update the catalog
334 const catalogId
= itemDescriptor
.uiState
.catalogId
;
335 const catalogs
= this.getCatalogs().map(catalog
=> {
336 if (catalog
.id
=== catalogId
) {
338 const descriptorId
= itemDescriptor
.id
;
339 // replace the old descriptor with the updated one
340 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
341 if (d
.id
=== descriptorId
) {
342 itemDescriptor
.model
.uiState
.modified
= true;
343 this.addSnapshot(itemDescriptor
.model
);
344 return itemDescriptor
.model
;
351 this.setState({catalogs
: catalogs
, requiresSave
: true})
354 deleteSelectedCatalogItem() {
355 SelectionManager
.getSelections().forEach(selectedId
=> {
356 const item
= this.getCatalogItemByUid(selectedId
);
358 this.deleteCatalogItem(item
);
361 SelectionManager
.clearSelectionAndRemoveOutline();
364 deleteCatalogItem(item
) {
365 const snapshot
= JSON
.stringify(item
);
366 function confirmDeleteCancel(event
) {
368 event
.preventDefault();
369 ModalOverlayActions
.hideModalOverlay();
371 const remove
= () => {
372 // item is deleted or does not exist on server, so remove from ui
373 this.removeCatalogItem(item
);
374 this.setState({catalogs
: this.getCatalogs()});
375 const activeItem
= ComposerAppStore
.getState().item
;
376 if (activeItem
&& activeItem
.id
=== item
.id
) {
377 CatalogItemsActions
.editCatalogItem
.defer(null);
379 ModalOverlayActions
.hideModalOverlay();
382 // item failed to delete on server so revert ui
383 const revertTo
= JSON
.parse(snapshot
);
384 this.updateCatalogItem(revertTo
);
385 const activeItem
= ComposerAppStore
.getState().item
;
386 if (activeItem
&& activeItem
.id
=== revertTo
.id
) {
387 SelectionManager
.select(activeItem
);
388 CatalogItemsActions
.editCatalogItem
.defer(revertTo
);
389 SelectionManager
.refreshOutline();
393 if (item
.uiState
.isNew
) {
394 CatalogDataStore
.confirmDelete(remove
, confirmDeleteCancel
);
396 const confirmDeleteOK
= event
=> {
397 event
.preventDefault();
398 item
.uiState
.deleted
= true;
399 this.setState({catalogs
: this.getCatalogs()});
400 ModalOverlayActions
.showModalOverlay
.defer();
401 this.getInstance().deleteCatalogItem(item
.uiState
.type
, item
.id
)
403 .then(ModalOverlayActions
.hideModalOverlay
, ModalOverlayActions
.hideModalOverlay
)
405 console
.log('overcoming ES6 unhandled rejection red herring');
408 CatalogDataStore
.confirmDelete(confirmDeleteOK
, confirmDeleteCancel
);
413 static confirmDelete(onClickYes
, onClickCancel
) {
414 ModalOverlayActions
.showModalOverlay
.defer((
415 <div className
="actions panel">
416 <div className
="panel-header">
417 <h1
>Delete the selected catalog item
?</h1
>
419 <div className
="panel-body">
420 <a className
="action confirm-yes primary-action Button" onClick
={onClickYes
}>Yes
, delete selected catalog item
</a
>
421 <a className
="action cancel secondary-action Button" onClick
={onClickCancel
}>No
, cancel
</a
>
427 createCatalogItem(type
= 'nsd') {
428 const newItem
= createItem(type
);
429 this.saveItem(newItem
)
432 duplicateSelectedCatalogItem() {
433 // make request to backend to duplicate an item
434 const srcItem
= this.getFirstSelectedCatalogItem();
436 CatalogPackageManagerActions
.copyCatalogPackage
.defer(srcItem
);
442 if (!this.snapshots
[item
.id
]) {
443 this.snapshots
[item
.id
] = [];
445 this.snapshots
[item
.id
].push(JSON
.stringify(item
));
449 resetSnapshots(item
) {
451 this.snapshots
[item
.id
] = [];
452 this.addSnapshot(item
);
456 editCatalogItem(item
) {
458 this.addSnapshot(item
);
459 // replace the given item in the catalog
460 const catalogs
= this.getCatalogs().map(catalog
=> {
461 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
462 // note only one item can be "open" at a time
463 // so remove the flag from all the other items
464 d
.uiState
.isOpenForEdit
= (d
.id
=== item
.id
);
465 if (d
.uiState
.isOpenForEdit
) {
472 this.setState({catalogs
: catalogs
});
473 this.catalogItemMetaDataChanged(item
);
477 cancelCatalogItemChanges() {
478 const activeItem
= ComposerAppStore
.getState().item
;
480 const snapshots
= this.snapshots
[activeItem
.id
];
481 if (snapshots
.length
) {
482 const revertTo
= JSON
.parse(snapshots
[0]);
483 this.updateCatalogItem(revertTo
);
484 // TODO should the cancel action clear the undo/redo stack back to the beginning?
485 this.resetSnapshots(revertTo
);
486 this.setState({requiresSave
: false});
487 CatalogItemsActions
.editCatalogItem
.defer(revertTo
);
493 const activeItem
= ComposerAppStore
.getState().item
;
495 this.saveItem(activeItem
);
501 const success
= () => {
502 delete item
.uiState
.modified
;
503 if (item
.uiState
.isNew
) {
504 this.addNewItemToCatalog(item
);
505 delete item
.uiState
.isNew
;
507 this.updateCatalogItem(item
);
509 // TODO should the save action clear the undo/redo stack back to the beginning?
510 this.resetSnapshots(item
);
511 ModalOverlayActions
.hideModalOverlay
.defer();
512 CatalogItemsActions
.editCatalogItem
.defer(item
);
514 const failure
= () => {
515 ModalOverlayActions
.hideModalOverlay
.defer();
516 CatalogItemsActions
.editCatalogItem
.defer(item
);
518 const exception
= () => {
519 console
.warn('unable to save catalog item', item
);
520 ModalOverlayActions
.hideModalOverlay
.defer();
521 CatalogItemsActions
.editCatalogItem
.defer(item
);
523 ModalOverlayActions
.showModalOverlay
.defer();
524 this.getInstance().saveCatalogItem(item
).then(success
, failure
).catch(exception
);
528 exportSelectedCatalogItems(draggedItem
) {
529 // collect the selected items and delegate to the catalog package manager action creator
530 const selectedItems
= this.getAllSelectedCatalogItems();
531 if (selectedItems
.length
) {
532 CatalogPackageManagerActions
.downloadCatalogPackage
.defer({
533 selectedItems
: selectedItems
,
534 selectedFormat
: 'mano',
535 selectedGrammar
: 'osm'
537 this.resetSelectionState();
540 saveCatalogItemError(data
){
541 let error
= JSON
.parse(data
.error
.responseText
);
542 const errorMsg
= error
&& error
.body
&& error
.body
['rpc-reply'] && JSON
.stringify(error
.body
['rpc-reply']['rpc-error'], null, ' ')
543 ComposerAppActions
.showError
.defer({
544 errorMessage
: 'Unable to save the descriptor.\n' + errorMsg
549 export default alt
.createStore(CatalogDataStore
, 'CatalogDataStore');