Hi,
I am trying to render a network of my data using react-viz in the dashboard of my Splunk App . For the past few days, I have been trying various things to get the code to work, but all I see is a blank screen. I have pasted my code below. Please let me know if you can identify where I might be going wrong.
network_dashboard.js:
require([
'jquery',
'splunkjs/mvc',
'splunkjs/mvc/simplexml/ready!'
], function($, mvc) {
function loadScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src=url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
function waitForReact() {
return new Promise((resolve) => {
const checkReact = () => {
if (window.React && window.ReactDOM && window.vis) {
resolve();
} else {
setTimeout(checkReact, 100);
}
};
checkReact();
});
}
Promise.all([
loadScript('https://unpkg.com/react@17/umd/react.production.min.js'),
loadScript('https://unpkg.com/react-dom@17/umd/react-dom.production.min.js'),
loadScript('https://unpkg.com/vis-network/dist/vis-network.min.js')
])
.then(waitForReact)
.then(() => {
console.log('React, ReactDOM, and vis-network are loaded and available');
initApp();
})
.catch(error => {
console.error('Error loading scripts:', error);
});
function initApp() {
const NetworkPage = () => {
const [nodes, setNodes] = React.useState([]);
const [edges, setEdges] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [clickedEdge, setClickedEdge] = React.useState(null);
const [clickedNode, setClickedNode] = React.useState(null);
const [showTransparent, setShowTransparent] = React.useState(false);
React.useEffect(() => {
// Static data for debugging
const staticNodes = [
{'id': 1, 'label': 'wininit.exe', 'type': 'process', 'rank': 0},
{'id': 2, 'label': 'services.exe', 'type': 'process', 'rank': 1},
{'id': 3, 'label': 'sysmon.exe', 'type': 'process', 'rank': 2},
{'id': 4, 'label': 'comb-file', 'type': 'file', 'rank': 1, 'nodes': [
'c:\\windows\\system32\\mmc.exe',
'c:\\mozillafirefox\\firefox.exe',
'c:\\windows\\system32\\cmd.exe',
'c:\\windows\\system32\\dllhost.exe',
'c:\\windows\\system32\\conhost.exe',
'c:\\wireshark\\tshark.exe',
'c:\\confer\\repwmiutils.exe',
'c:\\windows\\system32\\searchprotocolhost.exe',
'c:\\windows\\system32\\searchfilterhost.exe',
'c:\\windows\\system32\\consent.exe',
'c:\\python27\\python.exe',
'c:\\windows\\system32\\audiodg.exe',
'c:\\confer\\repux.exe',
'c:\\windows\\system32\\taskhost.exe'
]},
{'id': 5, 'label': 'c:\\wireshark\\dumpcap.exe', 'type': 'file', 'rank': 1},
{'id': 6, 'label': 'c:\\windows\\system32\\audiodg.exe', 'type': 'file', 'rank': 1}
];
const staticEdges = [
{'source': 1, 'target': 2, 'label': 'procstart', 'alname': null, 'time': '2022-07-19 16:00:17.074477', 'transparent': false},
{'source': 2, 'target': 3, 'label': 'procstart', 'alname': null, 'time': '2022-07-19 16:00:17.531504', 'transparent': false},
{'source': 4, 'target': 3, 'label': 'moduleload', 'alname': null, 'time': '2022-07-19 16:01:03.194938', 'transparent': false},
{'source': 5, 'target': 3, 'label': 'moduleload', 'alname': 'Execution - SysInternals Use', 'time': '2022-07-19 16:01:48.497418', 'transparent': false},
{'source': 6, 'target': 3, 'label': 'moduleload', 'alname': 'Execution - SysInternals Use', 'time': '2022-07-19 16:05:04.581065', 'transparent': false}
];
const sortedEdges = staticEdges.sort((a, b) => new Date(a.time) - new Date(b.time));
const nodesByRank = staticNodes.reduce((acc, node) => {
const rank = node.rank || 0;
if (!acc[rank]) acc[rank] = [];
acc[rank].push(node);
return acc;
}, {});
const nodePositions = {};
const rankSpacingX = 200;
const ySpacing = 100;
Object.keys(nodesByRank).forEach(rank => {
const nodesInRank = nodesByRank[rank];
nodesInRank.sort((a, b) => {
const aEdges = staticEdges.filter(edge => edge.source === a.id || edge.target === a.id);
const bEdges = staticEdges.filter(edge => edge.source === b.id || edge.target === b.id);
return aEdges.length - bEdges.length;
});
const totalNodesInRank = nodesInRank.length;
nodesInRank.forEach((node, index) => {
nodePositions[node.id] = {
x: rank * rankSpacingX,
y: index * ySpacing - (totalNodesInRank * ySpacing) / 2,
};
});
});
const positionedNodes = staticNodes.map(node => ({
...node,
x: nodePositions[node.id].x,
y: nodePositions[node.id].y,
}));
setNodes(positionedNodes);
setEdges(sortedEdges);
setLoading(false);
}, []);
const handleNodeClick = (event) => {
const { nodes: clickedNodes } = event;
if (clickedNodes.length > 0) {
const nodeId = clickedNodes[0];
const clickedNode = nodes.find(node => node.id === nodeId);
setClickedNode(clickedNode || null);
}
};
const handleEdgeClick = (event) => {
const { edges: clickedEdges } = event;
if (clickedEdges.length > 0) {
const edgeId = clickedEdges[0];
const clickedEdge = edges.find(edge => `${edge.source}-${edge.target}` === edgeId);
setClickedEdge(clickedEdge || null);
}
};
const handleClosePopup = () => {
setClickedEdge(null);
setClickedNode(null);
};
const toggleTransparentEdges = () => {
setShowTransparent(prevState => !prevState);
};
if (loading) {
return React.createElement('div', null, 'Loading...');
}
const formatFilePath = (filePath) => {
const parts = filePath.split('\\');
if (filePath.length > 12 && parts[0] !== 'comb-file') {
return `${parts[0]}\\...`;
}
return filePath;
};
const filteredNodes = showTransparent ? nodes : nodes.filter(node =>
edges.some(edge => (edge.source === node.id || edge.target === node.id) && !edge.transparent)
);
const filteredEdges = showTransparent ? edges : edges.filter(edge => !edge.transparent);
const options = {
layout: { hierarchical: false },
edges: {
color: { color: '#000000', highlight: '#ff0000', hover: '#ff0000' },
arrows: { to: { enabled: true, scaleFactor: 1 } },
smooth: { type: 'cubicBezier', roundness: 0.2 },
font: { align: 'top', size: 12 },
},
nodes: {
shape: 'dot',
size: 20,
font: { size: 14, face: 'Arial' },
},
interaction: {
dragNodes: true,
hover: true,
selectConnectedEdges: false,
},
physics: {
enabled: false,
stabilization: { enabled: true, iterations: 300, updateInterval: 50 },
},
};
const graphData = {
nodes: filteredNodes.map(node => {
let label = node.label;
if (node.type === 'file' && node.label !== 'comb-file') {
label = formatFilePath(node.label);
}
return {
id: node.id,
label: label,
title: node.type === 'file' ? node.label : '',
x: node.x,
y: node.y,
shape: node.type === 'process' ? 'circle' :
node.type === 'socket' ? 'diamond' :
'box',
size: node.type === 'socket' ? 40 : 20,
font: { size: node.type === 'socket' ? 10 : 14, vadjust: node.type === 'socket' ? -50 : 0 },
color: {
background: node.transparent ? "rgba(151, 194, 252, 0.5)" : "rgb(151, 194, 252)",
border: "#2B7CE9",
highlight: { background: node.transparent ? "rgba(210, 229, 255, 0.1)" : "#D2E5FF", border: "#2B7CE9" },
},
className: node.transparent && !showTransparent ? 'transparent' : '',
};
}),
edges: filteredEdges.map(edge => ({
from: edge.source,
to: edge.target,
label: edge.label,
color: edge.alname && edge.transparent ? '#ff9999' :
edge.alname ? '#ff0000' :
edge.transparent ? '#d3d3d3' :
'#000000',
id: `${edge.source}-${edge.target}`,
font: { size: 12, align: 'horizontal', background: 'white', strokeWidth: 0 },
className: edge.transparent && !showTransparent ? 'transparent' : '',
})),
};
// Render the network visualization
return React.createElement(
'div',
{ className: 'network-container' },
React.createElement(
'button',
{ className: 'toggle-button', onClick: toggleTransparentEdges },
showTransparent ? "Hide Transparent Edges" : "Show Transparent Edges"
),
React.createElement(
'div',
{ id: 'network' },
React.createElement(vis.Network, {
graph: graphData,
options: options,
events: {
select: handleNodeClick,
doubleClick: handleEdgeClick
}
})
),
clickedNode && React.createElement('div', { className: 'popup' },
React.createElement('button', { onClick: handleClosePopup }, 'Close'),
React.createElement('h2', null, `Node: ${clickedNode.label}`),
React.createElement('p', null, `Type: ${clickedNode.type}`)
),
clickedEdge && React.createElement('div', { className: 'popup' },
React.createElement('button', { onClick: handleClosePopup }, 'Close'),
React.createElement('h2', null, `Edge: ${clickedEdge.label}`),
React.createElement('p', null, `AL Name: ${clickedEdge.alname || 'N/A'}`)
)
);
};
const rootElement = document.getElementById('root');
if (rootElement) {
ReactDOM.render(React.createElement(NetworkPage), rootElement);
} else {
console.error('Root element not found');
}
}
});
network_dashboard.css:
/* src/components/NetworkPage.css */
.network-container {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
#network-visualization {
height: 100%;
width: 100%;
}
/* Toggle button styling */
.toggle-button {
/* position: absolute;*/
top: 10px;
left: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 20px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.toggle-button:hover {
background-color: #0056b3;
}
/* Popup styling */
.popup {
background-color: white;
border: 1px solid #ccc;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
font-size: 14px;
width: 100%;
height: 100%;
position: relative;
}
/* Custom Scrollbar Styles */
.scrollable-popup {
max-height: 150px;
overflow-y: auto;
scrollbar-width: thin; /* Firefox */
scrollbar-color: transparent; /* Firefox */
}
.scrollable-popup::-webkit-scrollbar {
width: 8px; /* WebKit */
}
.scrollable-popup::-webkit-scrollbar-track {
background: transparent; /* WebKit */
}
.scrollable-popup::-webkit-scrollbar-thumb {
background: grey; /* WebKit */
border-radius: 8px;
}
.scrollable-popup::-webkit-scrollbar-thumb:hover {
background: darkgrey; /* WebKit */
}
/* Popup edge and node styling */
.popup-edge {
border: 2px solid #ff0000;
color: #333;
}
.popup-node {
border: 2px solid #007bff;
color: #007bff;
}
.close-button {
position: absolute;
top: 5px;
right: 5px;
background: transparent;
border: none;
font-size: 16px;
cursor: pointer;
}
.close-button:hover {
color: red;
}
network_dashboard.xml
<dashboard script="network_dashboard.js" stylesheet="network_dashboard.css">
<label>Network Visualization</label>
<row>
<panel>
<html>
<div id="root" style="height: 800px;"></div>
</html>
</panel>
</row>
</dashboard>
@icecreamkid98- Yes there might be a newer approach that you should choose.
I hope this helps!!
@VatsalJagani If I am trying to implement within a splunk app, then would I be able to use: https://splunkui.splunk.com/Packages/react-ui/Overview or do I need to use: https://docs.splunk.com/Documentation/Splunk/latest/AdvancedDev/CustomVizTutorial
@icecreamkid98- You can use both into your existing or new App.
I hope this helps!!! Kindly upvote if it does!!!
This is the old way of using the custom JS and CSS for react visualisation, instead can you follow new framework to develop react app