This commit is contained in:
Kaushik Narayan R 2025-01-04 13:44:53 -07:00
parent 6733a3be8e
commit 090ba0b085
4 changed files with 118 additions and 66 deletions

9
package-lock.json generated
View File

@ -18,6 +18,7 @@
"axios": "^1.7.9", "axios": "^1.7.9",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.4.0",
"react-router-dom": "^7.1.1", "react-router-dom": "^7.1.1",
"react-toastify": "^11.0.2", "react-toastify": "^11.0.2",
"web-vitals": "^4.2.4" "web-vitals": "^4.2.4"
@ -14582,6 +14583,14 @@
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
"dev": true "dev": true
}, },
"node_modules/react-icons": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
"integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",

View File

@ -13,6 +13,7 @@
"axios": "^1.7.9", "axios": "^1.7.9",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.4.0",
"react-router-dom": "^7.1.1", "react-router-dom": "^7.1.1",
"react-toastify": "^11.0.2", "react-toastify": "^11.0.2",
"web-vitals": "^4.2.4" "web-vitals": "^4.2.4"

View File

@ -1,4 +1,8 @@
.btn_wrapper { .btn_wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: var(--mb-2); padding: var(--mb-2);
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;

View File

@ -1,11 +1,11 @@
import React, { useCallback, useContext, useEffect } from "react"; import React, { useCallback, useContext, useEffect, useState } from "react";
import { import {
ReactFlow, ReactFlow,
Controls, Controls,
Background, Background,
useNodesState,
useEdgesState,
addEdge, addEdge,
applyNodeChanges,
applyEdgeChanges,
useReactFlow, useReactFlow,
MarkerType, MarkerType,
BackgroundVariant, BackgroundVariant,
@ -15,6 +15,8 @@ import {
type ReactFlowInstance, type ReactFlowInstance,
type Node, type Node,
type Edge, type Edge,
type OnNodesChange,
type OnEdgesChange,
type OnConnect, type OnConnect,
} from "@xyflow/react"; } from "@xyflow/react";
import Dagre, { type GraphLabel } from "@dagrejs/dagre"; import Dagre, { type GraphLabel } from "@dagrejs/dagre";
@ -22,6 +24,9 @@ import Dagre, { type GraphLabel } from "@dagrejs/dagre";
import "@xyflow/react/dist/style.css"; import "@xyflow/react/dist/style.css";
import styles from "./Graph.module.css"; import styles from "./Graph.module.css";
import { IoIosGitNetwork } from "react-icons/io";
import { WiCloudRefresh } from "react-icons/wi";
import { import {
showErrorToastNotification, showErrorToastNotification,
showInfoToastNotification, showInfoToastNotification,
@ -77,20 +82,31 @@ const proOptions: ProOptions = { hideAttribution: true };
const Graph = () => { const Graph = () => {
const refreshAuth = useContext(RefreshAuthContext); const refreshAuth = useContext(RefreshAuthContext);
const flowInstance = useReactFlow(); const flowInstance = useReactFlow();
const [playlistNodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [playlistNodes, setPlaylistNodes] = useState<Node[]>(initialNodes);
const [linkEdges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [linkEdges, setLinkEdges] = useState<Edge[]>(initialEdges);
const onFlowInit = (instance: ReactFlowInstance) => { const onFlowInit = (_instance: ReactFlowInstance) => {
console.debug("flow loaded"); console.debug("flow loaded");
}; };
const onNodesChange: OnNodesChange = useCallback(
(changes) => setPlaylistNodes((nds) => applyNodeChanges(changes, nds)),
[setPlaylistNodes]
);
const onEdgesChange: OnEdgesChange = useCallback(
(changes) => setLinkEdges((eds) => applyEdgeChanges(changes, eds)),
[setLinkEdges]
);
const onConnect: OnConnect = useCallback( const onConnect: OnConnect = useCallback(
(params) => { (connection) => {
setEdges((eds) => addEdge(params, eds)); setLinkEdges((eds) => addEdge(connection, eds));
console.debug("new connection"); console.debug(
console.debug(params); `new connection: ${connection.source} -> ${connection.target}`
);
// call API to create link
}, },
[setEdges] [setLinkEdges]
); );
type getLayoutedElementsOpts = { type getLayoutedElementsOpts = {
@ -137,7 +153,7 @@ const Graph = () => {
edges: [], edges: [],
}; };
finalLayout.edges = edges; finalLayout.edges = [...edges];
finalLayout.nodes.push( finalLayout.nodes.push(
...connectedNodes.map((node) => { ...connectedNodes.map((node) => {
const position = g.node(node.id); const position = g.node(node.id);
@ -176,68 +192,83 @@ const Graph = () => {
direction, direction,
}); });
setNodes([...layouted.nodes]); setPlaylistNodes([...layouted.nodes]);
setEdges([...layouted.edges]); setLinkEdges([...layouted.edges]);
setTimeout(flowInstance.fitView); setTimeout(flowInstance.fitView);
console.debug("layout applied"); console.debug("layout applied");
}; };
useEffect(() => { const fetchGraph = useCallback(async () => {
const fetchGraph = async () => { const resp = await apiFetchGraph();
const resp = await apiFetchGraph(); if (resp === undefined) {
if (resp === undefined) { showErrorToastNotification("Please try again after sometime");
showErrorToastNotification("Please try again after sometime"); return;
return; }
} if (resp.status === 200) {
if (resp.status === 200) { console.debug(
// place playlist nodes `graph fetched with ${resp.data.playlists?.length} nodes and ${resp.data.links?.length} edges`
setNodes( );
resp.data.playlists?.map((pl, idx) => { // place playlist nodes
return { setPlaylistNodes(
id: `${pl.playlistID}`, resp.data.playlists?.map((pl, idx) => {
position: { return {
x: id: `${pl.playlistID}`,
nodeOffsets.unconnected.origin.x + position: {
Math.floor(idx / 15) * nodeOffsets.unconnected.scaling.x, x:
y: nodeOffsets.unconnected.origin.x +
nodeOffsets.unconnected.origin.y + Math.floor(idx / 15) * nodeOffsets.unconnected.scaling.x,
Math.floor(idx % 15) * nodeOffsets.unconnected.scaling.y, y:
nodeOffsets.unconnected.origin.y +
Math.floor(idx % 15) * nodeOffsets.unconnected.scaling.y,
},
data: {
label: pl.playlistName,
metadata: {
pl,
}, },
data: { },
label: pl.playlistName, };
metadata: { }) ?? []
pl, );
}, // connect links
}, setLinkEdges(
}; resp.data.links?.map((link, idx) => {
}) ?? [] return {
); id: `${idx}`,
// connect links source: link.from,
setEdges( target: link.to,
resp.data.links?.map((link, idx) => { };
return { }) ?? []
id: `${idx}`, );
source: link.from, showInfoToastNotification("Graph updated.");
target: link.to, return;
}; }
}) ?? [] if (resp.status >= 500) {
);
showInfoToastNotification("Graph updated.");
return;
}
if (resp.status >= 500) {
showErrorToastNotification(resp.data.message);
return;
}
if (resp.status === 401) {
refreshAuth();
}
showErrorToastNotification(resp.data.message); showErrorToastNotification(resp.data.message);
return; return;
}; }
if (resp.status === 401) {
await refreshAuth();
}
showErrorToastNotification(resp.data.message);
return;
}, [refreshAuth]);
const onArrange = () => {
arrangeLayout("TB");
};
const onRefresh = async () => {
await fetchGraph();
arrangeLayout("TB");
};
useEffect(() => {
fetchGraph(); fetchGraph();
}, [refreshAuth, setEdges, setNodes]); // TODO: how to invoke async and sync fns in order correctly inside useEffect?
// onRefresh();
}, [fetchGraph]);
return ( return (
<div className={styles.graph_wrapper}> <div className={styles.graph_wrapper}>
@ -262,7 +293,14 @@ const Graph = () => {
{/* </Panel> */} {/* </Panel> */}
</ReactFlow> </ReactFlow>
<div className={styles.operations_wrapper}> <div className={styles.operations_wrapper}>
<Button onClickMethod={() => arrangeLayout("TB")}>Arrange</Button> <Button onClickMethod={onRefresh}>
<WiCloudRefresh size={36} />
Refresh
</Button>
<Button onClickMethod={onArrange}>
<IoIosGitNetwork size={36} />
Arrange
</Button>
</div> </div>
</div> </div>
); );