Commit 10877b23 authored by Patrik Meijer's avatar Patrik Meijer
Browse files

WIP Add GraphEditor pieces

parent 72e11f65
......@@ -18,6 +18,7 @@ import DemoBasicContainmentCanvas from '../src/components/BasicContainmentCanvas
import DemoSingleConnectedNode from '../src/components/SingleConnectedNode/demo';
import DemoInfoCard from '../src/components/InfoCard/demo';
import DemoPartBrowser from '../src/components/PartBrowser/demo';
import DemoGraphEditor from '../src/components/GraphEditor/demo';
class demoApp extends Component {
......@@ -72,6 +73,10 @@ class demoApp extends Component {
component: <DemoPartBrowser/>,
title: 'PartBrowser',
},
{
component: <DemoGraphEditor/>,
title: 'GraphEditor',
},
];
return (
......
......@@ -177,13 +177,34 @@
return {
getId: () => id,
getParentId: () => {
const arr = id.split('/');
arr.pop();
return arr.join('/');
},
getGuid: () => 'a5008758-e9e8-7eb1-e995-e1793ef92a37',
getValidAttributeNames: () => attrNames,
getAttributeNames: () => attrNames,
getValidPointerNames: () => {
if (id.indexOf('/0') !== -1)
return ['src', 'dst'];
return [];
},
getPointerNames: () => {
return ['ptr', 'base'];
},
getValidSetNames: () => {
return ['set1', 'set2'];
},
getSetNames: () => {
return ['set1', 'set2'];
},
getMemberIds: () => {
return childrenIds;
},
getRegistryNames: () => {
return ['SVGIcon', 'position'];
},
getPointerId: (name) => {
return childrenIds[cnt % 5];
},
......@@ -693,4 +714,4 @@
somethingFinishedLoading();
}
}());
\ No newline at end of file
}());
......@@ -2450,6 +2450,37 @@
"integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
"dev": true
},
"cytoscape": {
"version": "3.2.12",
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.2.12.tgz",
"integrity": "sha512-epagPckuFpLO2hiKgKMXmCFBFM2K7XbJhYDQj8fH3riiEH+yFXpLYA30jV18TEbVcsq2c58ETOGnHE2YzSBr/Q==",
"requires": {
"heap": "^0.2.6",
"lodash.debounce": "^4.0.8"
}
},
"cytoscape-cose-bilkent": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.0.0.tgz",
"integrity": "sha1-Xwr75cVaegJkQrdWyjiM53wmKWc="
},
"cytoscape-dagre": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.2.2.tgz",
"integrity": "sha512-zsg36qNwua/L2stJSWkcbSDcvW3E6VZf6KRe6aLnQJxuXuz89tMqI5EVYVKEcNBgzTEzFMFv0PE3T0nD4m6VDw==",
"requires": {
"dagre": "^0.8.2"
}
},
"dagre": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.4.tgz",
"integrity": "sha512-Dj0csFDrWYKdavwROb9FccHfTC4fJbyF/oJdL9LNZJ8WUvl968P6PAKEriGqfbdArVJEmmfA+UyumgWEwcHU6A==",
"requires": {
"graphlib": "^2.1.7",
"lodash": "^4.17.4"
}
},
"damerau-levenshtein": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz",
......@@ -4203,6 +4234,14 @@
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
"dev": true
},
"graphlib": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.7.tgz",
"integrity": "sha512-TyI9jIy2J4j0qgPmOOrHTCtpPqJGN/aurBwc6ZT+bRii+di1I+Wv3obRhVrmBEXet+qkMaEX67dXrwsd3QQM6w==",
"requires": {
"lodash": "^4.17.5"
}
},
"handle-thing": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz",
......@@ -4282,6 +4321,11 @@
"minimalistic-assert": "^1.0.1"
}
},
"heap": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/heap/-/heap-0.2.6.tgz",
"integrity": "sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw="
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
......@@ -5187,8 +5231,7 @@
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
"dev": true
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
},
"lodash.isarguments": {
"version": "3.1.0",
......
/* globals document */
/**
* DISTRIBUTION STATEMENT C: U.S. Government agencies and their contractors.
* Other requests shall be referred to DARPA’s Public Release Center via email at prc@darpa.mil.
* @author pmeijer / https://github.com/pmeijer
*/
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
export default class ContextMenu extends Component {
static propTypes = {
gmeClient: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
nodeId: PropTypes.string.isRequired, // FIXME: should be nodeIds
eventX: PropTypes.number.isRequired,
eventY: PropTypes.number.isRequired,
onClose: PropTypes.func.isRequired,
setActiveNode: PropTypes.func.isRequired,
readOnly: PropTypes.bool,
};
static defaultProps = {
readOnly: false,
}
setActiveNode = () => {
const {nodeId, onClose, setActiveNode} = this.props;
setActiveNode(nodeId);
onClose();
}
deleteNode = () => {
const {gmeClient, nodeId, onClose} = this.props;
gmeClient.deleteNode(nodeId);
onClose();
}
render() {
const {
gmeClient,
nodeId,
eventX,
eventY,
readOnly,
data,
} = this.props;
// console.log(data);
const menuItems = [];
const nodeObj = gmeClient.getNode(nodeId);
if (false) {
const metaNodeObj = gmeClient.getNode(nodeObj.getMetaTypeId());
menuItems.push((
<MenuItem key="meta-type" onClick={this.props.onClose} disabled={readOnly}>
{`<<${metaNodeObj.getAttribute('name')}>>`}
</MenuItem>));
if (metaNodeObj.getAttribute('name') === 'System') {
menuItems.push((
<MenuItem key="open-sub-system" onClick={this.setActiveNode} >
Open Subsystem ...
</MenuItem>));
}
} else if (data.memberAttrs && data.memberAttrs.length > 0) {
let args;
data.memberAttrs.forEach((memAttr) => {
if (memAttr.name === 'args') {
args = memAttr.value ? `(${memAttr.value})` : '()';
}
});
if (args) {
menuItems.push((
<MenuItem key="invocation-args" onClick={this.props.onClose}>
Invocation arguments: {args}
</MenuItem>));
}
} else if (data.pointerName === 'post' || data.pointerName === 'pre') {
const methodNode = gmeClient.getNode(data.source);
const modeNode = gmeClient.getNode(data.target);
if (methodNode && modeNode) {
// const parentName = gmeClient.getNode(modeNode.getParentId()).getAttribute('name');
// const modeName = modeNode.getAttribute('name');
const methodName = methodNode.getAttribute('name');
menuItems.push((
<MenuItem key="pre-post-condition" onClick={this.props.onClose}>
{`${data.pointerName}-condition for ${methodName}`}
</MenuItem>));
}
} else if (data.setName === 'network-nodes') {
menuItems.push((
<MenuItem key="cell-network-nodes" onClick={this.props.onClose}>
{`${data.setName}`}
</MenuItem>));
}
if (menuItems.length === 0) {
this.props.onClose();
return <div/>;
}
return (
<Menu
id="simple-menu"
anchorEl={document.body}
style={{
position: 'absolute',
top: eventY - (document.body.clientHeight / 2),
left: eventX,
}}
open
onClose={this.props.onClose}
>
{menuItems}
</Menu>
);
}
}
/**
* DISTRIBUTION STATEMENT C: U.S. Government agencies and their contractors.
* Other requests shall be referred to DARPA’s Public Release Center via email at prc@darpa.mil.
* @author pmeijer / https://github.com/pmeijer
*/
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import Divider from '@material-ui/core/Divider';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import Switch from '@material-ui/core/Switch';
import Typography from '@material-ui/core/Typography';
import TrendingFlatIcon from '@material-ui/icons/TrendingFlat';
import CallSplitIcon from '@material-ui/icons/CallSplit';
import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord';
import RemoveIcon from '@material-ui/icons/Remove';
import MoreHorizIcon from '@material-ui/icons/MoreHoriz';
export default class FilterSelector extends Component {
static propTypes = {
validItems: PropTypes.object.isRequired,
activeItems: PropTypes.object.isRequired,
handleToggle: PropTypes.func.isRequired,
open: PropTypes.bool,
onClose: PropTypes.func.isRequired,
};
static defaultProps = {
open: true,
}
render() {
const {validItems, activeItems} = this.props;
const lists = [];
Object.keys(validItems).forEach((groupName) => {
const items = [];
validItems[groupName].forEach((item) => {
if (item.hidden) {
return;
}
const itemId = `${groupName}$${item.name}`;
let icon;
switch (groupName) {
case 'pointers':
icon = <TrendingFlatIcon style={{color: 'rgb(85, 123, 139)'}}/>;
break;
case 'sets':
icon = (item.name === 'network-nodes') ?
<MoreHorizIcon/> :
<CallSplitIcon className="rotate-90"/>;
break;
case 'nodes':
icon = item.isConnection ? <RemoveIcon/> : <FiberManualRecordIcon/>;
break;
default:
icon = <FiberManualRecordIcon/>;
break;
}
items.push((
<ListItem key={itemId}>
<ListItemIcon>
{icon}
</ListItemIcon>
<ListItemText primary={item.name} style={{marginRight: 24}}/>
<ListItemSecondaryAction>
<Switch
onChange={() => {
this.props.handleToggle(itemId);
}}
checked={activeItems[itemId]}
/>
</ListItemSecondaryAction>
</ListItem>));
});
if (items.length > 0) {
lists.push((
<div key={groupName} >
<Typography variant="subheading">
{groupName.toUpperCase()}
</Typography>
<List>
{items}
</List>
<Divider/>
</div>
));
}
});
return (
<Dialog open={this.props.open} onClose={this.props.onClose}>
<DialogTitle id="filter-dialog-title">Apply Filters</DialogTitle>
<DialogContent>
{lists}
</DialogContent>
</Dialog>
);
}
}
This diff is collapsed.
/**
* DISTRIBUTION STATEMENT C: U.S. Government agencies and their contractors.
* Other requests shall be referred to DARPA’s Public Release Center via email at prc@darpa.mil.
* React wrapper around cytoscape. If scalability ever becomes and issue - consider implementing a fine-grained
* element update.
* @author pmeijer / https://github.com/pmeijer
*/
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import cytoscape from 'cytoscape';
import coseBilkent from 'cytoscape-cose-bilkent';
import dagre from 'cytoscape-dagre';
cytoscape.use(coseBilkent);
// cytoscape.use(cycola);
cytoscape.use(dagre);
export default class ReactCytoscape extends Component {
static propTypes = {
cyRef: PropTypes.func.isRequired,
elements: PropTypes.object.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
containerID: PropTypes.string,
layout: PropTypes.object,
style: PropTypes.arrayOf(PropTypes.object),
cytoscapeOptions: PropTypes.object,
};
static defaultProps = {
containerID: 'cy',
layout: {name: 'cola'},
cytoscapeOptions: {},
style: [
{
selector: 'node',
css: {
content: function elemRender(ele) {
return ele.data('label') || ele.data('id');
},
'text-valign': 'center',
'text-halign': 'center',
},
},
{
selector: '$node > node',
css: {
'padding-top': '10px',
'padding-left': '10px',
'padding-bottom': '10px',
'padding-right': '10px',
'text-valign': 'top',
'text-halign': 'center',
'background-color': '#bbb',
},
},
{
selector: ':selected',
css: {
'background-color': 'black',
'line-color': 'black',
'target-arrow-color': 'black',
'source-arrow-color': 'black',
},
},
],
};
componentDidMount() {
const opts = Object.assign({
container: this.container,
boxSelectionEnabled: false,
autounselectify: true,
style: this.props.style,
layout: this.props.layout,
}, this.props.cytoscapeOptions);
this.cy = cytoscape(opts);
// this.cy.layout({name: 'cose-bilkent', coseBilkentOptions});
this.cy.json({elements: this.props.elements});
if (this.props.cyRef) {
this.props.cyRef(this.cy, this.container);
}
return this.cy;
}
componentWillReceiveProps(nextProps) {
const {
elements,
width,
height,
style,
} = this.props;
// TODO: Consider making more fine-grained updates here instead.
if (JSON.stringify(elements) !== JSON.stringify(nextProps.elements)) {
this.cy.json({elements: nextProps.elements});
}
if (width !== nextProps.width || height !== nextProps.height) {
this.cy.resize();
}
if (JSON.stringify(style) !== JSON.stringify(nextProps.style)) {
this.cy.style(nextProps.style);
}
if (elements.nodes && elements.nodes.length === 0 &&
nextProps.elements.nodes.length > 0) {
// Initial elements received -> fit graph to canvas.
this.cy.fit();
}
}
componentWillUnmount() {
if (this.cy) {
this.cy.destroy();
}
}
render() {
const styleContainer = Object.assign({
height: '100%',
width: '100%',
display: 'block',
}, ...this.props.style);
return (
<div
className="graph"
id={this.props.containerID}
ref={(elt) => {
this.container = elt;
}}
style={styleContainer}
/>);
}
}
/* globals window */
import React, {Component} from 'react';
import GraphEditor from './index';
import SubTreeWatcher from '../SubTreeWatcher';
const EXTRA_OPTIONS = {
};
export default class DemoGraphEditor extends Component {
constructor(props) {
super(props);
this.gmeClient = new window.GME.classes.Client(window.GME.gmeConfig);
}
render() {
return (
<div style={{width: '90%'}}>
<SubTreeWatcher gmeClient={this.gmeClient} activeNode="/2/3/4" options={EXTRA_OPTIONS}>
<GraphEditor
gmeClient={this.gmeClient}
activeSelection={[]}
// activeNode is passed on from SubTreeWatcher
// nodes is passed on from SubTreeWatcher
readOnly={false}
isActivePanel
setActiveNode={nodeId => console.log('setActiveNode:', nodeId)}
setActiveSelection={nodeIds => console.log('setActiveSelection:', nodeIds)}
width={800} // FIXME: Having to pass these are not that nice..
height={600}
/>
</SubTreeWatcher>
</div>
);
}
}
export {default} from './GraphEditor';
/**
* This component will create a territory of depth 10 (by default)
* and which ever react component is a child of the SubTreeWatcher will receive
* an object "nodes" as property that contains information of all nodes in the subtree
* starting from the activeNode. The "nodes" object is a map from path to info where info
* is of the format:
* {
* parent: nodeObj.getParentId(),
* metaType: getMetaTypeName(nodeObj),
* attributes: {
* name: 'FCO'
* },
* registries: {
* position: {x: 100, y: 100}
* },
* pointers: {