Skip to content
48 changes: 48 additions & 0 deletions front-end/src/components/CustomNodeUpload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, {useRef, useState} from "react";
import * as API from "../API";
import {Button} from "react-bootstrap";


export default function CustomNodeUpload({ onUpload }) {

const input = useRef(null);
const [status, setStatus] = useState("ready");

const uploadFile = async file => {
setStatus("loading");
const fd = new FormData();
fd.append("file", file);
API.uploadDataFile(fd)
.then(resp => {
onUpload();
setStatus("ready");
}).catch(() => {
setStatus("failed");
});
input.current.value = null;
};
const onFileSelect = e => {
e.preventDefault();
if (!input.current.files) return;
uploadFile(input.current.files[0]);
};

let content;
if (status === "loading") {
content = <div>Uploading file...</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"}} />
<Button size="sm" onClick={() => input.current.click()}
variant="success"
disabled={status === "loading"}>
Add Custom Node
</Button>
{content}
</>
)
}
79 changes: 66 additions & 13 deletions front-end/src/components/NodeMenu.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import * as _ from 'lodash';
import { Col } from 'react-bootstrap';
import { Col, OverlayTrigger, Tooltip } from 'react-bootstrap';
import CustomNodeUpload from "./CustomNodeUpload";


export default function NodeMenu(props) {
Expand All @@ -18,28 +19,80 @@ export default function NodeMenu(props) {
const config = data.options;
delete data.options;
return (
<NodeMenuItem key={data.node_key} nodeInfo={data} config={config} />
<NodeMenuItem key={data.node_key || data.filename}
nodeInfo={data} config={config} />
)}
)}
</ul>
</div>
)}
<CustomNodeUpload onUpload={props.onUpload} />
</Col>
);
}


/**
* Format docstring with newlines into tooltip content
* @param string - node docstring
* @returns {array} - array of strings and HTML elements
*/
function formatTooltip(string) {
const split = string.split("\n");
const out = [];
split.forEach((line, i) => {
out.push(line);
out.push(<br key={i} />);
});
out.pop();
return out;
}


function NodeMenuItem(props) {
if (!props.nodeInfo.missing_packages) {
const tooltip = props.nodeInfo.doc ? formatTooltip(props.nodeInfo.doc) : "This node has no documentation."
return (
<OverlayTrigger
placement="right"
delay={{ show: 250, hide: 250 }}
overlay={<NodeTooltip message={tooltip} />}>
<li className="NodeMenuItem"
draggable={true}
onDragStart={event => {
event.dataTransfer.setData(
'storm-diagram-node',
JSON.stringify(props));
}}
style={{color: props.nodeInfo.color}}>
{props.nodeInfo.name}
</li>
</OverlayTrigger>
)
} else {
let tooltip = "These Python modules could not be imported:\n\n"
+ props.nodeInfo.missing_packages.join("\n");
tooltip = formatTooltip(tooltip);
return (
<OverlayTrigger
placement="right"
delay={{ show: 250, hide: 250 }}
overlay={<NodeTooltip message={tooltip} />}>
<li className="NodeMenuItem invalid">{props.nodeInfo.filename}</li>
</OverlayTrigger>
)
}
}


// Overlay with props has to use ref forwarding
const NodeTooltip = React.forwardRef((props, ref) => {
return (
<li className="NodeMenuItem"
draggable={true}
onDragStart={event => {
event.dataTransfer.setData(
'storm-diagram-node',
JSON.stringify(props));
}}
style={{ color: props.nodeInfo.color }}>
{props.nodeInfo.name}
</li>
<Tooltip {...props} ref={ref}>
<div style={{textAlign: "left"}}>
{props.message}
</div>
</Tooltip>
)
}
});

12 changes: 10 additions & 2 deletions front-end/src/components/Workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,25 @@ class Workspace extends React.Component {
this.engine.setModel(this.model);
this.engine.setMaxNumberPointsPerLink(0);
this.state = {nodes: []};
this.getAvailableNodes = this.getAvailableNodes.bind(this);
this.load = this.load.bind(this);
this.clear = this.clear.bind(this);
this.handleNodeCreation = this.handleNodeCreation.bind(this);
this.execute = this.execute.bind(this);
}

componentDidMount() {
this.getAvailableNodes();
API.initWorkflow(this.model).catch(err => console.log(err));
}

/**
* Retrieve available nodes from server to display in menu
*/
getAvailableNodes() {
API.getNodes()
.then(nodes => this.setState({nodes: nodes}))
.catch(err => console.log(err));
API.initWorkflow(this.model).catch(err => console.log(err));
}

/**
Expand Down Expand Up @@ -107,7 +115,7 @@ class Workspace extends React.Component {
</Col>
</Row>
<Row className="Workspace">
<NodeMenu nodes={this.state.nodes} />
<NodeMenu nodes={this.state.nodes} onUpload={this.getAvailableNodes}/>
<Col xs={10}>
<div style={{position: 'relative', flexGrow: 1}}
onDrop={event => this.handleNodeCreation(event)}
Expand Down
14 changes: 11 additions & 3 deletions front-end/src/styles/Workspace.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,22 @@
linear-gradient(to bottom, rgba(54, 169, 231, 0.1) 1px, transparent 1px);
}
.NodeMenuItem {
cursor: pointer;
background-color: white;
border-radius: 5px;
margin-bottom: 3px;
box-shadow: 0px 2px 4px gray;
}
.NodeMenuItem[draggable] {
cursor: pointer;
}
.NodeMenuItem::before {
.NodeMenuItem[draggable]::before {
content: "+";
padding-right: 10px;
padding-left: 5px;
color: black;
}
.NodeMenuItem.invalid::before {
content: "\26a0";
padding-right: 10px;
padding-left: 5px;
color: red;
}