├── .gitignore
├── .github
└── FUNDING.yml
├── public
├── gremlin-visualizer.png
├── manifest.json
└── index.html
├── Dockerfile
├── src
├── logics
│ ├── actionHelper.js
│ └── utils.js
├── reducers
│ ├── gremlinReducer.js
│ ├── graphReducer.js
│ └── optionReducer.js
├── App.js
├── index.js
├── constants.js
└── components
│ ├── NetworkGraph
│ └── NetworkGraphComponent.js
│ ├── Header
│ └── HeaderComponent.js
│ └── Details
│ └── DetailsComponent.js
├── LICENSE
├── package.json
├── proxy-server.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ['https://www.buymeacoffee.com/prabushitha']
2 |
--------------------------------------------------------------------------------
/public/gremlin-visualizer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prabushitha/gremlin-visualizer/HEAD/public/gremlin-visualizer.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Gremlin-Visualizer",
3 | "name": "Gremlin Visualizer",
4 | "icons": [
5 | {
6 | "src": "gremlin-visualizer.png",
7 | "sizes": "64x64",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:10-alpine
2 |
3 | RUN npm cache clean --force && \
4 | npm config set strict-ssl false && \
5 | apk add wget unzip && \
6 | wget --no-check-certificate https://github.com/prabushitha/gremlin-visualizer/archive/master.zip && \
7 | unzip master.zip && \
8 | cd gremlin-visualizer-master && \
9 | npm install
10 |
11 | EXPOSE 3000 3001
12 |
13 | WORKDIR /gremlin-visualizer-master
14 |
15 | CMD npm start
16 |
--------------------------------------------------------------------------------
/src/logics/actionHelper.js:
--------------------------------------------------------------------------------
1 | import { extractEdgesAndNodes } from './utils';
2 | import { ACTIONS } from '../constants';
3 |
4 | export const onFetchQuery = (result, query, oldNodeLabels, dispatch) => {
5 | const { nodes, edges, nodeLabels } = extractEdgesAndNodes(result.data, oldNodeLabels);
6 | dispatch({ type: ACTIONS.ADD_NODES, payload: nodes });
7 | dispatch({ type: ACTIONS.ADD_EDGES, payload: edges });
8 | dispatch({ type: ACTIONS.SET_NODE_LABELS, payload: nodeLabels });
9 | dispatch({ type: ACTIONS.ADD_QUERY_HISTORY, payload: query });
10 | };
--------------------------------------------------------------------------------
/src/reducers/gremlinReducer.js:
--------------------------------------------------------------------------------
1 | import { ACTIONS } from '../constants';
2 |
3 | const initialState = {
4 | host: 'localhost',
5 | port: '8182',
6 | query: '',
7 | error: null
8 | };
9 |
10 | export const reducer = (state=initialState, action)=>{
11 | switch (action.type){
12 | case ACTIONS.SET_HOST: {
13 | return { ...state, host: action.payload }
14 | }
15 | case ACTIONS.SET_PORT: {
16 | return { ...state, port: action.payload }
17 | }
18 | case ACTIONS.SET_QUERY: {
19 | return { ...state, query: action.payload, error: null }
20 | }
21 | case ACTIONS.SET_ERROR: {
22 | return { ...state, error: action.payload }
23 | }
24 | default:
25 | return state;
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Grid } from '@material-ui/core';
3 | import { NetworkGraphComponent } from './components/NetworkGraph/NetworkGraphComponent';
4 | import { HeaderComponent } from './components/Header/HeaderComponent';
5 | import { DetailsComponent } from './components/Details/DetailsComponent';
6 |
7 |
8 | export class App extends React.Component{
9 | render(){
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { applyMiddleware, createStore, combineReducers, compose } from 'redux';
4 | import { createLogger } from 'redux-logger';
5 | import { Provider } from 'react-redux';
6 |
7 | import { reducer as gremlinReducer } from './reducers/gremlinReducer';
8 | import { reducer as graphReducer } from './reducers/graphReducer';
9 | import { reducer as optionReducer } from './reducers/optionReducer';
10 | import { App } from './App';
11 |
12 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
13 | const rootReducer = combineReducers({ gremlin: gremlinReducer, graph: graphReducer, options: optionReducer });
14 |
15 | const store = createStore(
16 | rootReducer,
17 | composeEnhancers(applyMiddleware(createLogger()))
18 | );
19 |
20 | //6. Render react element
21 | ReactDOM.render(, document.getElementById('root'));
22 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | const SERVER_URL = 'http://localhost:3001';
2 | export const QUERY_ENDPOINT = `${SERVER_URL}/query`;
3 | export const COMMON_GREMLIN_ERROR = 'Invalid query. Please execute a query to get a set of vertices';
4 | export const ACTIONS = {
5 | SET_HOST: 'SET_HOST',
6 | SET_PORT: 'SET_PORT',
7 | SET_QUERY: 'SET_QUERY',
8 | SET_ERROR: 'SET_ERROR',
9 | SET_NETWORK: 'SET_NETWORK',
10 | CLEAR_GRAPH: 'CLEAR_GRAPH',
11 | ADD_NODES: 'ADD_NODES',
12 | ADD_EDGES: 'ADD_EDGES',
13 | SET_SELECTED_NODE: 'SET_SELECTED_NODE',
14 | SET_SELECTED_EDGE: 'SET_SELECTED_EDGE',
15 | SET_IS_PHYSICS_ENABLED: 'SET_IS_PHYSICS_ENABLED',
16 | ADD_QUERY_HISTORY: 'ADD_QUERY_HISTORY',
17 | CLEAR_QUERY_HISTORY: 'CLEAR_QUERY_HISTORY',
18 | SET_NODE_LABELS: 'SET_NODE_LABELS',
19 | ADD_NODE_LABEL: 'ADD_NODE_LABEL',
20 | EDIT_NODE_LABEL: 'EDIT_NODE_LABEL',
21 | REMOVE_NODE_LABEL: 'REMOVE_NODE_LABEL',
22 | REFRESH_NODE_LABELS: 'REFRESH_NODE_LABELS',
23 | SET_NODE_LIMIT: 'SET_NODE_LIMIT'
24 | };
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Umesh Prabushitha Jayasinghe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gremlin-visualizer",
3 | "version": "1.0.0",
4 | "author": "Umesh Jayasinghe",
5 | "description": "Visualize graph network corresponding to a gremlin query",
6 | "license": "MIT",
7 | "dependencies": {
8 | "@material-ui/core": "^4.7.0",
9 | "@material-ui/icons": "^4.5.1",
10 | "axios": "^0.21.1",
11 | "body-parser": "^1.19.0",
12 | "cors": "^2.8.5",
13 | "express": "^4.17.1",
14 | "gremlin": "^3.4.4",
15 | "lodash": "^4.17.15",
16 | "react": "^16.12.0",
17 | "react-dom": "^16.12.0",
18 | "react-json-to-table": "^0.1.5",
19 | "react-redux": "^7.1.3",
20 | "react-scripts": "^3.2.0",
21 | "redux": "^4.0.4",
22 | "redux-logger": "^3.0.6",
23 | "vis-network": "^6.4.4"
24 | },
25 | "proxy": "http://localhost:3001",
26 | "scripts": {
27 | "client": "react-scripts start",
28 | "server": "node proxy-server.js",
29 | "start": "concurrently \"npm run server\" \"npm run client\"",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test --env=jsdom",
32 | "eject": "react-scripts eject"
33 | },
34 | "devDependencies": {
35 | "concurrently": "^5.0.0"
36 | },
37 | "browserslist": {
38 | "production": [
39 | ">0.2%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 1 chrome version",
45 | "last 1 firefox version",
46 | "last 1 safari version"
47 | ]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/NetworkGraph/NetworkGraphComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import vis from 'vis-network';
4 | import { ACTIONS } from '../../constants';
5 |
6 | class NetworkGraph extends React.Component{
7 | componentDidMount() {
8 | const data = {
9 | nodes: this.props.nodeHolder,
10 | edges: this.props.edgeHolder
11 | };
12 | const network = new vis.Network(this.refs.myRef, data, this.props.networkOptions);
13 |
14 | network.on('selectNode', (params) => {
15 | const nodeId = params.nodes && params.nodes.length > 0 ? params.nodes[0] : null;
16 | this.props.dispatch({ type: ACTIONS.SET_SELECTED_NODE, payload: nodeId });
17 | });
18 |
19 | network.on("selectEdge", (params) => {
20 | const edgeId = params.edges && params.edges.length === 1 ? params.edges[0] : null;
21 | const isNodeSelected = params.nodes && params.nodes.length > 0;
22 | if (!isNodeSelected && edgeId !== null) {
23 | this.props.dispatch({ type: ACTIONS.SET_SELECTED_EDGE, payload: edgeId });
24 | }
25 | });
26 |
27 | this.props.dispatch({ type: ACTIONS.SET_NETWORK, payload: network });
28 | }
29 |
30 | render(){
31 | return ();
32 | }
33 | }
34 |
35 | export const NetworkGraphComponent = connect((state)=>{
36 | return {
37 | nodeHolder: state.graph.nodeHolder,
38 | edgeHolder: state.graph.edgeHolder,
39 | networkOptions: state.options.networkOptions
40 | };
41 | })(NetworkGraph);
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
26 | Gremlin-Visualizer
27 |
28 |
29 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/logics/utils.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | const selectRandomField = (obj) => {
4 | let firstKey;
5 | for (firstKey in obj) break;
6 | return firstKey;
7 | };
8 |
9 | export const getDiffNodes = (newList, oldList) => {
10 | return _.differenceBy(newList, oldList, (node) => node.id);
11 | };
12 |
13 | export const getDiffEdges = (newList, oldList) => {
14 | return _.differenceBy(newList, oldList, (edge) => `${edge.from},${edge.to}`);
15 | };
16 |
17 | export const extractEdgesAndNodes = (nodeList, nodeLabels=[]) => {
18 | let edges = [];
19 | const nodes = [];
20 |
21 | const nodeLabelMap =_.mapValues( _.keyBy(nodeLabels, 'type'), 'field');
22 |
23 | _.forEach(nodeList, (node) => {
24 | const type = node.label;
25 | if (!nodeLabelMap[type]) {
26 | const field = selectRandomField(node.properties);
27 | const nodeLabel = { type, field };
28 | nodeLabels.push(nodeLabel);
29 | nodeLabelMap[type] = field;
30 | }
31 | const labelField = nodeLabelMap[type];
32 | const label = labelField in node.properties ? node.properties[labelField] : type;
33 | nodes.push({ id: node.id, label: String(label), group: node.label, properties: node.properties, type });
34 |
35 | edges = edges.concat(_.map(node.edges, edge => ({ ...edge, type: edge.label, arrows: { to: { enabled: true, scaleFactor: 0.5 } } })));
36 | });
37 |
38 | return { edges, nodes, nodeLabels }
39 | };
40 |
41 | export const findNodeById = (nodeList, id) => {
42 | return _.find(nodeList, node => node.id === id);
43 | };
44 |
45 | export const stringifyObjectValues = (obj) => {
46 | _.forOwn(obj, (value, key) => {
47 | if (!_.isString(value)) {
48 | obj[key] = JSON.stringify(value);
49 | }
50 | });
51 | };
52 |
--------------------------------------------------------------------------------
/proxy-server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 | const gremlin = require('gremlin');
4 | const cors = require('cors');
5 | const app = express();
6 | const port = 3001;
7 |
8 | app.use(cors({
9 | credentials: true,
10 | }));
11 |
12 | // parse application/json
13 | app.use(bodyParser.json());
14 |
15 | function mapToObj(inputMap) {
16 | let obj = {};
17 |
18 | inputMap.forEach((value, key) => {
19 | obj[key] = value
20 | });
21 |
22 | return obj;
23 | }
24 |
25 | function edgesToJson(edgeList) {
26 | return edgeList.map(
27 | edge => ({
28 | id: typeof edge.get('id') !== "string" ? JSON.stringify(edge.get('id')) : edge.get('id'),
29 | from: edge.get('from'),
30 | to: edge.get('to'),
31 | label: edge.get('label'),
32 | properties: mapToObj(edge.get('properties')),
33 | })
34 | );
35 | }
36 |
37 | function nodesToJson(nodeList) {
38 | return nodeList.map(
39 | node => ({
40 | id: node.get('id'),
41 | label: node.get('label'),
42 | properties: mapToObj(node.get('properties')),
43 | edges: edgesToJson(node.get('edges'))
44 | })
45 | );
46 | }
47 |
48 | function makeQuery(query, nodeLimit) {
49 | const nodeLimitQuery = !isNaN(nodeLimit) && Number(nodeLimit) > 0 ? `.limit(${nodeLimit})`: '';
50 | return `${query}${nodeLimitQuery}.dedup().as('node').project('id', 'label', 'properties', 'edges').by(__.id()).by(__.label()).by(__.valueMap().by(__.unfold())).by(__.outE().project('id', 'from', 'to', 'label', 'properties').by(__.id()).by(__.select('node').id()).by(__.inV().id()).by(__.label()).by(__.valueMap().by(__.unfold())).fold())`;
51 | }
52 |
53 | app.post('/query', (req, res, next) => {
54 | const gremlinHost = req.body.host;
55 | const gremlinPort = req.body.port;
56 | const nodeLimit = req.body.nodeLimit;
57 | const query = req.body.query;
58 |
59 | const client = new gremlin.driver.Client(`ws://${gremlinHost}:${gremlinPort}/gremlin`, { traversalSource: 'g', mimeType: 'application/json' });
60 |
61 | client.submit(makeQuery(query, nodeLimit), {})
62 | .then((result) => res.send(nodesToJson(result._items)))
63 | .catch((err) => next(err));
64 |
65 | });
66 |
67 | app.listen(port, () => console.log(`Simple gremlin-proxy server listening on port ${port}!`));
--------------------------------------------------------------------------------
/src/reducers/graphReducer.js:
--------------------------------------------------------------------------------
1 | import vis from 'vis-network';
2 | import _ from 'lodash';
3 | import { ACTIONS } from '../constants';
4 | import { getDiffNodes, getDiffEdges, findNodeById } from '../logics/utils';
5 |
6 | const initialState = {
7 | network: null,
8 | nodeHolder: new vis.DataSet([]),
9 | edgeHolder: new vis.DataSet([]),
10 | nodes: [],
11 | edges: [],
12 | selectedNode: {},
13 | selectedEdge: {},
14 | };
15 |
16 | export const reducer = (state=initialState, action)=>{
17 | switch (action.type){
18 | case ACTIONS.CLEAR_GRAPH: {
19 | state.nodeHolder.clear();
20 | state.edgeHolder.clear();
21 |
22 | return { ...state, nodes: [], edges: [], selectedNode:{}, selectedEdge: {} };
23 | }
24 | case ACTIONS.SET_NETWORK: {
25 | return { ...state, network: action.payload };
26 | }
27 | case ACTIONS.ADD_NODES: {
28 | const newNodes = getDiffNodes(action.payload, state.nodes);
29 | const nodes = [...state.nodes, ...newNodes];
30 | state.nodeHolder.add(newNodes);
31 | return { ...state, nodes };
32 | }
33 | case ACTIONS.ADD_EDGES: {
34 | const newEdges = getDiffEdges(action.payload, state.edges);
35 | const edges = [...state.edges, ...newEdges];
36 | state.edgeHolder.add(newEdges);
37 | return { ...state, edges };
38 | }
39 | case ACTIONS.SET_SELECTED_NODE: {
40 | const nodeId = action.payload;
41 | let selectedNode = {};
42 | if (nodeId !== null) {
43 | selectedNode = findNodeById(state.nodes, nodeId);
44 | }
45 | return { ...state, selectedNode, selectedEdge: {} };
46 | }
47 | case ACTIONS.SET_SELECTED_EDGE: {
48 | const edgeId = action.payload;
49 | let selectedEdge = {};
50 | if (edgeId !== null) {
51 | selectedEdge = findNodeById(state.edges, edgeId);
52 | }
53 | return { ...state, selectedEdge, selectedNode: {} };
54 | }
55 | case ACTIONS.REFRESH_NODE_LABELS: {
56 | const nodeLabelMap =_.mapValues( _.keyBy(action.payload, 'type'), 'field');
57 | _.map(state.nodes, node => {
58 | if (node.type in nodeLabelMap) {
59 | const field = nodeLabelMap[node.type];
60 | const label = node.properties[field];
61 | state.nodeHolder.update({id:node.id, label: label});
62 | return {...node, label };
63 | }
64 | return node;
65 | });
66 | return state;
67 | }
68 | default:
69 | return state;
70 | }
71 | };
72 |
--------------------------------------------------------------------------------
/src/reducers/optionReducer.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import { ACTIONS } from '../constants';
3 |
4 | const initialState = {
5 | nodeLabels: [],
6 | queryHistory: [],
7 | isPhysicsEnabled: true,
8 | nodeLimit: 100,
9 | networkOptions: {
10 | physics: {
11 | forceAtlas2Based: {
12 | gravitationalConstant: -26,
13 | centralGravity: 0.005,
14 | springLength: 230,
15 | springConstant: 0.18,
16 | avoidOverlap: 1.5
17 | },
18 | maxVelocity: 40,
19 | solver: 'forceAtlas2Based',
20 | timestep: 0.35,
21 | stabilization: {
22 | enabled: true,
23 | iterations: 50,
24 | updateInterval: 25
25 | }
26 | },
27 | nodes: {
28 | shape: "dot",
29 | size: 20,
30 | borderWidth: 2,
31 | font: {
32 | size: 11
33 | }
34 | },
35 | edges: {
36 | width: 2,
37 | font: {
38 | size: 11
39 | },
40 | smooth: {
41 | type: 'dynamic'
42 | }
43 | }
44 | }
45 | };
46 |
47 | export const reducer = (state=initialState, action)=>{
48 | switch (action.type){
49 | case ACTIONS.SET_IS_PHYSICS_ENABLED: {
50 | const isPhysicsEnabled = _.get(action, 'payload', true);
51 | return { ...state, isPhysicsEnabled };
52 | }
53 | case ACTIONS.ADD_QUERY_HISTORY: {
54 | return { ...state, queryHistory: [ ...state.queryHistory, action.payload] }
55 | }
56 | case ACTIONS.CLEAR_QUERY_HISTORY: {
57 | return { ...state, queryHistory: [] }
58 | }
59 | case ACTIONS.SET_NODE_LABELS: {
60 | const nodeLabels = _.get(action, 'payload', []);
61 | return { ...state, nodeLabels };
62 | }
63 | case ACTIONS.ADD_NODE_LABEL: {
64 | const nodeLabels = [...state.nodeLabels, {}];
65 | return { ...state, nodeLabels };
66 | }
67 | case ACTIONS.EDIT_NODE_LABEL: {
68 | const editIndex = action.payload.id;
69 | const editedNodeLabel = action.payload.nodeLabel;
70 |
71 | if (state.nodeLabels[editIndex]) {
72 | const nodeLabels = [...state.nodeLabels.slice(0, editIndex), editedNodeLabel, ...state.nodeLabels.slice(editIndex+1)];
73 | return { ...state, nodeLabels };
74 | }
75 | return state;
76 | }
77 | case ACTIONS.REMOVE_NODE_LABEL: {
78 | const removeIndex = action.payload;
79 | if (removeIndex < state.nodeLabels.length) {
80 | const nodeLabels = [...state.nodeLabels.slice(0, removeIndex), ...state.nodeLabels.slice(removeIndex+1)];
81 | return { ...state, nodeLabels };
82 | }
83 | return state;
84 | }
85 | case ACTIONS.SET_NODE_LIMIT: {
86 | const nodeLimit = action.payload;
87 | return { ...state, nodeLimit };
88 | }
89 | default:
90 | return state;
91 | }
92 | };
93 |
--------------------------------------------------------------------------------
/src/components/Header/HeaderComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Button, TextField } from '@material-ui/core';
4 | import axios from 'axios';
5 | import { ACTIONS, QUERY_ENDPOINT, COMMON_GREMLIN_ERROR } from '../../constants';
6 | import { onFetchQuery } from '../../logics/actionHelper';
7 |
8 | class Header extends React.Component {
9 | clearGraph() {
10 | this.props.dispatch({ type: ACTIONS.CLEAR_GRAPH });
11 | this.props.dispatch({ type: ACTIONS.CLEAR_QUERY_HISTORY });
12 | }
13 |
14 | sendQuery() {
15 | this.props.dispatch({ type: ACTIONS.SET_ERROR, payload: null });
16 | axios.post(
17 | QUERY_ENDPOINT,
18 | { host: this.props.host, port: this.props.port, query: this.props.query, nodeLimit: this.props.nodeLimit },
19 | { headers: { 'Content-Type': 'application/json' } }
20 | ).then((response) => {
21 | onFetchQuery(response, this.props.query, this.props.nodeLabels, this.props.dispatch);
22 | }).catch((error) => {
23 | this.props.dispatch({ type: ACTIONS.SET_ERROR, payload: COMMON_GREMLIN_ERROR });
24 | });
25 | }
26 |
27 | onHostChanged(host) {
28 | this.props.dispatch({ type: ACTIONS.SET_HOST, payload: host });
29 | }
30 |
31 | onPortChanged(port) {
32 | this.props.dispatch({ type: ACTIONS.SET_PORT, payload: port });
33 | }
34 |
35 | onQueryChanged(query) {
36 | this.props.dispatch({ type: ACTIONS.SET_QUERY, payload: query });
37 | }
38 |
39 | render(){
40 | return (
41 |
42 |
49 |
50 |
51 |
{this.props.error}
52 |
53 |
54 | );
55 | }
56 | }
57 |
58 | export const HeaderComponent = connect((state)=>{
59 | return {
60 | host: state.gremlin.host,
61 | port: state.gremlin.port,
62 | query: state.gremlin.query,
63 | error: state.gremlin.error,
64 | nodes: state.graph.nodes,
65 | edges: state.graph.edges,
66 | nodeLabels: state.options.nodeLabels,
67 | nodeLimit: state.options.nodeLimit
68 | };
69 | })(Header);
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gremlin-Visualizer
2 | This project is to visualize the graph network corresponding to a gremlin query.
3 |
4 | 
5 |
6 | ### Setting Up Gremlin Visualizer
7 | To setup gremlin visualizer, you need to have `node.js` and `npm` installed in your system.
8 |
9 | * Clone the project
10 | ```sh
11 | git clone https://github.com/prabushitha/gremlin-visualizer.git
12 | ```
13 | * Install dependencies
14 | ```sh
15 | npm install
16 | ```
17 | * Run the project
18 | ```sh
19 | npm start
20 | ```
21 | * Open the browser and navigate to
22 | ```sh
23 | http://localhost:3000
24 | ```
25 |
26 | Note - Frontend starts on port 3000 and simple Node.js server also starts on port 3001. If you need to change the ports, configure in `package.json`, `proxy-server.js`, `src/constants`
27 |
28 | #### Setting up with Docker
29 |
30 | You can build a Docker image of the gremlin visualizer with the included `Dockerfile`.
31 | This will use the current version of the `master` branch of the source GitHub repository.
32 | The Docker image can be built by calling the `docker build` command, for example:
33 |
34 | ```sh
35 | docker build --tag=gremlin-visualizer:latest .
36 | ```
37 |
38 | The image can also be downloaded from Docker hub: [`prabushitha/gremlin-visualizer:latest`](https://hub.docker.com/r/prabushitha/gremlin-visualizer).
39 |
40 | ```sh
41 | docker pull prabushitha/gremlin-visualizer:latest
42 | ```
43 |
44 | The Docker image can then be run by calling `docker run` and exposing the necessary ports for communication. See [Docker's documentation](https://docs.docker.com/engine/reference/commandline/run/) for more options on how to run the image.
45 |
46 | ```sh
47 | # if you built the image yourself
48 | docker run --rm -d -p 3000:3000 -p 3001:3001 --name=gremlin-visualizer --network=host gremlin-visualizer:latest
49 | # if you downloaded from Docker Hub
50 | docker run --rm -d -p 3000:3000 -p 3001:3001 --name=gremlin-visualizer --network=host prabushitha/gremlin-visualizer:latest
51 | ```
52 | Note that `--network=host` is not needed if you don't run your gremlin server in the host machine.
53 |
54 | The Docker container can be stopped by calling `docker stop gremlin-visualizer`.
55 |
56 | ### Usage
57 | * Start Gremlin-Visualizer as mentioned above
58 | * Start or tunnel a gremlin server
59 | * Specify the host and port of the gremlin server
60 | * Write an gremlin query to retrieve a set of nodes (eg. `g.V()`)
61 |
62 | ### Features
63 | * If you don't clear the graph and execute another gremlin query, results of previous query and new query will be merged and be shown.
64 | * Node and edge properties are shown once you click on a node/edge
65 | * Change the labels of nodes to any property
66 | * View the set of queries executed to generate the graph
67 | * Traverse in/out from the selected node
68 |
69 | ###
70 | ## Contributors
71 | * Umesh Jayasinghe (Github: prabushitha)
72 |
73 | ## Something Missing?
74 |
75 | If you have new ideas to improve please create a issue and make a pull request
76 |
--------------------------------------------------------------------------------
/src/components/Details/DetailsComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import {
4 | ExpansionPanel,
5 | ExpansionPanelSummary,
6 | Typography,
7 | ExpansionPanelDetails,
8 | List,
9 | ListItem,
10 | ListItemText,
11 | TextField,
12 | Fab,
13 | IconButton,
14 | Grid,
15 | Table,
16 | TableBody,
17 | TableRow,
18 | TableCell,
19 | FormControlLabel,
20 | Switch,
21 | Divider,
22 | Tooltip
23 | } from '@material-ui/core';
24 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
25 | import AddIcon from '@material-ui/icons/Add';
26 | import DeleteIcon from '@material-ui/icons/Delete';
27 | import RefreshIcon from '@material-ui/icons/Refresh';
28 | import ArrowBackIcon from '@material-ui/icons/ArrowBack';
29 | import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
30 | import _ from 'lodash';
31 | import { JsonToTable } from 'react-json-to-table';
32 | import { ACTIONS, COMMON_GREMLIN_ERROR, QUERY_ENDPOINT } from '../../constants';
33 | import axios from "axios";
34 | import { onFetchQuery} from '../../logics/actionHelper';
35 | import { stringifyObjectValues} from '../../logics/utils';
36 |
37 | class Details extends React.Component {
38 |
39 | onAddNodeLabel() {
40 | this.props.dispatch({ type: ACTIONS.ADD_NODE_LABEL });
41 | }
42 |
43 | onEditNodeLabel(index, nodeLabel) {
44 | this.props.dispatch({ type: ACTIONS.EDIT_NODE_LABEL, payload: { id: index, nodeLabel } });
45 | }
46 |
47 | onRemoveNodeLabel(index) {
48 | this.props.dispatch({ type: ACTIONS.REMOVE_NODE_LABEL, payload: index });
49 | }
50 |
51 | onEditNodeLimit(limit) {
52 | this.props.dispatch({ type: ACTIONS.SET_NODE_LIMIT, payload: limit });
53 | }
54 |
55 | onRefresh() {
56 | this.props.dispatch({ type: ACTIONS.REFRESH_NODE_LABELS, payload: this.props.nodeLabels });
57 | }
58 |
59 | onTraverse(nodeId, direction) {
60 | const query = `g.V('${nodeId}').${direction}()`;
61 | axios.post(
62 | QUERY_ENDPOINT,
63 | { host: this.props.host, port: this.props.port, query: query, nodeLimit: this.props.nodeLimit },
64 | { headers: { 'Content-Type': 'application/json' } }
65 | ).then((response) => {
66 | onFetchQuery(response, query, this.props.nodeLabels, this.props.dispatch);
67 | }).catch((error) => {
68 | this.props.dispatch({ type: ACTIONS.SET_ERROR, payload: COMMON_GREMLIN_ERROR });
69 | });
70 | }
71 |
72 | onTogglePhysics(enabled){
73 | this.props.dispatch({ type: ACTIONS.SET_IS_PHYSICS_ENABLED, payload: enabled });
74 | if (this.props.network) {
75 | const edges = {
76 | smooth: {
77 | type: enabled ? 'dynamic' : 'continuous'
78 | }
79 | };
80 | this.props.network.setOptions( { physics: enabled, edges } );
81 | }
82 | }
83 |
84 | generateList(list) {
85 | let key = 0;
86 | return list.map(value => {
87 | key = key+1;
88 | return React.cloneElement((
89 |
90 |
93 |
94 | ), {
95 | key
96 | })
97 | });
98 | }
99 |
100 | generateNodeLabelList(nodeLabels) {
101 | let index = -1;
102 | return nodeLabels.map( nodeLabel => {
103 | index = index+1;
104 | nodeLabel.index = index;
105 | return React.cloneElement((
106 |
107 | {
108 | const type = event.target.value;
109 | const field = nodeLabel.field;
110 | this.onEditNodeLabel(nodeLabel.index, { type, field })
111 | }}
112 | />
113 | {
114 | const field = event.target.value;
115 | const type = nodeLabel.type;
116 | this.onEditNodeLabel(nodeLabel.index, { type, field })
117 | }}/>
118 | this.onRemoveNodeLabel(nodeLabel.index)}>
119 |
120 |
121 |
122 | ), {
123 | key: index
124 | })
125 | });
126 | }
127 |
128 | render(){
129 | let hasSelected = false;
130 | let selectedType = null;
131 | let selectedId = null ;
132 | let selectedProperties = null;
133 | let selectedHeader = null;
134 | if (!_.isEmpty(this.props.selectedNode)) {
135 | hasSelected = true;
136 | selectedType = _.get(this.props.selectedNode, 'type');
137 | selectedId = _.get(this.props.selectedNode, 'id');
138 | selectedProperties = _.get(this.props.selectedNode, 'properties');
139 | stringifyObjectValues(selectedProperties);
140 | selectedHeader = 'Node';
141 | } else if (!_.isEmpty(this.props.selectedEdge)) {
142 | hasSelected = true;
143 | selectedType = _.get(this.props.selectedEdge, 'type');
144 | selectedId = _.get(this.props.selectedEdge, 'id');
145 | selectedProperties = _.get(this.props.selectedEdge, 'properties');
146 | selectedHeader = 'Edge';
147 | stringifyObjectValues(selectedProperties);
148 | }
149 |
150 |
151 | return (
152 |
153 |
154 |
155 |
156 | }
158 | aria-controls="panel1a-content"
159 | id="panel1a-header"
160 | >
161 | Query History
162 |
163 |
164 |
165 | {this.generateList(this.props.queryHistory)}
166 |
167 |
168 |
169 |
170 | }
172 | aria-controls="panel1a-content"
173 | id="panel1a-header"
174 | >
175 | Settings
176 |
177 |
178 |
179 |
180 |
181 | { this.onTogglePhysics(!this.props.isPhysicsEnabled); }}
186 | value="physics"
187 | color="primary"
188 | />
189 | }
190 | label="Enable Physics"
191 | />
192 |
193 |
194 |
195 |
196 |
197 | {
198 | const limit = event.target.value;
199 | this.onEditNodeLimit(limit)
200 | }} />
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 | Node Labels
209 |
210 |
211 |
212 | {this.generateNodeLabelList(this.props.nodeLabels)}
213 |
214 |
215 |
216 |
217 |
218 | Refresh
219 |
220 |
221 |
222 | Add Node Label
223 |
224 |
225 |
226 |
227 |
228 |
229 | {hasSelected &&
230 |
231 | Information: {selectedHeader}
232 | {selectedHeader === 'Node' &&
233 |
234 |
235 |
236 | this.onTraverse(selectedId, 'out')}>
237 | Traverse Out Edges
238 |
239 |
240 |
241 |
242 | this.onTraverse(selectedId, 'in')}>
243 | Traverse In Edges
244 |
245 |
246 |
247 |
248 |
249 | }
250 |
251 |
252 |
253 |
254 |
255 | Type
256 | {String(selectedType)}
257 |
258 |
259 | ID
260 | {String(selectedId)}
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 | }
269 |
270 |
271 | );
272 | }
273 | }
274 |
275 | export const DetailsComponent = connect((state)=>{
276 | return {
277 | host: state.gremlin.host,
278 | port: state.gremlin.port,
279 | network: state.graph.network,
280 | selectedNode: state.graph.selectedNode,
281 | selectedEdge: state.graph.selectedEdge,
282 | queryHistory: state.options.queryHistory,
283 | nodeLabels: state.options.nodeLabels,
284 | nodeLimit: state.options.nodeLimit,
285 | isPhysicsEnabled: state.options.isPhysicsEnabled
286 | };
287 | })(Details);
--------------------------------------------------------------------------------