diff --git a/back-end/pyworkflow/pyworkflow/node.py b/back-end/pyworkflow/pyworkflow/node.py index 0a3bc20..898fe1c 100644 --- a/back-end/pyworkflow/pyworkflow/node.py +++ b/back-end/pyworkflow/pyworkflow/node.py @@ -54,6 +54,7 @@ def get_execution_options(self, workflow, flow_nodes): if key in flow_nodes: replacement_value = flow_nodes[key].get_replacement_value() + option.set_value(replacement_value) else: replacement_value = option.get_value() diff --git a/back-end/pyworkflow/pyworkflow/nodes/flow_control/integer_input.py b/back-end/pyworkflow/pyworkflow/nodes/flow_control/integer_input.py index 086eb89..0ecb0cf 100644 --- a/back-end/pyworkflow/pyworkflow/nodes/flow_control/integer_input.py +++ b/back-end/pyworkflow/pyworkflow/nodes/flow_control/integer_input.py @@ -8,8 +8,8 @@ class IntegerNode(FlowNode): Allows for Strings to replace 'string' fields in Nodes """ name = "Integer Input" - num_in = 1 - num_out = 1 + num_in = 0 + num_out = 0 color = 'purple' OPTIONS = { diff --git a/back-end/pyworkflow/pyworkflow/nodes/flow_control/string_input.py b/back-end/pyworkflow/pyworkflow/nodes/flow_control/string_input.py index eaade18..81a785b 100644 --- a/back-end/pyworkflow/pyworkflow/nodes/flow_control/string_input.py +++ b/back-end/pyworkflow/pyworkflow/nodes/flow_control/string_input.py @@ -8,8 +8,8 @@ class StringNode(FlowNode): Allows for Strings to replace 'string' fields in Nodes """ name = "String Input" - num_in = 1 - num_out = 1 + num_in = 0 + num_out = 0 color = 'purple' OPTIONS = { diff --git a/back-end/pyworkflow/pyworkflow/workflow.py b/back-end/pyworkflow/pyworkflow/workflow.py index 6d29a07..a9515a8 100644 --- a/back-end/pyworkflow/pyworkflow/workflow.py +++ b/back-end/pyworkflow/pyworkflow/workflow.py @@ -172,7 +172,7 @@ def get_all_flow_var_options(self, node_id): for predecessor_id in self.get_node_predecessors(node_id): node = self.get_node(predecessor_id) - if node.node_type == 'FlowNode': + if node.node_type == 'flow_control': flow_variables.append(node.to_json()) return flow_variables @@ -314,7 +314,7 @@ def execute(self, node_id): except NodeException as e: raise e - if node_to_execute.data is None: + if node_to_execute.data is None and node_to_execute.node_type != "flow_control": raise WorkflowException('execute', 'There was a problem saving node output.') return node_to_execute @@ -384,7 +384,7 @@ def load_input_data(self, node_id): if node_to_retrieve is None: raise WorkflowException('retrieve node data', 'The workflow does not contain node %s' % predecessor_id) - if node_to_retrieve.node_type != 'FlowNode': + if node_to_retrieve.node_type != 'flow_control': input_data.append(self.retrieve_node_data(node_to_retrieve)) except WorkflowException: diff --git a/front-end/src/API.js b/front-end/src/API.js index a61e6e8..a184885 100644 --- a/front-end/src/API.js +++ b/front-end/src/API.js @@ -26,6 +26,16 @@ function fetchWrapper(endpoint, options = {}) { } +/** + * Retrieve node info from server side workflow + * @param {string} nodeId - ID of node to retrieve + * @returns {Promise} - server response (node info and flow variables) + */ +export async function getNode(nodeId) { + return fetchWrapper(`/node/${nodeId}`); +} + + /** * Add node to server-side workflow * @param {CustomNodeModel} node - JS node to add diff --git a/front-end/src/components/CustomNode/CustomNodeModel.js b/front-end/src/components/CustomNode/CustomNodeModel.js index c382e3a..a3d05ff 100644 --- a/front-end/src/components/CustomNode/CustomNodeModel.js +++ b/front-end/src/components/CustomNode/CustomNodeModel.js @@ -13,6 +13,24 @@ export default class CustomNodeModel extends NodeModel { this.configParams = options.option_types; this.options.status = options.status || "unconfigured"; + // add flow control input port + this.addPort( + new VPPortModel({ + in: true, + type: 'vp-port', + name: 'flow-in' + }) + ); + // if flow node, add flow control output port + if (this.options.node_type === "flow_control") { + this.addPort( + new VPPortModel({ + in: false, + type: 'vp-port', + name: 'flow-out' + }) + ); + } const nIn = options.num_in === undefined ? 1 : options.num_in; const nOut = options.num_out === undefined ? 1 : options.num_out; // setup in and out ports diff --git a/front-end/src/components/CustomNode/CustomNodeWidget.js b/front-end/src/components/CustomNode/CustomNodeWidget.js index a5a8e2b..6bc38eb 100644 --- a/front-end/src/components/CustomNode/CustomNodeWidget.js +++ b/front-end/src/components/CustomNode/CustomNodeWidget.js @@ -47,7 +47,10 @@ export default class CustomNodeWidget extends React.Component { render() { const engine = this.props.engine; - const ports = _.values(this.props.node.getPorts()); + const allPorts = _.values(this.props.node.getPorts()); + const ports = allPorts.filter(p => !p.options.name.includes("flow")); + const flowInPort = allPorts.find(p => p.options.name === "flow-in"); + const flowOutPort = allPorts.find(p => p.options.name === "flow-out"); // group ports by type (in/out) const sortedPorts = _.groupBy(ports, p => p.options.in === true ? "in" : "out"); // create PortWidget array for each type @@ -55,11 +58,19 @@ export default class CustomNodeWidget extends React.Component { for (let portType in sortedPorts) { portWidgets[portType] = sortedPorts[portType].map(port => -
+
); } + const flowPortWidgets = [flowInPort, flowOutPort].filter(p => p).map(port => + +
+ + ); + + let graphView; let width = 40; if (this.props.node.options.node_type !== "flow_control") { @@ -75,19 +86,23 @@ export default class CustomNodeWidget extends React.Component {
{this.props.node.options.name}
-
{String.fromCharCode(this.icon)}
- - {graphView} - +
+
+ {String.fromCharCode(this.icon)} +
+ + {graphView} + +
+ {flowPortWidgets}
{ portWidgets["in"] }
diff --git a/front-end/src/components/CustomNode/NodeConfig.js b/front-end/src/components/CustomNode/NodeConfig.js index 67c8f94..a30df87 100644 --- a/front-end/src/components/CustomNode/NodeConfig.js +++ b/front-end/src/components/CustomNode/NodeConfig.js @@ -12,13 +12,25 @@ export default class NodeConfig extends React.Component { this.state = { disabled: false, data: {}, - flowData: {} + flowData: {}, + flowNodes: [] }; this.updateData = this.updateData.bind(this); this.handleDelete = this.handleDelete.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } + getFlowNodes() { + if (!this.props.node) return; + API.getNode(this.props.node.options.id) + .then(node => this.setState({flowNodes: node.flow_variables})) + .catch(err => console.log(err)); + } + + componentDidUpdate(prevProps) { + if (!prevProps.show && this.props.show) this.getFlowNodes(); + } + // callback to update form data in state; // resulting state will be sent to node config callback updateData(key, value, flow = false) { @@ -82,7 +94,7 @@ export default class NodeConfig extends React.Component { value={this.props.node.config[key]} flowValue={this.props.node.options.option_replace ? this.props.node.options.option_replace[key] : null} - globals={this.props.globals} + flowNodes={this.state.flowNodes} disableFunc={(v) => this.setState({disabled: v})}/> )} @@ -149,7 +161,7 @@ function OptionInput(props) { } const hideFlow = props.node.options.is_global - || props.type === "file" || props.globals.length === 0 + || props.type === "file" || props.flowNodes.length === 0; return ( {props.label} @@ -159,7 +171,7 @@ function OptionInput(props) { {hideFlow ? null : @@ -290,7 +302,7 @@ function FlowVariableOverride(props) { const handleSelect = (event) => { const uuid = event.target.value; - const flow = props.flowNodes.find(d => d.id === uuid); + const flow = props.flowNodes.find(d => d.node_id === uuid); const obj = { node_id: uuid, is_global: flow.is_global @@ -308,9 +320,9 @@ function FlowVariableOverride(props) { )} diff --git a/front-end/src/components/VPLink/VPLinkModel.js b/front-end/src/components/VPLink/VPLinkModel.js index a29e2bb..832c6fd 100644 --- a/front-end/src/components/VPLink/VPLinkModel.js +++ b/front-end/src/components/VPLink/VPLinkModel.js @@ -5,8 +5,8 @@ export default class VPLinkModel extends DefaultLinkModel { constructor() { super({ type: 'default', - width: 5, - color: 'orange' + width: 2, + color: 'black' }); this.registerListener({ targetPortChanged: event => { diff --git a/front-end/src/components/VPPort/VPPortModel.js b/front-end/src/components/VPPort/VPPortModel.js index 4c0897a..c72778d 100644 --- a/front-end/src/components/VPPort/VPPortModel.js +++ b/front-end/src/components/VPPort/VPPortModel.js @@ -9,8 +9,16 @@ export default class VPPortModel extends DefaultPortModel { } canLinkToPort(port) { - // can't both be in or out ports - return port instanceof VPPortModel - && this.options.in !== port.options.in; + // if connecting to flow port, make sure this is a flow port + // and opposite of other's direction + if (port.options.name.includes("flow")) { + return this.options.name.includes("flow") + && this.options.in !== port.options.in + // otherwise, make sure this is NOT a flow port, and ensure + // in/out compatibility + } else { + return !this.options.name.includes("flow") + && this.options.in !== port.options.in + } } } diff --git a/front-end/src/components/Workspace.js b/front-end/src/components/Workspace.js index d12410b..1d06abb 100644 --- a/front-end/src/components/Workspace.js +++ b/front-end/src/components/Workspace.js @@ -53,7 +53,6 @@ class Workspace extends React.Component { API.getGlobalVars() .then(vars => { this.setState({globals: vars}); - this.model.globals = vars; }) .catch(err => console.log(err)); } diff --git a/front-end/src/styles/CustomNode.css b/front-end/src/styles/CustomNode.css index 0787cb9..2060c0b 100644 --- a/front-end/src/styles/CustomNode.css +++ b/front-end/src/styles/CustomNode.css @@ -14,6 +14,10 @@ position: relative; } +.custom-node-name { + margin-bottom: 3px; +} + .port-col { display: flex; flex-direction: column; @@ -40,21 +44,41 @@ border-left-color: mediumpurple; } +.flow-port-div { + position: absolute; + top: -5px; +} + +.flow-port-div-in { + left: -5px; +} + +.flow-port-div-out { + left: 85%; +} + +.flow-port { + width: 10px; + height: 10px; + border-radius: 5px; + background-color: purple; +} + +.custom-node-icons { + width: 100%; + position: absolute; + display: flex; + justify-content: space-evenly; + align-items: center; +} + .custom-node-configure { font-size: 1.5rem; cursor: pointer; - position: absolute; - left: 25%; - top: 50%; - transform: translate(-50%, -50%); } .custom-node-tabular { cursor: pointer; - position: absolute; - right: 25%; - top: 25%; - transform: translate(50%, -25%); } .custom-node-description {