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
8 changes: 6 additions & 2 deletions front-end/src/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ export async function deleteNode(node) {
* Update configuration of node in server-side workflow
* @param {CustomNodeModel} node - JS node to remove
* @param {Object} config - configuration from options form
* @param {Object} flowConfig - flow variable configuration options
* @returns {Promise<Object>} - server response (serialized node)
*/
export async function updateNode(node, config) {
export async function updateNode(node, config, flowConfig) {
node.config = config;
node.options.option_replace = flowConfig;
const payload = {...node.options, options: node.config};
const options = {
method: "POST",
Expand Down Expand Up @@ -212,9 +214,11 @@ export async function downloadDataFile(node) {
.then(async resp => {
if (!resp.ok) return Promise.reject(await resp.json());
contentType = resp.headers.get("content-type");
let filename = resp.headers.get("Content-Disposition");

if (contentType.startsWith("text")) {
resp.text().then(data => {
downloadFile(data, contentType, node.config["file"]);
downloadFile(data, contentType, filename);
})
}
}).catch(err => console.log(err));
Expand Down
5 changes: 3 additions & 2 deletions front-end/src/components/CustomNode/CustomNodeWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export default class CustomNodeWidget extends React.Component {
}).catch(err => console.log(err));
}

acceptConfiguration(formData) {
API.updateNode(this.props.node, formData).then(() => {
acceptConfiguration(optionsData, flowData) {
API.updateNode(this.props.node, optionsData, flowData).then(() => {
this.props.node.setStatus("configured");
this.forceUpdate();
this.props.engine.repaintCanvas();
Expand Down Expand Up @@ -77,6 +77,7 @@ export default class CustomNodeWidget extends React.Component {
<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}
Expand Down
134 changes: 113 additions & 21 deletions front-end/src/components/CustomNode/NodeConfig.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React, { useRef, useState, useEffect } from 'react';
import { Modal, Button, Form } from 'react-bootstrap';
import { Col, Modal, Button, Form } from 'react-bootstrap';
import propTypes from 'prop-types';
import * as _ from 'lodash';
import * as API from "../../API";
import * as API from '../../API';
import '../../styles/NodeConfig.css';

export default class NodeConfig extends React.Component {

constructor(props) {
super(props);
this.state = {
disabled: false,
data: {}
data: {},
flowData: {}
};
this.updateData = this.updateData.bind(this);
this.handleDelete = this.handleDelete.bind(this);
Expand All @@ -19,15 +21,26 @@ export default class NodeConfig extends React.Component {

// callback to update form data in state;
// resulting state will be sent to node config callback
updateData(key, value) {
this.setState((prevState) => ({
...prevState,
data: {
...prevState.data,
[key]: value
}
})
);
updateData(key, value, flow = false) {
if (flow) {
this.setState((prevState) => ({
...prevState,
flowData: {
...prevState.flowData,
[key]: value
}
})
);
} else {
this.setState((prevState) => ({
...prevState,
data: {
...prevState.data,
[key]: value
}
})
);
}
};

// confirm, fire delete callback, close modal
Expand All @@ -42,15 +55,21 @@ export default class NodeConfig extends React.Component {
handleSubmit(e) {
e.preventDefault();
console.log(this.state.data);
this.props.onSubmit(this.state.data);
// remove items from flow vars if null
const flowData = {...this.state.flowData};
for (let key in flowData) {
if (flowData[key] === null) delete flowData[key];
}
this.props.onSubmit(this.state.data, flowData);
this.props.toggleShow();
};

render() {
if (!this.props.node) return null;
return (
<Modal show={this.props.show} onHide={this.props.toggleShow} centered
onWheel={e => e.stopPropagation()}>
dialogClassName="NodeConfig"
onWheel={e => e.stopPropagation()}>
<Form onSubmit={this.handleSubmit}>
<Modal.Header>
<Modal.Title><b>{this.props.node.options.name}</b> Configuration</Modal.Title>
Expand All @@ -61,6 +80,9 @@ export default class NodeConfig extends React.Component {
onChange={this.updateData}
node={this.props.node}
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}
disableFunc={(v) => this.setState({disabled: v})}/>
)}
<Form.Group>
Expand Down Expand Up @@ -96,27 +118,61 @@ NodeConfig.propTypes = {
*/
function OptionInput(props) {

const [isFlow, setIsFlow] = useState(props.flowValue ? true : false);

const handleFlowCheck = (bool) => {
// if un-checking, fire callback with null so no stale value is in `option_replace`
if (!bool) props.onChange(props.keyName, null, true);
setIsFlow(bool);
};

// fire callback to update `option_replace` with flow node info
const handleFlowVariable = (value) => {
props.onChange(props.keyName, value, true);
};

let inputComp;
if (props.type === "file") {
inputComp = <FileUploadInput {...props} />
inputComp = <FileUploadInput {...props} disabled={isFlow} />
} else if (props.type === "string") {
<<<<<<< HEAD
inputComp = <SimpleInput {...props} type="text" disabled={isFlow} />
=======
inputComp = <SimpleInput {...props} type="text" />
} else if (props.type === "text") {
inputComp = <SimpleInput {...props} type="textarea"/>
>>>>>>> master
} else if (props.type === "int") {
inputComp = <SimpleInput {...props} type="number" />
inputComp = <SimpleInput {...props} type="number" disabled={isFlow} />
} else if (props.type === "boolean") {
<<<<<<< HEAD
inputComp = <BooleanInput {...props} disabled={isFlow} />
=======
inputComp = <BooleanInput {...props} />
} else if (props.type === "select") {
inputComp = <SelectInput {...props} />
>>>>>>> master
} else {
return (<></>)
}

const hideFlow = props.node.options.is_global
|| props.type === "file" || props.globals.length === 0
return (
<Form.Group>
<Form.Label>{props.label}</Form.Label>
<div style={{fontSize: '0.7rem'}}>{props.docstring}</div>
{ inputComp }
<Form.Label>{props.label}</Form.Label>
<div className="option-docstring">{props.docstring}</div>
<Form.Row>
<Col xs={hideFlow ? 12 : 8}>{ inputComp }</Col>
{hideFlow ? null :
<FlowVariableOverride keyName={props.keyName}
flowValue={props.flowValue || {}}
flowNodes={props.globals || []}
checked={isFlow}
onFlowCheck={handleFlowCheck}
onChange={handleFlowVariable} />
}
</Form.Row>
</Form.Group>
)
}
Expand Down Expand Up @@ -236,8 +292,44 @@ function BooleanInput(props) {

return (
<Form.Check type="checkbox" name={props.keyName}
checked={value}
onChange={handleChange} />
disabled={props.disabled}
checked={value}
onChange={handleChange} />
)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing some closing ) and } here. Causing a compile error when run.


function FlowVariableOverride(props) {

const handleSelect = (event) => {
const uuid = event.target.value;
const flow = props.flowNodes.find(d => d.id === uuid);
const obj = {
node_id: uuid,
is_global: flow.is_global
};
props.onChange(obj);
};
const handleCheck = (event) => { props.onFlowCheck(event.target.checked) };

return (
<Col>
<Form.Check type="checkbox" inline
label="Use Flow Variable"
checked={props.checked} onChange={handleCheck} />
{props.checked ?
<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}
</option>
)}
</Form.Control>
: null
}
</Col>
)
}

Expand Down
5 changes: 4 additions & 1 deletion front-end/src/components/Workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ class Workspace extends React.Component {

getGlobalVars() {
API.getGlobalVars()
.then(vars => this.setState({globals: vars}))
.then(vars => {
this.setState({globals: vars});
this.model.globals = vars;
})
.catch(err => console.log(err));
}

Expand Down
7 changes: 7 additions & 0 deletions front-end/src/styles/NodeConfig.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.option-docstring {
font-size: 0.7rem;
}

.NodeConfig {
min-width: 50%;
}
8 changes: 3 additions & 5 deletions front-end/src/styles/Workspace.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
.Workspace {
border: 1px solid grey;
}
.NodeMenu, .GlobalFlowMenu {
padding: 1rem;
margin: 0.5rem 0;
margin-bottom: 1rem;
box-shadow: 0px 3px 6px grey;
}
.NodeMenu ul {
Expand All @@ -23,7 +20,8 @@
font-size: 1rem;
}
.diagram-canvas {
height: 75vh;
box-shadow: 0px 2px 4px grey;
height: 100vh;
background-size: 20px 20px;
background-image:
linear-gradient(to right, rgba(54, 169, 231, 0.1) 1px, transparent 1px),
Expand Down
20 changes: 15 additions & 5 deletions pyworkflow/pyworkflow/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,19 @@ def __init__(self, node_info):
def execute(self, predecessor_data, flow_vars):
raise NotImplementedError()

def get_execution_options(self, flow_nodes):
def get_execution_options(self, workflow, flow_nodes):
"""Replace Node options with flow variables.

If the user has specified any flow variables to replace Node options,
perform the replacement and return a dict with all options to use for
execution. If no flow variables are included, this method will return
a copy of all Node options unchanged.
a copy of all Node options.

For any 'file' options, the value will be replaced with a path based on
the Workflow's root directory.

Args:
workflow: Workflow object to construct file paths
flow_nodes: dict of FlowNodes used to replace options

Returns:
Expand All @@ -49,7 +53,12 @@ def get_execution_options(self, flow_nodes):
for key, option in self.options.items():

if key in flow_nodes:
option.set_value(flow_nodes[key].get_replacement_value())
replacement_value = flow_nodes[key].get_replacement_value()
else:
replacement_value = option.get_value()

if key == 'file':
option.set_value(workflow.path(replacement_value))

execution_options[key] = option

Expand All @@ -64,8 +73,9 @@ def validate(self):
Raises:
ParameterValidationError: invalid Parameter value
"""
for option in self.options.values():
option.validate()
for key, option in self.options.items():
if key not in self.option_replace:
option.validate()

def validate_input_data(self, num_input_data):
"""Validate Node input data.
Expand Down
15 changes: 11 additions & 4 deletions pyworkflow/pyworkflow/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ def execute(self, node_id):
try:
# Validate input data, and replace flow variables
node_to_execute.validate_input_data(len(preceding_data))
execution_options = node_to_execute.get_execution_options(flow_nodes)
execution_options = node_to_execute.get_execution_options(self, flow_nodes)

# Pass in data to current Node to use in execution
output = node_to_execute.execute(preceding_data, execution_options)
Expand Down Expand Up @@ -353,7 +353,7 @@ def load_flow_nodes(self, option_replace):
else:
flow_node = self.get_node(flow_node_id)

if flow_node is None or flow_node.node_type != 'FlowNode':
if flow_node is None or flow_node.node_type != 'flow_control':
raise WorkflowException('load flow vars', 'The workflow does not contain FlowNode %s' % flow_node_id)

flow_nodes[key] = flow_node
Expand Down Expand Up @@ -419,8 +419,15 @@ def download_file(self, node_id):
return None

try:
# TODO: Change to generic "file" option to allow for more than WriteCsv
to_open = self.path(node.options['file'].get_value())
# Check if file option is overridden by flow variable
if 'file' in node.option_replace:
flow_node = self.load_flow_nodes({'file': node.option_replace['file']})
filename = flow_node['file'].get_replacement_value()
else:
filename = node.options['file'].get_value()

# Construct path to file in Workflow dir
to_open = self.path(filename)
return open(to_open)
except KeyError:
raise WorkflowException('download_file', '%s does not have an associated file' % node_id)
Expand Down
8 changes: 2 additions & 6 deletions vp/node/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,9 @@ def create_node(request):
"""Pass all request info to Node Factory.

"""
# Any filenames/paths passed through as-is
# A Workflow-specific path is constructed at execution
json_data = json.loads(request.body)
# for options with type 'file', replace value with FileStorage path
for field, info in json_data.get("option_types", dict()).items():
if info["type"] == "file" or info["label"] == "Filename":
opt_value = json_data["options"][field]
if opt_value is not None:
json_data["options"][field] = request.pyworkflow.path(opt_value)

try:
return node_factory(json_data)
Expand Down
Loading