update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b third try
[osm/UI.git] / skyquake / plugins / composer / src / src / stores / CatalogDataStore.js
1
2 /*
3 *
4 * Copyright 2016 RIFT.IO Inc
5 *
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
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
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.
17 *
18 */
19 'use strict';
20
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'
42
43 const defaults = {
44 catalogs: [],
45 catalogItemExportFormats: ['mano', 'rift'],
46 catalogItemExportGrammars: ['osm', 'tosca']
47 };
48
49 const areCatalogItemsMetaDataEqual = function (catItem, activeItem) {
50 function getDefaultPositionMap() {
51 if (!activeItem.uiState.containerPositionMap) {
52 return activeItem.uiState.containerPositionMap;
53 }
54 let defaultPositionMap = {};
55 defaultPositionMap[activeItem.id] = activeItem.uiState.defaultLayoutPosition;
56 return defaultPositionMap;
57 }
58 const activeItemMetaData = activeItem.uiState.containerPositionMap;
59 const catItemMetaData = catItem.uiState.containerPositionMap;
60 return catItemMetaData === undefined || _isEqual(catItemMetaData, activeItemMetaData);
61 };
62
63 function createItem(type) {
64 let newItem = DescriptorModelMetaFactory.createModelInstanceForType(type);
65 if (newItem) {
66 newItem.id = guid();
67 UID.assignUniqueId(newItem);
68 newItem.uiState.isNew = true;
69 newItem.uiState.modified = true;
70 }
71 return newItem;
72 }
73
74 class CatalogDataStore {
75
76 constructor() {
77 this.catalogs = defaults.catalogs;
78 this.isLoading = true;
79 this.snapshots = {};
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
93 });
94 this.queueDirtyCheck = _debounce(() => this.saveDirtyDescriptorsToSessionStorage(), 500);
95 }
96
97 resetSelectionState = () => {
98 this.selectedFormat = defaults.catalogItemExportFormats[0];
99 this.selectedGrammar = defaults.catalogItemExportGrammars[0];
100 }
101
102 getCatalogs() {
103 return this.catalogs || (this.catalogs = []);
104 }
105
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);
111 }
112 return result;
113 }, []);
114 if (dirtyDescriptors.length) {
115 let newCatalog = Object.assign({}, catalog);
116 newCatalog.descriptors = dirtyDescriptors;
117 result.push(newCatalog);
118 }
119 return result;
120 }, []);
121 window.sessionStorage.setItem(this.userProfile.userId + '@' + this.userProfile.domain, JSON.stringify({
122 dirtyCatalogs
123 }));
124 }
125
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;
131 }
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);
136 if (descriptor) {
137 this.addSnapshot(descriptor);
138 _merge(descriptor, dirtyDescriptor);
139 } else {
140 dirtyCatalog.descriptors.splice(index, 1);
141 this.queueDirtyCheck();
142 }
143 })
144 });
145 this.isNotMergedWithSessionStorage = false;
146 return catalogs;
147 }
148
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 });
155 } else {
156 this.isNotMergedWithSessionStorage = true;
157 }
158 }
159 }
160
161 getTransientCatalogs() {
162 return this.state.catalogs || (this.state.catalogs = []);
163 }
164
165 getAllSelectedCatalogItems() {
166 return this.getCatalogs().reduce((r, d) => {
167 d.descriptors.forEach(d => {
168 if (SelectionManager.isSelected(d) /*d.uiState.selected*/) {
169 r.push(d);
170 }
171 });
172 return r;
173 }, []);
174 }
175
176 getFirstSelectedCatalogItem() {
177 return this.getCatalogs().reduce((r, catalog) => {
178 return r.concat(catalog.descriptors.filter(d => SelectionManager.isSelected(d) /*d.uiState.selected*/));
179 }, [])[0];
180 }
181
182 getCatalogItemById(id) {
183 return this.getCatalogs().reduce((r, catalog) => {
184 return r.concat(catalog.descriptors.filter(d => d.id === id));
185 }, [])[0];
186 }
187
188 getTransientCatalogItemById(id) {
189 return this.getTransientCatalogs().reduce((r, catalog) => {
190 return r.concat(catalog.descriptors.filter(d => d.id === id));
191 }, [])[0];
192 }
193
194 getCatalogItemByUid(uid) {
195 return this.getCatalogs().reduce((r, catalog) => {
196 return r.concat(catalog.descriptors.filter(d => UID.from(d) === uid));
197 }, [])[0];
198 }
199
200 getTransientCatalogItemByUid(uid) {
201 return this.getTransientCatalogs().reduce((r, catalog) => {
202 return r.concat(catalog.descriptors.filter(d => UID.from(d) === uid));
203 }, [])[0];
204 }
205
206 removeCatalogItem(deleteItem = {}) {
207 this.getCatalogs().map(catalog => {
208 catalog.descriptors = catalog.descriptors.filter(d => d.id !== deleteItem.id);
209 return catalog;
210 });
211 }
212
213 addNewItemToCatalog(newItem) {
214 const type = newItem.uiState.type;
215 this.getCatalogs().filter(d => d.type === type).forEach(catalog => {
216 catalog.descriptors.push(newItem);
217 });
218 // update indexes and integrate new model into catalog
219 this.updateCatalogIndexes(this.getCatalogs());
220 return this.getCatalogItemById(newItem.id);
221 }
222
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() !== '') {
231 try {
232 descriptor.uiState = JSON.parse(descriptor.meta);
233 } catch (ignore) {
234 console.warn('Unable to deserialize the uiState property.');
235 }
236 } else if (typeof descriptor.meta === 'object') {
237 descriptor.uiState = descriptor.meta;
238 descriptor.meta = JSON.stringify(descriptor.meta);
239 }
240
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);
247 }
248 if (catalog.type === 'vnfd') {
249 vnfdLookup[descriptor.id] = descriptor;
250 }
251 return descriptor;
252 });
253 return catalog;
254 });
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];
261 if (!vnfd) {
262 throw new ReferenceError('no VNFD found in the VNFD Catalog for the constituent-vnfd: ' + d);
263 }
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'];
269 return instance;
270 });
271 }
272 return descriptor;
273 });
274 });
275 return updatedCatalogs;
276 }
277
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) {
284 return item;
285 }
286 return d;
287 });
288 }
289 return catalog;
290 });
291 this.setState({ catalogs: catalogs });
292 }
293
294 mergeEditsIntoLatestFromServer(catalogsFromServer = []) {
295
296 // if the UI has modified items use them instead of the server items
297
298 const currentData = this.getCatalogs();
299
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;
304 return r;
305 }, {});
306
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;
311 return r;
312 }, {});
313
314 const newItemsMap = currentData.reduce((result, catalog) => {
315 result[catalog.id] = catalog.descriptors.filter(d => d.uiState.isNew);
316 return result;
317 }, {});
318
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];
325 }
326 if (itemMetaMap[key]) {
327 Object.assign(d.uiState, itemMetaMap[key]);
328 }
329 return d;
330 });
331 if (newItemsMap[catalog.id]) {
332 catalog.descriptors = catalog.descriptors.concat(newItemsMap[catalog.id]);
333 }
334 });
335
336 return catalogsFromServer;
337
338 }
339
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);
345 }
346 this.setState({
347 catalogs: catalogs,
348 isLoading: false
349 });
350 }
351
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);
368 }
369 }
370 return d;
371 });
372 }
373 return catalog;
374 });
375 this.setState({ catalogs: catalogs });
376 this.queueDirtyCheck();
377 }
378
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.'
383 });
384 }
385
386 selectCatalogItem(item = {}) {
387 SelectionManager.select(item);
388 }
389
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);
395 if (modified) {
396 item.uiState.modified = true;
397 this.updateCatalogItem(item);
398 this.addSnapshot(item);
399 this.queueDirtyCheck();
400 }
401 }
402
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) {
408 // find the catalog
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;
416 }
417 return d;
418 });
419 }
420 return catalog;
421 });
422 this.setState({ catalogs: catalogs })
423 this.queueDirtyCheck();
424 }
425
426 deleteSelectedCatalogItem() {
427 SelectionManager.getSelections().forEach(selectedId => {
428 const item = this.getCatalogItemByUid(selectedId);
429 if (item) {
430 this.deleteCatalogItem(item);
431 }
432 });
433 SelectionManager.clearSelectionAndRemoveOutline();
434 }
435
436 deleteCatalogItem(item) {
437 if (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)
443 .catch(function () {
444 console.log('overcoming ES6 unhandled rejection red herring');
445 });
446 }, );
447 }
448 }
449
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>
456 </div>
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>
460 </div>
461 </div>
462 ));
463 }
464
465 createCatalogItem(type = 'nsd') {
466 const newItem = createItem(type);
467 this.saveItem(newItem)
468 }
469
470 duplicateSelectedCatalogItem() {
471 // make request to backend to duplicate an item
472 const srcItem = this.getFirstSelectedCatalogItem();
473 if (srcItem) {
474 CatalogPackageManagerActions.copyCatalogPackage.defer(srcItem);
475 }
476 }
477
478 addSnapshot(item) {
479 if (item) {
480 if (!this.snapshots[item.id]) {
481 this.snapshots[item.id] = [];
482 }
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);
488 }
489 }
490
491 getLatestSnapshot(item) {
492 if (this.snapshots[item.id]) {
493 return JSON.parse(this.snapshots[item.id][this.snapshots[item.id].length - 1]);
494 }
495 this.getCatalogs().forEach(catalog => {
496 if (catalog.id === item.uiState.catalogId) {
497 catalog.descriptors.forEach(d => {
498 if (d.id === item.id) {
499 return d;
500 }
501 });
502 }
503 });
504 return {};
505 }
506
507 resetSnapshots(item) {
508 if (item) {
509 this.snapshots[item.id] = [];
510 this.addSnapshot(item);
511 }
512 }
513
514 editCatalogItem(item) {
515 if (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) {
524 return item;
525 }
526 return d;
527 });
528 return catalog;
529 });
530 this.setState({ catalogs: catalogs });
531 this.catalogItemMetaDataChanged(item);
532 }
533 }
534
535 cancelCatalogItemChanges() {
536 const activeItem = ComposerAppStore.getState().item;
537 if (activeItem) {
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();
546 }
547 }
548 }
549
550 saveCatalogItem() {
551 const activeItem = ComposerAppStore.getState().item;
552 if (activeItem) {
553 this.saveItem(activeItem);
554 }
555 }
556
557 saveItem(item) {
558 if (item) {
559 const success = () => {
560 delete item.uiState.modified;
561 if (item.uiState.isNew) {
562 this.addNewItemToCatalog(item);
563 delete item.uiState.isNew;
564 } else {
565 this.updateCatalogItem(item);
566 }
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();
572 };
573 const failure = () => {
574 ModalOverlayActions.hideModalOverlay.defer();
575 CatalogItemsActions.editCatalogItem.defer(item);
576 };
577 const exception = () => {
578 console.warn('unable to save catalog item', item);
579 ModalOverlayActions.hideModalOverlay.defer();
580 CatalogItemsActions.editCatalogItem.defer(item);
581 };
582 ModalOverlayActions.showModalOverlay.defer();
583 this.getInstance().saveCatalogItem(item).then(success, failure).catch(exception);
584 }
585 }
586
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'
595 });
596 this.resetSelectionState();
597 }
598 }
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) {
604 return d;
605 }
606 });
607 }
608 return gotIt;
609 }, null);
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
616 });
617 ComposerAppActions.recordDescriptorError.defer({
618 descriptor: container,
619 type: data.catalogType,
620 id: data.itemId,
621 error: data.error.responseText
622 })
623 }
624 }
625
626 export default alt.createStore(CatalogDataStore, 'CatalogDataStore');
627