From 8a58b40d342e66aa50f7fce8152cf7baa48e147c Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Tue, 28 Apr 2020 23:36:12 -0400 Subject: [PATCH 01/15] feat(ui): store global flow nodes on DiagramModel --- front-end/src/components/Workspace.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/front-end/src/components/Workspace.js b/front-end/src/components/Workspace.js index 38937a4..d12410b 100644 --- a/front-end/src/components/Workspace.js +++ b/front-end/src/components/Workspace.js @@ -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)); } From c8e07d7c28a3ae4523f9646bb3cd17faaf0bb609 Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Tue, 28 Apr 2020 23:38:10 -0400 Subject: [PATCH 02/15] feat(ui): pass global flow nodes to NodeConfig --- front-end/src/components/CustomNode/CustomNodeWidget.js | 1 + 1 file changed, 1 insertion(+) diff --git a/front-end/src/components/CustomNode/CustomNodeWidget.js b/front-end/src/components/CustomNode/CustomNodeWidget.js index f042588..c8997eb 100644 --- a/front-end/src/components/CustomNode/CustomNodeWidget.js +++ b/front-end/src/components/CustomNode/CustomNodeWidget.js @@ -65,6 +65,7 @@ export default class CustomNodeWidget extends React.Component {
{String.fromCharCode(this.icon)}
Date: Tue, 28 Apr 2020 23:39:39 -0400 Subject: [PATCH 03/15] feat(ui): handle flow variables in API update function --- front-end/src/API.js | 4 +++- front-end/src/components/CustomNode/CustomNodeWidget.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/front-end/src/API.js b/front-end/src/API.js index ef08f75..8fca9eb 100644 --- a/front-end/src/API.js +++ b/front-end/src/API.js @@ -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} - 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", diff --git a/front-end/src/components/CustomNode/CustomNodeWidget.js b/front-end/src/components/CustomNode/CustomNodeWidget.js index c8997eb..b5aa5ca 100644 --- a/front-end/src/components/CustomNode/CustomNodeWidget.js +++ b/front-end/src/components/CustomNode/CustomNodeWidget.js @@ -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(); From a0b59b7608e4339e76eeb77ec1315ea23801c9d5 Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Tue, 28 Apr 2020 23:40:30 -0400 Subject: [PATCH 04/15] feat(ui): maintain flow variables separately from normal options --- .../src/components/CustomNode/NodeConfig.js | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/front-end/src/components/CustomNode/NodeConfig.js b/front-end/src/components/CustomNode/NodeConfig.js index a12181a..92838f1 100644 --- a/front-end/src/components/CustomNode/NodeConfig.js +++ b/front-end/src/components/CustomNode/NodeConfig.js @@ -10,7 +10,8 @@ export default class NodeConfig extends React.Component { super(props); this.state = { disabled: false, - data: {} + data: {}, + flowData: {} }; this.updateData = this.updateData.bind(this); this.handleDelete = this.handleDelete.bind(this); @@ -19,15 +20,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 @@ -42,7 +54,12 @@ 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(); }; From 09d0f8b3d846694486b96d9219a4f25d2d5220af Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Tue, 28 Apr 2020 23:43:39 -0400 Subject: [PATCH 05/15] feat(ui): add component for selecting a flow variable override --- .../src/components/CustomNode/NodeConfig.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/front-end/src/components/CustomNode/NodeConfig.js b/front-end/src/components/CustomNode/NodeConfig.js index 92838f1..82b799c 100644 --- a/front-end/src/components/CustomNode/NodeConfig.js +++ b/front-end/src/components/CustomNode/NodeConfig.js @@ -242,5 +242,38 @@ function BooleanInput(props) { + + +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 ( + + + {props.checked ? + + + )} + + : null + } + ) } From 70f1f28355aa28a3b35b5239f3c018285725fb5b Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Tue, 28 Apr 2020 23:45:08 -0400 Subject: [PATCH 06/15] feat(ui): render flow node selector and handle selections --- .../src/components/CustomNode/NodeConfig.js | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/front-end/src/components/CustomNode/NodeConfig.js b/front-end/src/components/CustomNode/NodeConfig.js index 82b799c..6abd297 100644 --- a/front-end/src/components/CustomNode/NodeConfig.js +++ b/front-end/src/components/CustomNode/NodeConfig.js @@ -78,6 +78,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})}/> )} @@ -113,6 +116,19 @@ 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 = @@ -125,11 +141,24 @@ function OptionInput(props) { } else { return (<>) } + + const hideFlow = props.node.options.node_type === "flow_control" + || props.type === "file" || props.globals.length === 0 return ( - {props.label} -
{props.docstring}
- { inputComp } + {props.label} +
{props.docstring}
+ + { inputComp } + {hideFlow ? null : + + } +
) } From f58f8140c1d0ce6a669327576aec830a874b5b27 Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Tue, 28 Apr 2020 23:48:25 -0400 Subject: [PATCH 07/15] feat(ui): disable config option when flow node override selected --- .../src/components/CustomNode/NodeConfig.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/front-end/src/components/CustomNode/NodeConfig.js b/front-end/src/components/CustomNode/NodeConfig.js index 6abd297..888ef8b 100644 --- a/front-end/src/components/CustomNode/NodeConfig.js +++ b/front-end/src/components/CustomNode/NodeConfig.js @@ -131,13 +131,13 @@ function OptionInput(props) { let inputComp; if (props.type === "file") { - inputComp = + inputComp = } else if (props.type === "string") { - inputComp = + inputComp = } else if (props.type === "int") { - inputComp = + inputComp = } else if (props.type === "boolean") { - inputComp = + inputComp = } else { return (<>) } @@ -247,8 +247,9 @@ function SimpleInput(props) { return ( + disabled={props.disabled} + defaultValue={props.value} + onChange={handleChange} /> ) } @@ -269,8 +270,9 @@ function BooleanInput(props) { return ( + disabled={props.disabled} + checked={value} + onChange={handleChange} /> function FlowVariableOverride(props) { From bd5513006ff6f77cd85e610ff3ee5be1ca662422 Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Tue, 28 Apr 2020 23:49:15 -0400 Subject: [PATCH 08/15] refactor(ui): alter node config form styling --- front-end/src/components/CustomNode/NodeConfig.js | 8 +++++--- front-end/src/styles/NodeConfig.css | 7 +++++++ 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 front-end/src/styles/NodeConfig.css diff --git a/front-end/src/components/CustomNode/NodeConfig.js b/front-end/src/components/CustomNode/NodeConfig.js index 888ef8b..a698fde 100644 --- a/front-end/src/components/CustomNode/NodeConfig.js +++ b/front-end/src/components/CustomNode/NodeConfig.js @@ -1,8 +1,9 @@ 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 { @@ -67,7 +68,8 @@ export default class NodeConfig extends React.Component { if (!this.props.node) return null; return ( e.stopPropagation()}> + dialogClassName="NodeConfig" + onWheel={e => e.stopPropagation()}>
{this.props.node.options.name} Configuration diff --git a/front-end/src/styles/NodeConfig.css b/front-end/src/styles/NodeConfig.css new file mode 100644 index 0000000..9dcbda8 --- /dev/null +++ b/front-end/src/styles/NodeConfig.css @@ -0,0 +1,7 @@ +.option-docstring { + font-size: 0.7rem; +} + +.NodeConfig { + min-width: 50%; +} \ No newline at end of file From 210f3fcc4930d38dfe216e58cc0d896ac9789268 Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Tue, 28 Apr 2020 23:49:33 -0400 Subject: [PATCH 09/15] refactor(ui): alter workspace styling --- front-end/src/styles/Workspace.css | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/front-end/src/styles/Workspace.css b/front-end/src/styles/Workspace.css index a6ef99d..3be7a81 100644 --- a/front-end/src/styles/Workspace.css +++ b/front-end/src/styles/Workspace.css @@ -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 { @@ -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), From 5d7d63f24d8b9105caad13d84e1f2d0ca6dc867e Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Wed, 29 Apr 2020 21:11:30 -0400 Subject: [PATCH 10/15] fix: skip validation of options if being replaced with flow node --- pyworkflow/pyworkflow/node.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyworkflow/pyworkflow/node.py b/pyworkflow/pyworkflow/node.py index c59c521..70a3ebd 100644 --- a/pyworkflow/pyworkflow/node.py +++ b/pyworkflow/pyworkflow/node.py @@ -64,8 +64,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. From a8cf1f14d0b1b21315d907546f6eb1b05b0bf4fe Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Wed, 29 Apr 2020 21:12:02 -0400 Subject: [PATCH 11/15] fix: check flow nodes against correctly styled node type --- pyworkflow/pyworkflow/workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyworkflow/pyworkflow/workflow.py b/pyworkflow/pyworkflow/workflow.py index 43db599..fbc15ad 100644 --- a/pyworkflow/pyworkflow/workflow.py +++ b/pyworkflow/pyworkflow/workflow.py @@ -351,7 +351,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 From 65fea7a151e0065a4a15b7b3f1a849804506cb74 Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Thu, 30 Apr 2020 18:54:18 -0400 Subject: [PATCH 12/15] fix(ui): add missing parens/brackets --- front-end/src/components/CustomNode/NodeConfig.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/front-end/src/components/CustomNode/NodeConfig.js b/front-end/src/components/CustomNode/NodeConfig.js index a698fde..9b8b0eb 100644 --- a/front-end/src/components/CustomNode/NodeConfig.js +++ b/front-end/src/components/CustomNode/NodeConfig.js @@ -275,6 +275,8 @@ function BooleanInput(props) { disabled={props.disabled} checked={value} onChange={handleChange} /> + ) +} function FlowVariableOverride(props) { From 748e54f7e5306923b4c465b36e701ab77c85d86a Mon Sep 17 00:00:00 2001 From: Samir Reddigari Date: Thu, 30 Apr 2020 18:57:12 -0400 Subject: [PATCH 13/15] fix(ui): hide overrides only for global nodes --- front-end/src/components/CustomNode/NodeConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/src/components/CustomNode/NodeConfig.js b/front-end/src/components/CustomNode/NodeConfig.js index 9b8b0eb..1b1b1ab 100644 --- a/front-end/src/components/CustomNode/NodeConfig.js +++ b/front-end/src/components/CustomNode/NodeConfig.js @@ -144,7 +144,7 @@ function OptionInput(props) { return (<>) } - const hideFlow = props.node.options.node_type === "flow_control" + const hideFlow = props.node.options.is_global || props.type === "file" || props.globals.length === 0 return ( From bfacf204c44867dc40053cbc8121546439df58a2 Mon Sep 17 00:00:00 2001 From: Matt Thomas Date: Fri, 1 May 2020 16:57:18 -0400 Subject: [PATCH 14/15] fix: Constructs `file` input on-demand to avoid flow var filepath bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now, raw filenames/inputs are stored in the Node (removing the construction of a path on Node update - in `node/views.py`). When a file is needed, the path is constructed at that time (either Node execution or file download). --- pyworkflow/pyworkflow/node.py | 15 ++++++++++++--- pyworkflow/pyworkflow/workflow.py | 13 ++++++++++--- vp/node/views.py | 8 ++------ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/pyworkflow/pyworkflow/node.py b/pyworkflow/pyworkflow/node.py index 70a3ebd..b0fcdca 100644 --- a/pyworkflow/pyworkflow/node.py +++ b/pyworkflow/pyworkflow/node.py @@ -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: @@ -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 diff --git a/pyworkflow/pyworkflow/workflow.py b/pyworkflow/pyworkflow/workflow.py index fbc15ad..94cb232 100644 --- a/pyworkflow/pyworkflow/workflow.py +++ b/pyworkflow/pyworkflow/workflow.py @@ -302,7 +302,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) @@ -417,8 +417,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) diff --git a/vp/node/views.py b/vp/node/views.py index 740c85a..be5302a 100644 --- a/vp/node/views.py +++ b/vp/node/views.py @@ -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) From 8c081b54f7f24fcdcebb9f3a6fffc2db3dbb36d5 Mon Sep 17 00:00:00 2001 From: Matt Thomas Date: Fri, 1 May 2020 17:46:50 -0400 Subject: [PATCH 15/15] fix: Passes filename in response to use for download --- front-end/src/API.js | 4 +++- vp/workflow/views.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/front-end/src/API.js b/front-end/src/API.js index 8fca9eb..22a1b49 100644 --- a/front-end/src/API.js +++ b/front-end/src/API.js @@ -205,9 +205,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)); diff --git a/vp/workflow/views.py b/vp/workflow/views.py index 15769a1..98a2b04 100644 --- a/vp/workflow/views.py +++ b/vp/workflow/views.py @@ -293,6 +293,7 @@ def download_file(request): # Construct response response = HttpResponse(content_type=content) + response['Content-Disposition'] = os.path.basename(f.name) response.write(f.read()) # File not opened with `with`; need to close