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 _merge
from 'lodash/merge'
25 import _debounce
from 'lodash/debounce';
26 import cc
from 'change-case'
27 import alt
from '../alt'
28 import UID
from '../libraries/UniqueId'
29 import guid
from '../libraries/guid'
30 import React
from 'react'
31 import DescriptorModel
from '../libraries/model/DescriptorModel'
32 import DescriptorModelMetaFactory
from '../libraries/model/DescriptorModelMetaFactory'
33 import DescriptorModelFactory
from '../libraries/model/DescriptorModelFactory'
34 import CatalogPackageManagerActions
from '../actions/CatalogPackageManagerActions'
35 import CatalogDataSourceActions
from '../actions/CatalogDataSourceActions'
36 import CatalogItemsActions
from '../actions/CatalogItemsActions'
37 import ModalOverlayActions
from '../actions/ModalOverlayActions'
38 import ComposerAppActions
from '../actions/ComposerAppActions'
39 import CatalogDataSource
from '../sources/CatalogDataSource'
40 import ComposerAppStore
from '../stores/ComposerAppStore'
41 import SelectionManager
from '../libraries/SelectionManager'
45 catalogItemExportFormats
: ['mano', 'rift'],
46 catalogItemExportGrammars
: ['osm', 'tosca']
49 const areCatalogItemsMetaDataEqual = function (catItem
, activeItem
) {
50 function getDefaultPositionMap() {
51 if (!activeItem
.uiState
.containerPositionMap
) {
52 return activeItem
.uiState
.containerPositionMap
;
54 let defaultPositionMap
= {};
55 defaultPositionMap
[activeItem
.id
] = activeItem
.uiState
.defaultLayoutPosition
;
56 return defaultPositionMap
;
58 const activeItemMetaData
= activeItem
.uiState
.containerPositionMap
;
59 const catItemMetaData
= catItem
.uiState
.containerPositionMap
;
60 return catItemMetaData
=== undefined || _isEqual(catItemMetaData
, activeItemMetaData
);
63 function createItem(type
) {
64 let newItem
= DescriptorModelMetaFactory
.createModelInstanceForType(type
);
67 UID
.assignUniqueId(newItem
);
68 newItem
.uiState
.isNew
= true;
69 newItem
.uiState
.modified
= true;
74 class CatalogDataStore
{
77 this.catalogs
= defaults
.catalogs
;
78 this.isLoading
= true;
80 this.selectedFormat
= defaults
.catalogItemExportFormats
[0];
81 this.selectedGrammar
= defaults
.catalogItemExportGrammars
[0];
82 this.registerAsync(CatalogDataSource
);
83 this.bindActions(CatalogDataSourceActions
);
84 this.bindActions(CatalogItemsActions
);
85 this.exportPublicMethods({
86 getCatalogs
: this.getCatalogs
,
87 getCatalogItemById
: this.getCatalogItemById
,
88 getCatalogItemByUid
: this.getCatalogItemByUid
,
89 getTransientCatalogs
: this.getTransientCatalogs
,
90 getTransientCatalogItemById
: this.getTransientCatalogItemById
,
91 getTransientCatalogItemByUid
: this.getTransientCatalogItemByUid
,
92 setUserProfile
: this.setUserProfile
94 this.queueDirtyCheck
= _debounce(() => this.saveDirtyDescriptorsToSessionStorage(), 500);
97 resetSelectionState
= () => {
98 this.selectedFormat
= defaults
.catalogItemExportFormats
[0];
99 this.selectedGrammar
= defaults
.catalogItemExportGrammars
[0];
103 return this.catalogs
|| (this.catalogs
= []);
106 saveDirtyDescriptorsToSessionStorage() {
107 const dirtyCatalogs
= this.catalogs
.reduce((result
, catalog
) => {
108 const dirtyDescriptors
= catalog
.descriptors
.reduce((result
, descriptor
) => {
109 if (descriptor
.uiState
.modified
&& !descriptor
.uiState
.deleted
) {
110 result
.push(descriptor
);
114 if (dirtyDescriptors
.length
) {
115 let newCatalog
= Object
.assign({}, catalog
);
116 newCatalog
.descriptors
= dirtyDescriptors
;
117 result
.push(newCatalog
);
121 window
.sessionStorage
.setItem(this.userProfile
.userId
+ '@' + this.userProfile
.domain
, JSON
.stringify({
126 mergeDirtyDescriptorsFromSessionStorage(catalogs
) {
127 let userProfileDirtyCatalogs
= window
.sessionStorage
.getItem(this.userProfile
.userId
+ '@' + this.userProfile
.domain
);
128 let dirtyCatalogs
= [];
129 if (userProfileDirtyCatalogs
) {
130 dirtyCatalogs
= JSON
.parse(userProfileDirtyCatalogs
).dirtyCatalogs
;
132 dirtyCatalogs
.forEach((dirtyCatalog
) => {
133 let catalog
= catalogs
.find((c
) => c
.id
=== dirtyCatalog
.id
);
134 dirtyCatalog
.descriptors
.forEach((dirtyDescriptor
, index
) => {
135 let descriptor
= catalog
.descriptors
.find((d
) => d
.id
=== dirtyDescriptor
.id
);
137 this.addSnapshot(descriptor
);
138 _merge(descriptor
, dirtyDescriptor
);
140 dirtyCatalog
.descriptors
.splice(index
, 1);
141 this.queueDirtyCheck();
145 this.isNotMergedWithSessionStorage
= false;
149 setUserProfile
= (userProfile
) => {
150 if (!this.userProfile
) {
151 this.userProfile
= userProfile
;
152 if (this.catalogs
.length
) {
153 const catalogs
= this.mergeDirtyDescriptorsFromSessionStorage(this.catalogs
);
154 this.setState({ catalogs
});
156 this.isNotMergedWithSessionStorage
= true;
161 getTransientCatalogs() {
162 return this.state
.catalogs
|| (this.state
.catalogs
= []);
165 getAllSelectedCatalogItems() {
166 return this.getCatalogs().reduce((r
, d
) => {
167 d
.descriptors
.forEach(d
=> {
168 if (SelectionManager
.isSelected(d
) /*d.uiState.selected*/) {
176 getFirstSelectedCatalogItem() {
177 return this.getCatalogs().reduce((r
, catalog
) => {
178 return r
.concat(catalog
.descriptors
.filter(d
=> SelectionManager
.isSelected(d
) /*d.uiState.selected*/));
182 getCatalogItemById(id
) {
183 return this.getCatalogs().reduce((r
, catalog
) => {
184 return r
.concat(catalog
.descriptors
.filter(d
=> d
.id
=== id
));
188 getTransientCatalogItemById(id
) {
189 return this.getTransientCatalogs().reduce((r
, catalog
) => {
190 return r
.concat(catalog
.descriptors
.filter(d
=> d
.id
=== id
));
194 getCatalogItemByUid(uid
) {
195 return this.getCatalogs().reduce((r
, catalog
) => {
196 return r
.concat(catalog
.descriptors
.filter(d
=> UID
.from(d
) === uid
));
200 getTransientCatalogItemByUid(uid
) {
201 return this.getTransientCatalogs().reduce((r
, catalog
) => {
202 return r
.concat(catalog
.descriptors
.filter(d
=> UID
.from(d
) === uid
));
206 removeCatalogItem(deleteItem
= {}) {
207 this.getCatalogs().map(catalog
=> {
208 catalog
.descriptors
= catalog
.descriptors
.filter(d
=> d
.id
!== deleteItem
.id
);
213 addNewItemToCatalog(newItem
) {
214 const type
= newItem
.uiState
.type
;
215 this.getCatalogs().filter(d
=> d
.type
=== type
).forEach(catalog
=> {
216 catalog
.descriptors
.push(newItem
);
218 // update indexes and integrate new model into catalog
219 this.updateCatalogIndexes(this.getCatalogs());
220 return this.getCatalogItemById(newItem
.id
);
223 updateCatalogIndexes(catalogs
) {
224 // associate catalog identity with individual descriptors so we can
225 // update the catalog when any given descriptor is updated also add
226 // vnfd model to the nsd object to make it easier to render an nsd
227 const vnfdLookup
= {};
228 const updatedCatalogs
= catalogs
.map(catalog
=> {
229 catalog
.descriptors
.map(descriptor
=> {
230 if (typeof descriptor
.meta
=== 'string' && descriptor
.meta
.trim() !== '') {
232 descriptor
.uiState
= JSON
.parse(descriptor
.meta
);
234 console
.warn('Unable to deserialize the uiState property.');
236 } else if (typeof descriptor
.meta
=== 'object') {
237 descriptor
.uiState
= descriptor
.meta
;
238 descriptor
.meta
= JSON
.stringify(descriptor
.meta
);
241 const uiState
= descriptor
.uiState
|| (descriptor
.uiState
= {});
242 uiState
.catalogId
= catalog
.id
;
243 uiState
.catalogName
= catalog
.name
;
244 uiState
.type
= catalog
.type
;
245 if (!UID
.hasUniqueId(uiState
)) {
246 UID
.assignUniqueId(uiState
);
248 if (catalog
.type
=== 'vnfd') {
249 vnfdLookup
[descriptor
.id
] = descriptor
;
255 updatedCatalogs
.filter(d
=> d
.type
=== 'nsd').forEach(catalog
=> {
256 catalog
.descriptors
= catalog
.descriptors
.map(descriptor
=> {
257 if (descriptor
['constituent-vnfd']) {
258 descriptor
.vnfd
= descriptor
['constituent-vnfd'].map(d
=> {
259 const vnfdId
= d
['vnfd-id-ref'];
260 const vnfd
= vnfdLookup
[vnfdId
];
262 throw new ReferenceError('no VNFD found in the VNFD Catalog for the constituent-vnfd: ' + d
);
264 // create an instance of this vnfd to carry transient ui state properties
265 const instance
= _cloneDeep(vnfd
);
266 instance
.uiState
['member-vnf-index'] = d
['member-vnf-index'];
267 instance
['vnf-configuration'] = d
['vnf-configuration'];
268 instance
['start-by-default'] = d
['start-by-default'];
275 return updatedCatalogs
;
278 updateCatalogItem(item
) {
279 // replace the given item in the catalog
280 const catalogs
= this.getCatalogs().map(catalog
=> {
281 if (catalog
.id
=== item
.uiState
.catalogId
) {
282 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
283 if (d
.id
=== item
.id
) {
291 this.setState({ catalogs
: catalogs
});
294 mergeEditsIntoLatestFromServer(catalogsFromServer
= []) {
296 // if the UI has modified items use them instead of the server items
298 const currentData
= this.getCatalogs();
300 const modifiedItemsMap
= currentData
.reduce((result
, catalog
) => {
301 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
.modified
));
302 }, []).reduce((r
, d
) => {
303 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
;
307 const itemMetaMap
= currentData
.reduce((result
, catalog
) => {
308 return result
.concat(catalog
.descriptors
.filter(d
=> d
.uiState
));
309 }, []).reduce((r
, d
) => {
310 r
[d
.uiState
.catalogId
+ '/' + d
.id
] = d
.uiState
;
314 const newItemsMap
= currentData
.reduce((result
, catalog
) => {
315 result
[catalog
.id
] = catalog
.descriptors
.filter(d
=> d
.uiState
.isNew
);
319 catalogsFromServer
.forEach(catalog
=> {
320 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
321 const key
= d
.uiState
.catalogId
+ '/' + d
.id
;
322 if (modifiedItemsMap
[key
]) {
323 // use local modified item instead of the server item
324 return modifiedItemsMap
[key
];
326 if (itemMetaMap
[key
]) {
327 Object
.assign(d
.uiState
, itemMetaMap
[key
]);
331 if (newItemsMap
[catalog
.id
]) {
332 catalog
.descriptors
= catalog
.descriptors
.concat(newItemsMap
[catalog
.id
]);
336 return catalogsFromServer
;
340 loadCatalogsSuccess(context
) {
341 const fromServer
= this.updateCatalogIndexes(context
.data
);
342 let catalogs
= this.mergeEditsIntoLatestFromServer(fromServer
);
343 if (this.isNotMergedWithSessionStorage
) {
344 catalogs
= this.mergeDirtyDescriptorsFromSessionStorage(catalogs
);
352 deleteCatalogItemSuccess(response
) {
353 let catalogType
= response
.catalogType
;
354 let itemId
= response
.itemId
;
355 const catalogs
= this.getCatalogs().map(catalog
=> {
356 if (catalog
.type
=== catalogType
) {
357 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
358 // We are just going to mark it as deleted here so it will be hidden from view.
359 // We will let the next catalog refresh actually remove it from the in memory store.
360 // This is to avoid having it reappear because a timing issue with a catalog refresh.
361 // The incoming refresh may still contain the item and it would then reappear till the next refresh.
362 if (d
.id
=== itemId
) {
363 d
.uiState
.deleted
= true;
364 const activeItem
= ComposerAppStore
.getState().item
;
365 if (activeItem
&& activeItem
.id
=== itemId
) {
366 ComposerAppActions
.showDescriptor
.defer();
367 CatalogItemsActions
.editCatalogItem
.defer(null);
375 this.setState({ catalogs
: catalogs
});
376 this.queueDirtyCheck();
379 deleteCatalogItemError(data
) {
380 console
.log('Unable to delete', data
.catalogType
, 'id:', data
.itemId
, 'Error:', data
.error
.responseText
);
381 ComposerAppActions
.showError
.defer({
382 errorMessage
: 'Unable to delete ' + data
.catalogType
+ ' id: ' + data
.itemId
+ '. Check to see if it is in use.'
386 selectCatalogItem(item
= {}) {
387 SelectionManager
.select(item
);
390 catalogItemMetaDataChanged(item
) {
391 let requiresSave
= false;
392 let previousVersion
= this.getLatestSnapshot(item
);
393 // compare just the catalog uiState data
394 const modified
= !areCatalogItemsMetaDataEqual(previousVersion
, item
);
396 item
.uiState
.modified
= true;
397 this.updateCatalogItem(item
);
398 this.addSnapshot(item
);
399 this.queueDirtyCheck();
403 catalogItemDescriptorChanged(itemDescriptor
) {
404 // when a descriptor object is modified in the canvas we have to update the catalog
405 const catalogId
= itemDescriptor
.uiState
.catalogId
;
406 const catalogs
= this.getCatalogs().map(catalog
=> {
407 if (catalog
.id
=== catalogId
) {
409 const descriptorId
= itemDescriptor
.id
;
410 // replace the old descriptor with the updated one
411 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
412 if (d
.id
=== descriptorId
) {
413 itemDescriptor
.model
.uiState
.modified
= true;
414 this.addSnapshot(itemDescriptor
.model
);
415 return itemDescriptor
.model
;
422 this.setState({ catalogs
: catalogs
})
423 this.queueDirtyCheck();
426 deleteSelectedCatalogItem() {
427 SelectionManager
.getSelections().forEach(selectedId
=> {
428 const item
= this.getCatalogItemByUid(selectedId
);
430 this.deleteCatalogItem(item
);
433 SelectionManager
.clearSelectionAndRemoveOutline();
436 deleteCatalogItem(item
) {
438 CatalogDataStore
.confirmDelete(event
=> {
439 event
.preventDefault();
440 ModalOverlayActions
.showModalOverlay
.defer();
441 this.getInstance().deleteCatalogItem(item
.uiState
.type
, item
.id
)
442 .then(ModalOverlayActions
.hideModalOverlay
, ModalOverlayActions
.hideModalOverlay
)
444 console
.log('overcoming ES6 unhandled rejection red herring');
450 static confirmDelete(onClickYes
, onClickCancel
) {
451 const cancelDelete
= onClickCancel
|| (e
=> ModalOverlayActions
.hideModalOverlay
.defer());
452 ModalOverlayActions
.showModalOverlay
.defer((
453 <div className
="actions panel">
454 <div className
="panel-header">
455 <h1
>Delete the selected catalog item
?</h1
>
457 <div className
="panel-body">
458 <a className
="action confirm-yes primary-action Button" onClick
={onClickYes
}>Yes
, delete selected catalog item
</a
>
459 <a className
="action cancel secondary-action Button" onClick
={cancelDelete
}>No
, cancel
</a
>
465 createCatalogItem(type
= 'nsd') {
466 const newItem
= createItem(type
);
467 this.saveItem(newItem
)
470 duplicateSelectedCatalogItem() {
471 // make request to backend to duplicate an item
472 const srcItem
= this.getFirstSelectedCatalogItem();
474 CatalogPackageManagerActions
.copyCatalogPackage
.defer(srcItem
);
480 if (!this.snapshots
[item
.id
]) {
481 this.snapshots
[item
.id
] = [];
483 // save the snapshot with a new id for an in memory instance
484 let uid
= UID
.from(item
);
485 UID
.assignUniqueId(item
);
486 this.snapshots
[item
.id
].push(JSON
.stringify(item
));
487 UID
.assignUniqueId(item
, uid
);
491 getLatestSnapshot(item
) {
492 if (this.snapshots
[item
.id
]) {
493 return JSON
.parse(this.snapshots
[item
.id
][this.snapshots
[item
.id
].length
- 1]);
495 this.getCatalogs().forEach(catalog
=> {
496 if (catalog
.id
=== item
.uiState
.catalogId
) {
497 catalog
.descriptors
.forEach(d
=> {
498 if (d
.id
=== item
.id
) {
507 resetSnapshots(item
) {
509 this.snapshots
[item
.id
] = [];
510 this.addSnapshot(item
);
514 editCatalogItem(item
) {
516 this.addSnapshot(item
);
517 // replace the given item in the catalog
518 const catalogs
= this.getCatalogs().map(catalog
=> {
519 catalog
.descriptors
= catalog
.descriptors
.map(d
=> {
520 // note only one item can be "open" at a time
521 // so remove the flag from all the other items
522 d
.uiState
.isOpenForEdit
= (d
.id
=== item
.id
);
523 if (d
.uiState
.isOpenForEdit
) {
530 this.setState({ catalogs
: catalogs
});
531 this.catalogItemMetaDataChanged(item
);
535 cancelCatalogItemChanges() {
536 const activeItem
= ComposerAppStore
.getState().item
;
538 const snapshots
= this.snapshots
[activeItem
.id
];
539 if (snapshots
.length
) {
540 const revertTo
= JSON
.parse(snapshots
[0]);
541 this.updateCatalogItem(revertTo
);
542 // TODO should the cancel action clear the undo/redo stack back to the beginning?
543 this.resetSnapshots(revertTo
);
544 CatalogItemsActions
.editCatalogItem
.defer(revertTo
);
545 this.queueDirtyCheck();
551 const activeItem
= ComposerAppStore
.getState().item
;
553 this.saveItem(activeItem
);
559 const success
= () => {
560 delete item
.uiState
.modified
;
561 if (item
.uiState
.isNew
) {
562 this.addNewItemToCatalog(item
);
563 delete item
.uiState
.isNew
;
565 this.updateCatalogItem(item
);
567 // TODO should the save action clear the undo/redo stack back to the beginning?
568 this.resetSnapshots(item
);
569 ModalOverlayActions
.hideModalOverlay
.defer();
570 CatalogItemsActions
.editCatalogItem
.defer(item
);
571 this.queueDirtyCheck();
573 const failure
= () => {
574 ModalOverlayActions
.hideModalOverlay
.defer();
575 CatalogItemsActions
.editCatalogItem
.defer(item
);
577 const exception
= () => {
578 console
.warn('unable to save catalog item', item
);
579 ModalOverlayActions
.hideModalOverlay
.defer();
580 CatalogItemsActions
.editCatalogItem
.defer(item
);
582 ModalOverlayActions
.showModalOverlay
.defer();
583 this.getInstance().saveCatalogItem(item
).then(success
, failure
).catch(exception
);
587 exportSelectedCatalogItems(draggedItem
) {
588 // collect the selected items and delegate to the catalog package manager action creator
589 const selectedItems
= this.getAllSelectedCatalogItems();
590 if (selectedItems
.length
) {
591 CatalogPackageManagerActions
.downloadCatalogPackage
.defer({
592 selectedItems
: selectedItems
,
593 selectedFormat
: 'mano',
594 selectedGrammar
: 'osm'
596 this.resetSelectionState();
599 saveCatalogItemError(data
) {
600 const descriptor
= this.getCatalogs().reduce((gotIt
, catalog
) => {
601 if (!gotIt
&& (catalog
.type
=== data
.catalogType
)) {
602 return catalog
.descriptors
.find(d
=> {
603 if (d
.id
=== data
.itemId
) {
610 const container
= data
.catalogType
=== 'nsd' ?
611 DescriptorModelFactory
.newNetworkService(descriptor
, null)
612 : DescriptorModelFactory
.newVirtualNetworkFunction(descriptor
, null)
613 ComposerAppActions
.showError
.defer({
614 errorMessage
: 'Unable to save the descriptor.',
615 rpcError
: data
.error
.responseText
617 ComposerAppActions
.recordDescriptorError
.defer({
618 descriptor
: container
,
619 type
: data
.catalogType
,
621 error
: data
.error
.responseText
626 export default alt
.createStore(CatalogDataStore
, 'CatalogDataStore');