Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions back-end/pyworkflow/pyworkflow/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
6 changes: 3 additions & 3 deletions back-end/pyworkflow/pyworkflow/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions front-end/src/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object>} - 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
Expand Down
18 changes: 18 additions & 0 deletions front-end/src/components/CustomNode/CustomNodeModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 30 additions & 15 deletions front-end/src/components/CustomNode/CustomNodeWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,30 @@ 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
const portWidgets = {};
for (let portType in sortedPorts) {
portWidgets[portType] = sortedPorts[portType].map(port =>
<PortWidget engine={engine} port={port} key={port.getID()}>
<div className="triangle-port" />
<div className="triangle-port" />
</PortWidget>
);
}

const flowPortWidgets = [flowInPort, flowOutPort].filter(p => p).map(port =>
<PortWidget engine={engine} port={port} key={port.getID()}
className={`flow-port-div flow-port-div-${port.options.in ? "in" : "out"}`}>
<div className="flow-port" />
</PortWidget>
);


let graphView;
let width = 40;
if (this.props.node.options.node_type !== "flow_control") {
Expand All @@ -75,19 +86,23 @@ export default class CustomNodeWidget extends React.Component {
<div className="custom-node-wrapper">
<div className="custom-node-name">{this.props.node.options.name}</div>
<div className="custom-node" style={{ borderColor: this.props.node.options.color, width: width }}>
<div className="custom-node-configure" onClick={this.toggleConfig}>{String.fromCharCode(this.icon)}</div>
<NodeConfig node={this.props.node}
globals={this.props.engine.model.globals || []}
show={this.state.showConfig}
toggleShow={this.toggleConfig}
onDelete={this.handleDelete}
onSubmit={this.acceptConfiguration} />
{graphView}
<GraphView node={this.props.node}
show={this.state.showGraph}
toggleShow={this.toggleGraph}
onDelete={this.handleDelete}
onSubmit={this.acceptConfiguration} />
<div className="custom-node-icons">
<div className="custom-node-configure" onClick={this.toggleConfig}>
{String.fromCharCode(this.icon)}
</div>
<NodeConfig node={this.props.node}
show={this.state.showConfig}
toggleShow={this.toggleConfig}
onDelete={this.handleDelete}
onSubmit={this.acceptConfiguration} />
{graphView}
<GraphView node={this.props.node}
show={this.state.showGraph}
toggleShow={this.toggleGraph}
onDelete={this.handleDelete}
onSubmit={this.acceptConfiguration} />
</div>
{flowPortWidgets}
<div className="port-col port-col-in">
{ portWidgets["in"] }
</div>
Expand Down
28 changes: 20 additions & 8 deletions front-end/src/components/CustomNode/NodeConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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})}/>
)}
<Form.Group>
Expand Down Expand Up @@ -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 (
<Form.Group>
<Form.Label>{props.label}</Form.Label>
Expand All @@ -159,7 +171,7 @@ function OptionInput(props) {
{hideFlow ? null :
<FlowVariableOverride keyName={props.keyName}
flowValue={props.flowValue || {}}
flowNodes={props.globals || []}
flowNodes={props.flowNodes || []}
checked={isFlow}
onFlowCheck={handleFlowCheck}
onChange={handleFlowVariable} />
Expand Down Expand Up @@ -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
Expand All @@ -308,9 +320,9 @@ function FlowVariableOverride(props) {
<Form.Control as="select" name={props.keyName} onChange={handleSelect}
value={props.flowValue.node_id}>
<option/>
{props.flowNodes.map(gfv =>
<option key={gfv.id} value={gfv.id}>
{gfv.options.var_name}
{props.flowNodes.map(fv =>
<option key={fv.node_id} value={fv.node_id}>
{fv.options ? fv.options.var_name : fv.option_values.var_name}
</option>
)}
</Form.Control>
Expand Down
4 changes: 2 additions & 2 deletions front-end/src/components/VPLink/VPLinkModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
14 changes: 11 additions & 3 deletions front-end/src/components/VPPort/VPPortModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
1 change: 0 additions & 1 deletion front-end/src/components/Workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
40 changes: 32 additions & 8 deletions front-end/src/styles/CustomNode.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
position: relative;
}

.custom-node-name {
margin-bottom: 3px;
}

.port-col {
display: flex;
flex-direction: column;
Expand All @@ -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 {
Expand Down