Skip to content

Instantly share code, notes, and snippets.

@tonilastre
Last active April 4, 2023 15:43
Show Gist options
  • Save tonilastre/71f6759621886ae8f307656cb0b9ea32 to your computer and use it in GitHub Desktop.
Save tonilastre/71f6759621886ae8f307656cb0b9ea32 to your computer and use it in GitHub Desktop.
Run Cypher queries from browser and visualize graph results with Memgraph Orb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Javascript Browser Example | Memgraph</title>
<script src="https://cdn.jsdelivr.net/npm/neo4j-driver"></script>
<script src="https://unpkg.com/@memgraph/orb/dist/browser/orb.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto&family=Roboto+Mono&display=swap" rel="stylesheet">
<style>
body {
font-size: 1rem;
font-family: 'Roboto', sans-serif;
font-weight: 400;
line-height: 1.5;
}
header {
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
background-color: #ddd;
}
main {
padding: 0.5rem;
flex-grow: 1;
}
input {
width: 100%;
padding: 0.5rem 1.5rem 0.5rem 0.5rem;
border: 1px solid #e6e6e6;
border-radius: 5px;
background-color: #fff;
font-size: 1rem;
font-family: 'Roboto Mono', monospace;
color: #231f20;
}
input:focus {
outline: none;
}
button {
width: 5rem;
background: #fb6e00;
color: white;
border: 0;
border-radius: 5px;
}
button:hover {
background: red;
cursor: pointer;
}
.container {
display: flex;
flex-direction: column;
position: absolute;
inset: 0;
}
.connection-icon {
height: 10px;
}
.connection-closed {
color: red;
}
.connection-opened {
color: green;
}
.connection[data-is-opened="true"] .connection-icon {
fill: green;
}
.connection[data-is-opened="false"] .connection-icon {
fill: red;
}
.connection[data-is-opened="true"] .connection-closed {
display: none;
}
.connection[data-is-opened="false"] .connection-closed {
display: inline;
}
.connection[data-is-opened="true"] .connection-opened {
display: inline;
}
.connection[data-is-opened="false"] .connection-opened {
display: none;
}
.setup {
display: flex;
gap: 0.5rem;
}
.hidden {
display: none;
}
#error {
color: red;
}
#info {
padding: 0.5rem;
font-family: 'Roboto Mono', monospace;
z-index: 9999;
position: absolute;
inset: auto 0 0 0;
background-color: white;
border-top: 1px solid #e6e6e6;
word-wrap: break-word;
}
#info > strong {
color: #fb6e00;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="connection" data-is-opened="false">
<svg class="connection-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" />
</svg>
<span class="connection-closed">Disconnected from <span class="bolt-url">localhost:7687</span>. Please check that your Memgraph is running and listening on <span class="bolt-url">localhost:7687</span></span>
<span class="connection-opened">Connected to <span class="bolt-url">localhost:7687</span></span>
</div>
<div class="setup">
<input type="text" value="MATCH (n)-[e]->(m) RETURN n, e, m;" placeholder="Add your Cypher query here and click run..." />
<button>Run</button>
</div>
</header>
<main>
<div id="info" class="hidden"></div>
<div id="error" class="hidden"></div>
<div id="graph"></div>
</main>
</div>
<script>
const inputElem = document.querySelector("input");
const buttonElem = document.querySelector("button");
const graphElem = document.getElementById("graph");
const errorElem = document.getElementById("error");
const infoElem = document.getElementById("info");
const connectionElem = document.querySelector(".connection");
const BOLT_HOSTNAME = 'localhost:7687';
const orb = new Orb.Orb(graphElem);
const driver = neo4j.driver(`bolt://${BOLT_HOSTNAME}`, neo4j.auth.basic("", ""));
// Using Orb to render nodes and edges
const renderGraph = ({ nodes, edges }) => {
orb.data.setDefaultStyle(getGraphStyling({ nodes, edges }));
orb.data.setup({ nodes, edges });
orb.view.render(() => {
orb.view.recenter();
});
orb.events.on(Orb.OrbEventType.NODE_CLICK, (event) => {
infoElem.innerHTML = `<strong>Node clicked: </strong>` + JSON.stringify(event.node.data)
infoElem.classList.remove('hidden');
});
orb.events.on(Orb.OrbEventType.EDGE_CLICK, (event) => {
infoElem.innerHTML = `<strong>Edge clicked: </strong>` + JSON.stringify(event.edge.data)
infoElem.classList.remove('hidden');
});
orb.events.on(Orb.OrbEventType.MOUSE_CLICK, (event) => {
if (!event.subject) {
infoElem.classList.add('hidden');
}
});
};
// Using Orb to get a default node/edge styling
const getGraphStyling = ({ nodes, edges }) => {
const colorByLabels = {};
nodes.forEach((node) => {
const labels = node.labels.join(':');
if (!colorByLabels[labels]) {
colorByLabels[labels] = Orb.Color.getRandomColor();
}
});
return {
getNodeStyle(node) {
const labels = node.data.labels.join(':');
const name = node.data.properties?.title ?? node.data.properties?.name ?? labels;
return {
size: 5,
color: colorByLabels[labels] ?? Orb.Color.getRandomColor(),
label: name,
};
},
getEdgeStyle(edge) {
return {
width: 0.3,
color: '#ababab',
label: edges.length < 50 ? edge.type : '',
};
},
};
};
const extractGraphFromMgResult = (mgResult) => {
const nodeById = {};
const edgeById = {};
mgResult.records.forEach((record) => {
Object.values(record).forEach((value) => {
if (_isMemgraphNode(value)) {
nodeById[value.id] = value;
}
if (_isMemgraphEdge(value)) {
edgeById[value.id] = value;
}
if (_isMemgraphPath(value)) {
value.nodes.forEach((node) => nodeById[node.id] = node);
value.relationships.forEach((edge) => edgeById[edge.id] = edge);
}
});
});
return { nodes: Object.values(nodeById), edges: Object.values(edgeById) };
};
// Parsing functions to handle Neo4j/Memgraph result objects
const _isNumber = (value) => typeof value === 'number';
const _isString = (value) => typeof value === 'string';
const _isObject = (value) => typeof value === 'object' && value !== null;
const _isArray = (value) => Array.isArray(value);
const _MG_NODE = 'node';
const _MG_EDGE = 'relationship';
const _MG_PATH = 'path';
const _isMemgraphNode = (field) => {
return (
_isObject(field) &&
_isNumber(field.id) &&
field.type === _MG_NODE
);
};
const _isMemgraphEdge = (field) => {
return (
_isObject(field) &&
_isNumber(field.id) &&
_isNumber(field.start) &&
_isNumber(field.end) &&
field.type === _MG_EDGE
);
};
const _isMemgraphPath = (field) => {
return (
_isObject(field) &&
_isArray(field.nodes) &&
field.nodes.every((node) => _isMemgraphNode(node)) &&
_isArray(field.relationships) &&
field.relationships.every((edge) => _isMemgraphEdge(edge)) &&
field.type === _MG_PATH
);
};
const _toMemgraphNode = (neo4jNode) => {
return {
id: parseNeo4jField(neo4jNode.identity),
labels: parseNeo4jField(neo4jNode.labels),
properties: parseNeo4jField(neo4jNode.properties),
type: _MG_NODE,
};
};
const _toMemgraphEdge = (neo4jEdge) => {
return {
id: parseNeo4jField(neo4jEdge.identity),
start: parseNeo4jField(neo4jEdge.start),
end: parseNeo4jField(neo4jEdge.end),
label: parseNeo4jField(neo4jEdge.type),
properties: parseNeo4jField(neo4jEdge.properties),
type: _MG_EDGE,
};
};
const _toMemgraphPath = (neo4jPath) => {
const nodeById = {};
const edgeById = {};
(neo4jPath.segments ?? []).forEach((segment) => {
if (_isNeo4jNode(segment.start)) {
const node = _toMemgraphNode(segment.start);
nodeById[node.id] = node;
}
if (_isNeo4jNode(segment.end)) {
const node = _toMemgraphNode(segment.end);
nodeById[node.id] = node;
}
if (_isNeo4jEdge(segment.relationship)) {
const edge = _toMemgraphEdge(segment.relationship);
edgeById[edge.id] = edge;
}
});
return {
nodes: Object.values(nodeById),
relationships: Object.values(edgeById),
type: _MG_PATH,
};
};
const _isNeo4jNumber = (field) => {
return (
_isObject(field) &&
Object.keys(field).length === 2 &&
_isNumber(field.low) &&
_isNumber(field.high)
);
};
const _isNeo4jNode = (field) => {
return (
_isObject(field) &&
_isNeo4jNumber(field.identity) &&
_isArray(field.labels) &&
field.labels.every((label) => _isString(label)) &&
_isObject(field.properties)
);
};
const _isNeo4jEdge = (field) => {
return (
_isObject(field) &&
_isNeo4jNumber(field.identity) &&
_isNeo4jNumber(field.start) &&
_isNeo4jNumber(field.end) &&
_isString(field.type) &&
_isObject(field.properties)
);
};
const _isNeo4jPath = (field) => {
return (
_isObject(field) &&
_isNeo4jNode(field.start) &&
_isNeo4jNode(field.end) &&
_isArray(field.segments) &&
field.segments.every((segment) => {
return (
_isObject(segment) &&
_isNeo4jNode(segment.start) &&
_isNeo4jEdge(segment.relationship) &&
_isNeo4jNode(segment.end)
);
})
);
}
const parseNeo4jField = (field) => {
if (field === undefined || field === null) {
return null;
}
if (_isArray(field)) {
return field.map((item) => parseNeo4jField(item));
}
if (_isNeo4jNumber(field)) {
return field.toNumber();
}
if (_isNeo4jNode(field)) {
return _toMemgraphNode(field);
}
if (_isNeo4jEdge(field)) {
return _toMemgraphEdge(field);
}
if (_isNeo4jPath(field)) {
return _toMemgraphPath(field);
}
if (_isObject(field)) {
const newObject = {};
Object.keys(field).forEach((key) => {
if (field.hasOwnProperty(key)) {
newObject[key] = parseNeo4jField(field[key]);
}
});
return newObject;
}
return field;
}
const parseNeo4jRecord = (record) => {
if (!_isObject(record)) {
return {};
}
const newRecord = {};
record.keys.forEach((key) => {
newRecord[key] = parseNeo4jField(record.get(key));
});
return newRecord;
}
const parseNeo4jResult = (result) => {
if (!_isObject(result)) {
return { records: [] };
}
return {
records: (result.records ?? []).map((r) => parseNeo4jRecord(r)),
summary: result.summary,
};
}
const runCypherQuery = async (query) => {
const session = driver.session();
try {
const neo4jResult = await session.run(query);
return parseNeo4jResult(neo4jResult);
} catch (error) {
throw error;
} finally {
session.close();
}
}
// Handling UI elements and results
const runQueryFromInputValue = async () => {
const query = inputElem.value ?? '';
try {
const mgResult = await runCypherQuery(query);
const graph = extractGraphFromMgResult(mgResult);
if (graph.nodes.length === 0 && graph.edges.length === 0) {
throw new Error(`Query was successful, but the graph can't be shown because there are no nodes and edges in the response.`);
}
hideError();
renderGraph(graph);
} catch (error) {
console.error(error);
showError(error);
}
};
const showError = (error) => {
errorElem.innerHTML = error.message;
errorElem.classList.remove('hidden');
graphElem.classList.add('hidden');
if (error.message.includes('WebSocket connection failure')) {
setIsDisconnected();
}
};
const hideError = () => {
setIsConnected();
errorElem.classList.add('hidden');
graphElem.classList.remove('hidden');
};
const setIsConnected = () => connectionElem.setAttribute('data-is-opened', 'true');
const setIsDisconnected = () => connectionElem.setAttribute('data-is-opened', 'false');
// Event handlers for running a cypher query
buttonElem.addEventListener('click', () => runQueryFromInputValue());
inputElem.addEventListener('keyup', (e) => {
if (e.key === 'Enter' || e.keyCode === 13) {
runQueryFromInputValue();
}
});
// Setting up the final bolt endpoint to the UI
document.querySelectorAll('.bolt-url').forEach((spanElem) => spanElem.innerHTML = BOLT_HOSTNAME);
// Running a test query to check connection
runCypherQuery('MATCH (n) RETURN n LIMIT 1').then(() => setIsConnected());
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment