Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add/edit/delete global flow variables in UI #76

Merged
merged 13 commits into from
Apr 30, 2020
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
15 changes: 13 additions & 2 deletions front-end/src/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export async function deleteNode(node) {
const options = {
method: "DELETE"
};
return fetchWrapper(`/node/${id}`, options);
const endpoint = node.options.is_global ? "node/global" : "node";
return fetchWrapper(`/${endpoint}/${id}`, options);
}


Expand All @@ -68,7 +69,8 @@ export async function updateNode(node, config) {
method: "POST",
body: JSON.stringify(payload)
};
return fetchWrapper(`/node/${node.options.id}`, options)
const endpoint = node.options.is_global ? "node/global" : "node";
return fetchWrapper(`/${endpoint}/${node.options.id}`, options)
}


Expand Down Expand Up @@ -99,6 +101,15 @@ export async function getNodes() {
}


/**
* Get global flow variables for workflow
* @returns {Promise<Object>} - server response (global flow variables)
*/
export async function getGlobalVars() {
return fetchWrapper("/workflow/globals");
}


/**
* Start a new workflow on the server
* @param {DiagramModel} model - Diagram model
Expand Down
8 changes: 5 additions & 3 deletions front-end/src/components/CustomNode/NodeConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default class NodeConfig extends React.Component {
};

render() {
if (!this.props.node) return null;
return (
<Modal show={this.props.show} onHide={this.props.toggleShow} centered
onWheel={e => e.stopPropagation()}>
Expand Down Expand Up @@ -190,12 +191,13 @@ function SimpleInput(props) {
setValue(event.target.value);
};

const {keyName, onChange} = props;
const {keyName, onChange, type} = props;
// whenever value changes, fire callback to update config form
useEffect(() => {
onChange(keyName, value);
const formValue = type === "number" ? Number(value) : value;
onChange(keyName, formValue);
},
[value, keyName, onChange]);
[value, keyName, onChange, type]);

return (
<Form.Control type={props.type} name={props.keyName}
Expand Down
119 changes: 119 additions & 0 deletions front-end/src/components/GlobalFlowMenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { useState } from 'react';
import { Dropdown, ButtonGroup, Table } from 'react-bootstrap';
import CustomNodeModel from './CustomNode/CustomNodeModel';
import NodeConfig from './CustomNode/NodeConfig';
import * as API from '../API';


export default function GlobalFlowMenu(props) {
const [show, setShow] = useState(false);
const [activeNode, setActiveNode] = useState();
const [creating, setCreating] = useState(false);
const toggleShow = () => setShow(!show);

// Create CustomNodeModel from JSON data,
// whether from menu item or global flow variable
function nodeFromData(data) {
const info = {...data, is_global: true};
const config = info.options;
delete info.options;
if (!info.option_types) {
info.option_types = lookupOptionTypes(info.node_key);
}
const node = new CustomNodeModel(info, config);
return node;
}

// Look up option types from appropriate menu item.
// The option types aren't included in the global flow
// serialization from the server.
function lookupOptionTypes(nodeKey) {
const keyMatches = props.menuItems.filter(d => d.node_key === nodeKey);
if (!keyMatches.length) return {};
return keyMatches[0].option_types || {};
}

const handleEdit = (data, create = false) => {
setCreating(create);
const node = nodeFromData(data);
setActiveNode(node);
setShow(true);
};

const handleSubmit = (data) => {
const node = activeNode;
if (creating) {
node.config = data;
API.addNode(node)
.then(() => props.onUpdate())
.catch(err => console.log(err));
} else {
API.updateNode(node, data)
.then(() => props.onUpdate())
.catch(err => console.log(err));
}
};

const handleDelete = (data) => {
const msg = "Are you sure you want to delete the global flow variable?";
if (window.confirm(msg)) {
const node = nodeFromData(data)
API.deleteNode(node)
.then(() => props.onUpdate())
.catch(err => console.log(err));
}
};

return (
<div className="GlobalFlowMenu">
<h3>Flow Variables</h3>
<Table size="sm">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Value</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{props.nodes.map(node =>
<tr key={node.id}>
<td>{node.options.var_name}</td>
<td>{node.name}</td>
<td className="text-primary">{node.options.default_value}</td>
<td className="edit-global" title="Edit Variable"
onClick={() => handleEdit(node, false)}>
&#x270E;
</td>
<td className="delete-global" title="Delete Variable"
onClick={() => handleDelete(node)}>
x
</td>
</tr>
)}

</tbody>
</Table>
<Dropdown as={ButtonGroup}>
<Dropdown.Toggle split variant="success" size="sm" id="dropdown-split-basic">
Add Global Flow Variable&nbsp;
</Dropdown.Toggle>
<Dropdown.Menu>
{props.menuItems.map((node, i) =>
<Dropdown.Item key={node.key || i}
onClick={() => handleEdit(node, true)}>
{node.name}
</Dropdown.Item>
)}
</Dropdown.Menu>
</Dropdown>
<NodeConfig node={activeNode}
show={show} toggleShow={toggleShow}
onSubmit={handleSubmit} />
</div>
)
}


9 changes: 5 additions & 4 deletions front-end/src/components/NodeMenu.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import React from 'react';
import * as _ from 'lodash';
import { Col, OverlayTrigger, Tooltip } from 'react-bootstrap';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import CustomNodeUpload from "./CustomNodeUpload";


export default function NodeMenu(props) {
// construct menu from JSON of node types
return (
<Col xs={2} className="node-menu">
<div className="NodeMenu">
<h3>Node Menu</h3>
<div>Drag-and-drop nodes to build a workflow.</div>
<hr />
{_.map(props.nodes, (items, section) =>
<div key={`node-menu-${section}`}>
<b>{section}</b>
<span className="node-section-title">{section}</span>
<ul>
{ _.map(items, item => {
const data = {...item}; // copy so we can mutate
Expand All @@ -27,7 +28,7 @@ export default function NodeMenu(props) {
</div>
)}
<CustomNodeUpload onUpload={props.onUpload} />
</Col>
</div>
);
}

Expand Down
33 changes: 27 additions & 6 deletions front-end/src/components/Workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import VPPortFactory from './VPPort/VPPortFactory';
import * as API from '../API';
import NodeMenu from './NodeMenu';
import '../styles/Workspace.css';
import GlobalFlowMenu from "./GlobalFlowMenu";

class Workspace extends React.Component {

Expand All @@ -21,17 +22,22 @@ class Workspace extends React.Component {
this.model = new DiagramModel();
this.engine.setModel(this.model);
this.engine.setMaxNumberPointsPerLink(0);
this.state = {nodes: []};
this.state = {nodes: [], globals: []};
this.getAvailableNodes = this.getAvailableNodes.bind(this);
this.getGlobalVars = this.getGlobalVars.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));
API.initWorkflow(this.model)
.then(() => {
this.getAvailableNodes();
this.getGlobalVars();
})
.catch(err => console.log(err));
}

/**
Expand All @@ -43,6 +49,12 @@ class Workspace extends React.Component {
.catch(err => console.log(err));
}

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

/**
* Load diagram JSON and render
* @param diagramData: serialized diagram JSON
Expand All @@ -51,6 +63,7 @@ class Workspace extends React.Component {
this.model.deserializeModel(diagramData, this.engine);
// redraw is buggy if you don't wait a little bit
setTimeout(() => this.engine.repaintCanvas(), 100);
this.getGlobalVars();
}

/**
Expand All @@ -59,7 +72,9 @@ class Workspace extends React.Component {
clear() {
if (window.confirm("Clear diagram? You will lose all work.")) {
this.model.getNodes().forEach(n => n.remove());
API.initWorkflow(this.model).catch(err => console.log(err));
API.initWorkflow(this.model)
.then(() => this.getGlobalVars())
.catch(err => console.log(err));
this.engine.repaintCanvas();
}
}
Expand Down Expand Up @@ -115,8 +130,14 @@ class Workspace extends React.Component {
</Col>
</Row>
<Row className="Workspace">
<NodeMenu nodes={this.state.nodes} onUpload={this.getAvailableNodes}/>
<Col xs={10}>
<Col xs={3}>
<GlobalFlowMenu menuItems={this.state.nodes["Flow Control"] || []}
nodes={this.state.globals}
onUpdate={this.getGlobalVars}
diagramModel={this.model}/>
<NodeMenu nodes={this.state.nodes} onUpload={this.getAvailableNodes}/>
</Col>
<Col xs={9} style={{paddingLeft: 0}}>
<div style={{position: 'relative', flexGrow: 1}}
onDrop={event => this.handleNodeCreation(event)}
onDragOver={event => event.preventDefault() }>
Expand Down
22 changes: 18 additions & 4 deletions front-end/src/styles/Workspace.css
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
.Workspace {
border: 1px solid grey;
}
.node-menu {
background-color: #ccc;
.NodeMenu, .GlobalFlowMenu {
padding: 1rem;
margin: 0.5rem 0;
box-shadow: 0px 3px 6px grey;
}
.node-menu ul {
.NodeMenu ul {
list-style: none;
padding-left: 0px;
}
.node-section-title {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
.GlobalFlowMenu {
font-size: 0.8rem;
}
.edit-global, .delete-global {
cursor: pointer;
font-size: 1rem;
}
.diagram-canvas {
height: 75vh;
background-size: 20px 20px;
Expand All @@ -17,7 +31,7 @@
}
.NodeMenuItem {
background-color: white;
margin-bottom: 3px;
margin-bottom: 4px;
box-shadow: 0px 2px 4px gray;
}
.NodeMenuItem[draggable] {
Expand Down
2 changes: 2 additions & 0 deletions pyworkflow/pyworkflow/node_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def node_factory(node_info):
def flow_node(node_key, node_info):
if node_key == 'StringNode':
return StringNode(node_info)
elif node_key == 'IntegerNode':
return IntegerNode(node_info)
else:
return None

Expand Down
1 change: 1 addition & 0 deletions pyworkflow/pyworkflow/nodes/flow_control/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .string_input import StringNode
from .integer_input import IntegerNode
25 changes: 25 additions & 0 deletions pyworkflow/pyworkflow/nodes/flow_control/integer_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pyworkflow.node import FlowNode, NodeException
from pyworkflow.parameters import *


class IntegerNode(FlowNode):
"""StringNode object

Allows for Strings to replace 'string' fields in Nodes
"""
name = "Integer Input"
num_in = 1
num_out = 1
color = 'purple'

OPTIONS = {
"default_value": IntegerParameter(
"Default Value",
docstring="Value this node will pass as a flow variable"
),
"var_name": StringParameter(
"Variable Name",
default="my_var",
docstring="Name of the variable to use in another Node"
)
}