Merge pull request #9346 from guerler/workflow_editor_node_terminals

Remove Backbone and Globals dependency from Workflow client code
This commit is contained in:
John Chilton
2020-02-18 13:55:05 -05:00
committed by GitHub
40 changed files with 1673 additions and 1656 deletions
+1 -2
View File
@@ -93,8 +93,7 @@ export { panelManagement } from "onload/globalInits/panelManagement";
export { mountMakoTags } from "components/Tags";
export { mountJobMetrics } from "components/JobMetrics";
export { mountJobParameters } from "components/JobParameters";
export { mountWorkflowEditor, mountWorkflowPanel } from "components/Workflow/Editor/mount";
export { mountToolBoxWorkflow } from "components/Panels/mount";
export { mountWorkflowEditor } from "components/Workflow/Editor/mount";
export { mountPageDisplay } from "components/PageDisplay";
// Used in common.mako
@@ -65,33 +65,26 @@ describe("ToolSection", () => {
}
});
expect(wrapper.vm.opened).to.equal(true);
const $sectionName = wrapper.find(".name");
$sectionName.trigger("click");
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(false);
wrapper.vm.queryFilter = "";
wrapper.setProps({ queryFilter: "" });
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(false);
wrapper.vm.queryFilter = "test";
wrapper.setProps({ queryFilter: "test" });
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(true);
wrapper.vm.disableFilter = true;
wrapper.setProps({ disableFilter: true });
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(true);
wrapper.vm.queryFilter = "";
wrapper.setProps({ queryFilter: "" });
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(false);
$sectionName.trigger("click");
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(true);
wrapper.vm.queryFilter = "test";
wrapper.setProps({ queryFilter: "test" });
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(false);
});
@@ -1,6 +1,8 @@
<template>
<div :id="side" class="unified-panel-outer-wrap" :style="styles">
<component :is="currentPanel" v-bind="currentPanelProperties" />
<slot name="panel">
<component :is="currentPanel" v-bind="currentPanelProperties" />
</slot>
<div class="unified-panel-footer">
<div
class="panel-collapse"
@@ -22,8 +22,8 @@
@onClick="onInsertModule"
/>
<tool-section
:category="dataManagers"
:key="dataManagers.id"
:category="dataManagerSection"
:key="dataManagerSection.id"
:query-filter="query"
:disable-filter="true"
@onClick="onInsertTool"
@@ -40,7 +40,7 @@
<tool-section
:category="workflowSection"
:key="workflowSection.name"
operation-icon="fa fa-copy"
operation-icon="fa fa-files-o"
operation-title="Insert individual steps."
:query-filter="query"
:disable-filter="true"
@@ -53,6 +53,7 @@
</template>
<script>
import _l from "utils/localization";
import ToolSection from "./Common/ToolSection";
import ToolSearch from "./Common/ToolSearch";
import { filterToolSections } from "./utilities";
@@ -74,20 +75,29 @@ export default {
type: Array,
required: true
},
workflowSection: {
type: Object
},
workflowGlobals: {
type: Object
workflows: {
type: Array
},
dataManagers: {
type: Object
type: Array
},
moduleSections: {
type: Array
}
},
computed: {
workflowSection() {
return {
name: _l("Workflows"),
elems: this.workflows
};
},
dataManagerSection() {
return {
name: _l("Data Managers"),
elems: this.dataManagers
};
},
categories() {
return filterToolSections(this.toolsLayout, this.results);
},
@@ -114,18 +124,18 @@ export default {
},
onInsertTool(tool, evt) {
evt.preventDefault();
this.workflowGlobals.app.add_node_for_tool(tool.id, tool.name);
this.$emit("onInsertTool", tool.id, tool.name);
},
onInsertModule(module, evt) {
evt.preventDefault();
this.workflowGlobals.app.add_node_for_module(module.name, module.title);
this.$emit("onInsertModule", module.name, module.title);
},
onInsertWorkflow(workflow, evt) {
evt.preventDefault();
this.workflowGlobals.app.add_node_for_subworkflow(workflow.latest_id, workflow.name);
this.$emit("onInsertWorkflow", workflow.latest_id, workflow.name);
},
onInsertWorkflowSteps(workflow) {
this.workflowGlobals.app.copy_into_workflow(workflow.id, workflow.step_count);
this.$emit("onInsertWorkflowSteps", workflow.id, workflow.step_count);
}
}
};
@@ -1,31 +0,0 @@
/**
* Endpoint for mounting Panel components
*/
import Vue from "vue";
import _l from "utils/localization";
import SidePanel from "./SidePanel";
import ToolBoxWorkflow from "./ToolBoxWorkflow";
export const mountToolBoxWorkflow = options => {
const component = Vue.extend(SidePanel);
return new component({
propsData: {
side: "left",
currentPanel: ToolBoxWorkflow,
currentPanelProperties: {
toolbox: options.toolbox,
workflowGlobals: options.workflow_globals,
moduleSections: options.module_sections,
dataManagers: {
name: _l("Data Managers"),
elems: options.data_managers
},
workflowSection: {
name: _l("Workflows"),
elems: options.workflows
}
}
},
el: "#left"
});
};
@@ -21,6 +21,10 @@ describe("SelectionDialog.vue", () => {
options: "<tree-options />",
search: "<cool-search />"
},
stubs: {
"tree-options": "<div id='tree-options'/>",
"cool-search": "<div id='cool-search'/>"
},
propsData: mockOptions,
localVue
});
@@ -29,16 +33,16 @@ describe("SelectionDialog.vue", () => {
it("loads correctly in loading state, shows options when optionsShow becomes true", async () => {
expect(wrapper.find(".fa-spinner").text()).to.equals("");
expect(wrapper.contains(".fa-spinner")).to.equals(true);
expect(wrapper.contains("tree-options")).to.equals(false);
expect(wrapper.contains("#tree-options")).to.equals(false);
wrapper.setProps({ optionsShow: true });
await localVue.nextTick();
expect(wrapper.contains(".fa-spinner")).to.equals(false);
expect(wrapper.contains("tree-options")).to.equals(true);
expect(wrapper.contains("#tree-options")).to.equals(true);
});
it("loads search correctly", async () => {
await localVue.nextTick();
expect(wrapper.contains("cool-search")).to.equals(true);
expect(wrapper.contains("#cool-search")).to.equals(true);
});
it("hideModal called on click cancel", async () => {
@@ -11,7 +11,7 @@ upstream component or environment that is accessed through props and events -->
class="toggle-link"
@click.prevent="toggleTagDisplay"
>
{{ linkText | localize }}
{{ linkText }}
</a>
<vue-tags-input
v-if="tagsVisible"
@@ -20,7 +20,7 @@ upstream component or environment that is accessed through props and events -->
:tags="tagModels"
:autocomplete-items="autocompleteTags"
:disabled="disabled"
:placeholder="'Add Tags' | localize"
placeholder="Add Tags"
:add-on-key="triggerKeys"
@before-adding-tag="beforeAddingTag"
@before-deleting-tag="beforeDeletingTag"
@@ -0,0 +1,37 @@
import Vue from "vue";
import { mount } from "@vue/test-utils";
import Attributes from "./Attributes";
import { __RewireAPI__ as rewire } from "./Attributes";
describe("Attributes", () => {
beforeEach(() => {
rewire.__Rewire__(
"Services",
class {
async updateWorkflow() {
return {};
}
}
);
});
it("test attributes", async () => {
const wrapper = mount(Attributes, {
propsData: {
id: "workflow_id",
name: "workflow_name",
tags: ["workflow_tag_0", "workflow_tag_1"],
parameters: ["workflow_parameter_0", "workflow_parameter_1"],
versions: ["workflow_version_0"]
}
});
const name = wrapper.find("#workflow-name");
expect(name.element.value).to.equal("workflow_name");
wrapper.setProps({ name: "new_workflow_name" });
await Vue.nextTick();
expect(name.element.value).to.equal("new_workflow_name");
const parameters = wrapper.findAll(".list-group-item");
expect(parameters.length).to.equal(2);
expect(parameters.at(0).text()).to.equal("1: workflow_parameter_0");
expect(parameters.at(1).text()).to.equal("2: workflow_parameter_1");
});
});
@@ -0,0 +1,139 @@
<template>
<div id="edit-attributes" class="right-content p-2">
<b-alert :variant="messageVariant" :show="!!message">
{{ message }}
</b-alert>
<div id="workflow-name-area">
<b>Name</b>
<b-input id="workflow-name" :value="name" @change="onRename" />
</div>
<div id="workflow-version-area" class="mt-2">
<b>Version</b>
<b-form-select v-model="versionCurrent" @change="onVersion">
<b-form-select-option v-for="v in versionOptions" :key="v.version" :value="v.version">
{{ v.label }}
</b-form-select-option>
</b-form-select>
</div>
<div v-if="hasParameters" id="workflow-parameters-area" class="mt-2">
<b>Parameters</b>
<b-list-group>
<b-list-group-item v-for="[key, p] in parameters.entries()" :key="key"
>{{ key + 1 }}: {{ p }}
</b-list-group-item>
</b-list-group>
</div>
<div id="workflow-annotation-area" class="mt-2">
<b>Annotation</b>
<b-textarea id="workflow-annotation" :value="annotation" @change="onAnnotation" />
<div class="form-text text-muted">
These notes will be visible when this workflow is viewed.
</div>
</div>
<div class="mt-2">
<b>Tags</b>
<Tags :tags="tagsCurrent" @input="onTags" />
<div class="form-text text-muted">
Apply tags to make it easy to search for and find items with the same tag.
</div>
</div>
</div>
</template>
<script>
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { Services } from "components/Workflow/services";
import Tags from "components/Common/Tags";
Vue.use(BootstrapVue);
export default {
name: "Attributes",
components: {
Tags
},
props: {
id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
tags: {
type: Array,
required: true
},
annotation: {
type: String
},
version: {
type: Number
},
versions: {
type: Array
},
parameters: {
type: Array
}
},
data() {
return {
message: null,
messageVariant: null,
tagsCurrent: this.tags,
versionCurrent: this.version
};
},
created() {
this.services = new Services();
},
computed: {
hasParameters() {
return this.parameters.length > 0;
},
versionOptions() {
const versions = [];
for (let i = 0; i < this.versions.length; i++) {
const current_wf = this.versions[i];
let label = `Version ${current_wf.version}, ${current_wf.steps} steps`;
if (i == this.version) {
label = `${label} (active)`;
}
versions.push({
version: i,
label: label
});
}
return versions;
}
},
methods: {
onTags(tags) {
this.tagsCurrent = tags;
this.onAttributes({ tags });
},
onAnnotation(annotation) {
this.onAttributes({ annotation });
},
onRename(name) {
this.onAttributes({ name });
this.$emit("onRename", name);
},
onVersion() {
this.$emit("onVersion", this.versionCurrent);
},
onError(error) {
this.message = error;
this.messageVariant = "danger";
},
onAttributes(data) {
this.services.updateWorkflow(this.id, data).catch(error => {
this.onError(error);
});
}
}
};
</script>
@@ -0,0 +1,33 @@
<template>
<div class="unified-panel">
<div class="unified-panel-header" unselectable="on">
<div class="unified-panel-header-inner">
<slot name="buttons" />
</div>
</div>
<div class="unified-panel-body workflow-right">
<div v-if="canvas" class="m-1">
<slot name="attributes" />
<div id="right-content" class="right-content" />
</div>
<ReportHelp v-else />
</div>
</div>
</template>
<script>
import ReportHelp from "./ReportHelp";
export default {
name: "EditorPanel",
components: {
ReportHelp
},
props: {
canvas: {
type: Boolean,
required: true
}
}
};
</script>
@@ -1,82 +1,230 @@
<template>
<div id="center" class="inbound">
<div class="unified-panel-header" unselectable="on">
<div class="unified-panel-header-inner">
<span class="sr-only">Workflow Editor</span>
{{ editorConfig.name }}
<WorkflowOptions
:canvas="isCanvas"
@onSave="onSave"
@onSaveAs="onSaveAs"
@onRun="onRun"
@onDownload="onDownload"
@onReport="onReport"
@onReportHelp="onReportHelp"
@onLayout="onLayout"
@onEdit="onEdit"
@onAttributes="onAttributes"
<div id="columns" class="workflow-client">
<SidePanel id="left" side="left">
<template v-slot:panel>
<ToolBoxWorkflow
:toolbox="toolbox"
:module-sections="module_sections"
:data-managers="data_managers"
:workflows="workflows"
@onInsertTool="onInsertTool"
@onInsertModule="onInsertModule"
@onInsertWorkflow="onInsertWorkflow"
@onInsertWorkflowSteps="onInsertWorkflowSteps"
/>
</template>
</SidePanel>
<div id="center" class="workflow-center inbound">
<div class="unified-panel-header" unselectable="on">
<div class="unified-panel-header-inner">
<span class="sr-only">Workflow Editor</span>
{{ name }}
</div>
</div>
</div>
<div class="unified-panel-body" id="workflow-canvas-body" v-show="isCanvas">
<div id="canvas-viewport" class="workflow-canvas-content">
<div id="canvas-container" />
</div>
<div id="workflow-parameters-box">
<span class="workflow-parameters-box-title">
Workflow Parameters
</span>
<div id="workflow-parameters-container" />
</div>
<div class="workflow-overview" aria-hidden="true">
<div class="workflow-overview-body">
<div id="overview">
<canvas width="0" height="0" id="overview-canvas" />
<div id="overview-viewport" />
<div id="workflow-canvas" class="unified-panel-body workflow-canvas" v-show="isCanvas">
<ZoomControl :zoom-level="zoomLevel" @onZoom="onZoom" />
<div id="canvas-viewport">
<div ref="canvas" id="canvas-container" />
</div>
<div class="workflow-overview" aria-hidden="true">
<div class="workflow-overview-body">
<div id="overview-container">
<canvas width="0" height="0" id="overview-canvas" />
<div id="overview-viewport" />
</div>
</div>
</div>
</div>
<div class="unified-panel-body workflow-report-body" v-show="!isCanvas">
<MarkdownEditor ref="report-editor" initial-markdown="" :onupdate="onReportUpdate" :toolbar="false" />
</div>
</div>
<div class="unified-panel-body workflow-report-body" v-show="!isCanvas">
<markdown-editor ref="report-editor" initial-markdown="" :onupdate="onReportUpdate" :toolbar="false" />
</div>
<SidePanel id="right" side="right">
<template v-slot:panel>
<EditorPanel :canvas="isCanvas" class="workflow-panel">
<template v-slot:attributes>
<WorkflowAttributes
:id="id"
:name="name"
:tags="tags"
:parameters="parameters"
:annotation="annotation"
:version="version"
:versions="versions"
@onVersion="onVersion"
@onRename="onRename"
/>
</template>
<template v-slot:buttons>
<WorkflowOptions
:canvas="isCanvas"
@onSave="onSave"
@onSaveAs="onSaveAs"
@onRun="onRun"
@onDownload="onDownload"
@onReport="onReport"
@onLayout="onLayout"
@onEdit="onEdit"
@onAttributes="onAttributes"
/>
</template>
</EditorPanel>
</template>
</SidePanel>
</div>
</template>
<script>
import { WorkflowView } from "mvc/workflow/workflow-view";
import { getDatatypes, getModule, getVersions, saveWorkflow, loadWorkflow } from "./services";
import {
showWarnings,
showUpgradeMessage,
copyIntoWorkflow,
getWorkflowParameters,
showAttributes,
showForm,
saveAs
} from "./utilities";
import WorkflowManager from "mvc/workflow/workflow-manager";
import WorkflowOptions from "./Options";
import MarkdownEditor from "components/Markdown/MarkdownEditor";
import ToolBoxWorkflow from "components/Panels/ToolBoxWorkflow";
import SidePanel from "components/Panels/SidePanel";
import { getAppRoot } from "onload/loadConfig";
import { showReportHelp } from "./reportHelp";
import reportDefault from "./reportDefault";
import EditorPanel from "./EditorPanel";
import { hide_modal, show_message, show_modal } from "layout/modal";
import WorkflowAttributes from "./Attributes";
import ZoomControl from "./ZoomControl";
export default {
components: { MarkdownEditor, WorkflowOptions },
components: {
EditorPanel,
MarkdownEditor,
SidePanel,
ToolBoxWorkflow,
WorkflowOptions,
WorkflowAttributes,
ZoomControl
},
props: {
editorConfig: {
type: Object
id: {
type: String
},
version: {
type: Number
},
name: {
type: String
},
tags: {
type: Array
},
annotation: {
type: String
},
module_sections: {
type: Array
},
data_managers: {
type: Array
},
workflows: {
type: Array
},
toolbox: {
type: Array
}
},
data() {
return {
isCanvas: true
isCanvas: true,
versions: [],
parameters: [],
zoomLevel: 7
};
},
mounted() {
this.workflowView = new WorkflowView(this.editorConfig, this.$refs["report-editor"]);
created() {
getDatatypes().then(response => {
const datatypes = response.datatypes;
const datatypes_mapping = response.datatypes_mapping;
this.manager = new WorkflowManager({ datatypes_mapping }, this.$refs.canvas);
this.manager
.on("onRemoveNode", () => {
showAttributes();
})
.on("onActiveNode", node => {
showForm(this.manager, node, datatypes);
})
.on("onNodeChange", () => {
this.parameters = getWorkflowParameters(this.manager.nodes);
});
this.loadCurrent(this.id, this.version);
});
},
methods: {
onInsertTool(tool_id, tool_name) {
if (!this.isCanvas) {
this.isCanvas = true;
return;
}
var node = this.manager.create_node("tool", tool_name, tool_id);
const requestData = {
type: "tool",
tool_id: tool_id,
_: "true"
};
getModule(requestData).then(response => {
this.manager.set_node(node, response);
});
},
onInsertModule(module_id, module_name) {
if (!this.isCanvas) {
this.isCanvas = true;
return;
}
var node = this.manager.create_node(module_id, module_name);
const requestData = {
type: module_id,
_: "true"
};
getModule(requestData).then(response => {
this.manager.set_node(node, response);
});
},
onInsertWorkflow(workflow_id, workflow_name) {
if (!this.isCanvas) {
this.isCanvas = true;
return;
}
var node = this.manager.create_node("subworkflow", workflow_name, workflow_id);
const requestData = {
type: "subworkflow",
content_id: workflow_id,
_: "true"
};
getModule(requestData).then(response => {
this.manager.set_node(node, response);
});
},
onInsertWorkflowSteps(workflow_id, step_count) {
if (!this.isCanvas) {
this.isCanvas = true;
return;
}
copyIntoWorkflow(this.manager, workflow_id, step_count);
},
onDownload() {
window.location = `${getAppRoot()}api/workflows/${this.editorConfig.id}/download?format=json-download`;
window.location = `${getAppRoot()}api/workflows/${this.id}/download?format=json-download`;
},
onSaveAs() {
this.workflowView.workflow_save_as();
saveAs(this.manager);
},
onLayout() {
this.workflowView.layout_editor();
this.manager.layout_auto();
},
onAttributes() {
this.workflowView.workflow.clear_active_node();
showAttributes();
},
onEdit() {
this.isCanvas = true;
@@ -84,94 +232,64 @@ export default {
onReport() {
this.isCanvas = false;
},
onRename(name) {
this.name = name;
},
onReportUpdate(markdown) {
this.workflowView.report_changed(markdown);
this.manager.has_changes = true;
this.manager.report.markdown = markdown;
},
onRun() {
window.location = `${getAppRoot()}workflows/run?id=${this.editorConfig.id}`;
window.location = `${getAppRoot()}workflows/run?id=${this.id}`;
},
onZoom(zoomLevel) {
this.zoomLevel = this.manager.canvas_manager.setZoom(zoomLevel);
},
onSave() {
this.workflowView.save_current_workflow();
show_message("Saving workflow...", "progress");
saveWorkflow(this.manager, this.id)
.then(data => {
showWarnings(data);
getVersions(this.id).then(versions => {
this.versions = versions;
hide_modal();
});
})
.catch(response => {
show_modal("Saving workflow failed...", response, { Ok: hide_modal });
});
},
onReportHelp() {
showReportHelp();
onVersion(version) {
if (version != this.manager.workflow_version) {
if (this.manager.has_changes) {
const r = window.confirm(
"There are unsaved changes to your workflow which will be lost. Continue ?"
);
if (r == false) {
this.version = this.manager.workflow_version;
return;
}
}
this.version = version;
this.loadCurrent(this.id, version);
}
},
loadCurrent(id, version) {
show_message("Loading workflow...", "progress");
loadWorkflow(this.manager, id, version)
.then(data => {
const report = data.report || {};
const markdown = report.markdown || reportDefault;
this.$refs["report-editor"].input = markdown;
showUpgradeMessage(this.manager, data);
getVersions(this.id).then(versions => {
this.versions = versions;
});
})
.catch(response => {
show_modal("Loading workflow failed...", response, { Ok: hide_modal });
});
}
}
};
</script>
<style scoped>
#center {
z-index: 0;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.workflow-report-body {
display: flex;
}
.workflow-overview-body {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
border-top: solid gray 1px;
border-left: solid grey 1px;
}
#canvas-container {
position: absolute;
width: 100%;
height: 100%;
}
#workflow-parameters-box {
display: none;
position: absolute;
right: 0px;
border: solid grey 1px;
padding: 5px;
background: #eeeeee;
z-index: 20000;
overflow: auto;
max-width: 300px;
max-height: 300px;
}
.workflow-parameters-box-title {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
#overview {
position: absolute;
}
#overview-canvas {
background: white;
width: 100%;
height: 100%;
}
#overview-viewport {
position: absolute;
width: 0px;
height: 0px;
border: solid blue 1px;
z-index: 10;
}
</style>
<style>
canvas {
position: absolute;
z-index: 10;
}
canvas.dragging {
position: absolute;
z-index: 1000;
}
</style>
@@ -0,0 +1,30 @@
import Vue from "vue";
import { mount } from "@vue/test-utils";
import Node from "./Node";
describe("Node", () => {
it("test attributes", async () => {
const wrapper = mount(Node, {
propsData: {
id: "node-id",
title: "node-title",
type: "tool"
}
});
const icon = wrapper.find("i");
expect(icon.classes())
.to.be.an("array")
.that.includes("fa-wrench");
const toolLinks = wrapper.findAll("a");
expect(toolLinks.length).to.equal(2);
wrapper.setProps({ type: "subworkflow" });
await Vue.nextTick();
expect(icon.classes())
.to.be.an("array")
.that.includes("fa-sitemap");
const subworkflowLinks = wrapper.findAll("a");
expect(subworkflowLinks.length).to.equal(1);
const workflowTitle = wrapper.find(".node-title");
expect(workflowTitle.text()).to.equal("node-title");
});
});
@@ -2,7 +2,7 @@
<div>
<div class="toolFormTitle unselectable clearfix">
<i :class="iconClass" />
<span class="nodeTitle">{{ title }}</span>
<span class="node-title">{{ title }}</span>
<span class="sr-only">&nbsp;Node</span>
<div class="buttons float-right">
<a
@@ -23,15 +23,21 @@
</div>
</div>
<div class="toolFormBody">
<div><span class="fa fa-spinner fa-spin" /> Loading details...</div>
<div>
<loading-span message="Loading details" />
</div>
</div>
</div>
</template>
<script>
import WorkflowIcons from "mvc/workflow/workflow-icons";
import WorkflowIcons from "components/Workflow/icons";
import LoadingSpan from "components/LoadingSpan";
export default {
components: {
LoadingSpan
},
props: {
id: {
type: String
@@ -8,7 +8,6 @@
variant="link"
aria-label="Save Workflow"
v-b-tooltip.hover
v-b-tooltip.bottom
@click="$emit('onSave')"
>
<span class="fa fa-floppy-o" />
@@ -20,7 +19,6 @@
variant="link"
aria-label="Edit Report"
v-b-tooltip.hover
v-b-tooltip.bottom
@click="$emit('onReport')"
>
<span class="fa fa-edit" />
@@ -34,7 +32,6 @@
variant="link"
aria-label="Workflow Options"
v-b-tooltip.hover
v-b-tooltip.bottom
>
<template v-slot:button-content>
<span class="fa fa-cog" />
@@ -51,26 +48,12 @@
variant="link"
aria-label="Run Workflow"
v-b-tooltip.hover
v-b-tooltip.bottom
@click="$emit('onRun')"
>
<span class="fa fa-play" />
</b-button>
</span>
<span v-else>
<b-button
id="workflow-report-help-button"
role="button"
title="Report Syntax Help"
sm="size"
variant="link"
aria-label="Report Syntax Help"
v-b-tooltip.hover
v-b-tooltip.bottom
@click="$emit('onReportHelp')"
>
<span class="fa fa-question" />
</b-button>
<b-button
id="workflow-canvas-button"
role="button"
@@ -0,0 +1,76 @@
<template>
<div class="p-4">
<h3>Workflow Reports</h3>
<p>
The <b>Markdown</b> document you see will be used to generate a workflow invocation report. You can use
Markdown with embedded commands for extracting and displaying parts of the workflow, its invocation
metadata, its inputs and outputs, etc..
</p>
<div v-html="markdownGeneralHelpHtml" />
<h3>Workflow Commands</h3>
<p>
These commands do not take any arguments and do reference the whole workflow. For instance, the following
example would display a representation of the workflow in the resulting report:
</p>
<pre>
\`\`\`galaxy
workflow_display()
\`\`\`
</pre
>
<dl>
<dt><tt>workflow_display</tt></dt>
<dd>Embed a description of the workflow itself in the resulting report.</dd>
<dt><tt>invocation_inputs</tt></dt>
<dd>Embed the labeled workflow inputs in the resulting report.</dd>
<dt><tt>invocation_outputs</tt></dt>
<dd>Embed the labeled workflow outputs in the resulting report.</dd>
</dl>
<h3>Step Commands</h3>
<p>
These commands reference a workflow step label and refer to job corresponding to that step. A current
limitation is the report will break if these refer to a collection mapping step, these must identify a
single job. For instance, the following example would show the job parameters the step labeled 'qc' was run
with:
</p>
<pre>
\`\`\`galaxy
job_parameters(step="qc")
\`\`\`
</pre
>
<dt><tt>tool_stderr</tt></dt>
<dd>Embed the tool standard error stream for this step in the resulting report.</dd>
<dt><tt>tool_stdout</tt></dt>
<dd>Embed the tool standard output stream for this step in the resulting report.</dd>
<dt><tt>job_metrics</tt></dt>
<dd>Embed the job metrics for this step in the resulting report (if user has permission).</dd>
<dt><tt>job_parameters</tt></dt>
<dd>Embed the tool parameters for this step in the resulting report.</dd>
<h3>Input/Output Commands</h3>
<p>
These commands reference a workflow input or output by label. For instance, the following example would
display the dataset collection corresponding to output "Merged BAM":
</p>
<pre>
\`\`\`galaxy
history_dataset_collection_display(output="Merged Bam")
\`\`\`
</pre
>
<div v-html="datasetCommandsHtml" />
</div>
</template>
<script>
import { datasetCommandsHtml, markdownGeneralHelpHtml } from "components/Markdown/help";
export default {
data() {
return {
datasetCommandsHtml,
markdownGeneralHelpHtml
};
}
};
</script>
@@ -1,106 +0,0 @@
<template>
<div class="unified-panel">
<div class="unified-panel-header" unselectable="on">
<div class="unified-panel-header-inner">
<div class="panel-header-text">Details</div>
</div>
</div>
<div class="unified-panel-body workflow-right">
<b-alert class="m-2" :variant="messageVariant" :show="!!message">
{{ message }}
</b-alert>
<div id="edit-attributes" class="right-content p-2">
<div id="workflow-name-area">
<b>Name</b>
<b-input id="workflow-name" :value="name" @change="onRename" />
</div>
<div id="workflow-version-area" class="mt-2">
<b>Version</b>
<div class="ui-select">
<select class="ui-input" id="workflow-version-switch">
Select version
</select>
</div>
</div>
<div id="workflow-annotation-area" class="mt-2">
<b>Annotation</b>
<b-textarea id="workflow-annotation" :value="annotation" @change="onAnnotation" />
<div class="form-text text-muted">
These notes will be visible when this workflow is viewed.
</div>
</div>
<div class="mt-2">
<b>Tags</b>
<Tags :tags="tags" @input="onTags" />
<div class="form-text text-muted">
Apply tags to make it easy to search for and find items with the same tag.
</div>
</div>
</div>
<div id="right-content" class="right-content" />
</div>
</div>
</template>
<script>
import Tags from "components/Common/Tags";
import { Services } from "components/Workflow/services";
export default {
name: "EditorPanel",
components: {
Tags
},
data() {
return {
message: null,
messageVariant: null
};
},
props: {
id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
tags: {
type: Array,
required: true
},
annotation: {
type: String
}
},
created() {
this.services = new Services();
},
methods: {
onTags(tags) {
this.tags = tags;
this.services
.updateWorkflow(this.id, {
tags: this.tags
})
.catch(error => {
this.onError(error);
});
},
onAnnotation(annotation) {
this.services.updateWorkflow(this.id, { annotation }).catch(error => {
this.onError(error);
});
},
onRename(name) {
this.services.updateWorkflow(this.id, { name }).catch(error => {
this.onError(error);
});
},
onError: function(message) {
this.message = message;
this.messageVariant = "danger";
}
}
};
</script>
@@ -0,0 +1,80 @@
<template>
<span class="zoom-control float-right btn-group-horizontal">
<b-button
role="button"
class="fa fa-minus"
title="Zoom Out"
size="sm"
aria-label="Zoom Out"
v-b-tooltip.hover
@click="onZoomOut"
/>
<b-button
role="button"
class="zoom-reset"
variant="light"
title="Reset Zoom Level"
size="sm"
aria-label="Reset Zoom Level"
v-b-tooltip.hover
@click="onZoomReset"
>
{{ this.zoomPercentage }}%
</b-button>
<b-button
role="button"
class="fa fa-plus"
title="Zoom In"
size="sm"
aria-label="Zoom In"
v-b-tooltip.hover
@click="onZoomIn"
/>
</span>
</template>
<script>
import { zoomLevels } from "mvc/workflow/workflow-canvas";
export default {
props: {
zoomLevel: {
type: Number,
required: true
}
},
data() {
return {
zoomDefault: this.zoomLevel
};
},
computed: {
zoomPercentage() {
return Math.floor(zoomLevels[this.zoomLevel] * 100);
}
},
methods: {
onZoomIn() {
this.$emit("onZoom", this.zoomLevel + 1);
},
onZoomOut() {
this.$emit("onZoom", this.zoomLevel - 1);
},
onZoomReset() {
this.$emit("onZoom", this.zoomDefault);
}
}
};
</script>
<style scoped>
.zoom-reset {
width: 4rem;
}
.zoom-control {
position: absolute;
left: 1rem;
bottom: 1rem;
cursor: pointer;
z-index: 1002;
}
</style>
@@ -2,26 +2,14 @@
* Endpoint for mounting WorkflowEditor from non-Vue environment (editor.mako).
*/
import Vue from "vue";
import SidePanel from "components/Panels/SidePanel";
import Index from "./Index";
import Node from "./Node";
import WorkflowPanel from "./WorkflowPanel";
export const mountWorkflowEditor = editorConfig => {
const propsData = { editorConfig };
export const mountWorkflowEditor = propsData => {
const component = Vue.extend(Index);
return new component({ propsData: propsData, el: "#center" });
};
export const mountWorkflowPanel = propsData => {
const component = Vue.extend(SidePanel);
return new component({
propsData: {
side: "right",
currentPanel: WorkflowPanel,
currentPanelProperties: propsData
},
el: "#right"
propsData: propsData,
el: "#columns"
});
};
@@ -0,0 +1,18 @@
export default `
# Workflow Execution Report
## Workflow Inputs
\`\`\`galaxy
invocation_inputs()
\`\`\`
## Workflow Outputs
\`\`\`galaxy
invocation_outputs()
\`\`\`
## Workflow
\`\`\`galaxy
workflow_display()
\`\`\`
`;
@@ -1,88 +0,0 @@
import { datasetCommandsHtml, markdownGeneralHelpHtml } from "components/Markdown/help";
const reportHelp = `
<div>
<h3>Overview</h3>
<p>
This document Markdown document will be used to generate a report
for invocations of this workflow. This document should be Markdown
with embedded command for extracting and displaying parts of the workflow,
its invocation metadata, its inputs and outputs, etc..
</p>
${markdownGeneralHelpHtml}
<h3>Workflow Commands</h3>
<p>
These commands to do not take any arguments and reference the whole workflow. For
instance, the following example would display a representation of the workflow in the
resulting report:
</p>
<pre>
\`\`\`galaxy
workflow_display()
\`\`\`
</pre>
<dl>
<dt><tt>workflow_display</tt></dt>
<dd>Embed a description of the workflow itself in the resulting report.</dd>
<dt><tt>invocation_inputs</tt></dt>
<dd>Embed the labeled workflow inputs in the resulting report.</dd>
<dt><tt>invocation_outputs</tt></dt>
<dd>Embed the labeled workflow outputs in the resulting report.</dd>
</dl>
<h3>Step Commands</h3>
<p>
These commands reference a workflow step label and refer to job corresponding
to that step. A current limitation is the report will break if these refer to
a collection mapping step, these must identify a single job. For instance, the
following example would show the job parameters the step labeled 'qc' was run
with:
</p>
<pre>
\`\`\`galaxy
job_parameters(step="qc")
\`\`\`
</pre>
<dt><tt>tool_stderr</tt></dt>
<dd>Embed the tool standard error stream for this step in the resulting report.</dd>
<dt><tt>tool_stdout</tt></dt>
<dd>Embed the tool standard output stream for this step in the resulting report.</dd>
<dt><tt>job_metrics</tt></dt>
<dd>Embed the job metrics for this step in the resulting report (if user has permission).</dd>
<dt><tt>job_parameters</tt></dt>
<dd>Embed the tool parameters for this step in the resulting report.</dd>
<h3>Input/Output Commands</h3>
<p>
These commands reference a workflow input or output by label. For instance, the
following example would display the dataset collection corresponding to output "Merged BAM":
</p>
<pre>
\`\`\`galaxy
history_dataset_collection_display(output="Merged Bam")
\`\`\`
</pre>
${datasetCommandsHtml}
`;
import $ from "jquery";
import { hide_modal, show_modal } from "layout/modal";
export function showReportHelp() {
const reportHelpBody = $(reportHelp);
show_modal("Workflow Invocation Report Help", reportHelpBody, {
Ok: hide_modal
});
}
@@ -0,0 +1,68 @@
import axios from "axios";
import { rethrowSimple } from "utils/simple-error";
import { getAppRoot } from "onload/loadConfig";
/** Workflow data request helper **/
export async function getVersions(id) {
try {
const response = await axios.get(`${getAppRoot()}api/workflows/${id}/versions`);
return response.data;
} catch (e) {
rethrowSimple(e);
}
}
export async function getDatatypes() {
try {
const datatypesRequest = await axios.get(`${getAppRoot()}api/datatypes`);
const datatypes = datatypesRequest.data;
const mappingRequest = await axios.get(`${getAppRoot()}api/datatypes/mapping`);
const datatypes_mapping = mappingRequest.data;
return { datatypes, datatypes_mapping };
} catch (e) {
rethrowSimple(e);
}
}
export async function getModule(request_data) {
try {
const response = await axios.post(`${getAppRoot()}api/workflows/build_module`, request_data);
return response.data;
} catch (e) {
rethrowSimple(e);
}
}
export async function loadWorkflow(workflow, id, version) {
try {
const versionQuery = version ? `version=${version}` : "";
const { data } = await axios.get(`${getAppRoot()}workflow/load_workflow?_=true&id=${id}&${versionQuery}`);
workflow.remove_all();
workflow.from_simple(data, true);
workflow.has_changes = false;
workflow.fit_canvas_to_nodes();
workflow.scroll_to_nodes();
workflow.canvas_manager.draw_overview();
return data;
} catch (e) {
rethrowSimple(e);
}
}
export async function saveWorkflow(workflow, id) {
workflow.rectify_workflow_outputs();
if (workflow.has_changes) {
try {
const requestData = { workflow: workflow.to_simple(), from_tool_form: true };
const { data } = await axios.put(`${getAppRoot()}api/workflows/${id}`, requestData);
workflow.name = data.name;
workflow.has_changes = false;
workflow.stored = true;
workflow.workflow_version = data.version;
return data;
} catch (e) {
rethrowSimple(e);
}
}
return {};
}
@@ -0,0 +1,210 @@
import _ from "underscore";
import $ from "jquery";
import { getAppRoot } from "onload/loadConfig";
import _l from "utils/localization";
import Utils from "utils/utils";
import WorkflowIcons from "components/Workflow/icons";
import { DefaultForm, ToolForm } from "mvc/workflow/workflow-forms";
import { loadWorkflow } from "./services";
import { hide_modal, show_message, show_modal } from "layout/modal";
export function copyIntoWorkflow(workflow, id = null, stepCount = null) {
const _copy_into_workflow_ajax = () => {
// Load workflow definition
show_message("Importing workflow", "progress");
loadWorkflow(workflow, id, null).then(data => {
workflow.from_simple(data, false);
// Determine if any parameters were 'upgraded' and provide message
var upgrade_message = "";
$.each(data.upgrade_messages, (k, v) => {
upgrade_message += `<li>Step ${parseInt(k, 10) + 1}: ${workflow.nodes[k].name}<ul>`;
$.each(v, (i, vv) => {
upgrade_message += `<li>${vv}</li>`;
});
upgrade_message += "</ul></li>";
});
if (upgrade_message) {
show_modal(
"Subworkflow embedded with changes",
`Problems were encountered loading this workflow (possibly a result of tool upgrades). Please review the following parameters and then save.<ul>${upgrade_message}</ul>`,
{ Continue: hide_modal }
);
} else {
hide_modal();
}
});
};
if (stepCount < 2) {
_copy_into_workflow_ajax();
} else {
// don't ruin the workflow by adding 50 steps unprompted.
show_modal(_l("Warning"), `This will copy ${stepCount} new steps into your workflow.`, {
Cancel: hide_modal,
Copy: _copy_into_workflow_ajax
});
}
}
export function showWarnings(data) {
const body = $("<div/>").text(data.message);
if (data.errors) {
body.addClass("warningmark");
var errlist = $("<ul/>");
$.each(data.errors, (i, v) => {
$("<li/>")
.text(v)
.appendTo(errlist);
});
body.append(errlist);
} else {
body.addClass("donemark");
}
if (data.errors) {
show_modal("Saving workflow", body, {
Ok: hide_modal
});
} else {
hide_modal();
}
}
export function showAttributes() {
$(".right-content").hide();
$("#edit-attributes").show();
}
export function showForm(workflow, node, datatypes) {
if (node && node.config_form) {
const content = node.config_form;
const cls = "right-content";
var id = `${cls}-${node.id}`;
var $container = $(`#${cls}`);
if ($container.find(`#${id}`).length === 0) {
var $el = $(`<div id="${id}" class="${cls}"/>`);
content.node = node;
content.workflow = workflow;
content.datatypes = datatypes;
content.icon = WorkflowIcons[node.type];
content.cls = "ui-portlet-section";
let formWrapper = null;
if (node.type == "tool") {
formWrapper = new ToolForm(content);
} else {
formWrapper = new DefaultForm(content);
}
$el.append(formWrapper.form.$el);
$container.append($el);
}
$(`.${cls}`).hide();
$container.find(`#${id}`).show();
$container.show();
$container.scrollTop();
}
}
export function showUpgradeMessage(workflow, data) {
// Determine if any parameters were 'upgraded' and provide message
var upgrade_message = "";
_.each(data.steps, (step, step_id) => {
var details = "";
if (step.errors) {
details += `<li>${step.errors}</li>`;
}
_.each(data.upgrade_messages[step_id], m => {
details += `<li>${m}</li>`;
});
if (details) {
upgrade_message += `<li>Step ${parseInt(step_id, 10) + 1}: ${
workflow.nodes[step_id].name
}<ul>${details}</ul></li>`;
}
});
if (upgrade_message) {
show_modal(
"Issues loading this workflow",
`Please review the following issues, possibly resulting from tool upgrades or changes.<p><ul>${upgrade_message}</ul></p>`,
{ Continue: hide_modal }
);
} else {
hide_modal();
}
}
export function getWorkflowParameters(nodes) {
const parameter_re = /\$\{.+?\}/g;
const parameters = [];
let matches = [];
Object.entries(nodes).forEach(([k, node]) => {
if (node.config_form && node.config_form.inputs) {
Utils.deepeach(node.config_form.inputs, d => {
if (typeof d.value == "string") {
var form_matches = d.value.match(parameter_re);
if (form_matches) {
matches = matches.concat(form_matches);
}
}
});
}
if (node.post_job_actions) {
Object.entries(node.post_job_actions).forEach(([k, pja]) => {
if (pja.action_arguments) {
Object.entries(pja.action_arguments).forEach(([k, action_argument]) => {
if (typeof action_argument === "string") {
const arg_matches = action_argument.match(parameter_re);
if (arg_matches) {
matches = matches.concat(arg_matches);
}
}
});
}
});
}
if (matches) {
Object.entries(matches).forEach(([k, element]) => {
if (parameters.indexOf(element) === -1) {
parameters.push(element);
}
});
}
});
Object.entries(parameters).forEach(([k, element]) => {
parameters[k] = element.substring(2, element.length - 1);
});
return parameters;
}
export function saveAs(workflow) {
var body = $(
'<form><label style="display:inline-block; width: 100%;">Save as name: </label><input type="text" id="workflow_rename" style="width: 80%;" autofocus/>' +
'<br><label style="display:inline-block; width: 100%;">Annotation: </label><input type="text" id="wf_annotation" style="width: 80%;" /></form>'
);
show_modal("Save As a New Workflow", body, {
OK: function() {
var rename_name =
$("#workflow_rename").val().length > 0 ? $("#workflow_rename").val() : `SavedAs_${workflow.name}`;
var rename_annotation = $("#wf_annotation").val().length > 0 ? $("#wf_annotation").val() : "";
$.ajax({
url: `${getAppRoot()}workflow/save_workflow_as`,
type: "POST",
data: {
workflow_name: rename_name,
workflow_annotation: rename_annotation,
from_tool_form: true,
workflow_data: function() {
return JSON.stringify(workflow.to_simple());
}
}
})
.done(id => {
window.onbeforeunload = undefined;
window.location = `${getAppRoot()}workflow/editor?id=${id}`;
hide_modal();
})
.fail(() => {
hide_modal();
alert("Saving this workflow failed. Please contact this site's administrator.");
});
},
Cancel: hide_modal
});
}
@@ -11,7 +11,7 @@ import Form from "mvc/form/form-view";
import FormData from "mvc/form/form-data";
import ToolFormBase from "mvc/tool/tool-form-base";
import Modal from "mvc/ui/ui-modal";
import WorkflowIcons from "mvc/workflow/workflow-icons";
import WorkflowIcons from "components/Workflow/icons";
var View = Backbone.View.extend({
initialize: function(options) {
@@ -1,5 +1,15 @@
import $ from "jquery";
// Colors used to render nodes in the workflow overview
const NODE_COLOR = "#25537b";
const NODE_ERROR_COLOR = "#e31a1e";
// Zoom levels to use for zooming the workflow canvas
export const zoomLevels = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.33, 1.5, 2, 2.5, 3, 4];
// Default zoome level
export const defaultZoomLevel = 7;
// FIXME: merge scroll panel into CanvasManager, clean up hardcoded stuff.
class ScrollPanel {
constructor(panel) {
@@ -73,12 +83,6 @@ class ScrollPanel {
}
}
// Zoom levels to use for zooming the workflow canvas
const zoomLevels = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.33, 1.5, 2, 2.5, 3, 4];
// Default zoome level (1)
const defaultZoomLevel = 7;
class CanvasManager {
constructor(app, canvas_viewport, overview) {
this.app = app;
@@ -90,7 +94,6 @@ class CanvasManager {
// Initialize zooming
this.zoomLevel = defaultZoomLevel;
this.canvasZoom = zoomLevels[defaultZoomLevel];
this.initZoomControls();
// Make overview box draggable
this.init_drag();
// Initialize Copy & Paste events
@@ -106,40 +109,8 @@ class CanvasManager {
this.cv.css("width", `${100 / this.canvasZoom}%`);
this.cv.css("height", `${100 / this.canvasZoom}%`);
// Update canvas size
this.app.workflow.fit_canvas_to_nodes();
}
initZoomControls() {
var zoomControl = $('<div class="btn-group-horizontal workflow-canvas-content"/>').css({
position: "absolute",
left: "1rem",
bottom: "1rem",
cursor: "pointer"
});
const zoomButton = $(
`<a class="btn btn-light" title="Reset Zoom Level">${zoomLevels[defaultZoomLevel] * 100}%</a>`
).css({
width: "4rem"
});
zoomControl.append(
$('<a class="btn btn-secondary fa fa-minus"/>').click(() => {
this.setZoom(this.zoomLevel - 1);
zoomButton.text(Math.floor(zoomLevels[this.zoomLevel] * 100) + "%");
})
);
zoomControl.append(
zoomButton.click(() => {
this.setZoom(defaultZoomLevel);
zoomButton.text(Math.floor(zoomLevels[this.zoomLevel] * 100) + "%");
})
);
zoomControl.append(
$('<a class="btn btn-secondary fa fa-plus"/>').click(() => {
this.setZoom(this.zoomLevel + 1);
zoomButton.text(Math.floor(zoomLevels[this.zoomLevel] * 100) + "%");
})
);
this.cv.closest("#workflow-canvas-body").append(zoomControl);
this.app.fit_canvas_to_nodes();
return this.zoomLevel;
}
init_drag() {
@@ -179,7 +150,7 @@ class CanvasManager {
move((d.offsetX + x_adjust) / this.canvasZoom, (d.offsetY + y_adjust) / this.canvasZoom);
})
.bind("dragend", () => {
self.app.workflow.fit_canvas_to_nodes();
self.app.fit_canvas_to_nodes();
self.draw_overview();
});
this.overview.click(e => {
@@ -193,7 +164,7 @@ class CanvasManager {
var new_x_offset = e.pageX - self.oc.offset().left - self.ov.width() / 2;
var new_y_offset = e.pageY - self.oc.offset().top - self.ov.height() / 2;
move(-((new_x_offset / o_w) * in_w), -((new_y_offset / o_h) * in_h));
self.app.workflow.fit_canvas_to_nodes();
self.app.fit_canvas_to_nodes();
self.draw_overview();
}
});
@@ -210,7 +181,7 @@ class CanvasManager {
})
.bind("dragend", () => {
self.overview.addClass("blockaclick");
self.app.workflow.fit_canvas_to_nodes();
self.app.fit_canvas_to_nodes();
self.draw_overview();
});
// Dragging for overview border (resize)
@@ -233,11 +204,11 @@ class CanvasManager {
// If it appears that the user is trying to copy/paste text, we
// pass that through.
if (window.getSelection().toString() === "") {
if (this.app.workflow.active_node && this.app.workflow.active_node.type !== "subworkflow") {
if (this.app.active_node && this.app.active_node.type !== "subworkflow") {
e.clipboardData.setData(
"application/json",
JSON.stringify({
nodeId: this.app.workflow.active_node.id
nodeId: this.app.active_node.id
})
);
}
@@ -259,8 +230,8 @@ class CanvasManager {
} catch (error) {
console.debug(error);
}
if (nodeId && Object.prototype.hasOwnProperty.call(this.app.workflow.nodes, nodeId)) {
this.app.workflow.nodes[nodeId].clone();
if (nodeId && Object.prototype.hasOwnProperty.call(this.app.nodes, nodeId)) {
this.app.nodes[nodeId].clone();
}
e.preventDefault();
}
@@ -329,8 +300,8 @@ class CanvasManager {
canvas_el.attr("width", o_w);
canvas_el.attr("height", o_h);
// Draw overview
$.each(this.app.workflow.nodes, (id, node) => {
c.fillStyle = "gray";
$.each(this.app.nodes, (id, node) => {
c.fillStyle = NODE_COLOR;
var node_element = $(node.element);
var position = node_element.position();
var x = (position.left / in_w) * o_w;
@@ -338,7 +309,7 @@ class CanvasManager {
var w = (node_element.width() / in_w) * o_w;
var h = (node_element.height() / in_h) * o_h;
if (node.errors) {
c.fillStyle = "#e31a1e";
c.fillStyle = NODE_ERROR_COLOR;
}
c.fillRect(x, y, w, h);
});
@@ -2,7 +2,8 @@ import $ from "jquery";
import { Toast } from "ui/toast";
function Connector(handle1, handle2) {
function Connector(manager, handle1, handle2) {
this.manager = manager;
this.canvas = null;
this.dragging = false;
this.inner_color = "#FFFFFF";
@@ -46,8 +47,7 @@ $.extend(Connector.prototype, {
const endRibbon = handle2 && handle2.isMappedOver();
const canvasClass = `${startRibbon ? "start-ribbon" : ""} ${endRibbon ? "end-ribbon" : ""}`;
var canvas_container = $("#canvas-container");
// FIXME: global
var canvasZoom = window.workflow_globals.canvas_manager.canvasZoom;
var canvasZoom = this.manager.canvasZoom;
if (!this.canvas) {
this.canvas = document.createElement("canvas");
canvas_container.append($(this.canvas));
@@ -1,5 +1,4 @@
import $ from "jquery";
import Backbone from "backbone";
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
import _l from "utils/localization";
@@ -8,8 +7,8 @@ import Form from "mvc/form/form-view";
import ToolFormBase from "mvc/tool/tool-form-base";
/** Default form wrapper for non-tool modules in the workflow editor. */
var Default = Backbone.View.extend({
initialize: function(options) {
export class DefaultForm {
constructor(options) {
var self = this;
var node = options.node;
this.form = new Form(
@@ -34,11 +33,11 @@ var Default = Backbone.View.extend({
_addLabelAnnotation(this.form);
this.form.render();
}
});
}
/** Tool form wrapper for the workflow editor. */
var Tool = Backbone.View.extend({
initialize: function(options) {
export class ToolForm {
constructor(options) {
var self = this;
var node = options.node;
this.form = new ToolFormBase(
@@ -90,9 +89,8 @@ var Tool = Backbone.View.extend({
}
})
);
},
_customize: function(form) {
}
_customize(form) {
var options = form.model.attributes;
Utils.deepeach(options.inputs, input => {
if (input.type) {
@@ -119,7 +117,7 @@ var Tool = Backbone.View.extend({
_addSections(form);
_addLabelAnnotation(form);
}
});
}
/** Augments the module form definition by adding label and annotation fields */
function _addLabelAnnotation(form) {
@@ -391,8 +389,3 @@ function _addSections(form) {
}
}
}
export default {
Default: Default,
Tool: Tool
};
@@ -1,18 +1,72 @@
import $ from "jquery";
import { getGalaxyInstance } from "app";
import Connector from "mvc/workflow/workflow-connector";
import { Toast } from "ui/toast";
import { Node } from "mvc/workflow/workflow-node";
import { mountWorkflowNode } from "components/Workflow/Editor/mount";
import WorkflowCanvas from "mvc/workflow/workflow-canvas";
import EventEmitter from "events";
class Workflow {
constructor(app, canvas_container) {
this.app = app;
this.canvas_container = canvas_container;
class Workflow extends EventEmitter {
constructor(options, canvas_container) {
super();
this.ext_to_type = options.datatypes_mapping.ext_to_class_name;
this.type_to_type = options.datatypes_mapping.class_to_classes;
this.canvas_container = $(canvas_container);
this.id_counter = 0;
this.nodes = {};
this.name = null;
this.has_changes = false;
this.active_form_has_changes = false;
this.workflowOutputLabels = {};
this.workflow_version = 0;
// Canvas overview management
this.canvas_manager = new WorkflowCanvas(this, $("#canvas-viewport"), $("#overview-container"));
// On load, set the size to the pref stored in local storage if it exists
var overview_size = localStorage.getItem("overview-size");
if (overview_size !== undefined) {
$(".workflow-overview").css({
width: overview_size,
height: overview_size
});
}
// Stores the size of the overview into local storage when it's resized
$(".workflow-overview").bind("dragend", function(e, d) {
var op = $(this).offsetParent();
var opo = op.offset();
var new_size = Math.max(op.width() - (d.offsetX - opo.left), op.height() - (d.offsetY - opo.top));
localStorage.setItem("overview-size", `${new_size}px`);
});
// Unload handler
window.onbeforeunload = () => {
if (this.has_changes) {
return "There are unsaved changes to your workflow which will be lost.";
}
};
}
set_node(node, data) {
const Galaxy = getGalaxyInstance();
node.init_field_data(data);
node.update_field_data(data);
// Post init/update, for new modules we want to default to
// nodes being outputs
// TODO: Overhaul the handling of all this when we modernize
// the editor, replace callout image manipulation with a simple
// class toggle, etc.
$.each(node.output_terminals, (ot_id, ot) => {
node.addWorkflowOutput(ot.name);
var callout = $(node.element).find(`.callout.${ot.name.replace(/(?=[()])/g, "\\")}`);
callout.find("img").attr("src", `${Galaxy.root}static/images/fugue/asterisk-small.png`);
});
this.activate_node(node);
}
isSubType(child, parent) {
child = this.ext_to_type[child];
parent = this.ext_to_type[parent];
return this.type_to_type[child] && parent in this.type_to_type[child];
}
canLabelOutputWith(label) {
if (label) {
@@ -59,10 +113,10 @@ class Workflow {
return true;
}
create_node(type, title_text, content_id) {
var node = this.app.prebuildNode(type, title_text, content_id);
var node = this.prebuildNode(type, title_text, content_id);
this.add_node(node);
this.fit_canvas_to_nodes();
this.app.canvas_manager.draw_overview();
this.canvas_manager.draw_overview();
this.activate_node(node);
return node;
}
@@ -75,12 +129,80 @@ class Workflow {
this.has_changes = true;
node.workflow = this;
}
prebuildNode(type, title_text, content_id) {
var self = this;
// Create node wrapper
const container = document.createElement("div");
container.className = "toolForm toolFormInCanvas";
document.getElementById("canvas-container").appendChild(container);
var $f = $(container);
// Create backbone model and view
var node = new Node(this, { element: $f });
node.type = type;
node.content_id = content_id;
// Mount node component as child dom to node wrapper
const child = document.createElement("div");
container.appendChild(child);
mountWorkflowNode(child, {
id: content_id,
type: type,
title: title_text,
node: node
});
// Set initial scroll position
$f.css("left", $(window).scrollLeft() + 20);
$f.css("top", $(window).scrollTop() + 20);
// Position in container
var o = $("#canvas-container").position();
var p = $("#canvas-container").parent();
var width = $f.outerWidth() + 50;
var height = $f.height();
$f.css({
left: -o.left + p.width() / 2 - width / 2,
top: -o.top + p.height() / 2 - height / 2
});
$f.css("width", width);
$f.bind("dragstart", () => {
self.activate_node(node);
})
.bind("dragend", function() {
self.node_changed(node);
self.fit_canvas_to_nodes();
self.canvas_manager.draw_overview();
})
.bind("dragclickonly", () => {
self.activate_node(node);
})
.bind("drag", function(e, d) {
// Move
var po = $(this)
.offsetParent()
.offset();
// Find relative offset and scale by zoom
var x = (d.offsetX - po.left) / self.canvas_manager.canvasZoom;
var y = (d.offsetY - po.top) / self.canvas_manager.canvasZoom;
$(this).css({ left: x, top: y });
// Redraw
$(this)
.find(".terminal")
.each(function() {
this.terminal.redraw();
});
});
return node;
}
remove_node(node) {
if (this.active_node == node) {
this.clear_active_node();
}
delete this.nodes[node.id];
this.has_changes = true;
this.emit("onRemoveNode");
}
remove_all() {
var wf = this;
@@ -220,7 +342,7 @@ class Workflow {
wf.workflow_version = data.version;
wf.report = data.report || {};
$.each(data.steps, (id, step) => {
var node = wf.app.prebuildNode(step.type, step.name, step.content_id);
var node = wf.prebuildNode(step.type, step.name, step.content_id);
// If workflow being copied into another, wipe UUID and let
// Galaxy assign new ones.
if (!initialImport) {
@@ -266,7 +388,7 @@ class Workflow {
}
$.each(v, (l, x) => {
var other_node = wf.nodes[parseInt(x.id) + offset];
var c = new Connector();
var c = new Connector(this.canvas_manager);
c.connect(other_node.output_terminals[x.output_name], node.input_terminals[k]);
c.redraw();
});
@@ -284,17 +406,6 @@ class Workflow {
}
});
}
check_changes_in_active_form() {
// If active form has changed, save it
if (this.active_form_has_changes) {
this.has_changes = true;
// Submit form.
$("#right-content")
.find("form")
.submit();
this.active_form_has_changes = false;
}
}
reload_active_node() {
if (this.active_node) {
var node = this.active_node;
@@ -308,28 +419,44 @@ class Workflow {
this.active_node = null;
}
document.activeElement.blur();
this.app.showAttributes();
this.emit("onClearActiveNode");
}
activate_node(node) {
if (this.active_node != node) {
this.check_changes_in_active_form();
this.clear_active_node();
this.app.showForm(node.config_form, node);
node.make_active();
this.active_node = node;
}
this.emit("onActiveNode", node);
}
node_changed(node, force) {
node_changed(node) {
this.has_changes = true;
if (this.active_node == node && force) {
// Force changes to be saved even on new connection (previously dumped)
this.check_changes_in_active_form();
this.app.showForm(node.config_form, node);
this.emit("onNodeChange", node);
}
scroll_to_nodes() {
var cv = $("#canvas-viewport");
var cc = $("#canvas-container");
var top;
var left;
if (cc.width() < cv.width()) {
left = (cv.width() - cc.width()) / 2;
} else {
left = 0;
}
this.app.showWorkflowParameters();
if (cc.height() < cv.height()) {
top = (cv.height() - cc.height()) / 2;
} else {
top = 0;
}
cc.css({ left: left, top: top });
}
layout_auto() {
this.layout();
this.fit_canvas_to_nodes();
this.scroll_to_nodes();
this.canvas_manager.draw_overview();
}
layout() {
this.check_changes_in_active_form();
this.has_changes = true;
// Prepare predecessor / successor tracking
var n_pred = {};
@@ -439,7 +566,7 @@ class Workflow {
return 0;
}
// Span of all elements
var canvasZoom = this.app.canvas_manager.canvasZoom;
var canvasZoom = this.canvas_manager.canvasZoom;
var bounds = this.bounds_for_all_nodes();
var position = this.canvas_container.position();
var parent = this.canvas_container.parent();
@@ -54,7 +54,7 @@ export class Node {
changed = this.addWorkflowOutput(outputName, label);
}
if (changed) {
this.app.workflow.updateOutputLabel(oldLabel, label);
this.app.updateOutputLabel(oldLabel, label);
this.markChanged();
this.nodeView.redrawWorkflowOutputs();
}
@@ -163,8 +163,7 @@ export class Node {
annotation: this.annotation,
post_job_actions: this.post_job_actions
};
var node = this.app.workflow.create_node(this.type, this.name, this.content_id);
var node = this.app.create_node(this.type, this.name, this.content_id);
Utils.request({
type: "POST",
url: `${getAppRoot()}api/workflows/build_module`,
@@ -177,7 +176,7 @@ export class Node {
var newData = Object.assign({}, data, copiedData);
node.init_field_data(newData);
node.update_field_data(newData);
this.app.workflow.activate_node(node);
this.app.activate_node(node);
}
});
}
@@ -188,7 +187,7 @@ export class Node {
$.each(this.output_terminals, (k, t) => {
t.destroy();
});
this.app.workflow.remove_node(this);
this.app.remove_node(this);
$(this.element).remove();
}
make_active() {
@@ -228,7 +227,7 @@ export class Node {
this.uuid = data.uuid;
this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : [];
var node = this;
var nodeView = new NodeView({
var nodeView = new NodeView(this.app, {
$el: this.element,
node: node
});
@@ -244,7 +243,7 @@ export class Node {
nodeView.addDataOutput(output);
});
nodeView.render();
this.app.workflow.node_changed(this, true);
this.app.node_changed(this);
}
update_field_data(data) {
var node = this;
@@ -347,9 +346,9 @@ export class Node {
var tmp = `<div style='color: red; text-style: italic;'>${text}</div>`;
this.config_form = tmp;
b.html(tmp);
this.app.workflow.node_changed(this);
this.app.node_changed(this);
}
markChanged() {
this.app.workflow.node_changed(this);
this.app.node_changed(this);
}
}
@@ -1,9 +1,6 @@
import $ from "jquery";
import _ from "underscore";
import Backbone from "backbone";
// TODO; tie into Galaxy state?
window.workflow_globals = window.workflow_globals || {};
import EventEmitter from "events";
function CollectionTypeDescription(collectionType) {
this.collectionType = collectionType;
@@ -106,35 +103,35 @@ $.extend(CollectionTypeDescription.prototype, {
}
});
var TerminalMapping = Backbone.Model.extend({
initialize: function(attr) {
class TerminalMapping extends EventEmitter {
constructor(attr) {
super();
this.mapOver = attr.mapOver || NULL_COLLECTION_TYPE_DESCRIPTION;
this.terminal = attr.terminal;
this.terminal.terminalMapping = this;
},
disableMapOver: function() {
this.setMapOver(NULL_COLLECTION_TYPE_DESCRIPTION);
},
setMapOver: function(collectionTypeDescription) {
// TODO: Can I use "attributes" or something to auto trigger "change"
// event?
this.mapOver = collectionTypeDescription;
this.trigger("change");
}
});
disableMapOver() {
this.setMapOver(NULL_COLLECTION_TYPE_DESCRIPTION);
}
setMapOver(collectionTypeDescription) {
this.mapOver = collectionTypeDescription;
this.emit("change");
}
}
var Terminal = Backbone.Model.extend({
initialize: function(attr) {
class Terminal {
constructor(attr) {
this.element = attr.element;
this.attributes = attr;
this.connectors = [];
},
connect: function(connector) {
}
connect(connector) {
this.connectors.push(connector);
if (this.node) {
this.node.markChanged();
}
},
disconnect: function(connector) {
}
disconnect(connector) {
this.connectors.splice($.inArray(connector, this.connectors), 1);
if (this.node) {
this.node.markChanged();
@@ -143,25 +140,25 @@ var Terminal = Backbone.Model.extend({
connector.handle2.resetCollectionTypeSource();
}
}
},
redraw: function() {
}
redraw() {
$.each(this.connectors, (_, c) => {
c.redraw();
});
},
destroy: function() {
}
destroy() {
$.each(this.connectors.slice(), (_, c) => {
c.destroy();
});
},
destroyInvalidConnections: function() {
}
destroyInvalidConnections() {
_.each(this.connectors, connector => {
if (connector) {
connector.destroyIfInvalid();
}
});
},
setMapOver: function(val) {
}
setMapOver(val) {
let output_val = val;
if (this.multiple) {
// emulate list input
@@ -179,22 +176,21 @@ var Terminal = Backbone.Model.extend({
outputTerminal.setMapOver(output_val);
});
}
},
mapOver: function() {
}
mapOver() {
if (!this.terminalMapping) {
return NULL_COLLECTION_TYPE_DESCRIPTION;
} else {
return this.terminalMapping.mapOver;
}
},
isMappedOver: function() {
}
isMappedOver() {
return this.terminalMapping && this.terminalMapping.mapOver.isCollection;
},
resetMapping: function() {
}
resetMapping() {
this.terminalMapping.disableMapOver();
},
resetCollectionTypeSource: function() {
}
resetCollectionTypeSource() {
const node = this.node;
_.each(node.output_terminals, function(output_terminal) {
const type_source = output_terminal.attributes.collection_type_source;
@@ -203,18 +199,18 @@ var Terminal = Backbone.Model.extend({
output_terminal.update(output_terminal.attributes);
}
});
},
}
// Subclasses should override this...
resetMappingIfNeeded() {}
}
resetMappingIfNeeded: function() {} // Subclasses should override this...
});
var OutputTerminal = Terminal.extend({
initialize: function(attr) {
Terminal.prototype.initialize.call(this, attr);
class OutputTerminal extends Terminal {
constructor(attr) {
super(attr);
this.datatypes = attr.datatypes;
this.optional = attr.optional;
this.force_datatype = attr.force_datatype;
},
}
/*
update: function(output) {
// This isn't needed for datatypes or optional because WorkflowNode.update_field_data just updates the
@@ -222,7 +218,7 @@ var OutputTerminal = Terminal.extend({
// don't disconnect from non-optional inputs.
},
*/
resetMappingIfNeeded: function() {
resetMappingIfNeeded() {
// If inputs were only mapped over to preserve
// an output just disconnected reset these...
if (!this.node.hasConnectedOutputTerminals() && !this.node.hasConnectedMappedInputTerminals()) {
@@ -235,9 +231,8 @@ var OutputTerminal = Terminal.extend({
if (noMappedInputs) {
this.resetMapping();
}
},
resetMapping: function() {
}
resetMapping() {
this.terminalMapping.disableMapOver();
_.each(this.connectors, connector => {
var connectedInput = connector.handle2;
@@ -250,14 +245,15 @@ var OutputTerminal = Terminal.extend({
}
});
}
});
}
var BaseInputTerminal = Terminal.extend({
initialize: function(attr) {
Terminal.prototype.initialize.call(this, attr);
class BaseInputTerminal extends Terminal {
constructor(attr) {
super(attr);
this.app = attr.app;
this.update(attr.input); // subclasses should implement this...
},
canAccept: function(other) {
}
canAccept(other) {
if (this._inputFilled()) {
return new ConnectionAcceptable(
false,
@@ -266,8 +262,8 @@ var BaseInputTerminal = Terminal.extend({
} else {
return this.attachable(other);
}
},
resetMappingIfNeeded: function() {
}
resetMappingIfNeeded() {
var mapOver = this.mapOver();
if (!mapOver.isCollection) {
return;
@@ -279,8 +275,8 @@ var BaseInputTerminal = Terminal.extend({
if (reset) {
this.resetMapping();
}
},
resetMapping: function() {
}
resetMapping() {
this.terminalMapping.disableMapOver();
if (!this.node.hasMappedOverInputTerminals()) {
_.each(this.node.output_terminals, terminal => {
@@ -289,11 +285,11 @@ var BaseInputTerminal = Terminal.extend({
terminal.resetMapping();
});
}
},
connected: function() {
}
connected() {
return this.connectors.length !== 0;
},
_inputFilled: function() {
}
_inputFilled() {
var inputFilled;
if (!this.connected()) {
inputFilled = false;
@@ -307,8 +303,8 @@ var BaseInputTerminal = Terminal.extend({
}
}
return inputFilled;
},
_collectionAttached: function() {
}
_collectionAttached() {
if (!this.connected()) {
return false;
} else {
@@ -327,8 +323,8 @@ var BaseInputTerminal = Terminal.extend({
}
}
}
},
_mappingConstraints: function() {
}
_mappingConstraints() {
// If this is a connected terminal, return list of collection types
// other terminals connected to node are constraining mapping to.
if (!this.node) {
@@ -349,8 +345,8 @@ var BaseInputTerminal = Terminal.extend({
constraints.push(_.first(_.values(this.node.output_terminals)).mapOver());
}
return constraints;
},
_producesAcceptableDatatype: function(other) {
}
_producesAcceptableDatatype(other) {
// other is a non-collection output...
for (var t in this.datatypes) {
var thisDatatype = this.datatypes[t];
@@ -366,7 +362,7 @@ var BaseInputTerminal = Terminal.extend({
if (
other_datatype == "input" ||
other_datatype == "_sniff_" ||
window.workflow_globals.app.isSubType(cat_outputs[other_datatype_i], thisDatatype)
this.app.isSubType(cat_outputs[other_datatype_i], thisDatatype)
) {
return new ConnectionAcceptable(true, null);
}
@@ -376,14 +372,14 @@ var BaseInputTerminal = Terminal.extend({
false,
`Effective output data type(s) [${cat_outputs}] do not appear to match input type(s) [${this.datatypes}].`
);
},
}
_producesAcceptableDatatypeAndOptionalness(other) {
if (!this.optional && !this.multiple && other.optional) {
return new ConnectionAcceptable(false, "Cannot connect an optional output to a non-optional input");
}
return this._producesAcceptableDatatype(other);
},
_otherCollectionType: function(other) {
}
_otherCollectionType(other) {
var otherCollectionType = NULL_COLLECTION_TYPE_DESCRIPTION;
if (other.isCollection) {
otherCollectionType = other.collectionType;
@@ -394,17 +390,17 @@ var BaseInputTerminal = Terminal.extend({
}
return otherCollectionType;
}
});
}
var InputTerminal = BaseInputTerminal.extend({
update: function(input) {
class InputTerminal extends BaseInputTerminal {
update(input) {
this.datatypes = input.extensions;
this.multiple = input.multiple;
this.optional = input.optional;
this.collection = false;
},
connect: function(connector) {
BaseInputTerminal.prototype.connect.call(this, connector);
}
connect(connector) {
super.connect(connector);
var other_output = connector.handle1;
if (!other_output) {
return;
@@ -413,8 +409,8 @@ var InputTerminal = BaseInputTerminal.extend({
if (otherCollectionType.isCollection) {
this.setMapOver(otherCollectionType);
}
},
attachable: function(other) {
}
attachable(other) {
var otherCollectionType = this._otherCollectionType(other);
var thisMapOver = this.mapOver();
if (otherCollectionType.isCollection) {
@@ -473,28 +469,25 @@ var InputTerminal = BaseInputTerminal.extend({
}
return this._producesAcceptableDatatypeAndOptionalness(other);
}
});
}
var InputParameterTerminal = BaseInputTerminal.extend({
update: function(input) {
class InputParameterTerminal extends BaseInputTerminal {
update(input) {
this.type = input.type;
this.optional = input.optional;
},
connect: function(connector) {
BaseInputTerminal.prototype.connect.call(this, connector);
},
effectiveType: function(parameterType) {
}
effectiveType(parameterType) {
return parameterType == "select" ? "text" : parameterType;
},
attachable: function(other) {
}
attachable(other) {
const effectiveThisType = this.effectiveType(this.type);
const effectiveOtherType = this.effectiveType(other.attributes.type);
return new ConnectionAcceptable(effectiveThisType == effectiveOtherType, "");
}
});
}
var InputCollectionTerminal = BaseInputTerminal.extend({
update: function(input) {
class InputCollectionTerminal extends BaseInputTerminal {
update(input) {
this.multiple = false;
this.collection = true;
this.collection_type = input.collection_type;
@@ -509,9 +502,9 @@ var InputCollectionTerminal = BaseInputTerminal.extend({
collectionTypes.push(ANY_COLLECTION_TYPE_DESCRIPTION);
}
this.collectionTypes = collectionTypes;
},
connect: function(connector) {
BaseInputTerminal.prototype.connect.call(this, connector);
}
connect(connector) {
super.connect(connector);
var other = connector.handle1;
if (!other) {
return;
@@ -537,8 +530,8 @@ var InputCollectionTerminal = BaseInputTerminal.extend({
var effectiveMapOver = this._effectiveMapOver(other);
this.setMapOver(effectiveMapOver);
},
_effectiveMapOver: function(other) {
}
_effectiveMapOver(other) {
var collectionTypes = this.collectionTypes;
var otherCollectionType = this._otherCollectionType(other);
var canMatch = _.some(collectionTypes, collectionType => collectionType.canMatch(otherCollectionType));
@@ -555,12 +548,12 @@ var InputCollectionTerminal = BaseInputTerminal.extend({
}
}
return NULL_COLLECTION_TYPE_DESCRIPTION;
},
_effectiveCollectionTypes: function() {
}
_effectiveCollectionTypes() {
var thisMapOver = this.mapOver();
return _.map(this.collectionTypes, t => thisMapOver.append(t));
},
attachable: function(other) {
}
attachable(other) {
var otherCollectionType = this._otherCollectionType(other);
if (otherCollectionType.isCollection) {
var effectiveCollectionTypes = this._effectiveCollectionTypes();
@@ -615,11 +608,11 @@ var InputCollectionTerminal = BaseInputTerminal.extend({
return new ConnectionAcceptable(false, "Cannot attach a data output to a collection input.");
}
}
});
}
var OutputCollectionTerminal = Terminal.extend({
initialize: function(attr) {
Terminal.prototype.initialize.call(this, attr);
class OutputCollectionTerminal extends Terminal {
constructor(attr) {
super(attr);
this.datatypes = attr.datatypes;
this.optional = attr.optional;
this.force_datatype = attr.force_datatype;
@@ -633,8 +626,8 @@ var OutputCollectionTerminal = Terminal.extend({
this.collectionType = ANY_COLLECTION_TYPE_DESCRIPTION;
}
this.isCollection = true;
},
update: function(output) {
}
update(output) {
this.optional = output.optional;
var newCollectionType;
if (output.collection_type) {
@@ -657,9 +650,9 @@ var OutputCollectionTerminal = Terminal.extend({
});
}
}
});
}
var OutputParameterTerminal = Terminal.extend({});
var OutputParameterTerminal = Terminal;
export default {
InputTerminal: InputTerminal,
@@ -1,8 +1,5 @@
import $ from "jquery";
// TODO; tie into Galaxy state?
window.workflow_globals = window.workflow_globals || {};
export class DataInputView {
constructor(options = {}) {
this.input = options.input;
@@ -31,7 +28,7 @@ export class DataInputView {
}
export class DataOutputView {
constructor(options = {}) {
constructor(app, options = {}) {
this.$el = $("<div class='form-row dataRow'/>");
this.output = options.output;
this.terminalElement = options.terminalElement;
@@ -46,13 +43,13 @@ export class DataOutputView {
this.$el.html(label);
this.calloutView = null;
if (["tool", "subworkflow"].indexOf(node.type) >= 0) {
const calloutView = new OutputCalloutView({
const calloutView = new OutputCalloutView(app, {
label: label,
output: output,
node: node
});
this.calloutView = calloutView;
this.$el.append(calloutView.el);
this.$el.append(calloutView.$el);
}
this.$el.css({
position: "absolute",
@@ -79,7 +76,7 @@ export class DataOutputView {
}
export class ParameterOutputView {
constructor(options = {}) {
constructor(app, options = {}) {
this.$el = $("<div class='form-row dataRow'/>");
this.output = options.output;
this.terminalElement = options.terminalElement;
@@ -90,13 +87,13 @@ export class ParameterOutputView {
this.$el.html(label);
this.calloutView = null;
if (["tool", "subworkflow"].indexOf(node.type) >= 0) {
const calloutView = new OutputCalloutView({
const calloutView = new OutputCalloutView(app, {
label: label,
output: output,
node: node
});
this.calloutView = calloutView;
this.$el.append(calloutView.el);
this.$el.append(calloutView.$el);
}
this.$el.css({
position: "absolute",
@@ -123,7 +120,7 @@ export class ParameterOutputView {
}
export class OutputCalloutView {
constructor(options = {}) {
constructor(app, options = {}) {
this.$el = $("<div/>");
this.label = options.label;
this.node = options.node;
@@ -138,13 +135,13 @@ export class OutputCalloutView {
const outputName = view.output.name;
if (node.isWorkflowOutput(outputName)) {
node.removeWorkflowOutput(outputName);
view.$("icon").removeClass("mark-terminal-active");
view.$el.find("icon").removeClass("mark-terminal-active");
} else {
node.addWorkflowOutput(outputName);
view.$("icon").addClass("mark-terminal-active");
view.$el.find("icon").addClass("mark-terminal-active");
}
window.workflow_globals.workflow.has_changes = true;
window.workflow_globals.canvas_manager.draw_overview();
app.has_changes = true;
app.canvas_manager.draw_overview();
})
)
.tooltip({
@@ -4,7 +4,8 @@ import TerminalViews from "mvc/workflow/workflow-view-terminals";
import { DataInputView, DataOutputView, ParameterOutputView } from "mvc/workflow/workflow-view-data";
export class NodeView {
constructor(options) {
constructor(app, options) {
this.app = app;
this.$el = options.$el;
this.node = options.node;
this.output_width = Math.max(150, this.$el.width());
@@ -22,7 +23,7 @@ export class NodeView {
}
renderToolLabel() {
this.$el.find(".nodeTitle").text(this.node.label || this.node.name);
this.$el.find(".node-title").text(this.node.label || this.node.name);
this.$el.attr("node-label", this.node.label);
}
@@ -62,7 +63,7 @@ export class NodeView {
terminalView = null;
}
if (!terminalView) {
terminalView = new terminalViewClass({
terminalView = new terminalViewClass(this.app, {
node: this.node,
input: input
});
@@ -91,7 +92,7 @@ export class NodeView {
} else if (output.parameter) {
terminalViewClass = TerminalViews.OutputParameterTerminalView;
}
return new terminalViewClass({
return new terminalViewClass(this.app, {
node: this.node,
output: output
});
@@ -99,7 +100,7 @@ export class NodeView {
outputViewforOutput(output, terminalView) {
const outputViewClass = output.parameter ? ParameterOutputView : DataOutputView;
return new outputViewClass({
return new outputViewClass(this.app, {
output: output,
terminalElement: terminalView.el,
nodeView: this
@@ -1,86 +1,58 @@
import $ from "jquery";
import _ from "underscore";
import Backbone from "backbone";
import Terminals from "mvc/workflow/workflow-terminals";
import Connector from "mvc/workflow/workflow-connector";
import ariaAlert from "utils/ariaAlert";
// TODO; tie into Galaxy state?
window.workflow_globals = window.workflow_globals || {};
var NODEINDEX = 0;
var TerminalMappingView = Backbone.View.extend({
tagName: "div",
className: "fa-icon-button fa fa-folder-o",
initialize: function(options) {
class TerminalMappingView {
constructor(options = {}) {
this.$el = $("<div class='fa-icon-button fa fa-folder-o' />");
var mapText = "Run tool in parallel over collection";
this.$el.tooltip({ delay: 500, title: mapText });
this.model.bind("change", _.bind(this.render, this));
},
render: function() {
if (this.model.mapOver.isCollection) {
this.model = options.model;
this.model.on("change", this.render.bind(this));
}
render() {
if (this.model.mapOver && this.model.mapOver.isCollection) {
this.$el.show();
} else {
this.$el.hide();
}
}
});
}
var InputTerminalMappingView = TerminalMappingView.extend({
events: {
click: "onClick",
mouseenter: "onMouseEnter",
mouseleave: "onMouseLeave"
},
onMouseEnter: function(e) {
var model = this.model;
if (!model.terminal.connected() && model.mapOver.isCollection) {
this.$el.css("color", "red");
}
},
onMouseLeave: function(e) {
this.$el.css("color", "black");
},
onClick: function(e) {
var model = this.model;
if (!model.terminal.connected() && model.mapOver.isCollection) {
// TODO: Consider prompting...
model.terminal.resetMapping();
}
}
});
var TerminalView = Backbone.View.extend({
setupMappingView: function(terminal) {
var terminalMapping = new this.terminalMappingClass({
class TerminalView {
setupMappingView(terminalMappingClass, terminal) {
var terminalMapping = new terminalMappingClass({
terminal: terminal
});
var terminalMappingView = new this.terminalMappingViewClass({
var terminalMappingView = new TerminalMappingView({
model: terminalMapping
});
terminalMappingView.render();
terminal.terminalMappingView = terminalMappingView;
this.terminalMappingView = terminalMappingView;
},
terminalElements: function() {
if (this.terminalMappingView) {
return [this.terminalMappingView.el, this.el];
} else {
return [this.el];
}
}
});
terminalElements() {
return [this.terminalMappingView.$el, this.el];
}
}
var BaseInputTerminalView = TerminalView.extend({
className: "terminal input-terminal",
initialize: function(options) {
class BaseInputTerminalView extends TerminalView {
constructor(app, options = {}, terminalMappingClass = null) {
super();
this.app = app;
this.el = document.createElement("div");
this.el.className = "terminal input-terminal";
this.$el = $(this.el);
const node = options.node;
const input = options.input;
const name = input.name;
node.cid = NODEINDEX++;
const id = `node-${node.cid}-input-${name}`;
const terminal = this.terminalForInput(input);
this.setupMappingView(terminal);
this.setupMappingView(terminalMappingClass, terminal);
this.el.terminal = terminal;
this.$el.attr("input-name", name);
this.$el.attr("id", id);
@@ -91,15 +63,15 @@ var BaseInputTerminalView = TerminalView.extend({
terminal.name = name;
terminal.label = input.label;
node.input_terminals[name] = terminal;
},
events: {
dropinit: "onDropInit",
dropstart: "onDropStart",
dropend: "onDropEnd",
drop: "onDrop",
hover: "onHover"
},
onDropInit: function(e, d) {
const self = this;
this.$el.on("dropinit", (e, d) => self.onDropInit(e, d));
this.$el.on("dropstart", (e, d) => self.onDropStart(e, d));
this.$el.on("dropend", (e, d) => self.onDropEnd(e, d));
this.$el.on("drop", (e, d) => self.onDrop(e, d));
this.$el.on("hover", () => self.onHover());
}
onDropInit(e, d = {}) {
var terminal = this.el.terminal;
// Accept a dragable if it is an output terminal and has a
// compatible type
@@ -115,8 +87,8 @@ var BaseInputTerminalView = TerminalView.extend({
}
return true;
},
onDropStart: function(e, d) {
}
onDropStart(e, d = {}) {
if (d.proxy.terminal) {
if (this.$el.hasClass("can-accept")) {
d.proxy.terminal.connectors[0].inner_color = "#BBFFBB";
@@ -131,21 +103,21 @@ var BaseInputTerminalView = TerminalView.extend({
}
}
}
},
onDropEnd: function(e, d) {
}
onDropEnd(e, d = {}) {
d.proxy.dropTooltip = "";
if (d.proxy.terminal) {
d.proxy.terminal.connectors[0].inner_color = "#FFFFFF";
}
},
onDrop: function(e, d) {
}
onDrop(e, d = {}) {
d.proxy.dropTooltip = "";
if (this.$el.hasClass("can-accept")) {
const terminal = this.el.terminal;
new Connector(d.drag.terminal, terminal).redraw();
new Connector(this.app.canvas_manager, d.drag.terminal, terminal).redraw();
}
},
onHover: function() {
}
onHover() {
const element = this.el;
const terminal = element.terminal;
// If connected, create a popup to allow disconnection
@@ -168,51 +140,61 @@ var BaseInputTerminalView = TerminalView.extend({
.append(t);
}
}
});
}
var InputTerminalView = BaseInputTerminalView.extend({
terminalMappingClass: Terminals.TerminalMapping,
terminalMappingViewClass: InputTerminalMappingView,
terminalForInput: function(input) {
export class InputTerminalView extends BaseInputTerminalView {
constructor(app, options = {}) {
super(app, options, Terminals.TerminalMapping);
}
terminalForInput(input) {
return new Terminals.InputTerminal({
app: this.app,
element: this.el,
input: input
});
}
});
}
var InputParameterTerminalView = BaseInputTerminalView.extend({
terminalMappingClass: Terminals.TerminalMapping,
terminalMappingViewClass: InputTerminalMappingView,
terminalForInput: function(input) {
export class InputParameterTerminalView extends BaseInputTerminalView {
constructor(app, options = {}) {
super(app, options, Terminals.TerminalMapping);
}
terminalForInput(input) {
return new Terminals.InputParameterTerminal({
app: this.app,
element: this.el,
input: input
});
}
});
}
var InputCollectionTerminalView = BaseInputTerminalView.extend({
terminalMappingClass: Terminals.TerminalMapping,
terminalMappingViewClass: InputTerminalMappingView,
terminalForInput: function(input) {
export class InputCollectionTerminalView extends BaseInputTerminalView {
constructor(app, options = {}) {
super(app, options, Terminals.TerminalMapping);
}
terminalForInput(input = {}) {
return new Terminals.InputCollectionTerminal({
app: this.app,
element: this.el,
input: input
});
}
});
}
var BaseOutputTerminalView = TerminalView.extend({
className: "terminal output-terminal",
initialize: function(options) {
export class BaseOutputTerminalView extends TerminalView {
constructor(app, options, terminalMappingClass = null) {
super();
this.app = app;
this.el = document.createElement("div");
this.el.className = "terminal output-terminal";
this.$el = $(this.el);
const node = options.node;
const output = options.output;
const name = output.name;
node.cid = NODEINDEX++;
const id = `node-${node.cid}-output-${name}`;
const terminal = this.terminalForOutput(output);
this.setupMappingView(terminal);
this.setupMappingView(terminalMappingClass, terminal);
this.el.terminal = terminal;
this.$el.attr(
"aria-label",
@@ -227,35 +209,31 @@ var BaseOutputTerminalView = TerminalView.extend({
terminal.name = name;
terminal.label = output.label;
node.output_terminals[name] = terminal;
},
events: {
drag: "onDrag",
dragstart: "onDragStart",
dragend: "onDragEnd",
keydown: "screenReaderSelectOutputNode"
},
screenReaderSelectOutputNode: function(e) {
const self = this;
this.$el.on("drag", (d, e) => self.onDrag(d, e));
this.$el.on("dragstart", (d, e) => self.onDragStart(d, e));
this.$el.on("dragend", (d, e) => self.onDragEnd(d, e));
this.$el.on("keydown", e => self.screenReaderSelectOutputNode(e));
}
screenReaderSelectOutputNode(e) {
const inputChoiceKeyDown = e => {
e.stopPropagation();
const currentItem = e.currentTarget;
const previousItem = currentItem.previousSibling;
const nextItem = currentItem.nextSibling;
const inputTerminal = currentItem.input.context.terminal;
const switchActiveItem = (currentActive, newActive) => {
newActive.classList.add("active");
newActive.focus();
currentActive.classList.remove("active");
};
const removeMenu = () => {
$(currentItem.parentNode).remove();
this.$el.removeAttr("aria-owns");
this.$el.attr("aria-grabbed", "false");
this.$el.focus();
};
switch (e.keyCode) {
case 40: // Down arrow
if (nextItem) {
@@ -273,7 +251,7 @@ var BaseOutputTerminalView = TerminalView.extend({
break;
case 32: // Space
removeMenu();
new Connector(this.el.terminal, inputTerminal).redraw();
new Connector(this.app.canvas_manager, this.el.terminal, inputTerminal).redraw();
ariaAlert("Node connected");
if (inputTerminal.connectors.length > 0) {
@@ -344,11 +322,10 @@ var BaseOutputTerminalView = TerminalView.extend({
ariaAlert("Node selected");
buildInputChoicesMenu();
}
},
onDrag: function(e, d) {
}
onDrag(e, d = {}) {
var onmove = () => {
// FIXME: global
var canvasZoom = window.workflow_globals.canvas_manager.canvasZoom;
var canvasZoom = this.app.canvas_manager.canvasZoom;
var po = $(d.proxy)
.offsetParent()
.offset();
@@ -357,18 +334,15 @@ var BaseOutputTerminalView = TerminalView.extend({
var y = d.offsetY - po.top;
$(d.proxy).css({ left: x / canvasZoom, top: y / canvasZoom });
d.proxy.terminal.redraw();
// FIXME: global
window.workflow_globals.canvas_manager.update_viewport_overlay();
this.app.canvas_manager.update_viewport_overlay();
};
onmove();
$("#canvas-container")
.get(0)
.scroll_panel.test(e, onmove);
},
onDragStart: function(e, d) {
}
onDragStart(e, d = {}) {
$(d.available).addClass("input-terminal-active");
// Save PJAs in the case of change datatype actions.
window.workflow_globals.workflow.check_changes_in_active_form();
// Drag proxy div
var h = $("<div class='drag-terminal'/>")
.appendTo("#canvas-container")
@@ -383,17 +357,13 @@ var BaseOutputTerminalView = TerminalView.extend({
}
});
h.terminal = new Terminals.OutputTerminal({ element: h });
var c = new Connector();
var c = new Connector(this.app.canvas_manager);
c.dragging = true;
c.connect(this.el.terminal, h.terminal);
return h;
},
onDragEnd: function(e, d) {
}
onDragEnd(e, d = {}) {
var connector = d.proxy.terminal.connectors[0];
// check_changes_in_active_form may change the state and cause a
// the connection to have already been destroyed. There must be better
// ways to handle this but the following check fixes some serious GUI
// bugs for now.
if (connector) {
connector.destroy();
}
@@ -404,12 +374,13 @@ var BaseOutputTerminalView = TerminalView.extend({
.get(0)
.scroll_panel.stop();
}
});
}
var OutputTerminalView = BaseOutputTerminalView.extend({
terminalMappingClass: Terminals.TerminalMapping,
terminalMappingViewClass: TerminalMappingView,
terminalForOutput: function(output) {
export class OutputTerminalView extends BaseOutputTerminalView {
constructor(app, options = {}) {
super(app, options, Terminals.TerminalMapping);
}
terminalForOutput(output) {
var type = output.extensions;
return new Terminals.OutputTerminal({
element: this.el,
@@ -418,12 +389,13 @@ var OutputTerminalView = BaseOutputTerminalView.extend({
optional: output.optional
});
}
});
}
var OutputCollectionTerminalView = BaseOutputTerminalView.extend({
terminalMappingClass: Terminals.TerminalMapping,
terminalMappingViewClass: TerminalMappingView,
terminalForOutput: function(output) {
export class OutputCollectionTerminalView extends BaseOutputTerminalView {
constructor(app, options = {}) {
super(app, options, Terminals.TerminalMapping);
}
terminalForOutput(output) {
var collection_type = output.collection_type;
var collection_type_source = output.collection_type_source;
return new Terminals.OutputCollectionTerminal({
@@ -435,25 +407,26 @@ var OutputCollectionTerminalView = BaseOutputTerminalView.extend({
optional: output.optional
});
}
});
}
var OutputParameterTerminalView = BaseOutputTerminalView.extend({
terminalMappingClass: Terminals.TerminalMapping,
terminalMappingViewClass: TerminalMappingView,
terminalForOutput: function(output) {
export class OutputParameterTerminalView extends BaseOutputTerminalView {
constructor(app, options = {}) {
super(app, options, Terminals.TerminalMapping);
}
terminalForOutput(output) {
return new Terminals.OutputParameterTerminal({
element: this.el,
type: output.type,
optional: output.optional
});
}
});
}
export default {
InputTerminalView: InputTerminalView,
InputParameterTerminalView: InputParameterTerminalView,
InputCollectionTerminalView: InputCollectionTerminalView,
OutputTerminalView: OutputTerminalView,
OutputParameterTerminalView: OutputParameterTerminalView,
InputCollectionTerminalView: InputCollectionTerminalView,
OutputCollectionTerminalView: OutputCollectionTerminalView
};
@@ -1,616 +0,0 @@
import _ from "underscore";
import $ from "jquery";
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
import _l from "utils/localization";
import Utils from "utils/utils";
import Workflow from "mvc/workflow/workflow-manager";
import WorkflowCanvas from "mvc/workflow/workflow-canvas";
import { Node } from "mvc/workflow/workflow-node";
import WorkflowIcons from "mvc/workflow/workflow-icons";
import FormWrappers from "mvc/workflow/workflow-forms";
import { mountWorkflowNode } from "components/Workflow/Editor/mount";
import "ui/editable-text";
import { hide_modal, show_message, show_modal } from "layout/modal";
// TODO; tie into Galaxy state?
window.workflow_globals = window.workflow_globals || {};
const DEFAULT_INVOCATION_REPORT = `
# Workflow Execution Report
## Workflow Inputs
\`\`\`galaxy
invocation_inputs()
\`\`\`
## Workflow Outputs
\`\`\`galaxy
invocation_outputs()
\`\`\`
## Workflow
\`\`\`galaxy
workflow_display()
\`\`\`
`;
export class WorkflowView {
constructor(options, reportsEditor = {}) {
var self = (window.workflow_globals.app = this);
this.options = options;
this.reportsEditor = reportsEditor;
// Canvas overview management
this.canvas_manager = window.workflow_globals.canvas_manager = new WorkflowCanvas(
this,
$("#canvas-viewport"),
$("#overview")
);
// Initialize workflow state
this.reset();
// get available datatypes for post job action options
this.datatypes = JSON.parse(
$.ajax({
url: `${getAppRoot()}api/datatypes`,
async: false
}).responseText
);
// get datatype mapping options
this.datatypes_mapping = JSON.parse(
$.ajax({
url: `${getAppRoot()}api/datatypes/mapping`,
async: false
}).responseText
);
// set mapping sub lists
this.ext_to_type = this.datatypes_mapping.ext_to_class_name;
this.type_to_type = this.datatypes_mapping.class_to_classes;
this.get_workflow_versions = function() {
const _workflow_version_dropdown = {};
const workflow_versions = JSON.parse(
$.ajax({
url: `${getAppRoot()}api/workflows/${self.options.id}/versions`,
async: false
}).responseText
);
for (let i = 0; i < workflow_versions.length; i++) {
const current_wf = workflow_versions[i];
let version_text = `Version ${current_wf.version}, ${current_wf.steps} steps`;
let selected = false;
if (i == self.workflow.workflow_version) {
version_text = `${version_text} (active)`;
selected = true;
}
_workflow_version_dropdown[version_text] = {
version: i,
selected: selected
};
}
return _workflow_version_dropdown;
};
this.build_version_select = function() {
const versions = this.get_workflow_versions();
$("#workflow-version-switch").empty();
$.each(versions, function(k, v) {
$("#workflow-version-switch").append(
$("<option></option>")
.html(k)
.val(v.version)
.selected(v.selected)
);
});
$("#workflow-version-switch").on("change", function() {
$("#workflow-version-switch").unbind("change");
if (this.value != self.workflow.workflow_version) {
if (self.workflow && self.workflow.has_changes) {
const r = window.confirm(
"There are unsaved changes to your workflow which will be lost. Continue ?"
);
if (r == false) {
// We rebuild the version select list, to reset the selected version
self.build_version_select();
return;
}
}
self.load_workflow(self.options.id, this.value);
}
});
};
this.load_workflow = function load_workflow(id, version) {
this._workflowLoadAjax(id, version, {
success: function(data) {
self.reset();
self.workflow.from_simple(data, true);
const report = data.report || {};
const markdown = report.markdown || DEFAULT_INVOCATION_REPORT;
self.reportsEditor.input = markdown;
self.workflow.has_changes = false;
self.workflow.fit_canvas_to_nodes();
self.scroll_to_nodes();
self.canvas_manager.draw_overview();
self.build_version_select();
// Determine if any parameters were 'upgraded' and provide message
var upgrade_message = "";
_.each(data.steps, (step, step_id) => {
var details = "";
if (step.errors) {
details += `<li>${step.errors}</li>`;
}
_.each(data.upgrade_messages[step_id], m => {
details += `<li>${m}</li>`;
});
if (details) {
upgrade_message += `<li>Step ${parseInt(step_id, 10) + 1}: ${
self.workflow.nodes[step_id].name
}<ul>${details}</ul></li>`;
}
});
if (upgrade_message) {
show_modal(
"Issues loading this workflow",
`Please review the following issues, possibly resulting from tool upgrades or changes.<p><ul>${upgrade_message}</ul></p>`,
{ Continue: hide_modal }
);
} else {
hide_modal();
}
self.showWorkflowParameters();
},
error: function(response) {
show_modal("Loading workflow failed.", response.err_msg, {
Ok: function(response) {
window.onbeforeunload = undefined;
window.document.location = `${getAppRoot()}workflows/list`;
}
});
},
beforeSubmit: function(data) {
show_message("Loading workflow", "progress");
}
});
};
// Load workflow definition
this.load_workflow(self.options.id, self.options.version);
// On load, set the size to the pref stored in local storage if it exists
var overview_size = localStorage.getItem("overview-size");
if (overview_size !== undefined) {
$(".workflow-overview").css({
width: overview_size,
height: overview_size
});
}
// Stores the size of the overview into local storage when it's resized
$(".workflow-overview").bind("dragend", function(e, d) {
var op = $(this).offsetParent();
var opo = op.offset();
var new_size = Math.max(op.width() - (d.offsetX - opo.left), op.height() - (d.offsetY - opo.top));
localStorage.setItem("overview-size", `${new_size}px`);
});
// Unload handler
window.onbeforeunload = () => {
if (self.workflow && self.workflow.has_changes) {
return "There are unsaved changes to your workflow which will be lost.";
}
};
}
copy_into_workflow(id = null, stepCount = null) {
const Galaxy = getGalaxyInstance();
if (stepCount < 2) {
this._copy_into_workflow_ajax(id);
} else {
// don't ruin the workflow by adding 50 steps unprompted.
var self = this;
Galaxy.modal.show({
title: _l("Warning"),
body: `This will copy ${stepCount} new steps into your workflow.`,
buttons: {
Cancel: function() {
Galaxy.modal.hide();
},
Copy: function() {
Galaxy.modal.hide();
self._copy_into_workflow_ajax(id);
}
}
});
}
}
_copy_into_workflow_ajax(workflowId) {
// Load workflow definition
var self = this;
this._workflowLoadAjax(workflowId, null, {
success: function(data) {
self.workflow.from_simple(data, false);
// Determine if any parameters were 'upgraded' and provide message
var upgrade_message = "";
$.each(data.upgrade_messages, (k, v) => {
upgrade_message += `<li>Step ${parseInt(k, 10) + 1}: ${self.workflow.nodes[k].name}<ul>`;
$.each(v, (i, vv) => {
upgrade_message += `<li>${vv}</li>`;
});
upgrade_message += "</ul></li>";
});
if (upgrade_message) {
show_modal(
"Subworkflow embedded with changes",
`Problems were encountered loading this workflow (possibly a result of tool upgrades). Please review the following parameters and then save.<ul>${upgrade_message}</ul>`,
{ Continue: hide_modal }
);
} else {
hide_modal();
}
},
beforeSubmit: function() {
show_message("Importing workflow", "progress");
}
});
}
// Global state for the whole workflow
reset() {
if (this.workflow) {
this.workflow.remove_all();
}
this.workflow = window.workflow_globals.workflow = new Workflow(this, $("#canvas-container"));
}
scroll_to_nodes() {
var cv = $("#canvas-viewport");
var cc = $("#canvas-container");
var top;
var left;
if (cc.width() < cv.width()) {
left = (cv.width() - cc.width()) / 2;
} else {
left = 0;
}
if (cc.height() < cv.height()) {
top = (cv.height() - cc.height()) / 2;
} else {
top = 0;
}
cc.css({ left: left, top: top });
}
_workflowLoadAjax(workflowId, version, options) {
$.ajax(
Utils.merge(options, {
url: `${getAppRoot()}workflow/load_workflow`,
data: { id: workflowId, _: "true", version: version },
dataType: "json",
cache: false
})
);
}
_moduleInitAjax(node, request_data) {
var self = this;
Utils.request({
type: "POST",
url: `${getAppRoot()}api/workflows/build_module`,
data: request_data,
success: function(data) {
const Galaxy = getGalaxyInstance();
node.init_field_data(data);
node.update_field_data(data);
// Post init/update, for new modules we want to default to
// nodes being outputs
// TODO: Overhaul the handling of all this when we modernize
// the editor, replace callout image manipulation with a simple
// class toggle, etc.
$.each(node.output_terminals, (ot_id, ot) => {
node.addWorkflowOutput(ot.name);
var callout = $(node.element).find(`.callout.${ot.name.replace(/(?=[()])/g, "\\")}`);
callout.find("img").attr("src", `${Galaxy.root}static/images/fugue/asterisk-small.png`);
});
self.workflow.activate_node(node);
}
});
}
// Add a new step to the workflow by tool id
add_node_for_tool(id, title) {
var node = this.workflow.create_node("tool", title, id);
this._moduleInitAjax(node, {
type: "tool",
tool_id: id,
_: "true"
});
}
// Add a new step to the workflow by tool id
add_node_for_subworkflow(id, title) {
var node = this.workflow.create_node("subworkflow", title, id);
this._moduleInitAjax(node, {
type: "subworkflow",
content_id: id,
_: "true"
});
}
add_node_for_module(type, title) {
var node = this.workflow.create_node(type, title);
this._moduleInitAjax(node, { type: type, _: "true" });
}
display_file_list(node) {
var addlist = "<select id='node_data_list' name='node_data_list'>";
for (var out_terminal in node.output_terminals) {
addlist += `<option value='${out_terminal}'>${out_terminal}</option>`;
}
addlist += "</select>";
return addlist;
}
showWorkflowParameters() {
var parameter_re = /\$\{.+?\}/g;
var workflow_parameters = [];
var wf_parm_container = $("#workflow-parameters-container");
var wf_parm_box = $("#workflow-parameters-box");
var new_parameter_content = "";
var matches = [];
$.each(this.workflow.nodes, (k, node) => {
if (node.config_form && node.config_form.inputs) {
Utils.deepeach(node.config_form.inputs, d => {
if (typeof d.value == "string") {
var form_matches = d.value.match(parameter_re);
if (form_matches) {
matches = matches.concat(form_matches);
}
}
});
}
if (node.post_job_actions) {
$.each(node.post_job_actions, (k, pja) => {
if (pja.action_arguments) {
$.each(pja.action_arguments, (k, action_argument) => {
if (typeof action_argument === "string") {
const arg_matches = action_argument.match(parameter_re);
if (arg_matches) {
matches = matches.concat(arg_matches);
}
}
});
}
});
}
if (matches) {
$.each(matches, (k, element) => {
if ($.inArray(element, workflow_parameters) === -1) {
workflow_parameters.push(element);
}
});
}
});
if (workflow_parameters && workflow_parameters.length !== 0) {
$.each(workflow_parameters, (k, element) => {
new_parameter_content += `<div>${element.substring(2, element.length - 1)}</div>`;
});
wf_parm_container.html(new_parameter_content);
wf_parm_box.show();
} else {
wf_parm_container.html(new_parameter_content);
wf_parm_box.hide();
}
}
showAttributes() {
$(".right-content").hide();
$("#edit-attributes").show();
}
showForm(content, node) {
const cls = "right-content";
var id = `${cls}-${node.id}`;
var $container = $(`#${cls}`);
const Galaxy = getGalaxyInstance();
if (content && $container.find(`#${id}`).length === 0) {
var $el = $(`<div id="${id}" class="${cls}"/>`);
content.node = node;
content.workflow = this.workflow;
content.datatypes = this.datatypes;
content.icon = WorkflowIcons[node.type];
content.cls = "ui-portlet-section";
if (node) {
var form_type = node.type == "tool" ? "Tool" : "Default";
$el.append(new FormWrappers[form_type](content).form.$el);
$container.append($el);
} else {
Galaxy.emit.debug("workflow-view::initialize()", "Node not found in workflow.");
}
}
$(`.${cls}`).hide();
$container.find(`#${id}`).show();
$container.show();
$container.scrollTop();
}
isSubType(child, parent) {
child = this.ext_to_type[child];
parent = this.ext_to_type[parent];
return this.type_to_type[child] && parent in this.type_to_type[child];
}
report_changed(report_markdown) {
this.workflow.has_changes = true;
this.workflow.report.markdown = report_markdown;
}
save_current_workflow() {
const self = this;
show_message("Saving workflow", "progress");
self.workflow.check_changes_in_active_form();
if (!self.workflow.has_changes) {
hide_modal();
return;
}
self.workflow.rectify_workflow_outputs();
Utils.request({
url: `${getAppRoot()}api/workflows/${self.options.id}`,
type: "PUT",
data: { workflow: self.workflow.to_simple(), from_tool_form: true },
success: function(data) {
var body = $("<div/>").text(data.message);
if (data.errors) {
body.addClass("warningmark");
var errlist = $("<ul/>");
$.each(data.errors, (i, v) => {
$("<li/>")
.text(v)
.appendTo(errlist);
});
body.append(errlist);
} else {
body.addClass("donemark");
}
self.workflow.name = data.name;
self.workflow.has_changes = false;
self.workflow.stored = true;
self.workflow.workflow_version = data.version;
self.showWorkflowParameters();
self.build_version_select();
if (data.errors) {
show_modal("Saving workflow", body, {
Ok: hide_modal
});
} else {
hide_modal();
}
},
error: function(response) {
show_modal("Saving workflow failed.", response.err_msg, { Ok: hide_modal });
}
});
}
workflow_save_as() {
const self = this;
var body = $(
'<form><label style="display:inline-block; width: 100%;">Save as name: </label><input type="text" id="workflow_rename" style="width: 80%;" autofocus/>' +
'<br><label style="display:inline-block; width: 100%;">Annotation: </label><input type="text" id="wf_annotation" style="width: 80%;" /></form>'
);
show_modal("Save As a New Workflow", body, {
OK: function() {
var rename_name =
$("#workflow_rename").val().length > 0
? $("#workflow_rename").val()
: `SavedAs_${self.workflow.name}`;
var rename_annotation = $("#wf_annotation").val().length > 0 ? $("#wf_annotation").val() : "";
$.ajax({
url: `${getAppRoot()}workflow/save_workflow_as`,
type: "POST",
data: {
workflow_name: rename_name,
workflow_annotation: rename_annotation,
from_tool_form: true,
workflow_data: function() {
return JSON.stringify(self.workflow.to_simple());
}
}
})
.done(id => {
window.onbeforeunload = undefined;
window.location = `${getAppRoot()}workflow/editor?id=${id}`;
hide_modal();
})
.fail(() => {
hide_modal();
alert("Saving this workflow failed. Please contact this site's administrator.");
});
},
Cancel: hide_modal
});
}
layout_editor() {
this.workflow.layout();
this.workflow.fit_canvas_to_nodes();
this.scroll_to_nodes();
this.canvas_manager.draw_overview();
}
prebuildNode(type, title_text, content_id) {
var self = this;
// Create node wrapper
const container = document.createElement("div");
container.className = "toolForm toolFormInCanvas";
document.getElementById("canvas-container").appendChild(container);
var $f = $(container);
// Create backbone model and view
var node = new Node(this, { element: $f });
node.type = type;
node.content_id = content_id;
// Mount node component as child dom to node wrapper
const child = document.createElement("div");
container.appendChild(child);
mountWorkflowNode(child, {
id: content_id,
type: type,
title: title_text,
node: node
});
// Set initial scroll position
$f.css("left", $(window).scrollLeft() + 20);
$f.css("top", $(window).scrollTop() + 20);
// Position in container
var o = $("#canvas-container").position();
var p = $("#canvas-container").parent();
var width = $f.outerWidth() + 50;
var height = $f.height();
$f.css({
left: -o.left + p.width() / 2 - width / 2,
top: -o.top + p.height() / 2 - height / 2
});
$f.css("width", width);
$f.bind("dragstart", () => {
self.workflow.activate_node(node);
})
.bind("dragend", function() {
self.workflow.node_changed(this);
self.workflow.fit_canvas_to_nodes();
self.canvas_manager.draw_overview();
})
.bind("dragclickonly", () => {
self.workflow.activate_node(node);
})
.bind("drag", function(e, d) {
// Move
var po = $(this)
.offsetParent()
.offset();
// Find relative offset and scale by zoom
var x = (d.offsetX - po.left) / self.canvas_manager.canvasZoom;
var y = (d.offsetY - po.top) / self.canvas_manager.canvasZoom;
$(this).css({ left: x, top: y });
// Redraw
$(this)
.find(".terminal")
.each(function() {
this.terminal.redraw();
});
});
return node;
}
}
@@ -1,18 +1,14 @@
/* global QUnit */
import $ from "jquery";
import testApp from "qunit/test-app";
import DatatypesMapping from "qunit/test-data/json/datatypes.mapping.json";
import sinon from "sinon";
import Utils from "utils/utils";
import { WorkflowView } from "mvc/workflow/workflow-view";
import WorkflowManager from "mvc/workflow/workflow-manager";
import { Node } from "mvc/workflow/workflow-node";
import { NodeView } from "mvc/workflow/workflow-view-node";
import Terminals from "mvc/workflow/workflow-terminals";
import TerminalsView from "mvc/workflow/workflow-view-terminals";
import Connector from "mvc/workflow/workflow-connector";
import { getAppRoot } from "onload/loadConfig";
// window.show_modal = function(a, b, c) {};
// window.hide_modal = function() {};
// create body and app
var create_app = function() {
@@ -28,23 +24,24 @@ var create_app = function() {
);
// build app
return new WorkflowView({
id: null,
urls: { get_datatypes: getAppRoot() + "api/datatypes/mapping" },
workflows: []
});
return new WorkflowManager(
{
datatypes: [],
datatypes_mapping: DatatypesMapping
},
$("#canvas-container")
);
};
QUnit.module("Input terminal model test", {
beforeEach: function() {
testApp.create();
this.node = new Node(create_app(), {});
this.app = create_app();
this.node = new Node(this.app, {});
this.input = { extensions: ["txt"], multiple: false, optional: false };
this.input_terminal = new Terminals.InputTerminal({ input: this.input });
this.input_terminal = new Terminals.InputTerminal({ app: this.app, input: this.input });
this.input_terminal.node = this.node;
},
afterEach: function() {
testApp.destroy();
delete this.node;
},
multiple: function() {
@@ -54,7 +51,7 @@ QUnit.module("Input terminal model test", {
test_connector: function() {
var outputTerminal = new Terminals.OutputTerminal({ datatypes: ["input"] });
var inputTerminal = this.input_terminal;
return new Connector(outputTerminal, inputTerminal);
return new Connector({}, outputTerminal, inputTerminal);
},
with_test_connector: function(f) {
this.test_connector();
@@ -230,12 +227,12 @@ QUnit.module("Connector test", {});
QUnit.test("connects only if both valid handles", function(assert) {
const input = { connect: sinon.spy() };
const output = { connect: sinon.spy() };
new Connector(input, null);
new Connector(null, output);
new Connector({}, input, null);
new Connector({}, null, output);
// Not attempts to connect...
assert.ok(!input.connect.called);
assert.ok(!output.connect.called);
new Connector(input, output);
new Connector({}, input, output);
assert.ok(input.connect.called);
assert.ok(output.connect.called);
});
@@ -243,7 +240,7 @@ QUnit.test("connects only if both valid handles", function(assert) {
QUnit.test("default attributes", function(assert) {
const input = { connect: sinon.spy() };
const output = { connect: sinon.spy() };
const connector = new Connector(input, output);
const connector = new Connector({}, input, output);
assert.equal(connector.dragging, false);
assert.equal(connector.canvas, null);
assert.equal(connector.inner_color, "#FFFFFF");
@@ -253,7 +250,7 @@ QUnit.test("default attributes", function(assert) {
QUnit.test("destroy", function(assert) {
const input = { connect: sinon.spy(), disconnect: sinon.spy() };
const output = { connect: sinon.spy(), disconnect: sinon.spy() };
const connector = new Connector(input, output);
const connector = new Connector({}, input, output);
connector.destroy();
assert.ok(input.disconnect.called);
assert.ok(output.disconnect.called);
@@ -274,7 +271,7 @@ QUnit.test("initial redraw", function(assert) {
return false;
}
};
const connector = new Connector(input, output);
const connector = new Connector({}, input, output);
const n = $("#canvas-container").find("canvas").length;
connector.redraw();
// Ensure canvas gets set
@@ -285,14 +282,11 @@ QUnit.test("initial redraw", function(assert) {
QUnit.module("Input collection terminal model test", {
beforeEach: function() {
testApp.create();
this.node = new Node(create_app(), {});
this.app = create_app();
this.node = new Node(this.app, {});
this.input = { extensions: ["txt"], collection_types: ["list"] };
this.input_terminal = new Terminals.InputCollectionTerminal({ input: this.input });
this.input_terminal = new Terminals.InputCollectionTerminal({ app: this.app, input: this.input });
this.input_terminal.node = this.node;
},
afterEach: function() {
testApp.destroy();
}
});
@@ -332,7 +326,6 @@ QUnit.test("Collection output cannot connect to different collection input type"
QUnit.module("Node unit test", {
beforeEach: function() {
testApp.create();
this.input_terminal = { destroy: sinon.spy(), redraw: sinon.spy() };
this.output_terminal = { destroy: sinon.spy(), redraw: sinon.spy() };
this.app = create_app();
@@ -341,15 +334,12 @@ QUnit.module("Node unit test", {
this.node.input_terminals.i1 = this.input_terminal;
this.node.output_terminals.o1 = this.output_terminal;
},
afterEach: function() {
testApp.destroy();
},
$: function(selector) {
return $(this.node.element.find(selector));
},
expect_workflow_node_changed: function(assert, f) {
const node = this.node;
const node_changed_spy = sinon.spy(this.app.workflow, "node_changed");
const node_changed_spy = sinon.spy(this.app, "node_changed");
f();
assert.ok(node_changed_spy.calledWith(node));
},
@@ -382,7 +372,7 @@ QUnit.test("make active", function(assert) {
});
QUnit.test("destroy", function(assert) {
var remove_node_spy = sinon.spy(this.app.workflow, "remove_node");
var remove_node_spy = sinon.spy(this.app, "remove_node");
this.node.destroy();
assert.ok(this.input_terminal.destroy.called);
assert.ok(this.output_terminal.destroy.called);
@@ -439,7 +429,7 @@ QUnit.test("init_field_data data", function(assert) {
assert.equal(this.$(".input-terminal").length, 1);
assert.equal(this.$(".rule").length, 1);
assert.equal(this.$(".toolFormBody").children().length, 3);
assert.equal(this.$(".nodeTitle").text(), "newnode");
assert.equal(this.$(".node-title").text(), "newnode");
assert.ok(
this.$(".toolFormTitle")
.find("i")
@@ -451,14 +441,14 @@ QUnit.test("init_field_data data", function(assert) {
QUnit.test("node title behavior", function(assert) {
this.expect_workflow_node_changed(assert, () => {
// Node created with name newnode
assert.equal(this.$(".nodeTitle").text(), "newnode");
assert.equal(this.$(".node-title").text(), "newnode");
// init_field_data_simple doesn't change label, so it should
// remain original name.
this.init_field_data_simple();
assert.equal(this.$(".nodeTitle").text(), "newnode");
assert.equal(this.$(".node-title").text(), "newnode");
// Despite awkward name, update does change the label...
this.update_field_data_with_new_input();
assert.equal(this.$(".nodeTitle").text(), "New Label");
assert.equal(this.$(".node-title").text(), "New Label");
});
});
@@ -482,7 +472,7 @@ QUnit.test("update_field_data preserves connectors", function(assert) {
// Call init with one input and output.
this.init_field_data_simple();
var connector = new Connector();
var connector = new Connector({});
var old_input_terminal = node.input_terminals.input1;
old_input_terminal.connectors.push(connector);
@@ -519,23 +509,20 @@ QUnit.test("update_field_data destroys old terminals", function(assert) {
QUnit.module("create_node", {
beforeEach: function() {
testApp.create();
this.app = create_app();
},
afterEach: function() {
testApp.destroy();
}
});
QUnit.test("node added to workflow", function(assert) {
var add_node_spy = sinon.spy(this.app.workflow, "add_node");
var node = this.app.workflow.create_node("tool", "Cat Files", "cat1");
var add_node_spy = sinon.spy(this.app, "add_node");
var node = this.app.create_node("tool", "Cat Files", "cat1");
assert.ok(add_node_spy.calledWith(node));
});
// global NodeView
QUnit.module("Node view ", {
beforeEach: function() {
this.app = create_app();
this.set_for_node({
input_terminals: {},
output_terminals: {},
@@ -550,7 +537,7 @@ QUnit.module("Node view ", {
},
set_for_node: function(node) {
var element = $("<div><div class='toolFormBody'></div></div>");
this.view = new NodeView({ node: node, $el: element });
this.view = new NodeView(this.app, { node: node, $el: element });
},
connectAttachedTerminal: function(inputType, outputType) {
this.view.addDataInput({ name: "TestName", extensions: [inputType] });
@@ -571,7 +558,7 @@ QUnit.module("Node view ", {
disableMapOver: function() {},
mapOver: Terminals.NULL_COLLECTION_TYPE_DESCRIPTION
};
return new Connector(outputTerminal, terminal);
return new Connector({}, outputTerminal, terminal);
},
connectAttachedMultiInputTerminal: function(inputType, outputType) {
this.view.addDataInput({ name: "TestName", extensions: [inputType], multiple: true });
@@ -592,7 +579,7 @@ QUnit.module("Node view ", {
disableMapOver: function() {},
mapOver: new Terminals.CollectionTypeDescription("list")
};
return new Connector(outputTerminal, terminal);
return new Connector({}, outputTerminal, terminal);
},
connectAttachedMappedOutput: function() {
this.view.addDataInput({ name: "TestName", extensions: ["txt"], input_type: "dataset_collection" });
@@ -613,7 +600,7 @@ QUnit.module("Node view ", {
disableMapOver: function() {},
mapOver: new Terminals.CollectionTypeDescription("list")
};
return new Connector(outputTerminal, terminal);
return new Connector({}, outputTerminal, terminal);
}
});
@@ -672,7 +659,7 @@ QUnit.test("replacing terminal on data input destroys invalid connections", func
var connector_destroy_spy = sinon.spy(connector, "destroy");
// Replacing input with same name - but now of type bam should destroy connection.
this.view.addDataInput({ name: "TestName", extensions: ["bam"] }, newElement);
var terminal = newElement.find(".input-terminal")[0].terminal;
newElement.find(".input-terminal")[0].terminal;
assert.ok(connector_destroy_spy.called);
});
@@ -683,7 +670,7 @@ QUnit.test("replacing terminal on data input with collection changes mapping vie
this.view.addDataInput({ name: "TestName", extensions: ["txt"], input_type: "dataset_collection" }, newElement);
// Input type changed to dataset_collection - old connections are reset.
// Would be nice to preserve these connections and make them map over.
var terminal = newElement.find(".input-terminal")[0].terminal;
newElement.find(".input-terminal")[0].terminal;
assert.ok(connector_destroy_spy.called);
});
@@ -692,16 +679,17 @@ QUnit.test("replacing terminal on data collection input with simple input change
var newElement = $("<div class='inputs'></div>");
var connector_destroy_spy = sinon.spy(connector, "destroy");
this.view.addDataInput({ name: "TestName", extensions: ["txt"], input_type: "dataset" }, newElement);
var terminal = newElement.find(".input-terminal")[0].terminal;
newElement.find(".input-terminal")[0].terminal;
assert.ok(connector_destroy_spy.called);
});
// global InputTerminalView
QUnit.module("Input terminal view", {
beforeEach: function() {
this.app = create_app();
this.node = { input_terminals: [] };
this.input = { name: "i1", extensions: "txt", multiple: false };
this.view = new TerminalsView.InputTerminalView({
this.view = new TerminalsView.InputTerminalView(this.app, {
node: this.node,
input: this.input
});
@@ -722,9 +710,10 @@ QUnit.test("terminal element", function(assert) {
QUnit.module("Output terminal view", {
beforeEach: function() {
this.app = create_app();
this.node = { output_terminals: [] };
this.output = { name: "o1", extensions: "txt" };
this.view = new TerminalsView.OutputTerminalView({
this.view = new TerminalsView.OutputTerminalView(this.app, {
node: this.node,
output: this.output
});
@@ -812,15 +801,6 @@ QUnit.test("equal", function(assert) {
assert.ok(!this.listType().equal(Terminals.NULL_COLLECTION_TYPE_DESCRIPTION));
});
QUnit.module("TerminalMapping", {
beforeEach: function() {
testApp.create();
},
afterEach: function() {
testApp.destroy();
}
});
QUnit.test("default constructor", function(assert) {
var terminal = {};
var mapping = new Terminals.TerminalMapping({ terminal: terminal });
@@ -844,22 +824,13 @@ QUnit.test("disableMapOver", function(assert) {
mapOver: new Terminals.CollectionTypeDescription("list")
});
var changeSpy = sinon.spy();
mapping.bind("change", changeSpy);
mapping.on("change", changeSpy);
mapping.disableMapOver();
assert.ok(mapping.mapOver === Terminals.NULL_COLLECTION_TYPE_DESCRIPTION);
assert.ok(changeSpy.called);
});
QUnit.module("terminal mapping logic", {
beforeEach: function() {
testApp.create();
},
afterEach: function() {
testApp.destroy();
if (this.inputTerminal1) {
this.inputTerminal1.element.remove();
}
},
newInputTerminal: function(mapOver, input, node) {
input = input || {};
node = node || this.newNode();
@@ -867,8 +838,9 @@ QUnit.module("terminal mapping logic", {
input["extensions"] = ["data"];
}
var inputEl = $("<div>")[0];
var inputTerminal = new Terminals.InputTerminal({ element: inputEl, input: input });
var inputTerminalMapping = new Terminals.TerminalMapping({ terminal: inputTerminal });
const app = create_app();
var inputTerminal = new Terminals.InputTerminal({ app: app, element: inputEl, input: input });
new Terminals.TerminalMapping({ terminal: inputTerminal });
inputTerminal.node = node;
if (mapOver) {
inputTerminal.setMapOver(new Terminals.CollectionTypeDescription(mapOver));
@@ -882,7 +854,12 @@ QUnit.module("terminal mapping logic", {
input["extensions"] = ["data"];
}
const inputEl = $("<div>")[0];
const inputTerminal = new Terminals.InputCollectionTerminal({ element: inputEl, input: input });
const app = create_app();
const inputTerminal = new Terminals.InputCollectionTerminal({
app: app,
element: inputEl,
input: input
});
new Terminals.TerminalMapping({ terminal: inputTerminal });
inputTerminal.node = node;
return inputTerminal;
@@ -915,7 +892,7 @@ QUnit.module("terminal mapping logic", {
datatypes: output.extensions,
collection_type: collectionType
});
var outputTerminalMapping = new Terminals.TerminalMapping({ terminal: outputTerminal });
new Terminals.TerminalMapping({ terminal: outputTerminal });
outputTerminal.node = node;
if (mapOver) {
outputTerminal.setMapOver(new Terminals.CollectionTypeDescription(mapOver));
@@ -924,14 +901,15 @@ QUnit.module("terminal mapping logic", {
},
newNode: function() {
var nodeEl = $("<div>")[0];
return new Node(create_app(), { element: nodeEl });
var app = create_app();
return new Node(app, { element: nodeEl });
},
_addExistingOutput: function(terminal, output, connected) {
var self = this;
var node = terminal.node;
if (connected) {
var inputTerminal = self.newInputTerminal();
new Connector(inputTerminal, output);
new Connector({}, inputTerminal, output);
}
this._addTerminalTo(output, node.output_terminals);
return output;
@@ -956,7 +934,7 @@ QUnit.module("terminal mapping logic", {
var connectedInput = this.newInputTerminal();
var node = terminal.node;
var outputTerminal = self.newOutputTerminal();
new Connector(connectedInput, outputTerminal);
new Connector({}, connectedInput, outputTerminal);
this._addTerminalTo(connectedInput, node.input_terminals);
return connectedInput;
},
@@ -1012,7 +990,7 @@ QUnit.test("unconstrained input can be mapped over", function(assert) {
QUnit.test("unmapped input can be mapped over if matching connected input terminals map type", function(assert) {
this.inputTerminal1 = this.newInputTerminal();
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
this.addConnectedInput(this.inputTerminal1);
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list"));
this.verifyAttachable(assert, this.inputTerminal1, "list");
@@ -1029,7 +1007,7 @@ QUnit.test(
"unmapped input can be attached to by output collection if matching connected input terminals map type",
function(assert) {
this.inputTerminal1 = this.newInputTerminal();
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
this.addConnectedInput(this.inputTerminal1);
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list"));
var outputTerminal = this.newOutputCollectionTerminal("list");
@@ -1041,7 +1019,7 @@ QUnit.test(
"unmapped input cannot be attached to by output collection if matching connected input terminals don't match map type",
function(assert) {
this.inputTerminal1 = this.newInputTerminal();
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
this.addConnectedInput(this.inputTerminal1);
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list"));
var outputTerminal = this.newOutputCollectionTerminal("paired");
@@ -1053,7 +1031,7 @@ QUnit.test(
"unmapped input can be attached to by output collection if effective output type (output+mapover) is same as mapped over input",
function(assert) {
this.inputTerminal1 = this.newInputTerminal();
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
this.addConnectedInput(this.inputTerminal1);
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list:paired"));
var outputTerminal = this.newOutputCollectionTerminal("paired");
@@ -1066,7 +1044,7 @@ QUnit.test(
"unmapped input cannot be attached to by output collection if effective output type (output+mapover) is not same as mapped over input (1)",
function(assert) {
this.inputTerminal1 = this.newInputTerminal();
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
this.addConnectedInput(this.inputTerminal1);
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list:paired"));
var outputTerminal = this.newOutputCollectionTerminal("list");
@@ -1079,7 +1057,7 @@ QUnit.test(
"unmapped input cannot be attached to by output collection if effective output type (output+mapover) is not same as mapped over input (2)",
function(assert) {
this.inputTerminal1 = this.newInputTerminal();
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
this.addConnectedInput(this.inputTerminal1);
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list:paired"));
var outputTerminal = this.newOutputCollectionTerminal("list");
+1 -146
View File
@@ -36,6 +36,7 @@ $fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free/webfonts/";
@import "charts.scss";
@import "message.scss";
@import "toolshed.scss";
@import "workflow.scss";
// ==== Select2 ====
/* fix for zero width select2 - remove when fixed there */
@@ -516,7 +517,6 @@ div.metadataHelpBody {
}
// Forms
div.toolFormBody div.toolFormTitle {
background: transparent;
border: none;
@@ -547,43 +547,6 @@ div.toolFormRow {
position: relative;
}
.toolForm.toolFormInCanvas {
@extend .card;
z-index: 100;
position: absolute;
border-color: $brand-primary;
&.toolForm-active {
z-index: 1001;
box-shadow: 0 0 0 2px $brand-primary;
}
.toolFormTitle {
@extend .card-header;
@extend .py-1;
@extend .px-2;
cursor: move;
background: $brand-secondary;
}
.toolFormBody {
@extend .card-body;
@extend .p-0;
@extend .mx-2;
.rule {
height: 0;
border: none;
border-bottom: dotted black 1px;
margin: 0 5px;
}
}
}
.toolForm.toolFormInCanvas.tool-node-error {
border-color: $state-danger-border;
.toolFormTitle {
background: $state-danger-bg;
border-color: $state-danger-border;
}
}
div.form,
div.toolForm {
border: solid $form-border 1px;
@@ -656,18 +619,6 @@ div.form-row-input label {
@extend .mt-3;
}
.workflow-right {
background: $white;
}
.workflow-right .right-content {
margin: 3px;
}
.workflow-right .right-content .section-row {
margin-bottom: 10px;
}
.form-row {
select,
textarea,
@@ -1666,102 +1617,6 @@ div.toolSectionTitle {
opacity: 1;
}
/* TEMPORARY, REFACTOR THIS -- This is only image-related styles from the
* workflow editor; everything should be moved out of the mako and into its own
* file */
#canvas-viewport {
background: $workflow-editor-bg url("../../images/light_gray_grid.gif");
width: 100%;
height: calc(100% - 2.5rem);
position: absolute;
overflow: hidden;
}
.base-terminal {
@extend .fa;
@extend .fa-circle;
@extend .mt-1;
color: $brand-light;
position: absolute;
z-index: 1500;
> icon {
@extend .fa;
@extend .fa-chevron-circle-right;
color: $brand-primary;
position: absolute;
left: 0px;
top: 0px;
}
}
.input-terminal {
@extend .base-terminal;
left: -9px;
}
.output-terminal {
@extend .base-terminal;
right: -9px;
&:hover > icon {
color: $brand-success;
}
}
.input-terminal-active.can-accept > icon {
color: $brand-success;
}
.input-terminal-active.cannot-accept > icon {
color: $brand-warning;
}
.drag-terminal {
@extend .fa;
@extend .fa-circle;
color: $brand-success;
position: absolute;
z-index: 1500;
}
.mark-terminal {
cursor: pointer;
color: $text-muted;
&.mark-terminal-active {
color: $brand-success;
}
}
.delete-terminal {
@extend .btn;
@extend .btn-sm;
@extend .btn-primary;
@extend .fa;
@extend .fa-minus-circle;
@extend .p-1;
@extend .mt-1;
position: absolute;
z-index: 2500;
top: 0px;
left: -15px;
}
.callout-terminal {
position: absolute;
}
.workflow-overview {
position: absolute;
width: 150px;
height: 150px;
right: 0px;
bottom: 0px;
border-top: solid gray 1px;
border-left: solid grey 1px;
padding: 7px 0 0 7px;
background: $workflow-overview-bg no-repeat url("../../images/resizable.png");
z-index: 20000;
overflow: hidden;
max-width: 300px;
max-height: 300px;
min-width: 50px;
min-height: 50px;
}
#input-choices-menu {
color: black;
}
/* For Vue */
[v-cloak] {
display: none;
+2 -1
View File
@@ -156,7 +156,8 @@ $state-running-border: $border-color;
// Workflow editor
$workflow-editor-bg: $white;
$workflow-overview-bg: $gray-200;
$workflow-overview-bg: $panel-bg-color;
$worfklow-node-width: 10rem;
// Margins
$spacer: 1rem;
+180
View File
@@ -0,0 +1,180 @@
.workflow-client {
.workflow-panel {
background: $white;
}
.workflow-center {
z-index: 0;
display: flex;
flex-direction: column;
flex-grow: 1;
.toolForm.toolFormInCanvas {
@extend .card;
z-index: 100;
width: $worfklow-node-width;
position: absolute;
border-color: $brand-primary;
&.toolForm-active {
z-index: 1001;
box-shadow: 0 0 0 2px $brand-primary;
}
.toolFormTitle {
@extend .card-header;
@extend .py-1;
@extend .px-2;
cursor: move;
background: $brand-secondary;
}
.toolFormBody {
@extend .card-body;
@extend .p-0;
@extend .mx-2;
.rule {
height: 0;
border: none;
border-bottom: dotted black 1px;
margin: 0 5px;
}
}
}
.toolForm.toolFormInCanvas.tool-node-error {
border-color: $state-danger-border;
.toolFormTitle {
background: $state-danger-bg;
border-color: $state-danger-border;
}
}
.workflow-canvas {
#canvas-viewport {
background: $workflow-editor-bg url("../../images/light_gray_grid.gif");
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.base-terminal {
@extend .fa;
@extend .fa-circle;
@extend .mt-1;
color: $brand-light;
position: absolute;
z-index: 1500;
> icon {
@extend .fa;
@extend .fa-chevron-circle-right;
color: $brand-primary;
position: absolute;
left: 0px;
top: 0px;
}
}
.input-terminal {
@extend .base-terminal;
left: -9px;
}
.output-terminal {
@extend .base-terminal;
right: -9px;
&:hover > icon {
color: $brand-success;
}
}
.input-terminal-active.can-accept > icon {
color: $brand-success;
}
.input-terminal-active.cannot-accept > icon {
color: $brand-warning;
}
.drag-terminal {
@extend .fa;
@extend .fa-circle;
color: $brand-success;
position: absolute;
z-index: 1500;
}
.mark-terminal {
cursor: pointer;
color: $text-muted;
&.mark-terminal-active {
color: $brand-success;
}
}
.delete-terminal {
@extend .btn;
@extend .btn-sm;
@extend .btn-primary;
@extend .fa;
@extend .fa-minus-circle;
@extend .p-1;
@extend .mt-1;
position: absolute;
z-index: 2500;
top: 0px;
left: -15px;
}
.callout-terminal {
position: absolute;
}
.workflow-overview {
border-top-left-radius: 0.3rem;
cursor: pointer;
position: absolute;
width: 150px;
height: 150px;
right: 0px;
bottom: 0px;
border-top: solid $border-color 1px;
border-left: solid $border-color 1px;
padding: 7px 0 0 7px;
background: $workflow-overview-bg no-repeat url("../../images/resizable.png");
z-index: 20000;
overflow: hidden;
max-width: 300px;
max-height: 300px;
min-width: 50px;
min-height: 50px;
.workflow-overview-body {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
}
}
#input-choices-menu {
color: black;
}
}
.workflow-report-body {
display: flex;
}
#canvas-container {
position: absolute;
width: 100%;
height: 100%;
}
#overview-container {
position: absolute;
}
#overview-canvas {
width: 100%;
height: 100%;
}
#overview-viewport {
@extend .rounded;
position: absolute;
width: 0px;
height: 0px;
border: solid $brand-info 3px;
background: $white;
opacity: 0.8;
z-index: 10;
}
canvas {
position: absolute;
z-index: 10;
}
canvas.dragging {
position: absolute;
z-index: 1000;
}
}
}
+2 -3
View File
@@ -270,7 +270,7 @@ workflow_editor:
selectors:
_: "[node-label='${label}']"
title: '${_} .nodeTitle'
title: '${_} .node-title'
destroy: '${_} .node-destroy'
clone: '${_} .node-clone'
@@ -280,8 +280,7 @@ workflow_editor:
input_mapping_icon: "${_} [name='${name}'] .fa-folder-o"
selectors:
canvas_body: '#workflow-canvas-body'
canvas_title: '#workflow-canvas-title'
canvas_body: '#workflow-canvas'
edit_annotation: '#workflow-annotation'
edit_name: '#workflow-name'
@@ -15,12 +15,9 @@
${parent.javascript_app()}
<script type="text/javascript">
var editorConfig = ${ h.dumps( editor_config ) };
editorConfig.workflow_globals = workflow_globals;
config.addInitialization(function(galaxy, config) {
console.log("workflow/editor.mako, editorConfig", editorConfig);
window.bundleEntries.mountWorkflowEditor(editorConfig);
window.bundleEntries.mountToolBoxWorkflow(editorConfig);
window.bundleEntries.mountWorkflowPanel(editorConfig);
});
</script>
</%def>
@@ -34,7 +31,7 @@
</%def>
<%def name="overlay(visible=False)">
${parent.overlay( "Loading workflow editor...",
${parent.overlay( "Loading workflow...",
"<div class='progress progress-striped progress-info active'><div class='progress-bar' style='width: 100%;'></div></div>", self.overlay_visible )}
</%def>