mirror of
https://github.com/20kaushik02/spotify-manager-web.git
synced 2025-12-06 08:54:07 +00:00
graph WiP
This commit is contained in:
parent
6d044d34d5
commit
ad25ce7f05
17
package-lock.json
generated
17
package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "spotify-manager-web",
|
"name": "spotify-manager-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
@ -2356,6 +2357,22 @@
|
|||||||
"postcss-selector-parser": "^6.0.10"
|
"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": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
|
|||||||
36
src/App.js
36
src/App.js
@ -105,23 +105,25 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<WidthContext.Provider value={width}>
|
<WidthContext.Provider value={width}>
|
||||||
<AuthContext.Provider value={auth}>
|
<AuthContext.Provider value={auth}>
|
||||||
<div className={styles.app_wrapper}>
|
<RefreshAuthContext.Provider value={refreshAuth}>
|
||||||
<BrowserRouter>
|
<div className={styles.app_wrapper}>
|
||||||
<ScrollToTop />
|
<BrowserRouter>
|
||||||
<Navbar />
|
<ScrollToTop />
|
||||||
<div className={styles.page_wrapper}>
|
<Navbar />
|
||||||
<AllRoutes />
|
<div className={styles.page_wrapper}>
|
||||||
</div>
|
<AllRoutes />
|
||||||
</BrowserRouter>
|
</div>
|
||||||
<ToastContainer
|
</BrowserRouter>
|
||||||
id={"notif-container"}
|
<ToastContainer
|
||||||
position={"bottom-center"}
|
id={"notif-container"}
|
||||||
theme={"dark"}
|
position={"bottom-center"}
|
||||||
stacked
|
theme={"dark"}
|
||||||
newestOnTop
|
stacked
|
||||||
draggable
|
newestOnTop
|
||||||
/>
|
draggable
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</RefreshAuthContext.Provider>
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
</WidthContext.Provider>
|
</WidthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -11,6 +10,5 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: calc(100%);
|
width: 100%;
|
||||||
margin-right: 10vw;
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/api/operations.js
Normal file
11
src/api/operations.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,3 +5,5 @@ export const authLogoutURL = backendDomain + "api/auth/logout"
|
|||||||
|
|
||||||
export const authHealthCheckURL = "auth-health";
|
export const authHealthCheckURL = "auth-health";
|
||||||
export const authRefreshURL = "api/auth/refresh";
|
export const authRefreshURL = "api/auth/refresh";
|
||||||
|
|
||||||
|
export const opFetchGraphURL = "api/operations/fetch";
|
||||||
|
|||||||
0
src/components/Button/Button.module.css
Normal file
0
src/components/Button/Button.module.css
Normal file
12
src/components/Button/index.jsx
Normal file
12
src/components/Button/index.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from "./Button.module.css";
|
||||||
|
|
||||||
|
const Button = ({ child }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{child}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button;
|
||||||
@ -4,10 +4,12 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 10vw;
|
width: 10vw;
|
||||||
|
position: sticky;
|
||||||
|
position: -webkit-sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow: auto;
|
||||||
padding: var(--mb-3);
|
padding: var(--mb-3);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,3 +4,14 @@
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
color: black;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useContext, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
Controls,
|
Controls,
|
||||||
@ -6,25 +6,64 @@ import {
|
|||||||
useNodesState,
|
useNodesState,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
addEdge,
|
addEdge,
|
||||||
|
Panel,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
import Dagre 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';
|
||||||
|
|
||||||
// const initialNodes = [];
|
import { showErrorToastNotification, showInfoToastNotification, showSuccessToastNotification } from '../../components/ToastNotification';
|
||||||
const initialNodes = [
|
|
||||||
{ id: '1', position: { x: 0, y: 0 }, data: { label: '1' } },
|
import { apiFetchGraph } from '../../api/operations';
|
||||||
{ id: '2', position: { x: 0, y: 100 }, data: { label: '2' } },
|
|
||||||
{ id: '3', position: { x: 50, y: 50 }, data: { label: '3' } },
|
import { RefreshAuthContext } from "../../App";
|
||||||
];
|
|
||||||
// const initialEdges = [];
|
const initialNodes = [];
|
||||||
const initialEdges = [
|
// const initialNodes = [
|
||||||
{ id: 'e1-2', source: '1', target: '2' }
|
// { 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 proOptions = { hideAttribution: true };
|
||||||
|
|
||||||
const Graph = () => {
|
const Graph = () => {
|
||||||
|
const refreshAuth = useContext(RefreshAuthContext);
|
||||||
const [playlistNodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [playlistNodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [linkEdges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [linkEdges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
@ -32,11 +71,104 @@ const Graph = () => {
|
|||||||
setEdges((eds) => addEdge(params, eds));
|
setEdges((eds) => addEdge(params, eds));
|
||||||
}, [setEdges]);
|
}, [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 (
|
return (
|
||||||
<div className={styles.graph_wrapper}>
|
<div className={styles.graph_wrapper}>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={playlistNodes}
|
nodes={playlistNodes}
|
||||||
edges={linkEdges}
|
edges={linkEdges}
|
||||||
|
defaultEdgeOptions={edgeOptions}
|
||||||
fitView
|
fitView
|
||||||
proOptions={proOptions}
|
proOptions={proOptions}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
@ -45,7 +177,14 @@ const Graph = () => {
|
|||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background variant='dots' gap={36} size={3} />
|
<Background variant='dots' gap={36} size={3} />
|
||||||
|
<Panel position="top-right">
|
||||||
|
<button onClick={() => arrangeLayout('TB')}>Arrange vertically</button>
|
||||||
|
<button onClick={() => arrangeLayout('LR')}>Arrange horizontally</button>
|
||||||
|
</Panel>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
<div className={styles.operations_wrapper}>
|
||||||
|
test
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-right: 10vw;
|
||||||
font-size: var(--headingFontSize);
|
font-size: var(--headingFontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,5 +2,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-right: 10vw;
|
||||||
font-size: var(--headingFontSize);
|
font-size: var(--headingFontSize);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,5 +2,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-right: 10vw;
|
||||||
font-size: var(--headingFontSize);
|
font-size: var(--headingFontSize);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
.pnf_wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 10vw;
|
||||||
|
font-size: var(--headingFontSize);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user