Skip to content

Commit

Permalink
Add option to import examples from libraries. Closes #1993 (#1994)
Browse files Browse the repository at this point in the history
* Add option to import examples from libraries. Closes #1993

* Add error handling

* Add built files to codeclimate ignore

* move generated files to build dir

* Add tests for upload library models plugin

* Add test asset
  • Loading branch information
brollb committed Feb 12, 2021
1 parent 331c76d commit dd529f2
Show file tree
Hide file tree
Showing 13 changed files with 439 additions and 45 deletions.
1 change: 1 addition & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ exclude_paths:
- src/visualizers/widgets/TensorPlotter/lib/
- src/visualizers/widgets/TensorPlotter/styles/simple-grid.min.css
- src/visualizers/widgets/TrainKeras/build
- src/visualizers/panels/ForgeActionButton/build
- src/visualizers/panels/TrainKeras/JSONImporter.js
- src/visualizers/panels/TrainKeras/changeset.js
- src/visualizers/widgets/InteractiveWorkspace/lib
62 changes: 62 additions & 0 deletions src/plugins/UploadLibraryModelToBlob/UploadLibraryModelToBlob.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*globals define*/

define([
'text!./metadata.json',
'plugin/PluginBase',
'fs',
'../../visualizers/panels/ForgeActionButton/Libraries.json',
], function (
pluginMetadata,
PluginBase,
fs,
Libraries,
) {
'use strict';

const fsp = fs.promises;
pluginMetadata = JSON.parse(pluginMetadata);

class UploadLibraryModelToBlob extends PluginBase {
constructor(libraries=Libraries) {
super();
this.pluginMetadata = pluginMetadata;
this.libraries = libraries;
}

async main(/*callback*/) {
const config = this.getCurrentConfig();
const {libraryName, modelName} = config;
const hash = await this.uploadLibraryModel(libraryName, modelName);
this.createMessage(this.rootNode, hash);
this.result.setSuccess(true);
//callback(null, this.result);
}

async uploadLibraryModel(libraryName, modelName) {
const data = await fsp.readFile(this.getLibraryModelPath(libraryName, modelName));
const hash = await this.blobClient.putFile(`${modelName}.webgmexm`, data);
return hash;
}

getLibraryModelPath(libraryName, modelName) {
const modelInfo = this.getLibraryModelInfo(libraryName, modelName);
return modelInfo.path;
}

getLibraryModelInfo(libraryName, modelName) {
const libraryInfo = this.libraries.find(libraryInfo => libraryInfo.name === libraryName);
if (!libraryInfo) {
throw new Error(`Library not found: ${libraryName}`);
}
const modelInfo = libraryInfo.models.find(modelInfo => modelInfo.name === modelName);
if (!modelInfo) {
throw new Error(`Model not found in ${libraryName}: ${modelName}`);
}
return modelInfo;
}
}

UploadLibraryModelToBlob.metadata = pluginMetadata;

return UploadLibraryModelToBlob;
});
15 changes: 15 additions & 0 deletions src/plugins/UploadLibraryModelToBlob/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "UploadLibraryModelToBlob",
"name": "UploadLibraryModelToBlob",
"version": "0.1.0",
"description": "",
"icon": {
"class": "glyphicon glyphicon-cog",
"src": ""
},
"disableServerSideExecution": false,
"disableBrowserSideExecution": false,
"dependencies": [],
"writeAccessRequired": false,
"configStructure": []
}
81 changes: 80 additions & 1 deletion src/visualizers/panels/ForgeActionButton/Actions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*globals define, WebGMEGlobal*/
/*globals define, WebGMEGlobal, $*/
// These are actions defined for specific meta types. They are evaluated from
// the context of the ForgeActionButton
define([
Expand All @@ -9,6 +9,8 @@ define([
'deepforge/globals',
'deepforge/viz/TextPrompter',
'deepforge/viz/StorageHelpers',
'text!./Libraries.json',
'./build/ExamplesDialog',
], function(
LibraryDialog,
Materialize,
Expand All @@ -17,12 +19,40 @@ define([
DeepForge,
TextPrompter,
StorageHelpers,
Libraries,
ExamplesDialog,
) {
Libraries = JSON.parse(Libraries);
var returnToLast = (place) => {
var returnId = DeepForge.last[place];
WebGMEGlobal.State.registerActiveObject(returnId);
};

async function importExample(client, example, parentId) {
const hash = await uploadExampleToBlob(client, example);
await Q.ninvoke(
client,
'importSelectionFromFile',
client.getActiveProjectId(),
client.getActiveBranchName(),
parentId,
hash,
);
}

async function uploadExampleToBlob(client, example) {
const pluginName = 'UploadLibraryModelToBlob';
const context = client.getCurrentPluginContext(pluginName);
context.pluginConfig = {
libraryName: example.library,
modelName: example.name,
};
const result = await Q.ninvoke(client, 'runServerPlugin', pluginName, context);
const [hashMessage] = result.messages;
const hash = hashMessage.message;
return hash;
}

var prototypeButtons = function(type, fromType) {
return [
{
Expand Down Expand Up @@ -120,6 +150,55 @@ define([
// TODO: Add support for adding (inherited) children

buttons = addButtons.concat(buttons);

const installedLibs = client.getLibraryNames()
.map(name => Libraries.find(lib => lib.name === name))
.filter(lib => !!lib);
const hasExampleModels = installedLibs.flatMap(lib => lib.models).length > 0;
if (hasExampleModels) {
buttons.unshift({
name: 'Import Example...',
icon: 'view_list',
action: function() {
const installedLibs = client.getLibraryNames()
.map(name => Libraries.find(lib => lib.name === name))
.filter(lib => !!lib);

installedLibs
.forEach(info => info.models.forEach(model => model.library = info.name));
const exampleModels = installedLibs.flatMap(lib => lib.models);

if (this.examplesDialog) {
this.examplesDialog.destroy();
}
this.examplesDialog = new ExamplesDialog(
{
target: document.body,
props: {
examples: exampleModels,
jquery: $,
client,
}
}
);
this.examplesDialog.events().addEventListener(
'importExample',
async event => {
const example = event.detail;
try {
Materialize.toast(`Importing ${example.name} from ${example.library}...`, 2000);
await importExample(client, example, this._currentNodeId);
Materialize.toast('Import complete!', 2000);
} catch(err) {
Materialize.toast(`Import failed: ${err.message}`, 3000);
throw err;
}
}
);

}
});
}
return buttons;
},
MyOperations_META: [
Expand Down
73 changes: 73 additions & 0 deletions src/visualizers/panels/ForgeActionButton/ExamplesDialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script>
let element;
export let examples = [];
export let jquery;
export let client;
import { onMount } from 'svelte';
onMount(() => jquery(element).modal('show'));
export function destroy() {
jquery(element).modal('hide');
}
export function events() {
return element;
}
async function importExample(example) {
const event = new CustomEvent('importExample', {detail: example});
element.dispatchEvent(event);
}
</script>

<div bind:this={element} class="examples-modal modal fade in" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" on:click|stopPropagation|preventDefault={destroy}>x</button>
<span class="title">Available Examples</span>
</div>
<div class="modal-body">
<div>
<table class="table highlight">
<thead>
<tr>
<th >Name</th>
<th >Library</th>
<th >Description</th>
</tr>
</thead>
<tbody>
{#each examples as example}
<tr>
<td>{example.name}</td>
<td>{example.library}</td>
<td class="description">{example.description}</td>
<!-- TODO: add loading icon? -->
<td on:click|stopPropagation|preventDefault={() => importExample(example)}><i class="material-icons">get_app</i></td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

<style>
.description {
font-style: italic;
}
.title {
font-size: 28px;
vertical-align: middle;
}
.examples-modal th {
text-align: left;
}
</style>
3 changes: 2 additions & 1 deletion src/visualizers/panels/ForgeActionButton/Libraries.json.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
description: ext.description,
nodeTypes: ext.nodeTypes,
initCode: ext.initCode,
seed: ext.seed
seed: ext.seed,
models: ext.models,
};
}), null, 2) %>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Binary file added test/assets/TestOperation.webgmexm
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*eslint-env node, mocha*/
/**
* Generated by PluginGenerator 2.20.5 from webgme on Thu Feb 11 2021 12:20:02 GMT-0600 (Central Standard Time).
*/

const testFixture = require('../../../globals');

describe('UploadLibraryModelToBlob', function () {
const gmeConfig = testFixture.getGmeConfig();
const logger = testFixture.logger.fork('UploadLibraryModelToBlob');
const PluginCliManager = testFixture.WebGME.PluginCliManager;

const assert = require('assert');
const {promisify} = require('util');
const manager = new PluginCliManager(null, logger, gmeConfig);
const pluginName = 'UploadLibraryModelToBlob';
const projectName = 'testProject';
const PIPELINES = '/f';
manager.executePlugin = promisify(manager.executePlugin);
manager.runPluginMain = promisify(manager.runPluginMain);
let context,
gmeAuth,
storage;

before(async function () {
gmeAuth = await testFixture.clearDBAndGetGMEAuth(gmeConfig, projectName);
storage = testFixture.getMemoryStorage(logger, gmeConfig, gmeAuth);
await storage.openDatabase();
const importParam = {
projectSeed: testFixture.path.join(testFixture.DF_SEED_DIR, 'devProject', 'devProject.webgmex'),
projectName: projectName,
branchName: 'master',
logger: logger,
gmeConfig: gmeConfig
};

const importResult = await testFixture.importProject(storage, importParam);
const {project, commitHash} = importResult;
await project.createBranch('test', commitHash);
context = {
project: project,
commitHash: commitHash,
branchName: 'test',
activeNode: PIPELINES,
namespace: 'pipeline',
};

});

after(async function () {
await storage.closeDatabase();
await gmeAuth.unload();
});

it('should return the hash in the first message', async function () {
const plugin = await manager.initializePlugin(pluginName);
plugin.libraries = [{
name: 'testlib',
models: [{
name: 'TestOperation',
path: 'test/assets/TestOperation.webgmexm'
}]
}];
const pluginConfig = {
libraryName: 'testlib',
modelName: 'TestOperation',
};
await manager.configurePlugin(plugin, pluginConfig, context);
const {messages} = await manager.runPluginMain(plugin);
assert.equal(messages.length, 1);
assert.equal(messages[0].message.length, 40);
const alphnum = /^[a-z0-9]+$/;
assert(alphnum.test(messages[0].message));
});

it('should throw error if library not found', async function () {
const plugin = await manager.initializePlugin(pluginName);
const pluginConfig = {
libraryName: 'IDontExist',
modelName: 'unused',
};
await manager.configurePlugin(plugin, pluginConfig, context);
await assert.rejects(
() => manager.runPluginMain(plugin),
/Library not found/
);
});

it('should throw error if model not found', async function () {
const plugin = await manager.initializePlugin(pluginName);
plugin.libraries = [{
name: 'testlib',
models: []
}];
const pluginConfig = {
libraryName: 'testlib',
modelName: 'unused',
};
await manager.configurePlugin(plugin, pluginConfig, context);
await assert.rejects(
() => manager.runPluginMain(plugin),
/Model not found/
);
});
});

0 comments on commit dd529f2

Please sign in to comment.