diff --git a/front-end/src/API.js b/front-end/src/API.js index 04f8043..3219c18 100644 --- a/front-end/src/API.js +++ b/front-end/src/API.js @@ -148,3 +148,17 @@ export async function addEdge(link) { export async function deleteEdge(link) { return handleEdge(link, "DELETE"); } + + +/** + * Upload a data file to be stored on the server + * @param {FormData} formData - FormData with file and nodeId + * @returns {Promise} - server response + */ +export async function uploadDataFile(formData) { + const options = { + method: "POST", + body: formData + }; + return fetchWrapper("/workflow/upload", options); +} diff --git a/front-end/src/components/CustomNode/NodeConfig.js b/front-end/src/components/CustomNode/NodeConfig.js index babb73b..de521d2 100644 --- a/front-end/src/components/CustomNode/NodeConfig.js +++ b/front-end/src/components/CustomNode/NodeConfig.js @@ -1,11 +1,13 @@ -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import { Modal, Button, Form } from 'react-bootstrap'; import propTypes from 'prop-types'; import * as _ from 'lodash'; +import * as API from "../../API"; -function NodeConfig(props) { +export default function NodeConfig(props) { const form = useRef(); + const [disabled, setDisabled] = useState(false); // confirm, fire delete callback, close modal const handleDelete = () => { @@ -34,7 +36,9 @@ function NodeConfig(props) { { _.map(props.node.configParams, (info, key) => + node={props.node} + value={props.node.config[key]} + disableFunc={setDisabled}/> )} Node Description @@ -44,7 +48,7 @@ function NodeConfig(props) { - + @@ -59,16 +63,23 @@ NodeConfig.propTypes = { toggleShow: propTypes.func, onDelete: propTypes.func, onSubmit: propTypes.func -} +}; function OptionInput(props) { + let inputComp; if (props.type === "file") { - inputComp = ; + inputComp = } else if (props.type === "string") { inputComp = ; + } else if (props.type === "integer") { + inputComp = ; } else { return (<>) } @@ -81,4 +92,58 @@ function OptionInput(props) { ) } -export default NodeConfig; + +function FileUpload(props) { + + const input = useRef(null); + const [fileName, setFileName] = useState(props.value || ""); + const [status, setStatus] = useState(props.value ? "ready" : "unconfigured"); + + const uploadFile = async file => { + props.disableFunc(true); + setStatus("loading"); + const fd = new FormData(); + fd.append("file", file); + fd.append("nodeId", props.node.options.id); + API.uploadDataFile(fd) + .then(resp => { + setFileName(resp.filename); + setStatus("ready"); + props.disableFunc(false); + setStatus("ready"); + }).catch(() => { + setStatus("failed"); + }); + input.current.value = null; + }; + const onFileSelect = e => { + e.preventDefault(); + if (!input.current.files) return; + uploadFile(input.current.files[0]); + }; + + if (status === "loading") return (
Uploading file...
); + const btnText = status === "ready" ? "Choose Different File" : "Choose File"; + let content; + if (status === "ready") { + const rxp = new RegExp(props.node.options.id + '-'); + content = ( +
+ File loaded:  + {fileName.replace(rxp, '')} +
+ ) + } else if (status === "failed") { + content = (
Upload failed. Try a new file.
); + } + return ( + <> + + + + {content} + + ) +} + diff --git a/front-end/src/components/NodeMenu.js b/front-end/src/components/NodeMenu.js index 52c0edb..b84d9c0 100644 --- a/front-end/src/components/NodeMenu.js +++ b/front-end/src/components/NodeMenu.js @@ -14,10 +14,11 @@ export default function NodeMenu(props) { {section}
    { _.map(items, item => { - const config = item.options; - delete item.options; + const data = {...item}; // copy so we can mutate + const config = data.options; + delete data.options; return ( - + )} )}
diff --git a/pyworkflow/pyworkflow/node.py b/pyworkflow/pyworkflow/node.py index 02d36f6..82fd5bc 100644 --- a/pyworkflow/pyworkflow/node.py +++ b/pyworkflow/pyworkflow/node.py @@ -120,7 +120,7 @@ class ReadCsvNode(IONode): 'filepath_or_buffer': { "type": "file", "name": "File", - "desc": "File to read" + "desc": "CSV File" }, 'sep': { "type": "string", @@ -142,7 +142,9 @@ def execute(self, predecessor_data, flow_vars): # TODO: FileStorage implemented in Django to store in /tmp # Better filename/path handling should be implemented. NodeUtils.replace_flow_vars(self.options, flow_vars) - df = pd.read_csv(**self.options) + kwargs = self.options.copy() # won't copy nested dicts though + del kwargs["description"] + df = pd.read_csv(**kwargs) return df.to_json() except Exception as e: raise NodeException('read csv', str(e)) diff --git a/vp/node/views.py b/vp/node/views.py index 2f7dc48..20baa4f 100644 --- a/vp/node/views.py +++ b/vp/node/views.py @@ -256,3 +256,23 @@ def retrieve_data(request, node_id): return JsonResponse(data, safe=False, status=200) except WorkflowException as e: return JsonResponse({e.action: e.reason}, status=500) + + +def create_node(payload): + """Pass all request info to Node Factory. + + """ + json_data = json.loads(payload) + # 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": + opt_value = json_data["options"][field] + if opt_value is not None: + json_data["options"][field] = fs.path(opt_value) + + try: + return node_factory(json_data) + except OSError as e: + return JsonResponse({'message': e.strerror}, status=404) + except NodeException as e: + return JsonResponse({e.action: e.reason}, status=400) diff --git a/vp/workflow/views.py b/vp/workflow/views.py index b4118fe..4d3ed58 100644 --- a/vp/workflow/views.py +++ b/vp/workflow/views.py @@ -211,6 +211,7 @@ def retrieve_csv(request, node_id): return response + @swagger_auto_schema(method='post', operation_summary='Uploads a file to server.', operation_description='Uploads a new file to server location.', @@ -222,9 +223,8 @@ def retrieve_csv(request, node_id): def upload_file(request): if 'file' not in request.data: return JsonResponse("Empty content", status=404) - f = request.data['file'] - - fs.save(f.name, f) - - return JsonResponse("File Uploaded", status=201, safe=False) \ No newline at end of file + node_id = request.data.get('nodeId', '') + save_name = f"{node_id}-{f.name}" + fs.save(save_name, f) + return JsonResponse({"filename": save_name}, status=201, safe=False) \ No newline at end of file