Skip to content

Commit dd529f2

Browse files
authored
Add option to import examples from libraries. Closes #1993 (#1994)
* 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
1 parent 331c76d commit dd529f2

13 files changed

Lines changed: 439 additions & 45 deletions

File tree

.codeclimate.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ exclude_paths:
4242
- src/visualizers/widgets/TensorPlotter/lib/
4343
- src/visualizers/widgets/TensorPlotter/styles/simple-grid.min.css
4444
- src/visualizers/widgets/TrainKeras/build
45+
- src/visualizers/panels/ForgeActionButton/build
4546
- src/visualizers/panels/TrainKeras/JSONImporter.js
4647
- src/visualizers/panels/TrainKeras/changeset.js
4748
- src/visualizers/widgets/InteractiveWorkspace/lib
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*globals define*/
2+
3+
define([
4+
'text!./metadata.json',
5+
'plugin/PluginBase',
6+
'fs',
7+
'../../visualizers/panels/ForgeActionButton/Libraries.json',
8+
], function (
9+
pluginMetadata,
10+
PluginBase,
11+
fs,
12+
Libraries,
13+
) {
14+
'use strict';
15+
16+
const fsp = fs.promises;
17+
pluginMetadata = JSON.parse(pluginMetadata);
18+
19+
class UploadLibraryModelToBlob extends PluginBase {
20+
constructor(libraries=Libraries) {
21+
super();
22+
this.pluginMetadata = pluginMetadata;
23+
this.libraries = libraries;
24+
}
25+
26+
async main(/*callback*/) {
27+
const config = this.getCurrentConfig();
28+
const {libraryName, modelName} = config;
29+
const hash = await this.uploadLibraryModel(libraryName, modelName);
30+
this.createMessage(this.rootNode, hash);
31+
this.result.setSuccess(true);
32+
//callback(null, this.result);
33+
}
34+
35+
async uploadLibraryModel(libraryName, modelName) {
36+
const data = await fsp.readFile(this.getLibraryModelPath(libraryName, modelName));
37+
const hash = await this.blobClient.putFile(`${modelName}.webgmexm`, data);
38+
return hash;
39+
}
40+
41+
getLibraryModelPath(libraryName, modelName) {
42+
const modelInfo = this.getLibraryModelInfo(libraryName, modelName);
43+
return modelInfo.path;
44+
}
45+
46+
getLibraryModelInfo(libraryName, modelName) {
47+
const libraryInfo = this.libraries.find(libraryInfo => libraryInfo.name === libraryName);
48+
if (!libraryInfo) {
49+
throw new Error(`Library not found: ${libraryName}`);
50+
}
51+
const modelInfo = libraryInfo.models.find(modelInfo => modelInfo.name === modelName);
52+
if (!modelInfo) {
53+
throw new Error(`Model not found in ${libraryName}: ${modelName}`);
54+
}
55+
return modelInfo;
56+
}
57+
}
58+
59+
UploadLibraryModelToBlob.metadata = pluginMetadata;
60+
61+
return UploadLibraryModelToBlob;
62+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"id": "UploadLibraryModelToBlob",
3+
"name": "UploadLibraryModelToBlob",
4+
"version": "0.1.0",
5+
"description": "",
6+
"icon": {
7+
"class": "glyphicon glyphicon-cog",
8+
"src": ""
9+
},
10+
"disableServerSideExecution": false,
11+
"disableBrowserSideExecution": false,
12+
"dependencies": [],
13+
"writeAccessRequired": false,
14+
"configStructure": []
15+
}

src/visualizers/panels/ForgeActionButton/Actions.js

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*globals define, WebGMEGlobal*/
1+
/*globals define, WebGMEGlobal, $*/
22
// These are actions defined for specific meta types. They are evaluated from
33
// the context of the ForgeActionButton
44
define([
@@ -9,6 +9,8 @@ define([
99
'deepforge/globals',
1010
'deepforge/viz/TextPrompter',
1111
'deepforge/viz/StorageHelpers',
12+
'text!./Libraries.json',
13+
'./build/ExamplesDialog',
1214
], function(
1315
LibraryDialog,
1416
Materialize,
@@ -17,12 +19,40 @@ define([
1719
DeepForge,
1820
TextPrompter,
1921
StorageHelpers,
22+
Libraries,
23+
ExamplesDialog,
2024
) {
25+
Libraries = JSON.parse(Libraries);
2126
var returnToLast = (place) => {
2227
var returnId = DeepForge.last[place];
2328
WebGMEGlobal.State.registerActiveObject(returnId);
2429
};
2530

31+
async function importExample(client, example, parentId) {
32+
const hash = await uploadExampleToBlob(client, example);
33+
await Q.ninvoke(
34+
client,
35+
'importSelectionFromFile',
36+
client.getActiveProjectId(),
37+
client.getActiveBranchName(),
38+
parentId,
39+
hash,
40+
);
41+
}
42+
43+
async function uploadExampleToBlob(client, example) {
44+
const pluginName = 'UploadLibraryModelToBlob';
45+
const context = client.getCurrentPluginContext(pluginName);
46+
context.pluginConfig = {
47+
libraryName: example.library,
48+
modelName: example.name,
49+
};
50+
const result = await Q.ninvoke(client, 'runServerPlugin', pluginName, context);
51+
const [hashMessage] = result.messages;
52+
const hash = hashMessage.message;
53+
return hash;
54+
}
55+
2656
var prototypeButtons = function(type, fromType) {
2757
return [
2858
{
@@ -120,6 +150,55 @@ define([
120150
// TODO: Add support for adding (inherited) children
121151

122152
buttons = addButtons.concat(buttons);
153+
154+
const installedLibs = client.getLibraryNames()
155+
.map(name => Libraries.find(lib => lib.name === name))
156+
.filter(lib => !!lib);
157+
const hasExampleModels = installedLibs.flatMap(lib => lib.models).length > 0;
158+
if (hasExampleModels) {
159+
buttons.unshift({
160+
name: 'Import Example...',
161+
icon: 'view_list',
162+
action: function() {
163+
const installedLibs = client.getLibraryNames()
164+
.map(name => Libraries.find(lib => lib.name === name))
165+
.filter(lib => !!lib);
166+
167+
installedLibs
168+
.forEach(info => info.models.forEach(model => model.library = info.name));
169+
const exampleModels = installedLibs.flatMap(lib => lib.models);
170+
171+
if (this.examplesDialog) {
172+
this.examplesDialog.destroy();
173+
}
174+
this.examplesDialog = new ExamplesDialog(
175+
{
176+
target: document.body,
177+
props: {
178+
examples: exampleModels,
179+
jquery: $,
180+
client,
181+
}
182+
}
183+
);
184+
this.examplesDialog.events().addEventListener(
185+
'importExample',
186+
async event => {
187+
const example = event.detail;
188+
try {
189+
Materialize.toast(`Importing ${example.name} from ${example.library}...`, 2000);
190+
await importExample(client, example, this._currentNodeId);
191+
Materialize.toast('Import complete!', 2000);
192+
} catch(err) {
193+
Materialize.toast(`Import failed: ${err.message}`, 3000);
194+
throw err;
195+
}
196+
}
197+
);
198+
199+
}
200+
});
201+
}
123202
return buttons;
124203
},
125204
MyOperations_META: [
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<script>
2+
let element;
3+
export let examples = [];
4+
export let jquery;
5+
export let client;
6+
import { onMount } from 'svelte';
7+
8+
onMount(() => jquery(element).modal('show'));
9+
10+
export function destroy() {
11+
jquery(element).modal('hide');
12+
}
13+
14+
export function events() {
15+
return element;
16+
}
17+
18+
async function importExample(example) {
19+
const event = new CustomEvent('importExample', {detail: example});
20+
element.dispatchEvent(event);
21+
}
22+
23+
</script>
24+
25+
<div bind:this={element} class="examples-modal modal fade in" tabindex="-1" role="dialog">
26+
<div class="modal-dialog modal-lg">
27+
<div class="modal-content">
28+
<div class="modal-header">
29+
<button type="button" class="close" on:click|stopPropagation|preventDefault={destroy}>x</button>
30+
<span class="title">Available Examples</span>
31+
</div>
32+
<div class="modal-body">
33+
<div>
34+
<table class="table highlight">
35+
<thead>
36+
<tr>
37+
<th >Name</th>
38+
<th >Library</th>
39+
<th >Description</th>
40+
</tr>
41+
</thead>
42+
<tbody>
43+
{#each examples as example}
44+
<tr>
45+
<td>{example.name}</td>
46+
<td>{example.library}</td>
47+
<td class="description">{example.description}</td>
48+
<!-- TODO: add loading icon? -->
49+
<td on:click|stopPropagation|preventDefault={() => importExample(example)}><i class="material-icons">get_app</i></td>
50+
</tr>
51+
{/each}
52+
</tbody>
53+
</table>
54+
</div>
55+
</div>
56+
</div>
57+
</div>
58+
</div>
59+
60+
<style>
61+
.description {
62+
font-style: italic;
63+
}
64+
65+
.title {
66+
font-size: 28px;
67+
vertical-align: middle;
68+
}
69+
70+
.examples-modal th {
71+
text-align: left;
72+
}
73+
</style>

src/visualizers/panels/ForgeActionButton/Libraries.json.ejs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
description: ext.description,
55
nodeTypes: ext.nodeTypes,
66
initCode: ext.initCode,
7-
seed: ext.seed
7+
seed: ext.seed,
8+
models: ext.models,
89
};
910
}), null, 2) %>

src/visualizers/panels/ForgeActionButton/build/ExamplesDialog.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/visualizers/panels/ForgeActionButton/build/ExamplesDialog.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/assets/TestOperation.webgmexm

1.32 KB
Binary file not shown.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*eslint-env node, mocha*/
2+
/**
3+
* Generated by PluginGenerator 2.20.5 from webgme on Thu Feb 11 2021 12:20:02 GMT-0600 (Central Standard Time).
4+
*/
5+
6+
const testFixture = require('../../../globals');
7+
8+
describe('UploadLibraryModelToBlob', function () {
9+
const gmeConfig = testFixture.getGmeConfig();
10+
const logger = testFixture.logger.fork('UploadLibraryModelToBlob');
11+
const PluginCliManager = testFixture.WebGME.PluginCliManager;
12+
13+
const assert = require('assert');
14+
const {promisify} = require('util');
15+
const manager = new PluginCliManager(null, logger, gmeConfig);
16+
const pluginName = 'UploadLibraryModelToBlob';
17+
const projectName = 'testProject';
18+
const PIPELINES = '/f';
19+
manager.executePlugin = promisify(manager.executePlugin);
20+
manager.runPluginMain = promisify(manager.runPluginMain);
21+
let context,
22+
gmeAuth,
23+
storage;
24+
25+
before(async function () {
26+
gmeAuth = await testFixture.clearDBAndGetGMEAuth(gmeConfig, projectName);
27+
storage = testFixture.getMemoryStorage(logger, gmeConfig, gmeAuth);
28+
await storage.openDatabase();
29+
const importParam = {
30+
projectSeed: testFixture.path.join(testFixture.DF_SEED_DIR, 'devProject', 'devProject.webgmex'),
31+
projectName: projectName,
32+
branchName: 'master',
33+
logger: logger,
34+
gmeConfig: gmeConfig
35+
};
36+
37+
const importResult = await testFixture.importProject(storage, importParam);
38+
const {project, commitHash} = importResult;
39+
await project.createBranch('test', commitHash);
40+
context = {
41+
project: project,
42+
commitHash: commitHash,
43+
branchName: 'test',
44+
activeNode: PIPELINES,
45+
namespace: 'pipeline',
46+
};
47+
48+
});
49+
50+
after(async function () {
51+
await storage.closeDatabase();
52+
await gmeAuth.unload();
53+
});
54+
55+
it('should return the hash in the first message', async function () {
56+
const plugin = await manager.initializePlugin(pluginName);
57+
plugin.libraries = [{
58+
name: 'testlib',
59+
models: [{
60+
name: 'TestOperation',
61+
path: 'test/assets/TestOperation.webgmexm'
62+
}]
63+
}];
64+
const pluginConfig = {
65+
libraryName: 'testlib',
66+
modelName: 'TestOperation',
67+
};
68+
await manager.configurePlugin(plugin, pluginConfig, context);
69+
const {messages} = await manager.runPluginMain(plugin);
70+
assert.equal(messages.length, 1);
71+
assert.equal(messages[0].message.length, 40);
72+
const alphnum = /^[a-z0-9]+$/;
73+
assert(alphnum.test(messages[0].message));
74+
});
75+
76+
it('should throw error if library not found', async function () {
77+
const plugin = await manager.initializePlugin(pluginName);
78+
const pluginConfig = {
79+
libraryName: 'IDontExist',
80+
modelName: 'unused',
81+
};
82+
await manager.configurePlugin(plugin, pluginConfig, context);
83+
await assert.rejects(
84+
() => manager.runPluginMain(plugin),
85+
/Library not found/
86+
);
87+
});
88+
89+
it('should throw error if model not found', async function () {
90+
const plugin = await manager.initializePlugin(pluginName);
91+
plugin.libraries = [{
92+
name: 'testlib',
93+
models: []
94+
}];
95+
const pluginConfig = {
96+
libraryName: 'testlib',
97+
modelName: 'unused',
98+
};
99+
await manager.configurePlugin(plugin, pluginConfig, context);
100+
await assert.rejects(
101+
() => manager.runPluginMain(plugin),
102+
/Model not found/
103+
);
104+
});
105+
});

0 commit comments

Comments
 (0)