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);
+}