diff --git a/src/visualizers/panels/InteractiveEditor/InteractiveEditorControl.js b/src/visualizers/panels/InteractiveEditor/InteractiveEditorControl.js new file mode 100644 index 000000000..35152239d --- /dev/null +++ b/src/visualizers/panels/InteractiveEditor/InteractiveEditorControl.js @@ -0,0 +1,218 @@ +/*globals define, WebGMEGlobal*/ + +define([ + 'deepforge/viz/ConfigDialog', + 'js/Constants', +], function ( + ConfigDialog, + CONSTANTS, +) { + + 'use strict'; + + class InteractiveEditorControl { + constructor(options) { + this._logger = options.logger.fork('Control'); + this.client = options.client; + this._embedded = options.embedded; + this._widget = options.widget; + this.initializeWidgetHandlers(this._widget); + this.territoryEventFilters = []; + + this._currentNodeId = null; + + this._logger.debug('ctor finished'); + } + + initializeWidgetHandlers (widget) { + const features = widget.getCapabilities(); + if (features.save) { + widget.save = () => this.save(); + } + widget.getConfigDialog = () => new ConfigDialog(this.client); + } + + selectedObjectChanged (nodeId) { + const desc = this.getObjectDescriptor(nodeId); + + this._logger.debug('activeObject nodeId \'' + nodeId + '\''); + + if (this._currentNodeId) { + this.client.removeUI(this._territoryId); + } + + this._currentNodeId = nodeId; + + if (typeof this._currentNodeId === 'string') { + const territory = this.getTerritory(nodeId); + this._widget.setTitle(desc.name.toUpperCase()); + + this._territoryId = this.client + .addUI(this, events => this._eventCallback(events)); + + this.client.updateTerritory(this._territoryId, territory); + } + } + + getTerritory(nodeId) { + const territory = {}; + territory[nodeId] = {children: 0}; + return territory; + } + + getMetaNode(name) { + const metanodes = this.client.getAllMetaNodes(); + return metanodes + .find(node => { + const namespace = node.getNamespace(); + const fullName = namespace ? namespace + '.' + node.getAttribute('name') : + node.getAttribute('name'); + + return fullName === name; + }); + } + + createNode(desc, parentId) { + if (!parentId) { + parentId = this._currentNodeId; + } + desc.pointers = desc.pointers || {}; + desc.attributes = desc.attributes || {}; + + const base = this.getMetaNode(desc.type) || this.client.getNode(desc.pointers.base); + const nodeId = this.client.createNode({ + parentId: parentId, + baseId: base.getId() + }); + + const attributes = Object.entries(desc.attributes); + attributes.forEach(entry => { + const [name, value] = entry; + this.client.setAttribute(nodeId, name, value); + }); + + const pointers = Object.entries(desc.pointers); + pointers.forEach(entry => { + const [name, id] = entry; + this.client.setPointer(nodeId, name, id); + }); + + return nodeId; + } + + save() { + this.client.startTransaction(); + const dataId = this.createNode(this._widget.getSnapshot()); + const implicitOpId = this.createNode(this._widget.getEditorState(), dataId); + this.client.setPointer(dataId, 'provenance', implicitOpId); + const operationId = this.createNode(this._widget.getOperation(), implicitOpId); + this.client.setPointer(implicitOpId, 'operation', operationId); + this.client.completeTransaction(); + } + + getObjectDescriptor (nodeId) { + const node = this.client.getNode(nodeId); + + if (node) { + return { + id: node.getId(), + name: node.getAttribute('name'), + childrenIds: node.getChildrenIds(), + parentId: node.getParentId(), + }; + } + } + + /* * * * * * * * Node Event Handling * * * * * * * */ + _eventCallback (events=[]) { + this._logger.debug('_eventCallback \'' + events.length + '\' items'); + + events + .filter(event => this.isRelevantEvent(event)) + .forEach(event => { + switch (event.etype) { + + case CONSTANTS.TERRITORY_EVENT_LOAD: + this.onNodeLoad(event.eid); + break; + case CONSTANTS.TERRITORY_EVENT_UPDATE: + this.onNodeUpdate(event.eid); + break; + case CONSTANTS.TERRITORY_EVENT_UNLOAD: + this.onNodeUnload(event.eid); + break; + default: + break; + } + }); + + this._logger.debug('_eventCallback \'' + events.length + '\' items - DONE'); + } + + onNodeLoad (gmeId) { + const description = this.getObjectDescriptor(gmeId); + this._widget.addNode(description); + } + + onNodeUpdate (gmeId) { + const description = this.getObjectDescriptor(gmeId); + this._widget.updateNode(description); + } + + onNodeUnload (gmeId) { + this._widget.removeNode(gmeId); + } + + isRelevantEvent (event) { + return this.territoryEventFilters + .reduce((keep, fn) => keep && fn(event), true); + } + + _stateActiveObjectChanged (model, activeObjectId) { + if (this._currentNodeId === activeObjectId) { + // The same node selected as before - do not trigger + } else { + this.selectedObjectChanged(activeObjectId); + } + } + + /* * * * * * * * Visualizer life cycle callbacks * * * * * * * */ + destroy () { + this._detachClientEventListeners(); + } + + _attachClientEventListeners () { + this._detachClientEventListeners(); + if (!this._embedded) { + WebGMEGlobal.State.on( + 'change:' + CONSTANTS.STATE_ACTIVE_OBJECT, + this._stateActiveObjectChanged, + this + ); + } + } + + _detachClientEventListeners () { + if (!this._embedded) { + WebGMEGlobal.State.off( + 'change:' + CONSTANTS.STATE_ACTIVE_OBJECT, + this._stateActiveObjectChanged + ); + } + } + + onActivate () { + this._attachClientEventListeners(); + + if (typeof this._currentNodeId === 'string') { + WebGMEGlobal.State.registerActiveObject(this._currentNodeId, {suppressVisualizerFromNode: true}); + } + } + + onDeactivate () { + this._detachClientEventListeners(); + } + } + + return InteractiveEditorControl; +}); diff --git a/src/visualizers/panels/InteractiveEditor/InteractiveEditorPanel.js b/src/visualizers/panels/InteractiveEditor/InteractiveEditorPanel.js new file mode 100644 index 000000000..b7b4d7e65 --- /dev/null +++ b/src/visualizers/panels/InteractiveEditor/InteractiveEditorPanel.js @@ -0,0 +1,97 @@ +/*globals define, _, WebGMEGlobal*/ + +define([ + 'js/PanelBase/PanelBaseWithHeader', + 'js/PanelManager/IActivePanel', + 'widgets/InteractiveEditor/InteractiveEditorWidget', + './InteractiveEditorControl' +], function ( + PanelBaseWithHeader, + IActivePanel, + InteractiveEditorWidget, + InteractiveEditorControl +) { + 'use strict'; + + function InteractiveEditorPanel(layoutManager, params) { + var options = {}; + //set properties from options + options[PanelBaseWithHeader.OPTIONS.LOGGER_INSTANCE_NAME] = 'InteractiveEditorPanel'; + options[PanelBaseWithHeader.OPTIONS.FLOATING_TITLE] = true; + + //call parent's constructor + PanelBaseWithHeader.apply(this, [options, layoutManager]); + + this._client = params.client; + this._embedded = params.embedded; + + this.initialize(); + + this.logger.debug('ctor finished'); + } + + //inherit from PanelBaseWithHeader + _.extend(InteractiveEditorPanel.prototype, PanelBaseWithHeader.prototype); + _.extend(InteractiveEditorPanel.prototype, IActivePanel.prototype); + + InteractiveEditorPanel.prototype.initialize = function () { + var self = this; + + //set Widget title + this.setTitle(''); + + this.widget = new InteractiveEditorWidget(this.logger, this.$el); + + this.widget.setTitle = function (title) { + self.setTitle(title); + }; + + this.control = new InteractiveEditorControl({ + logger: this.logger, + client: this._client, + embedded: this._embedded, + widget: this.widget + }); + + this.onActivate(); + }; + + /* OVERRIDE FROM WIDGET-WITH-HEADER */ + /* METHOD CALLED WHEN THE WIDGET'S READ-ONLY PROPERTY CHANGES */ + InteractiveEditorPanel.prototype.onReadOnlyChanged = function (isReadOnly) { + //apply parent's onReadOnlyChanged + PanelBaseWithHeader.prototype.onReadOnlyChanged.call(this, isReadOnly); + + }; + + InteractiveEditorPanel.prototype.onResize = function (width, height) { + this.logger.debug('onResize --> width: ' + width + ', height: ' + height); + this.widget.onWidgetContainerResize(width, height); + }; + + /* * * * * * * * Visualizer life cycle callbacks * * * * * * * */ + InteractiveEditorPanel.prototype.destroy = function () { + this.control.destroy(); + this.widget.destroy(); + + PanelBaseWithHeader.prototype.destroy.call(this); + WebGMEGlobal.KeyboardManager.setListener(undefined); + WebGMEGlobal.Toolbar.refresh(); + }; + + InteractiveEditorPanel.prototype.onActivate = function () { + this.widget.onActivate(); + this.control.onActivate(); + WebGMEGlobal.KeyboardManager.setListener(this.widget); + WebGMEGlobal.Toolbar.refresh(); + }; + + InteractiveEditorPanel.prototype.onDeactivate = function () { + this.widget.onDeactivate(); + this.control.onDeactivate(); + WebGMEGlobal.KeyboardManager.setListener(undefined); + WebGMEGlobal.Toolbar.refresh(); + }; + + return InteractiveEditorPanel; +}); diff --git a/src/visualizers/panels/InteractiveExplorer/InteractiveExplorerControl.js b/src/visualizers/panels/InteractiveExplorer/InteractiveExplorerControl.js new file mode 100644 index 000000000..bca1aad31 --- /dev/null +++ b/src/visualizers/panels/InteractiveExplorer/InteractiveExplorerControl.js @@ -0,0 +1,44 @@ +/*globals define */ + +define([ + 'panels/InteractiveEditor/InteractiveEditorControl', +], function ( + InteractiveEditorControl, +) { + + 'use strict'; + + class InteractiveExplorerControl extends InteractiveEditorControl { + ensureValidSnapshot(desc) { + const metadata = this.getMetaNode('pipeline.Metadata'); + const type = this.getMetaNode(desc.type); + + if (!type) { + throw new Error(`Invalid metadata type: ${type}`); + } + + if (!type.isTypeOf(metadata.getId())) { + throw new Error('Explorer can only create artifact metadata'); + } + } + + save() { + const snapshotDesc = this._widget.getSnapshot(); + this.ensureValidSnapshot(snapshotDesc); + + const features = this._widget.getCapabilities(); + this.client.startTransaction(); + const data = this.createNode(snapshotDesc); + if (features.provenance) { + const implicitOp = this.createNode(this._widget.getEditorState(), data); + this.client.setPointer(data.getId(), 'provenance', implicitOp.getId()); + const operation = this.createNode(this._widget.getOperation(), implicitOp); + this.client.setPointer(implicitOp.getId(), 'operation', operation.getId()); + } + this.client.completeTransaction(); + } + + } + + return InteractiveExplorerControl; +}); diff --git a/src/visualizers/panels/InteractiveExplorer/InteractiveExplorerPanel.js b/src/visualizers/panels/InteractiveExplorer/InteractiveExplorerPanel.js new file mode 100644 index 000000000..4f91b50f7 --- /dev/null +++ b/src/visualizers/panels/InteractiveExplorer/InteractiveExplorerPanel.js @@ -0,0 +1,33 @@ +/*globals define */ + +define([ + 'panels/InteractiveEditor/InteractiveEditorPanel', + 'widgets/InteractiveExplorer/InteractiveExplorerWidget', + './InteractiveExplorerControl', +], function ( + InteractiveEditorPanel, + InteractiveExplorerWidget, + InteractiveExplorerControl, +) { + 'use strict'; + + class InteractiveExplorerPanel extends InteractiveEditorPanel { + + initialize() { + this.setTitle(''); + this.widget = new InteractiveExplorerWidget(this.logger, this.$el); + this.widget.setTitle = title => this.setTitle(title); + + this.control = new InteractiveExplorerControl({ + logger: this.logger, + client: this._client, + embedded: this._embedded, + widget: this.widget + }); + + this.onActivate(); + } + } + + return InteractiveExplorerPanel; +}); diff --git a/src/visualizers/widgets/InteractiveEditor/InteractiveEditorWidget.js b/src/visualizers/widgets/InteractiveEditor/InteractiveEditorWidget.js new file mode 100644 index 000000000..e2fbca2ae --- /dev/null +++ b/src/visualizers/widgets/InteractiveEditor/InteractiveEditorWidget.js @@ -0,0 +1,99 @@ +/* globals define, $ */ +define([ + 'deepforge/compute/interactive/session-with-queue', + 'deepforge/viz/ConfigDialog', + 'deepforge/viz/InformDialog', + 'deepforge/compute/index', + 'deepforge/globals', + 'css!./styles/InteractiveEditorWidget.css', +], function( + Session, + ConfigDialog, + InformDialog, + Compute, + DeepForge, +) { + const COMPUTE_MESSAGE = 'Compute Required. Click to configure.'; + class InteractiveEditorWidget { + constructor(container) { + this.showComputeShield(container); + } + + showComputeShield(container) { + const overlay = $('
', {class: 'compute-shield'}); + container.append(overlay); + const msg = $(''); + msg.text(COMPUTE_MESSAGE); + overlay.append(msg); + overlay.on('click', async () => { + const {id, config} = await this.promptComputeConfig(); + try { + await this.createInteractiveSession(id, config); + const features = this.getCapabilities(); + if (features.save) { + DeepForge.registerAction('Save', 'save', 10, () => this.save()); + } + overlay.remove(); + } catch (err) { + const title = 'Compute Creation Error'; + const body = 'Unable to create compute. Please verify the credentials are correct.'; + // TODO: Detect authorization errors... + const dialog = new InformDialog(title, body); + dialog.show(); + } + }); + } + + getCapabilities() { + return { + suspend: this.isOveridden('getEditorState') && + this.isOveridden('resume'), + save: this.isOveridden('getSnapshot') && + this.isOveridden('getOperation') && + this.isOveridden('getEditorState'), + }; + } + + isOveridden(name) { + return this[name] !== InteractiveEditorWidget.prototype[name]; + } + + async promptComputeConfig() { + const dialog = new ConfigDialog(); + const computeMetadata = Compute.getAvailableBackends().map(id => Compute.getMetadata(id)); + const metadata = { + id: 'InteractiveComputeConfig', + name: 'Create Compute Instance', + version: '1.0.0', + description: '', + icon: { + class: 'glyphicon glyphicon-cog', + src: '' + }, + disableServerSideExecution: false, + disableBrowserSideExecution: false, + writeAccessRequired: false, + configStructure: [ + { + name: 'compute', + displayName: 'Compute', + description: 'Computational resources to use for execution.', + valueType: 'dict', + value: Compute.getBackend(Compute.getAvailableBackends()[0]).name, + valueItems: computeMetadata, + } + ] + }; + const allConfigs = await dialog.show(metadata); + const {name, config} = allConfigs[metadata.id].compute; + const id = computeMetadata.find(md => md.name === name).id; + return {id, config}; + } + + async createInteractiveSession(computeId, config) { + this.session = await Session.new(computeId, config); + } + } + + return InteractiveEditorWidget; +}); diff --git a/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.css b/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.css new file mode 100644 index 000000000..397cb14e3 --- /dev/null +++ b/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.css @@ -0,0 +1,22 @@ +/** + * This file is for any scss that you may want for this visualizer. + */ +.interactive-editor { + outline: none; } + +.compute-shield { + background-color: #00000099; + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; + z-index: 100; } + +.compute-shield span { + color: whitesmoke; + display: block; + font-size: 2em; + margin: auto; + padding-top: 25%; + text-align: center; } diff --git a/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.scss b/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.scss new file mode 100644 index 000000000..47ca20ac9 --- /dev/null +++ b/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.scss @@ -0,0 +1,26 @@ +/** + * This file is for any scss that you may want for this visualizer. + */ + +.interactive-editor { + outline: none; +} + +.compute-shield { + background-color:#00000099; + bottom:0; + left:0; + position:fixed; + right:0; + top:0; + z-index: 100; +} + +.compute-shield span { + color: whitesmoke; + display: block; + font-size: 2em; + margin: auto; + padding-top: 25%; + text-align: center; +} diff --git a/src/visualizers/widgets/InteractiveExplorer/InteractiveExplorerWidget.js b/src/visualizers/widgets/InteractiveExplorer/InteractiveExplorerWidget.js new file mode 100644 index 000000000..f0dbde675 --- /dev/null +++ b/src/visualizers/widgets/InteractiveExplorer/InteractiveExplorerWidget.js @@ -0,0 +1,25 @@ +/*globals define */ + +define([ + 'widgets/InteractiveEditor/InteractiveEditorWidget', + 'css!./styles/InteractiveExplorerWidget.css', +], function ( + InteractiveEditorWidget, +) { + 'use strict'; + + class InteractiveExplorerWidget extends InteractiveEditorWidget { + + getCapabilities() { + return { + suspend: this.isOveridden('getEditorState') && + this.isOveridden('resume'), + save: this.isOveridden('getSnapshot'), + provenance: this.isOveridden('getEditorState') && + this.isOveridden('getOperation'), + }; + } + } + + return InteractiveExplorerWidget; +}); diff --git a/src/visualizers/widgets/InteractiveExplorer/styles/InteractiveExplorerWidget.css b/src/visualizers/widgets/InteractiveExplorer/styles/InteractiveExplorerWidget.css new file mode 100644 index 000000000..b22d22900 --- /dev/null +++ b/src/visualizers/widgets/InteractiveExplorer/styles/InteractiveExplorerWidget.css @@ -0,0 +1,10 @@ +/** + * This file is for any css that you may want for this visualizer. + * + * Ideally, you would use the scss file also provided in this directory + * and then generate this file automatically from that. However, you can + * simply write css if you prefer + */ + +.interactive-explorer { + outline: none; } diff --git a/src/visualizers/widgets/InteractiveExplorer/styles/InteractiveExplorerWidget.scss b/src/visualizers/widgets/InteractiveExplorer/styles/InteractiveExplorerWidget.scss new file mode 100644 index 000000000..ffcb7cf81 --- /dev/null +++ b/src/visualizers/widgets/InteractiveExplorer/styles/InteractiveExplorerWidget.scss @@ -0,0 +1,7 @@ +/** + * This file is for any scss that you may want for this visualizer. + */ + +.interactive-explorer { + outline: none; +} diff --git a/webgme-setup.json b/webgme-setup.json index 338d116d1..05ed97c02 100644 --- a/webgme-setup.json +++ b/webgme-setup.json @@ -281,6 +281,20 @@ "panel": "src/visualizers/panels/OperationDepEditor", "secondary": false, "widget": "src/visualizers/widgets/OperationDepEditor" + }, + "InteractiveEditor": { + "src": "panels/InteractiveEditor/InteractiveEditorPanel", + "title": "InteractiveEditor", + "panel": "src/visualizers/panels/InteractiveEditor", + "secondary": false, + "widget": "src/visualizers/widgets/InteractiveEditor" + }, + "InteractiveExplorer": { + "src": "panels/InteractiveExplorer/InteractiveExplorerPanel", + "title": "InteractiveExplorer", + "panel": "src/visualizers/panels/InteractiveExplorer", + "secondary": false, + "widget": "src/visualizers/widgets/InteractiveExplorer" } }, "addons": {},