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 _isNumber
from 'lodash/isNumber'
22 import _cloneDeep
from 'lodash/cloneDeep'
23 import _isEmpty
from 'lodash/isEmpty'
24 import _mergeWith
from 'lodash/mergeWith'
25 import _uniqBy
from 'lodash/uniqBy'
26 import _isEqual
from 'lodash/isEqual'
27 import _findIndex
from 'lodash/findIndex'
28 import _remove
from 'lodash/remove'
30 import alt
from '../alt'
31 import UID
from '../libraries/UniqueId'
32 import DescriptorModelFactory
from '../libraries/model/DescriptorModelFactory'
33 import PanelResizeAction
from '../actions/PanelResizeAction'
34 import CatalogItemsActions
from '../actions/CatalogItemsActions'
35 import CanvasEditorActions
from '../actions/CanvasEditorActions'
36 import ComposerAppActions
from '../actions/ComposerAppActions'
37 import CatalogFilterActions
from '../actions/CatalogFilterActions'
38 import CanvasPanelTrayActions
from '../actions/CanvasPanelTrayActions'
39 import SelectionManager
from '../libraries/SelectionManager'
40 import CatalogDataStore
from '../stores/CatalogDataStore'
41 import isFullScreen
from '../libraries/isFullScreen'
43 import FileManagerSource
from '../components/filemanager/FileManagerSource';
44 import FileManagerActions
from '../components/filemanager/FileManagerActions';
46 import React
from 'react';
48 //Hack for crouton fix. Should eventually put composer in skyquake alt context
49 import SkyquakeComponent
from 'widgets/skyquake_container/skyquakeComponent.jsx';
50 let NotificationError
= null;
52 import utils
from '../libraries/utils';
54 class ComponentBridge
extends React
.Component
{
57 NotificationError
= this.props
.flux
.actions
.global
.showNotification
;
63 const getDefault
= (name
, defaultValue
) => {
64 const val
= window
.localStorage
.getItem('defaults-' + name
);
68 return setDefault(name
, 0);
73 setDefault(name
, defaultValue
);
77 const setDefault
= (name
, defaultValue
) => {
78 window
.localStorage
.setItem('defaults-' + name
, defaultValue
);
82 /* the top and bottom positions are managed by css; requires div to be display: absolute*/
84 left
: getDefault('catalog-panel-start-width', 300),
85 right
: getDefault('details-panel-start-width', 365),
86 bottom
: 25 + getDefault('defaults-forwarding-graphs-panel-start-height', 0),
88 zoom
: getDefault('zoom', 100),
89 filterCatalogBy
: 'nsd',
90 defaultPanelTrayOpenZoom
: (() => {
91 let zoom
= parseFloat(getDefault('panel-tray-zoom', 75));
95 zoom
= Math
.min(100, zoom
);
96 zoom
= Math
.max(25, zoom
);
97 setDefault('panel-tray-zoom', zoom
);
102 const autoZoomCanvasScale
= d3
.scale
.linear().domain([0, 300]).range([100, 50]).clamp(true);
104 const uiTransientState
= {};
106 class ComposerAppStore
{
109 //Bridge for crouton fix
110 this.ComponentBridgeElement
= SkyquakeComponent(ComponentBridge
);
112 this.exportAsync(FileManagerSource
)
113 // the catalog item currently being edited in the composer
115 // the left and right sides of the canvas area
118 right
: defaults
.right
,
119 bottom
: defaults
.bottom
121 uiTransientState
.restoreLayout
= this.layout
;
122 this.zoom
= defaults
.zoom
;
123 this.showMore
= defaults
.showMore
;
124 this.filterCatalogByTypeValue
= defaults
.filterCatalogBy
;
125 // transient ui state
128 this.messageType
= '';
129 this.showJSONViewer
= false;
130 this.showClassifiers
= {};
131 this.editPathsMode
= false;
132 this.fullScreenMode
= false;
133 this.panelTabShown
= 'descriptor';
134 //File manager values
136 this.filesState
= {};
137 this.downloadJobs
= {};
138 this.containers
= [];
139 this.newPathName
= '';
140 //End File manager values
142 onResize
: PanelResizeAction
.RESIZE
,
143 editCatalogItem
: CatalogItemsActions
.EDIT_CATALOG_ITEM
,
144 catalogItemMetaDataChanged
: CatalogItemsActions
.CATALOG_ITEM_META_DATA_CHANGED
,
145 catalogItemDescriptorChanged
: CatalogItemsActions
.CATALOG_ITEM_DESCRIPTOR_CHANGED
,
146 toggleShowMoreInfo
: CanvasEditorActions
.TOGGLE_SHOW_MORE_INFO
,
147 showMoreInfo
: CanvasEditorActions
.SHOW_MORE_INFO
,
148 showLessInfo
: CanvasEditorActions
.SHOW_LESS_INFO
,
149 applyDefaultLayout
: CanvasEditorActions
.APPLY_DEFAULT_LAYOUT
,
150 addVirtualLinkDescriptor
: CanvasEditorActions
.ADD_VIRTUAL_LINK_DESCRIPTOR
,
151 addForwardingGraphDescriptor
: CanvasEditorActions
.ADD_FORWARDING_GRAPH_DESCRIPTOR
,
152 addVirtualDeploymentDescriptor
: CanvasEditorActions
.ADD_VIRTUAL_DEPLOYMENT_DESCRIPTOR
,
153 selectModel
: ComposerAppActions
.SELECT_MODEL
,
154 outlineModel
: ComposerAppActions
.OUTLINE_MODEL
,
155 showError
: ComposerAppActions
.SHOW_ERROR
,
156 clearError
: ComposerAppActions
.CLEAR_ERROR
,
157 setDragState
: ComposerAppActions
.SET_DRAG_STATE
,
158 filterCatalogByType
: CatalogFilterActions
.FILTER_BY_TYPE
,
159 setCanvasZoom
: CanvasEditorActions
.SET_CANVAS_ZOOM
,
160 showJsonViewer
: ComposerAppActions
.SHOW_JSON_VIEWER
,
161 closeJsonViewer
: ComposerAppActions
.CLOSE_JSON_VIEWER
,
162 toggleCanvasPanelTray
: CanvasPanelTrayActions
.TOGGLE_OPEN_CLOSE
,
163 openCanvasPanelTray
: CanvasPanelTrayActions
.OPEN
,
164 closeCanvasPanelTray
: CanvasPanelTrayActions
.CLOSE
,
165 enterFullScreenMode
: ComposerAppActions
.ENTER_FULL_SCREEN_MODE
,
166 exitFullScreenMode
: ComposerAppActions
.EXIT_FULL_SCREEN_MODE
,
167 showAssets
: ComposerAppActions
.showAssets
,
168 showDescriptor
: ComposerAppActions
.showDescriptor
,
169 getFilelistSuccess
: FileManagerActions
.getFilelistSuccess
,
170 updateFileLocationInput
: FileManagerActions
.updateFileLocationInput
,
171 sendDownloadFileRequst
: FileManagerActions
.sendDownloadFileRequst
,
172 addFileSuccess
: FileManagerActions
.addFileSuccess
,
173 deletePackageFile
: FileManagerActions
.deletePackageFile
,
174 deleteFileSuccess
: FileManagerActions
.deleteFileSuccess
,
175 deleteFileError
: FileManagerActions
.deleteFileError
,
176 closeFileManagerSockets
: FileManagerActions
.closeFileManagerSockets
,
177 openFileManagerSockets
: FileManagerActions
.openFileManagerSockets
,
178 openDownloadMonitoringSocketSuccess
: FileManagerActions
.openDownloadMonitoringSocketSuccess
,
179 getFilelistSocketSuccess
: FileManagerActions
.getFilelistSocketSuccess
,
180 newPathNameUpdated
: FileManagerActions
.newPathNameUpdated
,
181 createDirectory
: FileManagerActions
.createDirectory
183 this.exportPublicMethods({
184 closeFileManagerSockets
: this.closeFileManagerSockets
.bind(this)
189 if (e
.type
=== 'resize-manager.resize.catalog-panel') {
190 const layout
= Object
.assign({}, this.layout
);
191 layout
.left
= Math
.max(0, layout
.left
- e
.moved
.x
);
192 if (layout
.left
!== this.layout
.left
) {
193 this.setState({layout
: layout
});
195 } else if (e
.type
=== 'resize-manager.resize.details-panel') {
196 const layout
= Object
.assign({}, this.layout
);
197 layout
.right
= Math
.max(0, layout
.right
+ e
.moved
.x
);
198 if (layout
.right
!== this.layout
.right
) {
199 this.setState({layout
: layout
});
201 } else if (/^resize-manager\.resize\.canvas-panel-tray/.test(e
.type
)) {
202 const layout
= Object
.assign({}, this.layout
);
203 layout
.bottom
= Math
.max(25, layout
.bottom
+ e
.moved
.y
);
204 if (layout
.bottom
!== this.layout
.bottom
) {
205 const zoom
= autoZoomCanvasScale(layout
.bottom
) ;
206 if (this.zoom
!== zoom
) {
207 this.setState({layout
: layout
, zoom
: zoom
});
209 this.setState({layout
: layout
});
212 } else if (e
.type
!== 'resize') {
213 console
.log('no resize handler for ', e
.type
, '. Do you need to add a handler in ComposerAppStore::onResize()?')
215 SelectionManager
.refreshOutline();
222 if(!document
.body
.classList
.contains('resizing')) {
223 containers
= [item
].reduce(DescriptorModelFactory
.buildCatalogItemFactory(CatalogDataStore
.getState().catalogs
), []);
225 containers
.filter(d
=> DescriptorModelFactory
.isConnectionPoint(d
)).forEach(d
=> {
226 d
.cpNumber
= ++cpNumber
;
227 containers
.filter(d
=> DescriptorModelFactory
.isVnfdConnectionPointRef(d
)).filter(ref
=> ref
.key
=== d
.key
).forEach(ref
=> ref
.cpNumber
= d
.cpNumber
);
229 this.setState({containers
: containers
, item
: _cloneDeep(item
)});
231 SelectionManager
.refreshOutline();
234 editCatalogItem(item
) {
236 self
.closeFileManagerSockets();
237 if (item
&& item
.uiState
) {
238 item
.uiState
.isOpenForEdit
= true;
239 if (item
.uiState
.type
!== 'nsd') {
240 this.closeCanvasPanelTray();
243 SelectionManager
.select(item
);
244 this.updateItem(item
);
246 this.openFileManagerSockets(item
);
249 catalogItemMetaDataChanged(item
) {
250 this.updateItem(item
);
253 catalogItemDescriptorChanged(itemDescriptor
) {
254 this.catalogItemMetaDataChanged(itemDescriptor
.model
);
258 this.setState({showMore
: true});
262 this.setState({showMore
: false});
266 NotificationError
.defer({msg
: data
.errorMessage
, type
: 'error'})
267 // this.setState({message: data.errorMessage, messageType: 'error'});
271 this.setState({message
: '', messageType
: ''});
274 toggleShowMoreInfo() {
275 this.setState({showMore
: !this.showMore
});
278 applyDefaultLayout() {
279 if (this.item
&& this.item
.uiState
&& this.item
.uiState
.containerPositionMap
) {
280 if (!_isEmpty(this.item
.uiState
.containerPositionMap
)) {
281 this.item
.uiState
.containerPositionMap
= {};
282 CatalogItemsActions
.catalogItemMetaDataChanged
.defer(this.item
);
287 addVirtualLinkDescriptor(dropCoordinates
= null) {
290 if (this.item
.uiState
.type
=== 'nsd') {
291 const nsdc
= DescriptorModelFactory
.newNetworkService(this.item
);
292 vld
= nsdc
.createVld();
293 } else if (this.item
.uiState
.type
=== 'vnfd') {
294 const vnfd
= DescriptorModelFactory
.newVirtualNetworkFunction(this.item
);
295 vld
= vnfd
.createVld();
298 vld
.uiState
.dropCoordinates
= dropCoordinates
;
299 SelectionManager
.clearSelectionAndRemoveOutline();
300 SelectionManager
.addSelection(vld
);
301 this.updateItem(vld
.getRoot().model
);
302 CatalogItemsActions
.catalogItemDescriptorChanged
.defer(vld
.getRoot());
307 addForwardingGraphDescriptor(dropCoordinates
= null) {
308 if (this.item
&& this.item
.uiState
.type
=== 'nsd') {
309 const nsdc
= DescriptorModelFactory
.newNetworkService(this.item
);
310 const fg
= nsdc
.createVnffgd();
311 fg
.uiState
.dropCoordinates
= dropCoordinates
;
312 SelectionManager
.clearSelectionAndRemoveOutline();
313 SelectionManager
.addSelection(fg
);
314 this.updateItem(nsdc
.model
);
315 CatalogItemsActions
.catalogItemDescriptorChanged
.defer(nsdc
);
319 addVirtualDeploymentDescriptor(dropCoordinates
= null) {
320 if (this.item
.uiState
.type
=== 'vnfd') {
321 const vnfd
= DescriptorModelFactory
.newVirtualNetworkFunction(this.item
);
322 const vdu
= vnfd
.createVdu();
323 vdu
.uiState
.dropCoordinates
= dropCoordinates
;
324 SelectionManager
.clearSelectionAndRemoveOutline();
325 SelectionManager
.addSelection(vdu
);
326 this.updateItem(vdu
.getRoot().model
);
327 CatalogItemsActions
.catalogItemDescriptorChanged
.defer(vdu
.getRoot());
331 selectModel(container
) {
332 if (SelectionManager
.select(container
)) {
333 const model
= DescriptorModelFactory
.isContainer(container
) ? container
.getRoot().model
: container
;
334 this.catalogItemMetaDataChanged(model
);
339 const uid
= UID
.from(obj
);
340 requestAnimationFrame(() => {
341 SelectionManager
.outline(Array
.from(document
.querySelectorAll(`[data-uid="${uid}"]`)));
346 SelectionManager
.clearSelectionAndRemoveOutline();
347 this.catalogItemMetaDataChanged(this.item
);
350 setDragState(dragState
) {
351 this.setState({drag
: dragState
});
354 filterCatalogByType(typeValue
) {
355 this.setState({filterCatalogByTypeValue
: typeValue
})
358 setCanvasZoom(zoom
) {
359 this.setState({zoom
: zoom
});
363 this.setState({showJSONViewer
: true});
367 this.setState({showJSONViewer
: false});
370 toggleCanvasPanelTray() {
371 const layout
= this.layout
;
372 if (layout
.bottom
> 25) {
373 this.closeCanvasPanelTray();
375 this.openCanvasPanelTray();
379 openCanvasPanelTray() {
381 left
: this.layout
.left
,
382 right
: this.layout
.right
,
385 const zoom
= defaults
.defaultPanelTrayOpenZoom
;
386 if (this.zoom
!== zoom
) {
387 this.setState({layout
: layout
, zoom
: zoom
, restoreZoom
: this.zoom
});
389 this.setState({layout
: layout
});
393 closeCanvasPanelTray() {
395 left
: this.layout
.left
,
396 right
: this.layout
.right
,
399 const zoom
= this.restoreZoom
|| autoZoomCanvasScale(layout
.bottom
);
400 if (this.zoom
!== zoom
) {
401 this.setState({layout
: layout
, zoom
: zoom
, restoreZoom
: null});
403 this.setState({layout
: layout
, restoreZoom
: null});
407 enterFullScreenMode() {
410 * https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
411 * This is an experimental api but works our target browsers and ignored by others
413 const eventNames
= ['fullscreenchange', 'mozfullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'];
415 const appRoot
= document
.body
;//.getElementById('RIFT_wareLaunchpadComposerAppRoot');
419 function onFullScreenChange() {
421 if (isFullScreen()) {
422 const layout
= comp
.layout
;
423 const restoreLayout
= _cloneDeep(layout
);
424 uiTransientState
.restoreLayout
= restoreLayout
;
427 comp
.setState({fullScreenMode
: true, layout
: layout
, restoreLayout
: restoreLayout
});
429 comp
.setState({fullScreenMode
: false, layout
: uiTransientState
.restoreLayout
});
434 if (this.fullScreenMode
=== false) {
436 if (appRoot
.requestFullscreen
) {
437 appRoot
.requestFullscreen();
438 } else if (appRoot
.msRequestFullscreen
) {
439 appRoot
.msRequestFullscreen();
440 } else if (appRoot
.mozRequestFullScreen
) {
441 appRoot
.mozRequestFullScreen();
442 } else if (appRoot
.webkitRequestFullscreen
) {
443 appRoot
.webkitRequestFullscreen(Element
.ALLOW_KEYBOARD_INPUT
);
446 eventNames
.map(name
=> {
447 document
.removeEventListener(name
, onFullScreenChange
);
448 document
.addEventListener(name
, onFullScreenChange
);
455 exitFullScreenMode() {
457 if (document
.exitFullscreen
) {
458 document
.exitFullscreen();
459 } else if (document
.msExitFullscreen
) {
460 document
.msExitFullscreen();
461 } else if (document
.mozCancelFullScreen
) {
462 document
.mozCancelFullScreen();
463 } else if (document
.webkitExitFullscreen
) {
464 document
.webkitExitFullscreen();
467 this.setState({fullScreenMode
: false});
472 panelTabShown
: 'assets'
477 panelTabShown
: 'descriptor'
481 //File Manager methods
482 getFilelistSuccess(data
) {
484 let filesState
= null;
485 if (self
.fileMonitoringSocketID
) {
487 if(data
.hasOwnProperty('contents')) {
488 filesState
= addInputState( _cloneDeep(this.filesState
),data
);
489 let normalizedData
= normalizeTree(data
);
492 data
: _mergeWith(normalizedData
.data
, self
.files
.data
, function(obj
, src
) {
493 return _uniqBy(obj
? obj
.concat(src
) : src
, 'name');
495 id
: normalizedData
.id
497 filesState
: filesState
504 if(!_isEqual(newState
.files
, this.files
) || ! _isEqual(newState
.fileState
, this.fileState
)) {
505 this.setState(newState
);
509 function normalizeTree(data
) {
514 data
.contents
.map(getContents
);
515 function getContents(d
) {
516 if(d
.hasOwnProperty('contents')) {
518 d
.contents
.map(function(c
,i
) {
519 if (!c
.hasOwnProperty('contents')) {
526 f
.data
[d
.name
] = contents
;
531 function addInputState(obj
, d
) {
533 if(d
.hasOwnProperty('contents')) {
534 d
.contents
.map(addInputState
.bind(null, obj
))
542 sendDownloadFileRequst(data
) {
543 let id
= data
.id
|| this.item
.id
;
544 let type
= data
.type
|| this.item
.uiState
.type
;
545 let path
= data
.path
;
547 this.getInstance().addFile(id
, type
, path
, url
, data
.refresh
);
549 updateFileLocationInput
= (data
) => {
550 let name
= data
.name
;
551 let value
= data
.value
;
552 var filesState
= _cloneDeep(this.filesState
);
553 filesState
[name
] = value
;
555 filesState
: filesState
558 addFileSuccess
= (data
) => {
560 let path
= data
.path
;
561 let fileName
= data
.fileName
;
562 let files
= _cloneDeep(this.files
);
563 let loadingIndex
= files
.data
[path
].push({
564 status
: 'DOWNLOADING',
565 name
: path
+ '/' + fileName
567 this.setState({files
: files
});
571 startWatchingJob
= () => {
572 let ws
= window
.multiplexer
.channel(this.jobSocketId
);
577 openDownloadMonitoringSocketSuccess
= (id
) => {
579 let ws
= window
.multiplexer
.channel(id
);
580 let downloadJobs
= _cloneDeep(self
.downloadJobs
);
581 let newFiles
= false;
582 ws
.onmessage
= (socket
) => {
583 if (self
.files
&& self
.files
.length
> 0) {
586 jobs
= JSON
.parse(socket
.data
);
588 newFiles
= _cloneDeep(self
.files
);
589 jobs
.map(function(j
) {
590 //check if not in completed state
591 let fullPath
= j
['package-path'];
592 let path
= fullPath
.split('/');
593 let fileName
= path
.pop();
594 path
= path
.join('/');
595 let index
= _findIndex(self
.files
.data
[path
], function(o
){
596 return fullPath
== o
.name
598 if((index
> -1) && newFiles
.data
[path
][index
]) {
599 newFiles
.data
[path
][index
].status
= j
.status
601 if(j
.status
.toUpperCase() == 'LOADING...' || j
.status
.toUpperCase() == 'IN_PROGRESS') {
602 newFiles
.data
[path
].push({
614 // console.log(JSON.parse(socket.data));
623 getFilelistSocketSuccess
= (id
) => {
625 let ws
= window
.multiplexer
.channel(id
);
626 ws
.onmessage
= (socket
) => {
627 if (self
.fileMonitoringSocketID
) {
630 data
= JSON
.parse(socket
.data
);
632 self
.getFilelistSuccess(data
)
637 fileMonitoringSocketID
: id
,
638 fileMonitoringSocket
: ws
642 closeFileManagerSockets() {
643 this.fileMonitoringSocketID
= null;
646 fileMonitoringSocketID
: null
648 // fileMonitoringSocket : null,
650 this.jobSocket
&& this.jobSocket
.close();
651 this.fileMonitoringSocket
&& this.fileMonitoringSocket
.close();
652 console
.log('closing');
654 openFileManagerSockets(i
) {
656 let item
= i
|| self
.item
;
657 // this.closeFileManagerSockets();
658 this.getInstance().openFileMonitoringSocket(item
.id
, item
.uiState
.type
).then(function() {
659 // // self.getInstance().openDownloadMonitoringSocket(item.id);
661 this.getInstance().openDownloadMonitoringSocket(item
.id
);
666 deletePackageFile(name
) {
667 let id
= this.item
.id
;
668 let type
= this.item
.uiState
.type
;
669 this.getInstance().deleteFile(id
, type
, name
);
671 deleteFileSuccess
= (data
) => {
672 let path
= data
.path
.split('/')
673 let files
= _cloneDeep(this.files
);
675 path
= path
.join('/');
676 let pathFiles
= files
.data
[path
]
677 _remove(pathFiles
, function(c
) {
678 return c
.name
== data
.path
;
685 deleteFileError
= (error
) => {
686 const filepath
= error
.path
;
687 const message
= error
.data
&& error
.data
.output
? ' (' + error
.data
.output
['error-trace'] + ')' : ' (server error)';
688 console
.log('Unable to delete', filepath
, 'Error:', message
);
689 ComposerAppActions
.showError
.defer({
690 errorMessage
: 'Unable to delete ' + filepath
+ message
+ '. '
694 newPathNameUpdated
= (event
) => {
695 const value
= event
.target
.value
;
700 createDirectory
= () => {
701 console
.log(this.newPathName
);
702 this.sendDownloadFileRequst({
704 type
: this.item
.uiState
.type
,
705 path
: this.item
.name
+ '/' + this.newPathName
,
706 url
: utils
.getSearchParams(window
.location
).dev_download_server
|| window
.location
.protocol
+ '//' + window
.location
.host
,
715 export default alt
.createStore(ComposerAppStore
, 'ComposerAppStore');