Dashboards & Visualizations

Custom Javascript Visualization in Dashboard

icecreamkid98
New Member

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>

 

 

 

 

 

 

 

Labels (2)
0 Karma

VatsalJagani
SplunkTrust
SplunkTrust

@icecreamkid98- Yes there might be a newer approach that you should choose.

 

 

 

I hope this helps!!

0 Karma

icecreamkid98
New Member

@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

0 Karma

VatsalJagani
SplunkTrust
SplunkTrust

@icecreamkid98- You can use both into your existing or new App.

 

I hope this helps!!! Kindly upvote if it does!!!

0 Karma

badrinath_itrs
Communicator

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 

0 Karma
Get Updates on the Splunk Community!

Enterprise Security Content Update (ESCU) | New Releases

In December, the Splunk Threat Research Team had 1 release of new security content via the Enterprise Security ...

Why am I not seeing the finding in Splunk Enterprise Security Analyst Queue?

(This is the first of a series of 2 blogs). Splunk Enterprise Security is a fantastic tool that offers robust ...

Index This | What are the 12 Days of Splunk-mas?

December 2024 Edition Hayyy Splunk Education Enthusiasts and the Eternally Curious!  We’re back with another ...