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!

Earn a $35 Gift Card for Answering our Splunk Admins & App Developer Survey

Survey for Splunk Admins and App Developers is open now! | Earn a $35 gift card!      Hello there,  Splunk ...

Continuing Innovation & New Integrations Unlock Full Stack Observability For Your ...

You’ve probably heard the latest about AppDynamics joining the Splunk Observability portfolio, deepening our ...

Monitoring Amazon Elastic Kubernetes Service (EKS)

As we’ve seen, integrating Kubernetes environments with Splunk Observability Cloud is a quick and easy way to ...