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 (1)
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
Career Survey
First 500 qualified respondents will receive a $20 gift card! Tell us about your professional Splunk journey.
Get Updates on the Splunk Community!

Thanks for the Memories! Splunk University, .conf25, and our Community

Thank you to everyone in the Splunk Community who joined us for .conf25, which kicked off with our iconic ...

Data Persistence in the OpenTelemetry Collector

This blog post is part of an ongoing series on OpenTelemetry. What happens if the OpenTelemetry collector ...

Introducing Splunk 10.0: Smarter, Faster, and More Powerful Than Ever

Now On Demand Whether you're managing complex deployments or looking to future-proof your data ...