From ad25ce7f05d1326e958445e56628822ac8acd47d Mon Sep 17 00:00:00 2001 From: Kaushik Narayan R Date: Mon, 30 Dec 2024 00:56:24 -0700 Subject: [PATCH] graph WiP --- package-lock.json | 17 ++ package.json | 1 + src/App.js | 36 ++-- src/App.module.css | 4 +- src/api/operations.js | 11 ++ src/api/paths.js | 2 + src/components/Button/Button.module.css | 0 src/components/Button/index.jsx | 12 ++ src/components/Navbar/Navbar.module.css | 8 +- src/pages/Graph/Graph.module.css | 11 ++ src/pages/Graph/index.jsx | 161 ++++++++++++++++-- src/pages/Landing/Landing.module.css | 1 + src/pages/Login/Login.module.css | 1 + src/pages/Logout/Logout.module.css | 1 + .../PageNotFound/PageNotFound.module.css | 7 + 15 files changed, 239 insertions(+), 34 deletions(-) create mode 100644 src/api/operations.js create mode 100644 src/components/Button/Button.module.css create mode 100644 src/components/Button/index.jsx diff --git a/package-lock.json b/package-lock.json index 5a54655..e6eff3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "spotify-manager-web", "version": "0.1.0", "dependencies": { + "@dagrejs/dagre": "^1.1.4", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", @@ -2356,6 +2357,22 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@dagrejs/dagre": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz", + "integrity": "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==", + "dependencies": { + "@dagrejs/graphlib": "2.2.4" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz", + "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==", + "engines": { + "node": ">17.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", diff --git a/package.json b/package.json index 7199be4..f06a0d6 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@dagrejs/dagre": "^1.1.4", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", diff --git a/src/App.js b/src/App.js index 982d74e..1e423a1 100644 --- a/src/App.js +++ b/src/App.js @@ -105,23 +105,25 @@ function App() { return ( -
- - - -
- -
-
- -
+ +
+ + + +
+ +
+
+ +
+
); diff --git a/src/App.module.css b/src/App.module.css index da56b71..763e024 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -2,7 +2,6 @@ min-height: 100vh; width: 100vw; display: flex; - align-items: center; justify-content: space-evenly; } @@ -11,6 +10,5 @@ flex-direction: column; align-items: center; justify-content: center; - width: calc(100%); - margin-right: 10vw; + width: 100%; } diff --git a/src/api/operations.js b/src/api/operations.js new file mode 100644 index 0000000..c4bae44 --- /dev/null +++ b/src/api/operations.js @@ -0,0 +1,11 @@ +import { axiosInstance } from "./axiosInstance"; +import { opFetchGraphURL } from "./paths"; + +export const apiFetchGraph = async () => { + try { + const response = await axiosInstance.get(opFetchGraphURL); + return response; + } catch (error) { + return error.response; + } +} diff --git a/src/api/paths.js b/src/api/paths.js index 3367bcc..1be76a7 100644 --- a/src/api/paths.js +++ b/src/api/paths.js @@ -5,3 +5,5 @@ export const authLogoutURL = backendDomain + "api/auth/logout" export const authHealthCheckURL = "auth-health"; export const authRefreshURL = "api/auth/refresh"; + +export const opFetchGraphURL = "api/operations/fetch"; diff --git a/src/components/Button/Button.module.css b/src/components/Button/Button.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Button/index.jsx b/src/components/Button/index.jsx new file mode 100644 index 0000000..515b076 --- /dev/null +++ b/src/components/Button/index.jsx @@ -0,0 +1,12 @@ +import React from 'react' +import styles from "./Button.module.css"; + +const Button = ({ child }) => { + return ( + <> + {child} + + ) +} + +export default Button; diff --git a/src/components/Navbar/Navbar.module.css b/src/components/Navbar/Navbar.module.css index 66c82b2..8f2cb0b 100644 --- a/src/components/Navbar/Navbar.module.css +++ b/src/components/Navbar/Navbar.module.css @@ -4,10 +4,12 @@ flex-direction: column; align-items: center; justify-content: space-around; - position: sticky; - top: 0; - left: 0; height: 100vh; width: 10vw; + position: sticky; + position: -webkit-sticky; + top: 0; + left: 0; + overflow: auto; padding: var(--mb-3); } diff --git a/src/pages/Graph/Graph.module.css b/src/pages/Graph/Graph.module.css index 2a8ffcd..75513f8 100644 --- a/src/pages/Graph/Graph.module.css +++ b/src/pages/Graph/Graph.module.css @@ -4,3 +4,14 @@ height: 100vh; color: black; } + +.operations_wrapper { + display: flex; + background-color: var(--bgNav); + flex-direction: column; + align-items: center; + justify-content: flex-start; + height: 100vh; + width: 10vw; + padding: var(--mb-3); +} diff --git a/src/pages/Graph/index.jsx b/src/pages/Graph/index.jsx index 2af2c94..88b3627 100644 --- a/src/pages/Graph/index.jsx +++ b/src/pages/Graph/index.jsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useContext, useEffect } from 'react'; import { ReactFlow, Controls, @@ -6,25 +6,64 @@ import { useNodesState, useEdgesState, addEdge, + Panel, } from '@xyflow/react'; +import Dagre from '@dagrejs/dagre'; import '@xyflow/react/dist/style.css'; import styles from './Graph.module.css'; -// const initialNodes = []; -const initialNodes = [ - { id: '1', position: { x: 0, y: 0 }, data: { label: '1' } }, - { id: '2', position: { x: 0, y: 100 }, data: { label: '2' } }, - { id: '3', position: { x: 50, y: 50 }, data: { label: '3' } }, -]; -// const initialEdges = []; -const initialEdges = [ - { id: 'e1-2', source: '1', target: '2' } -]; +import { showErrorToastNotification, showInfoToastNotification, showSuccessToastNotification } from '../../components/ToastNotification'; + +import { apiFetchGraph } from '../../api/operations'; + +import { RefreshAuthContext } from "../../App"; + +const initialNodes = []; +// const initialNodes = [ +// { id: '1', position: { x: 0, y: 0 }, data: { label: '1' } }, +// { id: '2', position: { x: 0, y: 100 }, data: { label: '2' } }, +// { id: '3', position: { x: 50, y: 50 }, data: { label: '3' } }, +// ]; +const initialEdges = []; +// const initialEdges = [ +// { id: 'e1-2', source: '1', target: '2' } +// ]; + +const nodeOffsets = { + connected: { + origin: { + x: 1000, + y: 0 + }, + scaling: { + x: 270, + y: 90 + } + }, + unconnected: { + origin: { + x: 0, + y: 0 + }, + scaling: { + x: 180, + y: 60 + } + } +} + +const edgeOptions = { + animated: true, + style: { + stroke: 'white', + }, +}; const proOptions = { hideAttribution: true }; const Graph = () => { + const refreshAuth = useContext(RefreshAuthContext); const [playlistNodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [linkEdges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -32,11 +71,104 @@ const Graph = () => { setEdges((eds) => addEdge(params, eds)); }, [setEdges]); + const getLayoutedElements = (nodes, edges, options = { direction: "TB" }) => { + const g = new Dagre.graphlib.Graph() + g.setDefaultEdgeLabel(() => ({})); + g.setGraph({ rankdir: options.direction }); + + edges.forEach((edge) => g.setEdge(edge.source, edge.target)); + + // const connectedNodes = new Set(edges.flatMap(edge => [edge.source, edge.target])); + // const unconnectedNodes = nodes.filter(node => !connectedNodes.has(node.id)); + nodes.forEach((node) => { + // if (connectedNodes.has(node.id)) { + g.setNode(node.id, { + ...node, + width: node.measured?.width ?? 0, + height: node.measured?.height ?? 0, + }) + // } + }); + + Dagre.layout(g); + + return { + nodes: nodes.map((node) => { + const position = g.node(node.id); + // We are shifting the dagre node position (anchor=center center) to the top left + // so it matches the React Flow node anchor point (top left). + const x = position.x - (node.measured?.width ?? 0) / 2; + const y = position.y - (node.measured?.height ?? 0) / 2; + + return { ...node, position: { x, y } }; + }), + edges, + }; + }; + + + useEffect(() => { + const fetchGraph = async () => { + const resp = await apiFetchGraph(); + if (resp === undefined) { + showErrorToastNotification("Please try again after sometime"); + return; + } + if (resp.status === 200) { + // place playlist nodes + setNodes(resp.data.playlists.map((pl, idx) => { + return { + id: `${pl.playlistID}`, + position: { + x: nodeOffsets.unconnected.origin.x + Math.floor(idx / 5) * nodeOffsets.unconnected.scaling.x, + y: nodeOffsets.unconnected.origin.y + Math.floor(idx % 5) * nodeOffsets.unconnected.scaling.y, + }, + data: { + label: pl.playlistName, + meta: { + name: pl.playlistName + } + } + } + })); + // connect links + setEdges(resp.data.links.map((link, idx) => { + return { + id: `${idx}`, + source: link.from, + target: link.to + } + })); + showInfoToastNotification("Graph updated."); + return; + } + if (resp.status >= 500) { + showErrorToastNotification(resp.data.message); + return; + } + if (resp.status === 401) { + await refreshAuth(); + } + showErrorToastNotification(resp.data.message); + return; + } + + fetchGraph(); + }, []); + + const arrangeLayout = (direction) => { + const layouted = getLayoutedElements(playlistNodes, linkEdges, { direction }); + + setNodes([...layouted.nodes]); + setEdges([...layouted.edges]); + } + return (
{ > + + + + +
+ test +
) } diff --git a/src/pages/Landing/Landing.module.css b/src/pages/Landing/Landing.module.css index a5b68e3..93a6a1c 100644 --- a/src/pages/Landing/Landing.module.css +++ b/src/pages/Landing/Landing.module.css @@ -3,6 +3,7 @@ flex-direction: column; align-items: center; justify-content: center; + margin-right: 10vw; font-size: var(--headingFontSize); } diff --git a/src/pages/Login/Login.module.css b/src/pages/Login/Login.module.css index 6d148b3..e65ddd6 100644 --- a/src/pages/Login/Login.module.css +++ b/src/pages/Login/Login.module.css @@ -2,5 +2,6 @@ display: flex; align-items: center; justify-content: center; + margin-right: 10vw; font-size: var(--headingFontSize); } diff --git a/src/pages/Logout/Logout.module.css b/src/pages/Logout/Logout.module.css index 4d1cd30..3bc4758 100644 --- a/src/pages/Logout/Logout.module.css +++ b/src/pages/Logout/Logout.module.css @@ -2,5 +2,6 @@ display: flex; align-items: center; justify-content: center; + margin-right: 10vw; font-size: var(--headingFontSize); } diff --git a/src/pages/PageNotFound/PageNotFound.module.css b/src/pages/PageNotFound/PageNotFound.module.css index e69de29..e68a12b 100644 --- a/src/pages/PageNotFound/PageNotFound.module.css +++ b/src/pages/PageNotFound/PageNotFound.module.css @@ -0,0 +1,7 @@ +.pnf_wrapper { + display: flex; + align-items: center; + justify-content: center; + margin-right: 10vw; + font-size: var(--headingFontSize); +}