Merge master -> master_vca_intg
[osm/UI.git] / skyquake / plugins / composer / src / src / stores / ComposerAppStore.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 _ from 'lodash'
22 import d3 from 'd3'
23 import alt from '../alt'
24 import UID from '../libraries/UniqueId'
25 import DescriptorModelFactory from '../libraries/model/DescriptorModelFactory'
26 import PanelResizeAction from '../actions/PanelResizeAction'
27 import CatalogItemsActions from '../actions/CatalogItemsActions'
28 import CanvasEditorActions from '../actions/CanvasEditorActions'
29 import ComposerAppActions from '../actions/ComposerAppActions'
30 import CatalogFilterActions from '../actions/CatalogFilterActions'
31 import CanvasPanelTrayActions from '../actions/CanvasPanelTrayActions'
32 import SelectionManager from '../libraries/SelectionManager'
33 import CatalogDataStore from '../stores/CatalogDataStore'
34 import isFullScreen from '../libraries/isFullScreen'
35
36 import FileManagerSource from '../components/filemanager/FileManagerSource';
37 import FileManagerActions from '../components/filemanager/FileManagerActions';
38
39 import React from 'react';
40
41 //Hack for crouton fix. Should eventually put composer in skyquake alt context
42 import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
43 let NotificationError = null;
44 class ComponentBridge extends React.Component {
45 constructor(props) {
46 super(props);
47 NotificationError = this.props.flux.actions.global.showNotification;
48 }
49 render(){
50 return <i></i>
51 }
52 }
53 const getDefault = (name, defaultValue) => {
54 const val = window.localStorage.getItem('defaults-' + name);
55 if (val) {
56 if (_.isNumber(val)) {
57 if (val < 0) {
58 return setDefault(name, 0);
59 }
60 }
61 return Number(val);
62 }
63 setDefault(name, defaultValue);
64 return defaultValue;
65 };
66
67 const setDefault = (name, defaultValue) => {
68 window.localStorage.setItem('defaults-' + name, defaultValue);
69 return defaultValue;
70 };
71
72 /* the top and bottom positions are managed by css; requires div to be display: absolute*/
73 const defaults = {
74 left: getDefault('catalog-panel-start-width', 300),
75 right: getDefault('details-panel-start-width', 365),
76 bottom: 25 + getDefault('defaults-forwarding-graphs-panel-start-height', 0),
77 showMore: false,
78 zoom: getDefault('zoom', 100),
79 filterCatalogBy: 'nsd',
80 defaultPanelTrayOpenZoom: (() => {
81 let zoom = parseFloat(getDefault('panel-tray-zoom', 75));
82 if (isNaN(zoom)) {
83 zoom = 75;
84 }
85 zoom = Math.min(100, zoom);
86 zoom = Math.max(25, zoom);
87 setDefault('panel-tray-zoom', zoom);
88 return zoom;
89 })()
90 };
91
92 const autoZoomCanvasScale = d3.scale.linear().domain([0, 300]).range([100, 50]).clamp(true);
93
94 const uiTransientState = {};
95
96 class ComposerAppStore {
97
98 constructor() {
99 //Bridge for crouton fix
100 this.ComponentBridgeElement = SkyquakeComponent(ComponentBridge);
101
102 this.exportAsync(FileManagerSource)
103 // the catalog item currently being edited in the composer
104 this.item = null;
105 // the left and right sides of the canvas area
106 this.layout = {
107 left: defaults.left,
108 right: defaults.right,
109 bottom: defaults.bottom
110 };
111 uiTransientState.restoreLayout = this.layout;
112 this.zoom = defaults.zoom;
113 this.showMore = defaults.showMore;
114 this.filterCatalogByTypeValue = defaults.filterCatalogBy;
115 // transient ui state
116 this.drag = null;
117 this.message = '';
118 this.messageType = '';
119 this.showJSONViewer = false;
120 this.showClassifiers = {};
121 this.editPathsMode = false;
122 this.fullScreenMode = false;
123 this.panelTabShown = 'descriptor';
124 //File manager values
125 this.files = false;
126 this.filesState = {};
127 this.downloadJobs = {};
128 //End File manager values
129 this.bindListeners({
130 onResize: PanelResizeAction.RESIZE,
131 editCatalogItem: CatalogItemsActions.EDIT_CATALOG_ITEM,
132 catalogItemMetaDataChanged: CatalogItemsActions.CATALOG_ITEM_META_DATA_CHANGED,
133 catalogItemDescriptorChanged: CatalogItemsActions.CATALOG_ITEM_DESCRIPTOR_CHANGED,
134 toggleShowMoreInfo: CanvasEditorActions.TOGGLE_SHOW_MORE_INFO,
135 showMoreInfo: CanvasEditorActions.SHOW_MORE_INFO,
136 showLessInfo: CanvasEditorActions.SHOW_LESS_INFO,
137 applyDefaultLayout: CanvasEditorActions.APPLY_DEFAULT_LAYOUT,
138 addVirtualLinkDescriptor: CanvasEditorActions.ADD_VIRTUAL_LINK_DESCRIPTOR,
139 addForwardingGraphDescriptor: CanvasEditorActions.ADD_FORWARDING_GRAPH_DESCRIPTOR,
140 addVirtualDeploymentDescriptor: CanvasEditorActions.ADD_VIRTUAL_DEPLOYMENT_DESCRIPTOR,
141 selectModel: ComposerAppActions.SELECT_MODEL,
142 outlineModel: ComposerAppActions.OUTLINE_MODEL,
143 showError: ComposerAppActions.SHOW_ERROR,
144 clearError: ComposerAppActions.CLEAR_ERROR,
145 setDragState: ComposerAppActions.SET_DRAG_STATE,
146 filterCatalogByType: CatalogFilterActions.FILTER_BY_TYPE,
147 setCanvasZoom: CanvasEditorActions.SET_CANVAS_ZOOM,
148 showJsonViewer: ComposerAppActions.SHOW_JSON_VIEWER,
149 closeJsonViewer: ComposerAppActions.CLOSE_JSON_VIEWER,
150 toggleCanvasPanelTray: CanvasPanelTrayActions.TOGGLE_OPEN_CLOSE,
151 openCanvasPanelTray: CanvasPanelTrayActions.OPEN,
152 closeCanvasPanelTray: CanvasPanelTrayActions.CLOSE,
153 enterFullScreenMode: ComposerAppActions.ENTER_FULL_SCREEN_MODE,
154 exitFullScreenMode: ComposerAppActions.EXIT_FULL_SCREEN_MODE,
155 showAssets: ComposerAppActions.showAssets,
156 showDescriptor: ComposerAppActions.showDescriptor,
157 getFilelistSuccess: FileManagerActions.getFilelistSuccess,
158 updateFileLocationInput: FileManagerActions.updateFileLocationInput,
159 sendDownloadFileRequst: FileManagerActions.sendDownloadFileRequst,
160 addFileSuccess: FileManagerActions.addFileSuccess,
161 deletePackageFile: FileManagerActions.deletePackageFile,
162 deleteFileSuccess: FileManagerActions.deleteFileSuccess,
163 closeFileManagerSockets: FileManagerActions.closeFileManagerSockets,
164 openFileManagerSockets: FileManagerActions.openFileManagerSockets,
165 openDownloadMonitoringSocketSuccess: FileManagerActions.openDownloadMonitoringSocketSuccess,
166 getFilelistSocketSuccess: FileManagerActions.getFilelistSocketSuccess
167 });
168 this.exportPublicMethods({
169 closeFileManagerSockets: this.closeFileManagerSockets.bind(this)
170 })
171 }
172
173 onResize(e) {
174 if (e.type === 'resize-manager.resize.catalog-panel') {
175 const layout = Object.assign({}, this.layout);
176 layout.left = Math.max(0, layout.left - e.moved.x);
177 if (layout.left !== this.layout.left) {
178 this.setState({layout: layout});
179 }
180 } else if (e.type === 'resize-manager.resize.details-panel') {
181 const layout = Object.assign({}, this.layout);
182 layout.right = Math.max(0, layout.right + e.moved.x);
183 if (layout.right !== this.layout.right) {
184 this.setState({layout: layout});
185 }
186 } else if (/^resize-manager\.resize\.canvas-panel-tray/.test(e.type)) {
187 const layout = Object.assign({}, this.layout);
188 layout.bottom = Math.max(25, layout.bottom + e.moved.y);
189 if (layout.bottom !== this.layout.bottom) {
190 const zoom = autoZoomCanvasScale(layout.bottom) ;
191 if (this.zoom !== zoom) {
192 this.setState({layout: layout, zoom: zoom});
193 } else {
194 this.setState({layout: layout});
195 }
196 }
197 } else if (e.type !== 'resize') {
198 console.log('no resize handler for ', e.type, '. Do you need to add a handler in ComposerAppStore::onResize()?')
199 }
200 SelectionManager.refreshOutline();
201 }
202
203 updateItem(item) {
204 if(!document.body.classList.contains('resizing')) {
205 this.setState({item: _.cloneDeep(item)});
206 }
207 SelectionManager.refreshOutline();
208 }
209
210 editCatalogItem(item) {
211 let self = this;
212 self.closeFileManagerSockets();
213 if (item && item.uiState) {
214 item.uiState.isOpenForEdit = true;
215 if (item.uiState.type !== 'nsd') {
216 this.closeCanvasPanelTray();
217 }
218 }
219 SelectionManager.select(item);
220 this.updateItem(item);
221 this.openFileManagerSockets(item)
222 }
223 catalogItemMetaDataChanged(item) {
224 this.updateItem(item);
225 }
226
227 catalogItemDescriptorChanged(itemDescriptor) {
228 this.catalogItemMetaDataChanged(itemDescriptor.model);
229 }
230
231 showMoreInfo() {
232 this.setState({showMore: true});
233 }
234
235 showLessInfo() {
236 this.setState({showMore: false});
237 }
238
239 showError(data) {
240 NotificationError.defer({msg: data.errorMessage, type: 'error'})
241 // this.setState({message: data.errorMessage, messageType: 'error'});
242 }
243
244 clearError() {
245 this.setState({message: '', messageType: ''});
246 }
247
248 toggleShowMoreInfo() {
249 this.setState({showMore: !this.showMore});
250 }
251
252 applyDefaultLayout() {
253 if (this.item && this.item.uiState && this.item.uiState.containerPositionMap) {
254 if (!_.isEmpty(this.item.uiState.containerPositionMap)) {
255 this.item.uiState.containerPositionMap = {};
256 CatalogItemsActions.catalogItemMetaDataChanged.defer(this.item);
257 }
258 }
259 }
260
261 addVirtualLinkDescriptor(dropCoordinates = null) {
262 let vld;
263 if (this.item) {
264 if (this.item.uiState.type === 'nsd') {
265 const nsdc = DescriptorModelFactory.newNetworkService(this.item);
266 vld = nsdc.createVld();
267 } else if (this.item.uiState.type === 'vnfd') {
268 const vnfd = DescriptorModelFactory.newVirtualNetworkFunction(this.item);
269 vld = vnfd.createVld();
270 }
271 if (vld) {
272 vld.uiState.dropCoordinates = dropCoordinates;
273 SelectionManager.clearSelectionAndRemoveOutline();
274 SelectionManager.addSelection(vld);
275 this.updateItem(vld.getRoot().model);
276 CatalogItemsActions.catalogItemDescriptorChanged.defer(vld.getRoot());
277 }
278 }
279 }
280
281 addForwardingGraphDescriptor(dropCoordinates = null) {
282 if (this.item && this.item.uiState.type === 'nsd') {
283 const nsdc = DescriptorModelFactory.newNetworkService(this.item);
284 const fg = nsdc.createVnffgd();
285 fg.uiState.dropCoordinates = dropCoordinates;
286 SelectionManager.clearSelectionAndRemoveOutline();
287 SelectionManager.addSelection(fg);
288 this.updateItem(nsdc.model);
289 CatalogItemsActions.catalogItemDescriptorChanged.defer(nsdc);
290 }
291 }
292
293 addVirtualDeploymentDescriptor(dropCoordinates = null) {
294 if (this.item.uiState.type === 'vnfd') {
295 const vnfd = DescriptorModelFactory.newVirtualNetworkFunction(this.item);
296 const vdu = vnfd.createVdu();
297 vdu.uiState.dropCoordinates = dropCoordinates;
298 SelectionManager.clearSelectionAndRemoveOutline();
299 SelectionManager.addSelection(vdu);
300 this.updateItem(vdu.getRoot().model);
301 CatalogItemsActions.catalogItemDescriptorChanged.defer(vdu.getRoot());
302 }
303 }
304
305 selectModel(container) {
306 if (SelectionManager.select(container)) {
307 const model = DescriptorModelFactory.isContainer(container) ? container.getRoot().model : container;
308 this.catalogItemMetaDataChanged(model);
309 }
310 }
311
312 outlineModel(obj) {
313 const uid = UID.from(obj);
314 requestAnimationFrame(() => {
315 SelectionManager.outline(Array.from(document.querySelectorAll(`[data-uid="${uid}"]`)));
316 });
317 }
318
319 clearSelection() {
320 SelectionManager.clearSelectionAndRemoveOutline();
321 this.catalogItemMetaDataChanged(this.item);
322 }
323
324 setDragState(dragState) {
325 this.setState({drag: dragState});
326 }
327
328 filterCatalogByType(typeValue) {
329 this.setState({filterCatalogByTypeValue: typeValue})
330 }
331
332 setCanvasZoom(zoom) {
333 this.setState({zoom: zoom});
334 }
335
336 showJsonViewer() {
337 this.setState({showJSONViewer: true});
338 }
339
340 closeJsonViewer() {
341 this.setState({showJSONViewer: false});
342 }
343
344 toggleCanvasPanelTray(event) {
345 const layout = this.layout;
346 const attrMap = event.target.attributes;
347 let panelEvent = null;
348 for(let k in attrMap) {
349 if(attrMap[k].name == 'data-event') {
350 panelEvent = attrMap[k].nodeValue;
351 }
352 }
353 if ((layout.bottom > 25) && ((panelEvent == this.displayedPanel) || panelEvent == 'arrow')) {
354 this.closeCanvasPanelTray();
355 } else {
356 this.openCanvasPanelTray();
357 }
358 this.setState({displayedPanel: panelEvent})
359 }
360
361 openCanvasPanelTray() {
362 const layout = {
363 left: this.layout.left,
364 right: this.layout.right,
365 bottom: 300
366 };
367 const zoom = defaults.defaultPanelTrayOpenZoom;
368 if (this.zoom !== zoom) {
369 this.setState({layout: layout, zoom: zoom, restoreZoom: this.zoom});
370 } else {
371 this.setState({layout: layout});
372 }
373 }
374
375 closeCanvasPanelTray() {
376 const layout = {
377 left: this.layout.left,
378 right: this.layout.right,
379 bottom: 25
380 };
381 const zoom = this.restoreZoom || autoZoomCanvasScale(layout.bottom);
382 if (this.zoom !== zoom) {
383 this.setState({layout: layout, zoom: zoom, restoreZoom: null});
384 } else {
385 this.setState({layout: layout, restoreZoom: null});
386 }
387 }
388
389 enterFullScreenMode() {
390
391 /**
392 * https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
393 * This is an experimental api but works our target browsers and ignored by others
394 */
395 const eventNames = ['fullscreenchange', 'mozfullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'];
396
397 const appRoot = document.body;//.getElementById('RIFT_wareLaunchpadComposerAppRoot');
398
399 const comp = this;
400
401 function onFullScreenChange() {
402
403 if (isFullScreen()) {
404 const layout = comp.layout;
405 const restoreLayout = _.cloneDeep(layout);
406 uiTransientState.restoreLayout = restoreLayout;
407 layout.left = 0;
408 layout.right = 0;
409 comp.setState({fullScreenMode: true, layout: layout, restoreLayout: restoreLayout});
410 } else {
411 comp.setState({fullScreenMode: false, layout: uiTransientState.restoreLayout});
412 }
413
414 }
415
416 if (this.fullScreenMode === false) {
417
418 if (appRoot.requestFullscreen) {
419 appRoot.requestFullscreen();
420 } else if (appRoot.msRequestFullscreen) {
421 appRoot.msRequestFullscreen();
422 } else if (appRoot.mozRequestFullScreen) {
423 appRoot.mozRequestFullScreen();
424 } else if (appRoot.webkitRequestFullscreen) {
425 appRoot.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
426 }
427
428 eventNames.map(name => {
429 document.removeEventListener(name, onFullScreenChange);
430 document.addEventListener(name, onFullScreenChange);
431 });
432
433 }
434
435 }
436
437 exitFullScreenMode() {
438
439 if (document.exitFullscreen) {
440 document.exitFullscreen();
441 } else if (document.msExitFullscreen) {
442 document.msExitFullscreen();
443 } else if (document.mozCancelFullScreen) {
444 document.mozCancelFullScreen();
445 } else if (document.webkitExitFullscreen) {
446 document.webkitExitFullscreen();
447 }
448
449 this.setState({fullScreenMode: false});
450
451 }
452 showAssets() {
453 this.setState({
454 panelTabShown: 'assets'
455 });
456 }
457 showDescriptor() {
458 this.setState({
459 panelTabShown: 'descriptor'
460 });
461 }
462
463 //File Manager methods
464 getFilelistSuccess(data) {
465 let self = this;
466 let filesState = null;
467 if (self.fileMonitoringSocketID) {
468 let newState = {};
469 if(data.hasOwnProperty('contents')) {
470 filesState = addInputState( _.cloneDeep(this.filesState),data);
471 // filesState = _.merge(self.filesState, addInputState({},data));
472 let normalizedData = normalizeTree(data);
473 newState = {
474 files: {
475 data: _.mergeWith(normalizedData.data, self.files.data, function(obj, src) {
476 return _.uniqBy(obj? obj.concat(src) : src, 'name');
477 }),
478 id: self.files.id || normalizedData.id
479 },
480 filesState: filesState
481 }
482 } else {
483 newState = {
484 files: false
485 }
486 }
487
488 this.setState(newState);
489 }
490 function normalizeTree(data) {
491 let f = {
492 id:[],
493 data:{}
494 };
495 data.contents.map(getContents);
496 function getContents(d) {
497 if(d.hasOwnProperty('contents')) {
498 let contents = [];
499 d.contents.map(function(c,i) {
500 if (!c.hasOwnProperty('contents')) {
501 contents.push(c);
502 } else {
503 getContents(c);
504 }
505 })
506 f.id.push(d.name);
507 f.data[d.name] = contents;
508 }
509 }
510 return f;
511 }
512 function addInputState(obj, d) {
513 d.newFile = '';
514 if(d.hasOwnProperty('contents')) {
515 d.contents.map(addInputState.bind(null, obj))
516 }
517 if(!obj[d.name]) {
518 obj[d.name] = '';
519 }
520 return obj;
521 }
522 }
523 sendDownloadFileRequst(data) {
524 let id = data.id || this.item.id;
525 let type = data.type || this.item.uiState.type;
526 let path = data.path;
527 let url = data.url;
528 this.getInstance().addFile(id, type, path, url);
529 }
530 updateFileLocationInput = (data) => {
531 let name = data.name;
532 let value = data.value;
533 var filesState = _.cloneDeep(this.filesState);
534 filesState[name] = value;
535 this.setState({
536 filesState: filesState
537 });
538 }
539 addFileSuccess = (data) => {
540 let path = data.path;
541 let fileName = data.fileName;
542 let files = _.cloneDeep(this.files);
543 let loadingIndex = files.data[path].push({
544 status: 'DOWNLOADING',
545 name: path + '/' + fileName
546 }) - 1;
547 this.setState({files: files});
548
549 }
550 startWatchingJob = () => {
551 let ws = window.multiplexer.channel(this.jobSocketId);
552 this.setState({
553 jobSocket:null
554 })
555 }
556 openDownloadMonitoringSocketSuccess = (id) => {
557 let self = this;
558 let ws = window.multiplexer.channel(id);
559 let downloadJobs = _.cloneDeep(self.downloadJobs);
560 let newFiles = false;
561 ws.onmessage = (socket) => {
562 if (self.files && self.files.length > 0) {
563 let jobs = [];
564 try {
565 jobs = JSON.parse(socket.data);
566 } catch(e) {}
567 newFiles = _.cloneDeep(self.files);
568 jobs.map(function(j) {
569 //check if not in completed state
570 let fullPath = j['package-path'];
571 let path = fullPath.split('/');
572 let fileName = path.pop();
573 path = path.join('/');
574 let index = _.findIndex(self.files.data[path], function(o){
575 return fullPath == o.name
576 });
577 if((index > -1) && newFiles.data[path][index]) {
578 newFiles.data[path][index].status = j.status
579 } else {
580 if(j.status.toUpperCase() == 'LOADING...' || j.status.toUpperCase() == 'IN_PROGRESS') {
581 newFiles.data[path].push({
582 status: j.status,
583 name: fullPath
584 })
585 } else {
586 // if ()
587 }
588 }
589 })
590 self.setState({
591 files: newFiles
592 })
593 // console.log(JSON.parse(socket.data));
594 }
595 }
596 this.setState({
597 jobSocketId: id,
598 jobSocket: ws
599 })
600
601 }
602 getFilelistSocketSuccess = (id) => {
603 let self = this;
604 let ws = window.multiplexer.channel(id);
605 ws.onmessage = (socket) => {
606 if (self.fileMonitoringSocketID) {
607 let data = [];
608 try {
609 data = JSON.parse(socket.data);
610 } catch(e) {}
611 self.getFilelistSuccess(data)
612 }
613 }
614
615 this.setState({
616 fileMonitoringSocketID: id,
617 fileMonitoringSocket: ws
618 })
619
620 }
621 closeFileManagerSockets() {
622 this.fileMonitoringSocketID = null;
623 this.setState({
624 jobSocketId : null,
625 fileMonitoringSocketID : null
626 // jobSocket : null,
627 // fileMonitoringSocket : null,
628 });
629 this.jobSocket && this.jobSocket.close();
630 this.fileMonitoringSocket && this.fileMonitoringSocket.close();
631 console.log('closing');
632 }
633 openFileManagerSockets(i) {
634 let self = this;
635 let item = i || self.item;
636 // this.closeFileManagerSockets();
637 this.getInstance().openFileMonitoringSocket(item.id, item.uiState.type).then(function() {
638 // // self.getInstance().openDownloadMonitoringSocket(item.id);
639 });
640 this.getInstance().openDownloadMonitoringSocket(item.id);
641 }
642 endWatchingJob(id) {
643
644 }
645 deletePackageFile(name) {
646 let id = this.item.id;
647 let type = this.item.uiState.type;
648 this.getInstance().deleteFile(id, type, name);
649 }
650 deleteFileSuccess = (data) => {
651 let path = data.path.split('/')
652 let files = _.cloneDeep(this.files);
653 path.pop();
654 path = path.join('/');
655 let pathFiles = files.data[path]
656 _.remove(pathFiles, function(c) {
657 return c.name == data.path;
658 });
659
660 this.setState({
661 files: files
662 })
663 }
664 }
665
666 export default alt.createStore(ComposerAppStore, 'ComposerAppStore');