diff --git a/bin/deepforge b/bin/deepforge index 304583e..369f55b 100755 --- a/bin/deepforge +++ b/bin/deepforge @@ -447,6 +447,10 @@ program } }); +// extensions +program + .command('extensions ', 'Manage deepforge extensions'); + module.exports = function(cmd) { var cmds = cmd.split(/\s+/).filter(w => !!w); cmds.unshift('./bin/deepforge'); diff --git a/bin/deepforge-extensions b/bin/deepforge-extensions new file mode 100755 index 0000000..f324e15 --- /dev/null +++ b/bin/deepforge-extensions @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +var Command = require('commander').Command, + program = new Command(), + extender = require('../utils/extender'); + +// Supported commands +// - add +// - remove +// - list +// - update +program + .command('add ') + .description('Add an extension to deepforge') + .option('-n, --name ', 'Project name (if different from )') + .action(project => { + console.log('loading extension from: ' + project); + extender.install(project) + .then(extConfig => + console.log(`The ${extConfig.name} extension has been added to deepforge.`)) + .fail(err => { + console.error('Could not install extension:\n'); + console.error(err); + process.exit(1); + }); + }); + +program + .command('remove ').alias('rm') + .description('Remove an extension from deepforge') + .action(name => { + try { + extender.uninstall(name); + console.log(`${name} has been successfully removed!`); + } catch (e) { + console.error('Could not remove extension:'); + console.error(e); + process.exit(1); + } + }); + +program + .command('list').alias('ls') + .description('List installed deepforge extensions') + .action(() => { + var allExtConfigs = extender.getExtensionsConfig(), + types = Object.keys(allExtConfigs), + hasContents = false, + names; + + for (var i = types.length; i--;) { + names = Object.keys(allExtConfigs[types[i]]); + if (names.length) { + hasContents = true; + console.log(types[i]); + for (var j = names.length; j--;) { + console.log(` ${names[j]}`); + } + } + } + + if (!hasContents) { + console.log('No installed extensions'); + } + }); + +program.parse(process.argv); diff --git a/config/components.json b/config/components.json index ce2647a..161217c 100644 --- a/config/components.json +++ b/config/components.json @@ -49,7 +49,7 @@ "icon": "import_export", "priority": -1 }, - "GenerateExecFile": { + "Export": { "icon": "play_for_work", "priority": -1 } diff --git a/package.json b/package.json index 09bdf1e..5e36f30 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ }, "scripts": { "start": "node app.js", + "postinstall": "node utils/reinstall-extensions.js", "start-dev": "NODE_ENV=dev node app.js", "local": "node ./bin/start-local.js", "worker": "node ./bin/start-worker.js", @@ -21,8 +22,10 @@ "graceful-fs": "^4.1.10", "lodash.difference": "^4.1.2", "lodash.merge": "^4.5.1", + "lodash.template": "^4.4.0", "mongodb": "^2.2.10", "nodemon": "^1.9.2", + "npm": "^4.0.5", "q": "1.4.1", "rimraf": "^2.4.0", "webgme": "^2.7.1", diff --git a/src/plugins/GenerateExecFile/GenerateExecFile.js b/src/plugins/Export/Export.js similarity index 86% rename from src/plugins/GenerateExecFile/GenerateExecFile.js rename to src/plugins/Export/Export.js index e743fe8..ea1d5e1 100644 --- a/src/plugins/GenerateExecFile/GenerateExecFile.js +++ b/src/plugins/Export/Export.js @@ -3,7 +3,6 @@ define([ 'text!./metadata.json', - 'text!./toboolean.lua', './format', 'plugin/PluginBase', 'deepforge/plugin/PtrCodeGen', @@ -13,7 +12,6 @@ define([ 'q' ], function ( pluginMetadata, - TOBOOLEAN, FORMATS, PluginBase, PtrCodeGen, @@ -33,13 +31,13 @@ define([ RESERVED = /^(and|break|do|else|elseifend|false|for|function|if|in|local|nil|not|orrepeat|return|then|true|until|while|print)$/; /** - * Initializes a new instance of GenerateExecFile. + * Initializes a new instance of Export. * @class * @augments {PluginBase} - * @classdesc This class represents the plugin GenerateExecFile. + * @classdesc This class represents the plugin Export. * @constructor */ - var GenerateExecFile = function () { + var Export = function () { // Call base class' constructor. PluginBase.call(this); this.initRecords(); @@ -50,13 +48,13 @@ define([ * This is also available at the instance at this.pluginMetadata. * @type {object} */ - GenerateExecFile.metadata = pluginMetadata; + Export.metadata = pluginMetadata; // Prototypical inheritance from PluginBase. - GenerateExecFile.prototype = Object.create(PluginBase.prototype); - GenerateExecFile.prototype.constructor = GenerateExecFile; + Export.prototype = Object.create(PluginBase.prototype); + Export.prototype.constructor = Export; - GenerateExecFile.prototype.initRecords = function() { + Export.prototype.initRecords = function() { this.pluginMetadata = pluginMetadata; this._srcIdFor = {}; // input path -> output data node path @@ -94,7 +92,7 @@ define([ * * @param {function(string, plugin.PluginResult)} callback - the result callback */ - GenerateExecFile.prototype.main = function (callback) { + Export.prototype.main = function (callback) { this.initRecords(); // Get all the children and call generate exec file @@ -107,6 +105,7 @@ define([ return this.core.loadChildren(this.activeNode) .then(nodes => this.generateOutputFiles(nodes)) + .catch(err => callback(err)) .then(hash => { this.result.addArtifact(hash); this.result.setSuccess(true); @@ -115,20 +114,20 @@ define([ .fail(err => callback(err)); }; - GenerateExecFile.prototype.getCurrentConfig = function () { + Export.prototype.getCurrentConfig = function () { var config = PluginBase.prototype.getCurrentConfig.call(this); config.staticInputs = config.staticInputs || []; return config; }; - GenerateExecFile.prototype.generateOutputFiles = function (children) { + Export.prototype.generateOutputFiles = function (children) { var name = this.core.getAttribute(this.activeNode, 'name'); return this.createCodeSections(children) .then(sections => { // Get the selected format var config = this.getCurrentConfig(), - format = config.format || 'Torch CLI', + format = config.format || 'Basic CLI', generate = FORMATS[format], staticInputs, files; @@ -167,7 +166,7 @@ define([ }); }; - GenerateExecFile.prototype.createCodeSections = function (children) { + Export.prototype.createCodeSections = function (children) { // Convert opNodes' jobs to the nested operations var opNodes, nodes; @@ -211,7 +210,7 @@ define([ .fail(err => this.logger.error(err)); }; - GenerateExecFile.prototype.unpackJobs = function (nodes) { + Export.prototype.unpackJobs = function (nodes) { return Q.all( nodes.map(node => { if (!this.isMetaTypeOf(node, this.META.Job)) { @@ -225,7 +224,7 @@ define([ ); }; - GenerateExecFile.prototype.sortOperations = function (operationDict, opIds) { + Export.prototype.sortOperations = function (operationDict, opIds) { var nextIds = [], sorted = opIds, dstIds, @@ -253,7 +252,7 @@ define([ .concat(this.sortOperations(operationDict, nextIds)); }; - GenerateExecFile.prototype.generateCodeSections = function(sortedOps) { + Export.prototype.generateCodeSections = function(sortedOps) { // Create the code sections: // - operation definitions // - pipeline definition @@ -286,8 +285,8 @@ define([ // Define the serializers/deserializers this.addCodeSerializers(code); - // Define the main body - this.addCodeMain(code); + // Define the main input names + code.mainInputNames = Object.keys(this.isInputOp).map(id => this._nameFor[id]); // Add custom class definitions this.addCustomClasses(code); @@ -299,12 +298,12 @@ define([ }; // expose this utility function to format extensions - var indent = GenerateExecFile.prototype.indent = function(text, spaces) { + var indent = Export.prototype.indent = function(text, spaces) { spaces = spaces || 3; return text.replace(/^/mg, new Array(spaces+1).join(' ')); }; - GenerateExecFile.prototype.defineOperationFn = function(operation) { + Export.prototype.defineOperationFn = function(operation) { var lines = [], args = operation.inputNames || []; @@ -322,7 +321,7 @@ define([ return lines.join('\n'); }; - GenerateExecFile.prototype.definePipelineFn = function(sortedOps, outputOps) { + Export.prototype.definePipelineFn = function(sortedOps, outputOps) { var inputArgs = Object.keys(this.isInputOp).map(id => this._nameFor[id]), name = this.core.getAttribute(this.activeNode, 'name'), safename = getUniqueName(name, this._opBaseNames), @@ -348,7 +347,7 @@ define([ return result; }; - GenerateExecFile.prototype.getOutputPair = function(operation) { + Export.prototype.getOutputPair = function(operation) { var input = operation.inputValues[0].slice(), value; @@ -358,11 +357,9 @@ define([ return [this._nameFor[operation.id], value]; }; - GenerateExecFile.prototype.addCodeSerializers = function(sections) { + Export.prototype.addCodeSerializers = function(sections) { var loadNodes = {}, - saveNodes = {}, - hasBool = false; - + saveNodes = {}; // Add the serializer fn names for each input sections.serializerFor = {}; @@ -376,21 +373,10 @@ define([ // Add the serializer definitions Object.keys(this.isInputOp).forEach(id => { var node = this.inputNode[id], - base = this.core.getBase(node), - type = this.core.getAttribute(base, 'name'), name = this._nameFor[id]; - if (type === 'boolean') { - hasBool = true; - sections.deserializerFor[name] = 'toboolean'; - } else if (type === 'number') { - sections.deserializerFor[name] = 'tonumber'; - } else if (type === 'string') { - sections.deserializerFor[name] = 'tostring'; - } else { - loadNodes[id] = node; - sections.deserializerFor[name] = `__load['${this._nameFor[id]}']`; - } + loadNodes[id] = node; + sections.deserializerFor[name] = `__load['${this._nameFor[id]}']`; }); sections.deserializers = this.createTorchFnDict( @@ -417,10 +403,6 @@ define([ 'path, data' ); - if (hasBool) { // add toboolean def - sections.deserializers += '\n' + TOBOOLEAN; - } - // Add a saveOutputs method for convenience sections.serializeOutputsDef = [ 'local function __saveOutputs(data)', @@ -438,18 +420,7 @@ define([ sections.serializeOutputs = '__saveOutputs(outputs)'; }; - GenerateExecFile.prototype.addCodeMain = function(sections) { - var pipelineName = Object.keys(sections.pipelines)[0], - args; - - // Create some names for the inputs - sections.mainInputNames = Object.keys(this.isInputOp).map(id => this._nameFor[id]); - args = sections.mainInputNames.map(name => `${sections.deserializerFor[name]}(${name})`); - - sections.main = `local outputs = ${pipelineName}(${args.join(', ')})`; - }; - - GenerateExecFile.prototype.createTorchFnDict = function(name, nodeDict, attr, args) { + Export.prototype.createTorchFnDict = function(name, nodeDict, attr, args) { return [ `local ${name} = {}`, Object.keys(nodeDict).map(id => { @@ -463,7 +434,7 @@ define([ ].join('\n'); }; - GenerateExecFile.prototype.addCustomClasses = function(sections) { + Export.prototype.addCustomClasses = function(sections) { var metaDict = this.core.getAllMetaNodes(this.rootNode), isClass, metanodes, @@ -528,7 +499,7 @@ define([ }); }; - GenerateExecFile.prototype.addCustomLayers = function(sections) { + Export.prototype.addCustomLayers = function(sections) { var metaDict = this.core.getAllMetaNodes(this.rootNode), isCustomLayer, metanodes, @@ -552,7 +523,7 @@ define([ }; - GenerateExecFile.prototype.getTypeDictFor = function (name, metanodes) { + Export.prototype.getTypeDictFor = function (name, metanodes) { var isType = {}; // Get all the custom layers for (var i = metanodes.length; i--;) { @@ -570,7 +541,7 @@ define([ return `"${attr}"`; }; - GenerateExecFile.prototype.getOpInvocation = function(op) { + Export.prototype.getOpInvocation = function(op) { var lines = [], attrs, refInits = [], @@ -603,13 +574,13 @@ define([ return lines.join('\n'); }; - GenerateExecFile.prototype.getOutputName = function(node) { + Export.prototype.getOutputName = function(node) { var basename = this.core.getAttribute(node, 'saveName'); return getUniqueName(basename, this._outputNames, true); }; - GenerateExecFile.prototype.getVariableName = function (/*node*/) { + Export.prototype.getVariableName = function (/*node*/) { var c = Object.keys(this.isInputOp).length; if (c !== 1) { @@ -619,7 +590,7 @@ define([ return 'input'; }; - GenerateExecFile.prototype.registerNode = function (node) { + Export.prototype.registerNode = function (node) { if (this.isMetaTypeOf(node, this.META.Operation)) { return this.registerOperation(node); } else if (this.isMetaTypeOf(node, this.META.Transporter)) { @@ -648,7 +619,7 @@ define([ return name; }; - GenerateExecFile.prototype.registerOperation = function (node) { + Export.prototype.registerOperation = function (node) { var name = this.core.getAttribute(node, 'name'), id = this.core.getPath(node), base = this.core.getBase(node), @@ -710,7 +681,7 @@ define([ }); }; - GenerateExecFile.prototype.registerTransporter = function (node) { + Export.prototype.registerTransporter = function (node) { var outputData = this.core.getPointerPath(node, 'src'), inputData = this.core.getPointerPath(node, 'dst'), srcOpId = this.getOpIdFor(outputData), @@ -729,7 +700,7 @@ define([ this._incomingCnts[dstOpId]++; }; - GenerateExecFile.prototype.getOpIdFor = function (dataId) { + Export.prototype.getOpIdFor = function (dataId) { var ids = dataId.split('/'), depth = ids.length; @@ -744,7 +715,7 @@ define([ // - add the references // - generate the code // - replace the `return ` w/ ` = ` - GenerateExecFile.prototype.createOperation = function (node) { + Export.prototype.createOperation = function (node) { var id = this.core.getPath(node), baseId = this.core.getPath(this.core.getBase(node)), attrNames = this.core.getValidAttributeNames(node), @@ -819,12 +790,12 @@ define([ }); }; - GenerateExecFile.prototype.genPtrSnippet = function (ptrName, pId) { + Export.prototype.genPtrSnippet = function (ptrName, pId) { return this.getPtrCodeHash(pId) .then(hash => this.blobClient.getObjectAsString(hash)); }; - GenerateExecFile.prototype.createHeader = function (title, length) { + Export.prototype.createHeader = function (title, length) { var len; title = ` ${title} `; length = length || HEADER_LENGTH; @@ -842,7 +813,7 @@ define([ }; - GenerateExecFile.prototype.genOperationCode = function (operation) { + Export.prototype.genOperationCode = function (operation) { var header = this.createHeader(`"${operation.name}" Operation`), codeParts = [], body = []; @@ -869,15 +840,7 @@ define([ return operation; }; - GenerateExecFile.prototype.assignResultToVar = function (code, name) { - var i = code.lastIndexOf('return'); + _.extend(Export.prototype, PtrCodeGen.prototype); - return code.substring(0, i) + - code.substring(i) - .replace('return', `${name} = `); - }; - - _.extend(GenerateExecFile.prototype, PtrCodeGen.prototype); - - return GenerateExecFile; + return Export; }); diff --git a/src/plugins/GenerateExecFile/deepforge.ejs b/src/plugins/Export/deepforge.ejs similarity index 100% rename from src/plugins/GenerateExecFile/deepforge.ejs rename to src/plugins/Export/deepforge.ejs diff --git a/src/plugins/GenerateExecFile/format.js b/src/plugins/Export/format.js similarity index 63% rename from src/plugins/GenerateExecFile/format.js rename to src/plugins/Export/format.js index e5b98ff..6642625 100644 --- a/src/plugins/GenerateExecFile/format.js +++ b/src/plugins/Export/format.js @@ -1,12 +1,13 @@ + /* globals define*/ // The supported export formats and metadata define([ - './formats/cli' + './formats/cli/cli' ], function( - TorchCLI + Format0 ) { return { - 'Torch CLI': TorchCLI + 'Basic CLI': Format0 }; }); diff --git a/src/plugins/Export/format.js.ejs b/src/plugins/Export/format.js.ejs new file mode 100644 index 0000000..56384f1 --- /dev/null +++ b/src/plugins/Export/format.js.ejs @@ -0,0 +1,21 @@ +<% // Add default format +formats.unshift({ name: 'cli', main: 'cli.js', displayName: 'Basic CLI' }) +%> +/* globals define*/ +// The supported export formats and metadata +define([ +<%= formats.map(function(format) { + return ' \'./formats/' + format.name + '/' + + path.basename(format.main.replace(/\.js$/, '')) + '\'' + }) + .join(',\n') %> +], function( +<%= formats.map(function(f, index) { return ' Format' + index; }).join(',\n') %> +) { + + return { +<%= formats.map(function(f, index) { + return ' \'' + f.displayName + '\': Format' + index; + }).join(',\n') %> + }; +}); diff --git a/src/plugins/GenerateExecFile/formats/cli.js b/src/plugins/Export/formats/cli/cli.js similarity index 72% rename from src/plugins/GenerateExecFile/formats/cli.js rename to src/plugins/Export/formats/cli/cli.js index ad383e1..1f63622 100644 --- a/src/plugins/GenerateExecFile/formats/cli.js +++ b/src/plugins/Export/formats/cli/cli.js @@ -8,16 +8,46 @@ define([ var INIT_CLASSES_FN = '__initClasses', INIT_LAYERS_FN = '__initLayers', + TOBOOLEAN, DEEPFORGE_CODE; // defined at the bottom (after the embedded template) + var deserializersFromString = function(sections) { + var hasBool = false; + + // Add serializers given cli string input + Object.keys(this.isInputOp).forEach(id => { + var node = this.inputNode[id], + base = this.core.getBase(node), + type = this.core.getAttribute(base, 'name'), + name = this._nameFor[id]; + + if (type === 'boolean') { + hasBool = true; + sections.deserializerFor[name] = 'toboolean'; + } else if (type === 'number') { + sections.deserializerFor[name] = 'tonumber'; + } else if (type === 'string') { + sections.deserializerFor[name] = 'tostring'; + } + }); + + if (hasBool) { + sections.deserializers += '\n' + TOBOOLEAN; + } + + return sections; + }; + var createExecFile = function (sections, staticInputs) { var classes, initClassFn, initLayerFn, code = []; - // concat all the sections into a single file + // Update deserializers for cli input + deserializersFromString.call(this, sections); + // concat all the sections into a single file // wrap the class/layer initialization in a fn // Add the classes ordered wrt their deps classes = sections.orderedClasses @@ -86,21 +116,28 @@ define([ return files; } else { + var pipelineName = Object.keys(sections.pipelines)[0], + main, + args; + + // Create some names for the inputs + args = sections.mainInputNames.map(name => `${sections.deserializerFor[name]}(${name})`); + + main = `local outputs = ${pipelineName}(${args.join(', ')})`; + // Grab the args from the cli code.push(sections.mainInputNames.map((name, index) => { return `local ${name} = arg[${index + 1}]`; }).join('\n')); // Add the main fn - code.push(sections.main); + code.push(main); // Save outputs to disk code.push(sections.serializeOutputs); return code.join('\n\n'); } - - return code.join('\n\n'); }; var deepforgeTxt = @@ -152,6 +189,15 @@ function deepforge.Image:title(name) -- nop end`; + TOBOOLEAN = +`local function toboolean(str) + if str == 'true' then + return true + elseif str == 'false' then + return false + end +end`; + DEEPFORGE_CODE = _.template(deepforgeTxt)({ initCode: `${INIT_CLASSES_FN}()\n${' '}${INIT_LAYERS_FN}()` }); diff --git a/src/plugins/GenerateExecFile/metadata.json b/src/plugins/Export/metadata.json similarity index 72% rename from src/plugins/GenerateExecFile/metadata.json rename to src/plugins/Export/metadata.json index ba40706..b49607b 100644 --- a/src/plugins/GenerateExecFile/metadata.json +++ b/src/plugins/Export/metadata.json @@ -1,7 +1,7 @@ { - "id": "GenerateExecFile", - "name": "Generate Execution File", - "version": "0.1.0", + "id": "Export", + "name": "Export", + "version": "1.0.0", "description": "", "icon": { "class": "glyphicon glyphicon-cog", diff --git a/src/plugins/GenerateExecFile/toboolean.lua b/src/plugins/Export/toboolean.lua similarity index 100% rename from src/plugins/GenerateExecFile/toboolean.lua rename to src/plugins/Export/toboolean.lua diff --git a/src/visualizers/panels/ForgeActionButton/ConfigDialog.css b/src/visualizers/panels/ForgeActionButton/ConfigDialog.css new file mode 100644 index 0000000..17146c3 --- /dev/null +++ b/src/visualizers/panels/ForgeActionButton/ConfigDialog.css @@ -0,0 +1,5 @@ +.config-section-header { + margin-top: 0; + color: #888; + font-style: italic; +} diff --git a/src/visualizers/panels/ForgeActionButton/ConfigDialog.js b/src/visualizers/panels/ForgeActionButton/ConfigDialog.js new file mode 100644 index 0000000..ff5f5cf --- /dev/null +++ b/src/visualizers/panels/ForgeActionButton/ConfigDialog.js @@ -0,0 +1,159 @@ +/* globals define, $*/ +define([ + 'js/Dialogs/PluginConfig/PluginConfigDialog', + 'text!js/Dialogs/PluginConfig/templates/PluginConfigDialog.html', + 'plugin/Export/Export/format', + 'css!./ConfigDialog.css' +], function( + PluginConfigDialog, + pluginConfigDialogTemplate, + ExportFormats +) { + var SECTION_DATA_KEY = 'section', + ATTRIBUTE_DATA_KEY = 'attribute', + //jscs:disable maximumLineLength + PLUGIN_CONFIG_SECTION_BASE = $('
'), + ENTRY_BASE = $('
'), + //jscs:enable maximumLineLength + DESCRIPTION_BASE = $('
'), + SECTION_HEADER = $('
'); + + var ConfigDialog = function(client, nodeId) { + PluginConfigDialog.call(this, {client: client}); + this._widgets = {}; + this._node = this._client.getNode(nodeId); + }; + + ConfigDialog.prototype = Object.create(PluginConfigDialog.prototype); + + ConfigDialog.prototype.show = function(globalOptions, pluginMetadata, extMetadata, callback) { + this._extMetadata = extMetadata; + return PluginConfigDialog.prototype.show.call(this, globalOptions, pluginMetadata, {}, callback); + }; + + ConfigDialog.prototype._initDialog = function() { + this._dialog = $(pluginConfigDialogTemplate); + + this._btnSave = this._dialog.find('.btn-save'); + this._divContainer = this._dialog.find('.modal-body'); + this._saveConfigurationCb = this._dialog.find('.save-configuration'); + this._modalHeader = this._dialog.find('.modal-header'); + + // Create the header + var iconEl = $('', { + class: this._pluginMetadata.icon.class || 'glyphicon glyphicon-cog' + }); + iconEl.addClass('plugin-icon pull-left'); + this._modalHeader.prepend(iconEl); + this._title = this._modalHeader.find('.modal-title'); + this._title.text(this._pluginMetadata.id + ' ' + 'v' + this._pluginMetadata.version); + + // Generate the config options + var formats = Object.keys(ExportFormats), + format = formats[0], + sectionHeader = SECTION_HEADER.clone(); + + sectionHeader.text('Static Artifacts'); + this._divContainer.append(sectionHeader); + this.generateConfigSection(this._pluginMetadata); + + if (formats.length > 1) { + this._divContainer.append($('
')); + sectionHeader = SECTION_HEADER.clone(); + sectionHeader.text('Export Options'); + this._divContainer.append(sectionHeader); + + this.generateConfigSection({ + id: 'FormatOptions', + configStructure: this._globalOptions + }); + this._widgets.FormatOptions.exportFormat.el.find('select').on('change', event => { + var format = event.target.value; + // Update the ext config + this.updateExtConfig(format); + }); + } + + this.updateExtConfig(format); + }; + + ConfigDialog.prototype.updateExtConfig = function (format) { + var extConfig = { + class: 'extension-config', + configStructure: ExportFormats[format].getConfigStructure ? + ExportFormats[format].getConfigStructure(this._client, this._node) : [] + }; + this._divContainer.find('.extension-config').remove(); + + if (extConfig.configStructure.length) { + this.generateConfigSection(extConfig); + } + }; + + ConfigDialog.prototype.generateConfigSection = function (metadata) { + var len = metadata.configStructure.length, + i, + el, + pluginConfigEntry, + widget, + descEl, + containerEl, + pluginSectionEl = PLUGIN_CONFIG_SECTION_BASE.clone(); + + pluginSectionEl.data(SECTION_DATA_KEY, metadata.id); + this._divContainer.append(pluginSectionEl); + containerEl = pluginSectionEl.find('.form-horizontal'); + + if (metadata.class) { + pluginSectionEl.addClass(metadata.class); + } + + this._widgets[metadata.id] = {}; + for (i = 0; i < len; i += 1) { + pluginConfigEntry = metadata.configStructure[i]; + descEl = undefined; + + // Make sure not modify the global metadata. + pluginConfigEntry = JSON.parse(JSON.stringify(pluginConfigEntry)); + if (this._client.getProjectAccess().write === false && pluginConfigEntry.writeAccessRequired === true) { + pluginConfigEntry.readOnly = true; + } + + widget = this._propertyGridWidgetManager.getWidgetForProperty(pluginConfigEntry); + this._widgets[metadata.id][pluginConfigEntry.name] = widget; + + el = ENTRY_BASE.clone(); + el.data(ATTRIBUTE_DATA_KEY, pluginConfigEntry.name); + + el.find('label.control-label').text(pluginConfigEntry.displayName); + + if (pluginConfigEntry.description && pluginConfigEntry.description !== '') { + descEl = descEl || DESCRIPTION_BASE.clone(); + descEl.text(pluginConfigEntry.description); + } + + if (pluginConfigEntry.minValue !== undefined && + pluginConfigEntry.minValue !== null && + pluginConfigEntry.minValue !== '') { + descEl = descEl || DESCRIPTION_BASE.clone(); + descEl.append(' The minimum value is: ' + pluginConfigEntry.minValue + '.'); + } + + if (pluginConfigEntry.maxValue !== undefined && + pluginConfigEntry.maxValue !== null && + pluginConfigEntry.maxValue !== '') { + descEl = descEl || DESCRIPTION_BASE.clone(); + descEl.append(' The maximum value is: ' + pluginConfigEntry.maxValue + '.'); + } + + el.find('.controls').append(widget.el); + if (descEl) { + el.find('.description').append(descEl); + } + + containerEl.append(el); + } + }; + + return ConfigDialog; +}); diff --git a/src/visualizers/panels/ForgeActionButton/ForgeActionButton.js b/src/visualizers/panels/ForgeActionButton/ForgeActionButton.js index 92967cd..f497997 100644 --- a/src/visualizers/panels/ForgeActionButton/ForgeActionButton.js +++ b/src/visualizers/panels/ForgeActionButton/ForgeActionButton.js @@ -4,7 +4,7 @@ define([ 'blob/BlobClient', 'js/Utils/SaveToDisk', - 'js/Dialogs/PluginConfig/PluginConfigDialog', + './ConfigDialog', 'js/Constants', 'panel/FloatingActionButton/FloatingActionButton', 'deepforge/viz/PipelineControl', @@ -17,11 +17,11 @@ define([ 'q', 'deepforge/globals', 'deepforge/Constants', - 'plugin/GenerateExecFile/GenerateExecFile/format' + 'plugin/Export/Export/format' ], function ( BlobClient, SaveToDisk, - PluginConfigDialog, + ConfigDialog, GME_CONSTANTS, PluginButton, PipelineControl, @@ -388,7 +388,7 @@ define([ /// Export Pipeline Support ForgeActionButton.prototype.exportPipeline = function() { var deferred = Q.defer(), - pluginId = 'GenerateExecFile', + pluginId = 'Export', metadata = WebGMEGlobal.allPluginsMetadata[pluginId], id = this._currentNodeId, node = this.client.getNode(id), @@ -429,9 +429,16 @@ define([ .map(id => this.client.getNode(id)) .filter(output => output.getAttribute('data')); - // get the output data node name + // get the name of node referenced from the input op inputNames = inputData - .map(node => node.getAttribute('name')) + .map(node => { + var cntrId = node.getParentId(), + opId = this._client.getNode(cntrId).getParentId(), + inputOp = this._client.getNode(opId), + targetNodeId = inputOp.getPointer('artifact').to; + + return this._client.getNode(targetNodeId).getAttribute('name'); + }) .sort(); // create config options from inputs @@ -447,18 +454,11 @@ define([ }); var exportFormats = Object.keys(ExportFormatDict), - configDialog = new PluginConfigDialog({client: this.client}), + configDialog = new ConfigDialog(this.client, this._currentNodeId), inputConfig = _.extend({}, metadata), + extOptions = [], globalOpts = []; - // Hide the divider if missing inputOpts or globalOpts - configDialog._initDialog = function() { - PluginConfigDialog.prototype._initDialog.apply(this, arguments); - if (!globalOpts.length || !inputOpts.length) { - this._divContainer.find('.global-and-plugin-divider').remove(); - } - }; - if (exportFormats.length > 1) { globalOpts.push({ // format options name: 'exportFormat', @@ -471,8 +471,9 @@ define([ } inputConfig.configStructure = inputOpts; - if (inputOpts.length || exportFormats.length > 1) { - configDialog.show(globalOpts, inputConfig, {}, (formatOpts, inputOpts) => { + // Try to get the extension options + if (inputOpts.length || exportFormats.length > 1|| extOptions.length) { + configDialog.show(globalOpts, inputConfig, (formatOpts, inputOpts) => { var context = this.client.getCurrentPluginContext(pluginId), exportFormat = (globalOpts.length && formatOpts) ? formatOpts.exportFormat : exportFormats[0], staticInputs = Object.keys(inputOpts || {}).filter(input => inputOpts[input]); @@ -508,5 +509,6 @@ define([ return deferred.promise; }; + return ForgeActionButton; }); diff --git a/test/plugins/GenerateExecFile/GenerateExecFile.spec.js b/test/plugins/Export/Export.spec.js similarity index 97% rename from test/plugins/GenerateExecFile/GenerateExecFile.spec.js rename to test/plugins/Export/Export.spec.js index 1ba57b6..7bab697 100644 --- a/test/plugins/GenerateExecFile/GenerateExecFile.spec.js +++ b/test/plugins/Export/Export.spec.js @@ -1,7 +1,7 @@ /*jshint node:true, mocha:true*/ 'use strict'; -describe('GenerateExecFile', function () { +describe('Export', function () { var testFixture = require('../../globals'), lua = require('../../../src/common/lua'), path = testFixture.path, @@ -9,12 +9,12 @@ describe('GenerateExecFile', function () { SEED_DIR = path.join(testFixture.DF_SEED_DIR, 'devProject'), gmeConfig = testFixture.getGmeConfig(), expect = testFixture.expect, - logger = testFixture.logger.fork('GenerateExecFile'), + logger = testFixture.logger.fork('Export'), PluginCliManager = testFixture.WebGME.PluginCliManager, manager = new PluginCliManager(null, logger, gmeConfig), BlobClient = require('webgme/src/server/middleware/blob/BlobClientWithFSBackend'), projectName = 'testProject', - pluginName = 'GenerateExecFile', + pluginName = 'Export', project, gmeAuth, storage, diff --git a/utils/extender.js b/utils/extender.js new file mode 100644 index 0000000..445723a --- /dev/null +++ b/utils/extender.js @@ -0,0 +1,199 @@ +// Utility for applying and removing deepforge extensions +// This utility is run by the cli when executing: +// +// deepforge extensions add +// deepforge extensions remove +// +var path = require('path'), + fs = require('fs'), + npm = require('npm'), + Q = require('q'), + rm_rf = require('rimraf'), + exists = require('exists-file'), + makeTpl = require('lodash.template'), + CONFIG_DIR = path.join(process.env.HOME, '.deepforge'), + EXT_CONFIG_NAME = 'extension.json', + EXTENSION_REGISTRY_NAME = 'extensions.json', + extConfigPath = path.join(CONFIG_DIR, EXTENSION_REGISTRY_NAME), + allExtConfigs; + +var values = obj => Object.keys(obj).map(key => obj[key]); + +// Create the extensions.json if doesn't exist. Otherwise, load it +if (!exists.sync(extConfigPath)) { + allExtConfigs = {}; +} else { + try { + allExtConfigs = JSON.parse(fs.readFileSync(extConfigPath, 'utf8')); + } catch (e) { + throw `Invalid config at ${extConfigPath}: ${e.toString()}`; + } +} + +var persistExtConfig = () => { + fs.writeFileSync(extConfigPath, JSON.stringify(allExtConfigs, null, 2)); +}; + +var extender = {}; + +extender.EXT_CONFIG_NAME = EXT_CONFIG_NAME; + +extender.isSupportedType = function(type) { + return extender.install[type] && extender.uninstall[type]; +}; + +extender.getExtensionsConfig = function() { + return allExtConfigs; +}; + +extender.getInstalledConfig = function(name) { + var group = values(allExtConfigs).find(typeGroup => { + return !!typeGroup[name]; + }); + return group && group[name]; +}; + +extender.install = function(project, isReinstall) { + // Install the project + return Q.ninvoke(npm, 'load', {}) + .then(() => Q.ninvoke(npm, 'install', project)) + .then(results => { + var installed = results[0], + extProject, + extRoot; + + extProject = installed[0][0]; + extRoot = installed[0][1]; + + // Check for the extensions.json in the project (look up type, etc) + var extConfigPath = path.join(extRoot, extender.EXT_CONFIG_NAME), + extConfig, + extType; + + // Check that the extensions file exists + if (!exists.sync(extConfigPath)) { + throw [ + `Could not find ${extender.EXT_CONFIG_NAME} for ${project}.`, + '', + `This is likely an issue w/ the deepforge extension (${project})` + ].join('\n'); + } + + try { + extConfig = JSON.parse(fs.readFileSync(extConfigPath, 'utf8')); + } catch(e) { // Invalid JSON + throw `Invalid ${extender.EXT_CONFIG_NAME}: ${e}`; + } + + // Try to add the extension to the project (using the extender) + extType = extConfig.type; + if (!extender.isSupportedType(extType)) { + throw `Unrecognized extension type: "${extType}"`; + } + extender.install[extType](extConfig, { + arg: project, + root: extRoot, + name: extProject + }, !!isReinstall); + + return extConfig; + }); +}; + +extender.uninstall = function(name) { + // Look up the extension in ~/.deepforge/extensions.json + var extConfig = extender.getInstalledConfig(name); + if (!extConfig) { + throw `Extension "${name}" not found`; + } + + // Run the uninstaller using the extender + var extType = extConfig.type; + extender.uninstall[extType](name); +}; + +var makeInstallFor = function(typeCfg) { + var saveExtensions = () => { + // regenerate the format.js file from the template + var installedExts = values(allExtConfigs[typeCfg.type]), + formatTemplate = makeTpl(fs.readFileSync(typeCfg.template, 'utf8')), + formatsIndex = formatTemplate({path: path, formats: installedExts}), + dstPath = typeCfg.template.replace(/\.ejs$/, ''); + + fs.writeFileSync(dstPath, formatsIndex); + persistExtConfig(); + }; + + // Given a... + // - template file + // - extension type + // - target path tpl + // create the installation/uninstallation functions + extender.install[typeCfg.type] = (config, project, isReinstall) => { + var dstPath, + pkgJsonPath = path.join(project.root, 'package.json'), + pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')), + content; + + // add the config to the current installed extensions of this type + project = project || config.project; + config.version = pkgJson.version; + config.project = project; + + allExtConfigs[typeCfg.type] = allExtConfigs[typeCfg.type] || {}; + + if (allExtConfigs[typeCfg.type][config.name] && !isReinstall) { + // eslint-disable-next-line no-console + console.error(`Extension ${config.name} already installed. Reinstalling...`); + } + + allExtConfigs[typeCfg.type][config.name] = config; + + // copy the main script to src/plugins/Export/formats//
+ dstPath = makeTpl(typeCfg.targetDir)(config); + if (!exists.sync(dstPath)) { + fs.mkdirSync(dstPath); + } + + try { + content = fs.readFileSync(path.join(project.root, config.main), 'utf8'); + } catch (e) { + throw 'Could not read the extension\'s main file: ' + e; + } + dstPath = path.join(dstPath, path.basename(config.main)); + fs.writeFileSync(dstPath, content); + + saveExtensions(); + }; + + // uninstall + extender.uninstall['Export:Pipeline'] = name => { + // Remove from config + allExtConfigs[typeCfg.type] = allExtConfigs[typeCfg.type] || {}; + + if (!allExtConfigs[typeCfg.type][name]) { + // eslint-disable-next-line no-console + console.log(`Extension ${name} not installed`); + return; + } + var config = allExtConfigs[typeCfg.type][name], + dstPath = makeTpl(typeCfg.targetDir)(config); + + // Remove the dstPath + delete allExtConfigs[typeCfg.type][name]; + rm_rf.sync(dstPath); + + // Re-generate template file + saveExtensions(); + }; + +}; + +var PLUGIN_ROOT = path.join(__dirname, '..', 'src', 'plugins', 'Export'); +makeInstallFor({ + type: 'Export:Pipeline', + template: path.join(PLUGIN_ROOT, 'format.js.ejs'), + targetDir: path.join(PLUGIN_ROOT, 'formats', '<%=name%>') +}); + +module.exports = extender; diff --git a/utils/reinstall-extensions.js b/utils/reinstall-extensions.js new file mode 100644 index 0000000..ffbb435 --- /dev/null +++ b/utils/reinstall-extensions.js @@ -0,0 +1,30 @@ +// Re-install all extensions +var extender = require('./extender'), + Q = require('q'), + extConfig = extender.getExtensionsConfig(), + types, + names, + currentInstall = Q(), + installCount = 0, + config; + +// Read the extensions and reinstall each of them +types = Object.keys(extConfig); +for (var i = types.length; i--;) { + names = Object.keys(extConfig[types[i]]); + if (names.length) { + installCount += names.length; + for (var j = names.length; j--;) { + // eslint-disable-next-line no-console + console.log(`Re-installing ${names[j]} extension...`); + config = extConfig[types[i]][names[j]]; + currentInstall = currentInstall + .then(() => extender.install(config.project.arg, true)); + } + } +} + +if (installCount) { + // eslint-disable-next-line no-console + currentInstall.then(() => console.log('Extensions reinstalled successfully')); +} diff --git a/webgme-setup.json b/webgme-setup.json index b65c888..8365211 100644 --- a/webgme-setup.json +++ b/webgme-setup.json @@ -29,9 +29,9 @@ "src": "src/plugins/CreateExecution", "test": "test/plugins/CreateExecution" }, - "GenerateExecFile": { - "src": "src/plugins/GenerateExecFile", - "test": "test/plugins/GenerateExecFile" + "Export": { + "src": "src/plugins/Export", + "test": "test/plugins/Export" }, "ImportArtifact": { "src": "src/plugins/ImportArtifact",