├── .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 |
43 | this.onHostChanged(event.target.value))} id="standard-basic" label="host" style={{width: '10%'}} /> 44 | this.onPortChanged(event.target.value))} id="standard-basic" label="port" style={{width: '10%'}} /> 45 | this.onQueryChanged(event.target.value))} id="standard-basic" label="gremlin query" style={{width: '60%'}} /> 46 | 47 | 48 | 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 | ![alt text](https://raw.githubusercontent.com/prabushitha/Readme-Materials/master/Gremlin-Visualizer.png) 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); --------------------------------------------------------------------------------