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