From dfec17d894971fd10a2d2f080777ceeb424257c3 Mon Sep 17 00:00:00 2001 From: yashsinghcodes Date: Wed, 12 Nov 2025 17:03:50 +0530 Subject: [PATCH] added CorrelationGraph file --- frontend/src/components/CorrelationGraph.jsx | 680 +++++++++++++++++++ 1 file changed, 680 insertions(+) create mode 100644 frontend/src/components/CorrelationGraph.jsx diff --git a/frontend/src/components/CorrelationGraph.jsx b/frontend/src/components/CorrelationGraph.jsx new file mode 100644 index 00000000..abd266fd --- /dev/null +++ b/frontend/src/components/CorrelationGraph.jsx @@ -0,0 +1,680 @@ +import React, { useMemo, useState, useEffect, useContext, memo } from "react"; +import { toast } from 'react-toastify'; +import { getTheme } from "../theme.jsx"; +import { Context } from "../context/ContextApi.jsx"; +import ReactJson from "react-json-view-ssr"; +import { GetIconInfo, collapseField, } from "../views/Workflows2.jsx"; +import defaultCytoscapeStyle from "../defaultCytoscapeStyle.jsx"; +import { v4 as uuidv4, } from "uuid"; + +import { + Typography, + Tooltip, + IconButton, +} from "@mui/material"; + +import { + Refresh as RefreshIcon, +} from "@mui/icons-material"; + +import cytoscape from "cytoscape"; +import CytoscapeComponent from "react-cytoscapejs"; + +//import spread from "cytoscape-spread"; +import BubbleSets from 'cytoscape-bubblesets'; +import coseBilkent from 'cytoscape-cose-bilkent'; + +cytoscape.use(coseBilkent) +cytoscape.use(BubbleSets) +const svgSize = 24; + + +// Uses correlations between locations to build a graph around relationships +// In the background, this is stored in the datastore_ngram index +const CorrelationGraph = (props) => { + const { globalUrl, userdata, datastoreData, } = props; + + const {themeMode, supportEmail, brandColor} = useContext(Context) + const theme = getTheme(themeMode, brandColor) + + const [relations, setRelations] = useState(null); + const [cy, setCy] = React.useState(); + const [correlations, setCorrelations] = useState([]) + const [visualisedData, setVisualisedData] = useState(undefined) + const [selectedData, setselectedData] = useState(undefined) + const [startNode, setStartNode] = useState(undefined) + const [bb, setBb] = useState(undefined) + const [handledKeys, setHandledKeys] = useState([]) + + const cystyle = useMemo(() => defaultCytoscapeStyle(theme), [themeMode]); + const spreadLayout = { + name: 'cose-bilkent', + nodeRepulsion: 12500, + idealEdgeLength: 200, + gravity: 0.40, + numIter: 10000, + animationDuration: 500, + fit: true, + + animate: true, + } + + const loadCorrelations = (datastoreData) => { + const searchData = { + "type": "datastore", + "key": datastoreData?.key, + + "category": datastoreData?.category, + } + + if (datastoreData?.category === undefined || datastoreData?.category === null || datastoreData?.category.length < 1) { + console.log("No category for datastore item, cannot load correlations.") + return + } + + const checkKey = `${datastoreData?.category}|${datastoreData?.key}` + if (handledKeys.includes(checkKey) === true) { + //toast.warn("Correlations for this datastore item have already been loaded.") + return + } + setHandledKeys([...handledKeys, checkKey]) + + const url = `${globalUrl}/api/v2/correlations` + fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(searchData), + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson?.success === false) { + toast.warn("Failed to load correlations. Please try again or contact support@shuffler if this persists.") + } else { + if (responseJson.length === 0) { + toast.warn("No correlations found for this datastore item.") + return + } + + const parsedInfo = { + parentKey: datastoreData?.key, + parentId: datastoreData?.id, + relations: responseJson, + } + + setCorrelations(parsedInfo) + } + }) + .catch((error) => { + toast(error.toString()); + }); + } + + useEffect(() => { + if (cy === undefined) { + return + } + + if (correlations === undefined || correlations === null || correlations?.relations === undefined) { + console.log("No correlations to load into graph: ", correlations) + return + } + + var startX = 250 + var startY = 400 + var added = false + //var xyIncrement = 125 + for (var key in correlations?.relations) { + var parentValue = correlations?.relations[key] + + if (parentValue?.ref === undefined || parentValue?.ref === null || parentValue?.ref?.length < 2) { + console.log("Skipping correlation with no ref: ", value) + continue + } + + parentValue.id = `node_${parentValue.key}` + const foundnode = cy.getElementById(parentValue.id) + if (foundnode !== undefined && foundnode !== null && foundnode.length > 0) { + console.log("Parent node already exists, skipping: ", parentValue) + continue + } + + parentValue.type = "COMMENT" + parentValue.height = 40 + + //parentValue.width = 200 + parentValue.width = Math.max(125, Math.min(300, parentValue.key.length * 10)) + + parentValue.is_valid = true + parentValue.color = "white" + parentValue.label = parentValue.key + parentValue.clusters = [parentValue.id, correlations.parentId] + + const parentNodeData = { + group: "nodes", + data: parentValue, + position: { + x: startX, + y: startY, + }, + } + + cy.add(parentNodeData) + + const newRootId = uuidv4() + cy.add({ + group: "edges", + data: { + id: newRootId, + source: correlations?.parentId, + target: parentValue.id, + correlation: true, + }, + }) + + added = true + for (var relKey in parentValue?.ref) { + var refKey = parentValue?.ref[relKey] + + + //const foundnode = cy.getElementById(`node_${key}`) + //if (foundnode !== undefined && foundnode !== null && foundnode.length > 0) { + // continue + //} + const original = refKey + var category = "" + if (refKey.includes("|")) { + const splitRef = refKey.split("|") + category = splitRef[0] + + // Add the rest of them to key + refKey = splitRef.slice(1).join("|") + } + + var value = { + "key": refKey, + "category": category, + "relation": parentValue?.key, + + "ref": original, + } + + value.id = `node_${value.ref}` + const foundnode = cy.getElementById(value.id) + if (foundnode !== undefined && foundnode !== null && foundnode.length > 0) { + // Check if the node points to this one? + console.log("Child node already exists, skipping: ", value.id, value) + + // Verify if an edge exists + const sourceExistingEdgeId = `edge_${parentValue.id}_${value.id}` + const sourceCheck = cy.getElementById(sourceExistingEdgeId) + if (sourceCheck === undefined || sourceCheck === null || sourceCheck.length < 1) { + const destinationExistingEdgeId = `edge_${value.id}_${parentValue.id}` + const destCheck = cy.getElementById(destinationExistingEdgeId) + if (destCheck === undefined || destCheck === null || destCheck.length < 1) { + // Get source and child - make sure their clusters align + const sourceNode = cy.getElementById(parentValue.id) + if (sourceNode === undefined || sourceNode === null || sourceNode.length < 1) { + } + + cy.add({ + group: "edges", + data: { + id: sourceExistingEdgeId, + source: parentValue.id, + target: value.id, + + correlation: true, + }, + }) + } + } else { + //console.log("Edge already exists between parent and child: ", existingEdgeId) + } + + continue + } + + value.height = 40 + value.width = 40 + value.clusters = [value.id, parentValue.id] + + const iconInfo = GetIconInfo({ + "app_name": category, + "name": category, + }) + + //console.log("Iconinfo: ", category, iconInfo) + if (iconInfo?.originalIcon === undefined || iconInfo?.originalIcon === null || iconInfo?.originalIcon.length < 2) { + value.type = "CORRELATION" + + // Get text length from value.key + //value.width = Math.max(125, Math.min(300, value.key.length * 10)) + value.backgroundcolor = randomColor() + } else { + + value.type = "CORRELATION" + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + value.large_image = svgpin_Url + if (iconInfo.icon === undefined || iconInfo.icon === null || iconInfo.icon.length < 1) { + console.log("Handle iconInfo.originalIcon instead") + } + + value.fillGradient = iconInfo.fillGradient + value.fillstyle = "solid" + value.app_name = "Shuffle Tools" + + if ( + value.fillGradient !== undefined && + value.fillGradient !== null && + value.fillGradient.length > 0 + ) { + value.fillstyle = "linear-gradient"; + } else { + value.iconBackground = iconInfo.iconBackgroundColor; + } + + } + + value.is_valid = true + value.color = "white" + //value.backgroundcolor = "blue" + value.label = value.key + if (value.category !== undefined && value.category !== null && value.category.length > 0) { + value.label = `${value.category}: ${value.key}` + } + + const nodeData = { + group: "nodes", + data: value, + position: { + x: startX, + y: startY, + }, + } + + cy.add(nodeData) + + const newChildRootId = `edge_${parentValue.id}_${value.id}` + cy.add({ + group: "edges", + data: { + id: newChildRootId, + source: parentValue.id, + target: value.id, + + correlation: true, + }, + }) + + added = true + } + } + + setTimeout(() => { + if (added === false) { + return + } + + cy.ready(() => { + /* + var layout = cy.makeLayout({ + name: 'circle', + radius: 300, + avoidOverlap: false, + startAngle: 3.14, + sweep: undefined, + clockwise: true, + + animate: true, + }) + */ + + var layout = cy.makeLayout(spreadLayout) + layout.run() + }) + }, 500) + + }, [correlations]) + + useEffect(() => { + var removeFields = ["type", "height", "width", "is_valid", "color", "label", "backgroundcolor", "success", "encrypted", "public_authorization", "suborg_distribution", "revision_id", "large_image", "fillstyle", "app_name", "fillGradient", ] + if (visualisedData !== undefined) { + var cleanedData = JSON.parse(JSON.stringify(visualisedData)) + for (var i = 0; i < removeFields.length; i++) { + delete cleanedData[removeFields[i]] + } + + setVisualisedData(cleanedData) + } + + if (selectedData !== undefined) { + var cleanedData2 = JSON.parse(JSON.stringify(selectedData)) + for (var j = 0; j < removeFields.length; j++) { + delete cleanedData2[removeFields[j]] + } + + setselectedData(cleanedData2) + } + + }, [visualisedData, selectedData]) + + const onNodeHover = (event) => { + const nodedata = JSON.parse(JSON.stringify(event.target.data())) + if (nodedata === undefined || nodedata === null) { + console.log("No node data on hover") + return + } + + const cytoscapeElement = document.getElementById("cytoscape_view") + if (cytoscapeElement !== undefined && cytoscapeElement !== null) { + if (nodedata?.category === undefined || nodedata?.category === null || nodedata?.category.length < 1) { + // Means it is a value + } else { + cytoscapeElement.style.cursor = "pointer" + } + } + + // Only do it 1/20 times as it's super fast anyway + // This is due to state management within events being weird + if (Math.random() < 0.95) { + setVisualisedData(nodedata) + } + + if (bb === undefined || bb === null) { + console.log("BB undefined (2)") + return + } + + if (nodedata?.clusters === undefined || nodedata?.clusters === null) { + toast.warn("No cluster data for this node.") + return + } + + for (var clusterKey in nodedata?.clusters) { + const cluster = nodedata?.clusters[clusterKey] + + var nodesInCluster = cy.nodes().filter((node) => { + const nodeData = node.data() + + // Check if it is an incoming or outgoing one + if (nodeData?.clusters === undefined || nodeData?.clusters === null) { + return false + } + + return nodeData?.clusters.includes(cluster) + }) + + const edgesInCluster = cy.edges().filter((edge) => { + const sourceNode = edge.source() + const targetNode = edge.target() + + const sourceData = sourceNode.data() + const targetData = targetNode.data() + if (sourceData?.clusters === undefined || sourceData?.clusters === null) { + return false + } + + if (targetData?.clusters === undefined || targetData?.clusters === null) { + return false + } + + return sourceData?.clusters.includes(cluster) && targetData?.clusters.includes(cluster) + }) + + bb.addPath(nodesInCluster, edgesInCluster, null) + } + + const incomingNodes = event.target.incomers('node') + bb.addPath(incomingNodes) + const outgoingNodes = event.target.outgoers('node') + bb.addPath(outgoingNodes) + } + + const onNodeHoverOut = (event) => { + const nodedata = event.target.data(); + + const cytoscapeElement = document.getElementById("cytoscape_view") + if (cytoscapeElement !== undefined && cytoscapeElement !== null) { + cytoscapeElement.style.cursor = "default" + } + + setVisualisedData(undefined) + + if (bb === undefined || bb === null) { + console.log("BB undefined") + return + } + + const activePaths = bb.getPaths() + for (var pathKey in activePaths) { + bb.removePath(activePaths[pathKey]) + } + } + + const onNodeSelect = (event) => { + const nodedata = event.target.data(); + if (nodedata === undefined || nodedata === null) { + return + } + + setselectedData(nodedata) + + loadCorrelations(nodedata) + } + + const onUnselect = (event) => { + const nodedata = event.target.data(); + if (nodedata === undefined || nodedata === null) { + return + } + + setselectedData(undefined) + } + + const randomColor = () => { + // Generate RGB values in a pleasant range (not too dark or oversaturated) + const r = 80 + Math.floor(Math.random() * 150); // 80–230 + const g = 80 + Math.floor(Math.random() * 150); + const b = 80 + Math.floor(Math.random() * 150); + + // Slightly mix towards neutral gray to make tones modern and less primary + const mix = 0.3; + const gray = (r + g + b) / 3; + const rr = Math.floor(r * (1 - mix) + gray * mix); + const gg = Math.floor(g * (1 - mix) + gray * mix); + const bb = Math.floor(b * (1 - mix) + gray * mix); + + return `rgb(${rr}, ${gg}, ${bb})`; + } + + useEffect(() => { + if (cy === undefined) { + console.log("No cy instance (yet)") + return + } + + // If cytoscape has nodes, skip + const existingNodes = cy.nodes() + if (existingNodes !== undefined && existingNodes !== null && existingNodes.length > 0) { + return + } + + if (datastoreData === undefined || datastoreData === null) { + console.log("No data for correlation graph (2): ", datastoreData) + return + } + + var parsedData = JSON.parse(JSON.stringify(datastoreData)) + + parsedData.id = `node_${datastoreData?.category}|${datastoreData?.key}` + parsedData.large_image = "/images/singul_green.png" + parsedData.type = "ACTION" + parsedData.height = 125 + parsedData.width = 125 + parsedData.color = "white" + parsedData.label = `start - ${parsedData?.category}: ${parsedData?.key}` + parsedData.clusters = [parsedData.id] + parsedData.is_valid = true + + // Default node + cy.add({ + group: "nodes", + data: parsedData, + position: { + x: 500, + y: 500, + }, + }) + + loadCorrelations(parsedData) + setStartNode(parsedData) + + const bubbleSets = cy.bubbleSets({ + zIndex: 4, + throttle: 10, + interactive: true, + + style: { + "fill": randomColor(), + }, + }) + + setBb(bubbleSets) + }, [cy]) + + useEffect(() => { + if (bb === undefined || bb === null) { + return + } + + cy.on("mouseover", "node", (e) => onNodeHover(e)) + cy.on("mouseout", "node", (e) => onNodeHoverOut(e)) + cy.on("select", "node", (e) => onNodeSelect(e)) + cy.on("unselect", "node", (e) => onUnselect(e)) + }, [bb]) + + if (datastoreData === undefined || datastoreData === null) { + console.log("No data for correlation graph: ", datastoreData) + return null + } + + const getRelation = (startNode, targetNode) => { + if (startNode?.id === targetNode?.id) { + return 0 + } + + const foundStartnode = cy.getElementById(startNode?.id) + if (foundStartnode === undefined || foundStartnode === null || foundStartnode.length < 1) { + return 0 + } + + const foundTargetnode = cy.getElementById(targetNode?.id) + if (foundTargetnode === undefined || foundTargetnode === null || foundTargetnode.length < 1) { + return 0 + } + + // Check range between them + const dijkstra = cy.elements().dijkstra(foundStartnode, (edge) => 1, false); + const distance = dijkstra.distanceTo(foundTargetnode); + if (distance === Infinity) { + return -1 + } + + if (distance <= 0) { + return 0 + } + + // Divide by 2 due to skipping "values" -> focus on keys containing them + return distance/2 + } + + const foundValue = visualisedData !== undefined ? visualisedData : selectedData !== undefined ? selectedData : undefined + const hoverName = foundValue !== undefined && foundValue?.key !== undefined ? foundValue?.key : "" + const hoverCategory = foundValue !== undefined && foundValue?.category !== undefined ? foundValue?.category : "" + const jumpsFromStart = hoverCategory === "" ? 0 : getRelation(startNode, foundValue) + + return ( +
+ + { + if (cy === undefined || cy === null) { + return + } + + cy.ready(() => { + var layout = cy.makeLayout(spreadLayout) + layout.run() + }) + }} + > + + + + + {visualisedData === undefined && selectedData === undefined ? null : +
+ + {hoverCategory}{hoverCategory === "" ? "" : ":"} {hoverName} + + {hoverCategory !== "" ? + + Degrees from start: {jumpsFromStart} + + : null} +
+ { + // return collapseField(jsonField) + //}} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} + displayDataTypes={false} + name={false} + /> +
+ } + + { + setCy(incy) + }} + /> +
+ ) +} + +export default CorrelationGraph