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!

Stay Connected: Your Guide to February Tech Talks, Office Hours, and Webinars!

&#x1f48c; Keep the new year’s momentum going with our February lineup of Community Office Hours, Tech Talks, ...

Preparing your Splunk Environment for OpenSSL3

The Splunk platform will transition to OpenSSL version 3 in a future release. Actions are required to prepare ...

Incident Response: Reduce Incident Recurrence with Automated Ticket Creation

Culture extends beyond work experience and coffee roast preferences on software engineering teams. Team ...