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