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];
},
......
......@@ -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>
);
}
}
/**
* 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.
*
* This component receives the data from the gme nodes and builds up the input data for cytoscape.
* It is relatively domain-agnostic, mainly the styles are tied with DCRYPPS (apart from what is passed in via the
* options such as filters).
* @author pmeijer / https://github.com/pmeijer
*/
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import update from 'immutability-helper';
import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip';
import LockIcon from '@material-ui/icons/Lock';
import TransformIcon from '@material-ui/icons/Transform';
import FullscreenIcon from '@material-ui/icons/Fullscreen';
import FilterIcon from '@material-ui/icons/FilterList';
import ReactCytoscape from './ReactCytoscape';
import ContextMenu from './ContextMenu';
import FilterSelector from './FilterSelector';
const CONSTANTS = {
CYTOSCAPE_POS_REG_KEY: 'cytoscapePosition',
};
const DEFAULT_STYLES = [
{
selector: 'node[hasChildren]',
style: {
content: 'data(label)',
// http://js.cytoscape.org/#style/background-image
'background-width': '20%',
'background-height': '20%',
},
},
{
selector: 'node[^hasChildren]',
style: {
content: 'data(label)',
// http://js.cytoscape.org/#style/background-image
'background-width': '80%',
'background-height': '80%',
},
},
{
selector: 'edge.pointer',
style: {
content: 'data(label)',
'line-color': 'rgb(0,0,255)',
'target-arrow-color': 'rgb(0,0,255)',
'target-arrow-shape': 'open',
},
},
{
selector: 'edge.set-member',
style: {
content: 'data(label)',
'line-color': 'rgb(255,0,255)',
},
},
{
selector: 'edge.base-pointer',
style: {
width: 1,
'line-color': 'rgb(255,0,0)',
'target-arrow-fill': 'hollow',
'target-arrow-color': 'rgb(255,0,0)',
'target-arrow-shape': 'triangle',
},
},
{
selector: 'node[hasChildren].in-active-selection',
style: {
// 'border-width': '2px',
'border-style': 'solid',
'border-opacity': '1',
'border-color': 'rgba(82, 168, 236, 0.6)',
},
},
{
selector: 'node[^hasChildren].in-active-selection',
style: {
'background-color': 'rgba(82, 168, 236, 0.6)',
},
},
// {
// selector: 'edge.in-active-selection',
// style: {
// width: 5,
// 'line-color': 'rgba(82, 168, 236, 0.6)',
// },
// },
];
const coseBilkentOptions = {
name: 'cose-bilkent',
// Called on `layoutready`
ready: function ready() {
console.log('coseBilkent ready');
},
// Called on `layoutstop`
stop: function stop() {
console.log('coseBilkent stop');
},
// Whether to include labels in node dimensions. Useful for avoiding label overlap
nodeDimensionsIncludeLabels: true,
// number of ticks per frame; higher is faster but more jerky
refresh: 30,
// Whether to fit the network view after when done
fit: true,
// Padding on fit
padding: 10,
// Whether to enable incremental mode
randomize: true,
// Node repulsion (non overlapping) multiplier
nodeRepulsion: 4500,
// Ideal (intra-graph) edge length
idealEdgeLength: 50,
// Divisor to compute edge forces
edgeElasticity: 0.45,
// Nesting factor (multiplier) to compute ideal edge length for inter-graph edges
nestingFactor: 0.1,
// Gravity force (constant)
gravity: 0.25,
// Maximum number of iterations to perform
numIter: 2500,
// Whether to tile disconnected nodes
tile: true,
// Type of layout animation. The option set is {'during', 'end', false}
animate: 'end',
// Amount of vertical space to put between degree zero nodes during tiling (can also be a function)
tilingPaddingVertical: 10,
// Amount of horizontal space to put between degree zero nodes during tiling (can also be a function)
tilingPaddingHorizontal: 10,
// Gravity range (constant) for compounds
gravityRangeCompound: 1.5,
// Gravity force (constant) for compounds
gravityCompound: 1.0,
// Gravity range (constant)
gravityRange: 3.8,
// Initial cooling factor for incremental layout
initialEnergyOnIncremental: 0.5,
};
export default class GraphEditor extends Component {
static propTypes = {
gmeClient: PropTypes.object.isRequired,
activeNode: PropTypes.string,
activeSelection: PropTypes.arrayOf(PropTypes.string).isRequired,
nodes: PropTypes.object.isRequired,
readOnly: PropTypes.bool,
isActivePanel: PropTypes.bool,
width: PropTypes.number,
height: PropTypes.number,
setActiveSelection: PropTypes.func.isRequired,
setActiveNode: PropTypes.func.isRequired,
validFilters: PropTypes.object,
};
static defaultProps = {
readOnly: false,
isActivePanel: true,
activeNode: null,
width: 0,
height: 0,
validFilters: {
pointers: [],
sets: [],
nodes: [],
},
};
constructor(props) {
super(props);
this.reposition = {};
this.cyId = `cy-${Date.now()}`;
}
state = {
showNodeMenu: null,
activeFilters: (() => {
const {validFilters} = this.props;
const res = {};
Object.keys(validFilters).forEach((filterType) => {
validFilters[filterType].forEach((f) => {
res[`${filterType}$${f.name}`] = f.active;
});
});
// {
// 'sets$members': true,
// 'nodes$MyMetaType: false,
// }
return res;
})(),
showFilterSelector: false,
};
componentWillReceiveProps(newProps) {
const {activeNode} = newProps;
if (activeNode !== this.props.activeNode) {
this.reposition = {};
}
}
onListItemClick = id => (
(/* e */) => {
this.props.setActiveSelection([id]);
})
onListItemDoubleClick = id => (
(/* e */) => {