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 September, the Splunk Threat Research Team had two releases of new security content via the Enterprise ...

New in Observability - Improvements to Custom Metrics SLOs, Log Observer Connect & ...

The latest enhancements to the Splunk observability portfolio deliver improved SLO management accuracy, better ...

Improve Data Pipelines Using Splunk Data Management

  Register Now   This Tech Talk will explore the pipeline management offerings Edge Processor and Ingest ...