mirror of
https://github.com/20kaushik02/spotify-manager-web.git
synced 2025-12-06 09:34:07 +00:00
graph arranging stuff
This commit is contained in:
parent
ad25ce7f05
commit
aa5579d855
@ -18,6 +18,7 @@ import Navbar from './components/Navbar';
|
|||||||
import AllRoutes from './routes/AllRoutes';
|
import AllRoutes from './routes/AllRoutes';
|
||||||
import { showErrorToastNotification, showInfoToastNotification, showWarnToastNotification } from './components/ToastNotification';
|
import { showErrorToastNotification, showInfoToastNotification, showWarnToastNotification } from './components/ToastNotification';
|
||||||
import { apiAuthCheck, apiAuthRefresh } from './api/auth';
|
import { apiAuthCheck, apiAuthRefresh } from './api/auth';
|
||||||
|
import { ReactFlowProvider } from '@xyflow/react';
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
export const WidthContext = createContext();
|
export const WidthContext = createContext();
|
||||||
@ -106,6 +107,7 @@ function App() {
|
|||||||
<WidthContext.Provider value={width}>
|
<WidthContext.Provider value={width}>
|
||||||
<AuthContext.Provider value={auth}>
|
<AuthContext.Provider value={auth}>
|
||||||
<RefreshAuthContext.Provider value={refreshAuth}>
|
<RefreshAuthContext.Provider value={refreshAuth}>
|
||||||
|
<ReactFlowProvider>
|
||||||
<div className={styles.app_wrapper}>
|
<div className={styles.app_wrapper}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
@ -123,6 +125,7 @@ function App() {
|
|||||||
draggable
|
draggable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</ReactFlowProvider>
|
||||||
</RefreshAuthContext.Provider>
|
</RefreshAuthContext.Provider>
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
</WidthContext.Provider>
|
</WidthContext.Provider>
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
.btn_wrapper {
|
||||||
|
padding: var(--mb-2);
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text);
|
||||||
|
box-shadow: 8px 8px var(--bg);
|
||||||
|
background-color: var(--bgLinkInactive);
|
||||||
|
}
|
||||||
@ -1,11 +1,17 @@
|
|||||||
import React from 'react'
|
import React from 'react';
|
||||||
import styles from "./Button.module.css";
|
import styles from "./Button.module.css";
|
||||||
|
|
||||||
const Button = ({ child }) => {
|
function Button({ children, onClickMethod }) {
|
||||||
|
const clickHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClickMethod();
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<button type="button"
|
||||||
{child}
|
className={styles.btn_wrapper}
|
||||||
</>
|
onClick={clickHandler}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,18 +6,20 @@ import {
|
|||||||
useNodesState,
|
useNodesState,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
addEdge,
|
addEdge,
|
||||||
Panel,
|
useReactFlow,
|
||||||
|
MarkerType,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import Dagre from '@dagrejs/dagre';
|
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';
|
||||||
|
|
||||||
import { showErrorToastNotification, showInfoToastNotification, showSuccessToastNotification } from '../../components/ToastNotification';
|
import { showErrorToastNotification, showInfoToastNotification } from '../../components/ToastNotification';
|
||||||
|
|
||||||
import { apiFetchGraph } from '../../api/operations';
|
import { apiFetchGraph } from '../../api/operations';
|
||||||
|
|
||||||
import { RefreshAuthContext } from "../../App";
|
import { RefreshAuthContext } from "../../App";
|
||||||
|
import Button from '../../components/Button';
|
||||||
|
|
||||||
const initialNodes = [];
|
const initialNodes = [];
|
||||||
// const initialNodes = [
|
// const initialNodes = [
|
||||||
@ -32,68 +34,84 @@ const initialEdges = [];
|
|||||||
|
|
||||||
const nodeOffsets = {
|
const nodeOffsets = {
|
||||||
connected: {
|
connected: {
|
||||||
origin: {
|
|
||||||
x: 1000,
|
|
||||||
y: 0
|
|
||||||
},
|
|
||||||
scaling: {
|
|
||||||
x: 270,
|
|
||||||
y: 90
|
|
||||||
}
|
|
||||||
},
|
|
||||||
unconnected: {
|
|
||||||
origin: {
|
origin: {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0
|
y: 0
|
||||||
},
|
},
|
||||||
scaling: {
|
scaling: {
|
||||||
x: 180,
|
x: 240,
|
||||||
y: 60
|
y: 80
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unconnected: {
|
||||||
|
origin: {
|
||||||
|
x: 800,
|
||||||
|
y: 0
|
||||||
|
},
|
||||||
|
scaling: {
|
||||||
|
x: 160,
|
||||||
|
y: 40
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {import('@xyflow/react').DefaultEdgeOptions} */
|
||||||
const edgeOptions = {
|
const edgeOptions = {
|
||||||
animated: true,
|
animated: true,
|
||||||
style: {
|
style: {
|
||||||
stroke: 'white',
|
stroke: "white",
|
||||||
|
strokeWidth: 2,
|
||||||
},
|
},
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
color: "white",
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const proOptions = { hideAttribution: true };
|
const proOptions = { hideAttribution: true };
|
||||||
|
|
||||||
const Graph = () => {
|
const Graph = () => {
|
||||||
const refreshAuth = useContext(RefreshAuthContext);
|
const refreshAuth = useContext(RefreshAuthContext);
|
||||||
|
const flowInstance = useReactFlow();
|
||||||
const [playlistNodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [playlistNodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [linkEdges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [linkEdges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
|
const onFlowInit = (instance) => {
|
||||||
|
console.debug("flow loaded");
|
||||||
|
}
|
||||||
|
|
||||||
const onConnect = useCallback((params) => {
|
const onConnect = useCallback((params) => {
|
||||||
setEdges((eds) => addEdge(params, eds));
|
setEdges((eds) => addEdge(params, eds));
|
||||||
|
console.debug("new connection");
|
||||||
|
console.debug(params);
|
||||||
}, [setEdges]);
|
}, [setEdges]);
|
||||||
|
|
||||||
const getLayoutedElements = (nodes, edges, options = { direction: "TB" }) => {
|
const getLayoutedElements = (nodes, edges, options = { direction: "TB" }) => {
|
||||||
const g = new Dagre.graphlib.Graph()
|
const g = new Dagre.graphlib.Graph()
|
||||||
g.setDefaultEdgeLabel(() => ({}));
|
g.setDefaultEdgeLabel(() => ({}));
|
||||||
g.setGraph({ rankdir: options.direction });
|
g.setGraph({ rankdir: options.direction, nodesep: 200, edgesep: 200, ranksep: 200 });
|
||||||
|
|
||||||
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
|
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
|
||||||
|
|
||||||
// const connectedNodes = new Set(edges.flatMap(edge => [edge.source, edge.target]));
|
const connectedNodesID = new Set(edges.flatMap(edge => [edge.source, edge.target]));
|
||||||
// const unconnectedNodes = nodes.filter(node => !connectedNodes.has(node.id));
|
const connectedNodes = nodes.filter(node => connectedNodesID.has(node.id));
|
||||||
|
const unconnectedNodes = nodes.filter(node => !connectedNodesID.has(node.id));
|
||||||
|
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
// if (connectedNodes.has(node.id)) {
|
|
||||||
g.setNode(node.id, {
|
g.setNode(node.id, {
|
||||||
...node,
|
...node,
|
||||||
width: node.measured?.width ?? 0,
|
width: node.measured?.width ?? 0,
|
||||||
height: node.measured?.height ?? 0,
|
height: node.measured?.height ?? 0,
|
||||||
})
|
})
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Dagre.layout(g);
|
Dagre.layout(g);
|
||||||
|
|
||||||
return {
|
let finalLayout = { edges };
|
||||||
nodes: nodes.map((node) => {
|
finalLayout.nodes = [
|
||||||
|
...connectedNodes.map((node) => {
|
||||||
const position = g.node(node.id);
|
const position = g.node(node.id);
|
||||||
// We are shifting the dagre node position (anchor=center center) to the top left
|
// 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).
|
// so it matches the React Flow node anchor point (top left).
|
||||||
@ -102,10 +120,31 @@ const Graph = () => {
|
|||||||
|
|
||||||
return { ...node, position: { x, y } };
|
return { ...node, position: { x, y } };
|
||||||
}),
|
}),
|
||||||
edges,
|
...unconnectedNodes.map((node, idx) => {
|
||||||
|
const position = {
|
||||||
|
x: nodeOffsets.unconnected.origin.x + Math.floor(idx / 20) * nodeOffsets.unconnected.scaling.x,
|
||||||
|
y: nodeOffsets.unconnected.origin.y + Math.floor(idx % 20) * nodeOffsets.unconnected.scaling.y,
|
||||||
};
|
};
|
||||||
|
const x = position.x - (node.measured?.width ?? 0) / 2;
|
||||||
|
const y = position.y - (node.measured?.height ?? 0) / 2;
|
||||||
|
|
||||||
|
return { ...node, position: { x, y } };
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
console.debug("layout generated");
|
||||||
|
return finalLayout;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const arrangeLayout = (direction) => {
|
||||||
|
const layouted = getLayoutedElements(playlistNodes, linkEdges, { direction });
|
||||||
|
|
||||||
|
setNodes([...layouted.nodes]);
|
||||||
|
setEdges([...layouted.edges]);
|
||||||
|
|
||||||
|
setTimeout(flowInstance.fitView);
|
||||||
|
console.debug("layout applied");
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchGraph = async () => {
|
const fetchGraph = async () => {
|
||||||
@ -120,13 +159,13 @@ const Graph = () => {
|
|||||||
return {
|
return {
|
||||||
id: `${pl.playlistID}`,
|
id: `${pl.playlistID}`,
|
||||||
position: {
|
position: {
|
||||||
x: nodeOffsets.unconnected.origin.x + Math.floor(idx / 5) * nodeOffsets.unconnected.scaling.x,
|
x: nodeOffsets.unconnected.origin.x + Math.floor(idx / 15) * nodeOffsets.unconnected.scaling.x,
|
||||||
y: nodeOffsets.unconnected.origin.y + Math.floor(idx % 5) * nodeOffsets.unconnected.scaling.y,
|
y: nodeOffsets.unconnected.origin.y + Math.floor(idx % 15) * nodeOffsets.unconnected.scaling.y,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
label: pl.playlistName,
|
label: pl.playlistName,
|
||||||
meta: {
|
metadata: {
|
||||||
name: pl.playlistName
|
pl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,16 +191,8 @@ const Graph = () => {
|
|||||||
showErrorToastNotification(resp.data.message);
|
showErrorToastNotification(resp.data.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchGraph();
|
fetchGraph();
|
||||||
}, []);
|
}, [refreshAuth, setEdges, setNodes]);
|
||||||
|
|
||||||
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}>
|
||||||
@ -169,21 +200,24 @@ const Graph = () => {
|
|||||||
nodes={playlistNodes}
|
nodes={playlistNodes}
|
||||||
edges={linkEdges}
|
edges={linkEdges}
|
||||||
defaultEdgeOptions={edgeOptions}
|
defaultEdgeOptions={edgeOptions}
|
||||||
|
connectionLineType="smoothstep"
|
||||||
fitView
|
fitView
|
||||||
proOptions={proOptions}
|
proOptions={proOptions}
|
||||||
|
colorMode={"light"}
|
||||||
|
onInit={onFlowInit}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background variant='dots' gap={36} size={3} />
|
<Background variant='dots' gap={36} size={3} />
|
||||||
<Panel position="top-right">
|
{/* <Panel position="top-right"> */}
|
||||||
<button onClick={() => arrangeLayout('TB')}>Arrange vertically</button>
|
{/* <button onClick={() => arrangeLayout('TB')}>Arrange vertically</button> */}
|
||||||
<button onClick={() => arrangeLayout('LR')}>Arrange horizontally</button>
|
{/* <button onClick={() => arrangeLayout('LR')}>Arrange horizontally</button> */}
|
||||||
</Panel>
|
{/* </Panel> */}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
<div className={styles.operations_wrapper}>
|
<div className={styles.operations_wrapper}>
|
||||||
test
|
<Button onClickMethod={() => arrangeLayout('TB')}>Arrange</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user