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
14 changes: 14 additions & 0 deletions front-end/src/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object>} - server response
*/
export async function uploadDataFile(formData) {
const options = {
method: "POST",
body: formData
};
return fetchWrapper("/workflow/upload", options);
}
79 changes: 72 additions & 7 deletions front-end/src/components/CustomNode/NodeConfig.js
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand Down Expand Up @@ -34,7 +36,9 @@ function NodeConfig(props) {
<Modal.Body>
{ _.map(props.node.configParams, (info, key) =>
<OptionInput key={key} {...info} keyName={key}
value={props.node.config[key]} />
node={props.node}
value={props.node.config[key]}
disableFunc={setDisabled}/>
)}
<Form.Group>
<Form.Label>Node Description</Form.Label>
Expand All @@ -44,7 +48,7 @@ function NodeConfig(props) {
</Form.Group>
</Modal.Body>
<Modal.Footer>
<Button variant="success" type="submit">Save</Button>
<Button variant="success" disabled={disabled} type="submit">Save</Button>
<Button variant="secondary" onClick={props.toggleShow}>Cancel</Button>
<Button variant="danger" onClick={handleDelete}>Delete</Button>
</Modal.Footer>
Expand All @@ -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 = <input type="file" name={props.keyName} />;
inputComp = <FileUpload disableFunc={props.disableFunc}
node={props.node}
name={props.keyName}
value={props.value} />
} else if (props.type === "string") {
inputComp = <Form.Control type="text" name={props.keyName}
defaultValue={props.value} />;
} else if (props.type === "integer") {
inputComp = <Form.Control type="number" name={props.keyName}
defaultValue={props.value} />;
} else {
return (<></>)
}
Expand All @@ -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 (<div>Uploading file...</div>);
const btnText = status === "ready" ? "Choose Different File" : "Choose File";
let content;
if (status === "ready") {
const rxp = new RegExp(props.node.options.id + '-');
content = (
<div>
<b style={{color: 'green'}}>File loaded:</b>&nbsp;
{fileName.replace(rxp, '')}
</div>
)
} else if (status === "failed") {
content = (<div>Upload failed. Try a new file.</div>);
}
return (
<>
<input type="file" ref={input} onChange={onFileSelect}
style={{display: "none"}} />
<input type="hidden" name={props.name} value={fileName} />
<Button size="sm" onClick={() => input.current.click()}>{btnText}</Button>
{content}
</>
)
}

7 changes: 4 additions & 3 deletions front-end/src/components/NodeMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ export default function NodeMenu(props) {
<b>{section}</b>
<ul>
{ _.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 (
<NodeMenuItem key={item.node_key} nodeInfo={item} config={config} />
<NodeMenuItem key={data.node_key} nodeInfo={data} config={config} />
)}
)}
</ul>
Expand Down
6 changes: 4 additions & 2 deletions pyworkflow/pyworkflow/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class ReadCsvNode(IONode):
'filepath_or_buffer': {
"type": "file",
"name": "File",
"desc": "File to read"
"desc": "CSV File"
},
'sep': {
"type": "string",
Expand All @@ -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))
Expand Down
20 changes: 20 additions & 0 deletions vp/node/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
10 changes: 5 additions & 5 deletions vp/workflow/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand All @@ -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)
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)