mirror of
https://github.com/20kaushik02/spotify-manager-web.git
synced 2025-12-06 06:44:07 +00:00
graph arranging stuff
This commit is contained in:
parent
ad25ce7f05
commit
aa5579d855
37
src/App.js
37
src/App.js
@ -18,6 +18,7 @@ import Navbar from './components/Navbar';
|
||||
import AllRoutes from './routes/AllRoutes';
|
||||
import { showErrorToastNotification, showInfoToastNotification, showWarnToastNotification } from './components/ToastNotification';
|
||||
import { apiAuthCheck, apiAuthRefresh } from './api/auth';
|
||||
import { ReactFlowProvider } from '@xyflow/react';
|
||||
|
||||
// Contexts
|
||||
export const WidthContext = createContext();
|
||||
@ -106,23 +107,25 @@ function App() {
|
||||
<WidthContext.Provider value={width}>
|
||||
<AuthContext.Provider value={auth}>
|
||||
<RefreshAuthContext.Provider value={refreshAuth}>
|
||||
<div className={styles.app_wrapper}>
|
||||
<BrowserRouter>
|
||||
<ScrollToTop />
|
||||
<Navbar />
|
||||
<div className={styles.page_wrapper}>
|
||||
<AllRoutes />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
<ToastContainer
|
||||
id={"notif-container"}
|
||||
position={"bottom-center"}
|
||||
theme={"dark"}
|
||||
stacked
|
||||
newestOnTop
|
||||
draggable
|
||||
/>
|
||||
</div>
|
||||
<ReactFlowProvider>
|
||||
<div className={styles.app_wrapper}>
|
||||
<BrowserRouter>
|
||||
<ScrollToTop />
|
||||
<Navbar />
|
||||
<div className={styles.page_wrapper}>
|
||||
<AllRoutes />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
<ToastContainer
|
||||
id={"notif-container"}
|
||||
position={"bottom-center"}
|
||||
theme={"dark"}
|
||||
stacked
|
||||
newestOnTop
|
||||
draggable
|
||||
/>
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
</RefreshAuthContext.Provider>
|
||||
</AuthContext.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";
|
||||
|
||||
const Button = ({ child }) => {
|
||||
function Button({ children, onClickMethod }) {
|
||||
const clickHandler = (e) => {
|
||||
e.preventDefault();
|
||||
onClickMethod();
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{child}
|
||||
</>
|
||||
<button type="button"
|
||||
className={styles.btn_wrapper}
|
||||
onClick={clickHandler}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -6,18 +6,20 @@ import {
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
addEdge,
|
||||
Panel,
|
||||
useReactFlow,
|
||||
MarkerType,
|
||||
} from '@xyflow/react';
|
||||
import Dagre from '@dagrejs/dagre';
|
||||
|
||||
import '@xyflow/react/dist/style.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 { RefreshAuthContext } from "../../App";
|
||||
import Button from '../../components/Button';
|
||||
|
||||
const initialNodes = [];
|
||||
// const initialNodes = [
|
||||
@ -32,68 +34,84 @@ const initialEdges = [];
|
||||
|
||||
const nodeOffsets = {
|
||||
connected: {
|
||||
origin: {
|
||||
x: 1000,
|
||||
y: 0
|
||||
},
|
||||
scaling: {
|
||||
x: 270,
|
||||
y: 90
|
||||
}
|
||||
},
|
||||
unconnected: {
|
||||
origin: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
scaling: {
|
||||
x: 180,
|
||||
y: 60
|
||||
x: 240,
|
||||
y: 80
|
||||
}
|
||||
},
|
||||
unconnected: {
|
||||
origin: {
|
||||
x: 800,
|
||||
y: 0
|
||||
},
|
||||
scaling: {
|
||||
x: 160,
|
||||
y: 40
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('@xyflow/react').DefaultEdgeOptions} */
|
||||
const edgeOptions = {
|
||||
animated: true,
|
||||
style: {
|
||||
stroke: 'white',
|
||||
stroke: "white",
|
||||
strokeWidth: 2,
|
||||
},
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: "white",
|
||||
width: 40,
|
||||
height: 40,
|
||||
}
|
||||
};
|
||||
|
||||
const proOptions = { hideAttribution: true };
|
||||
|
||||
const Graph = () => {
|
||||
const refreshAuth = useContext(RefreshAuthContext);
|
||||
const flowInstance = useReactFlow();
|
||||
const [playlistNodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [linkEdges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const onFlowInit = (instance) => {
|
||||
console.debug("flow loaded");
|
||||
}
|
||||
|
||||
const onConnect = useCallback((params) => {
|
||||
setEdges((eds) => addEdge(params, eds));
|
||||
console.debug("new connection");
|
||||
console.debug(params);
|
||||
}, [setEdges]);
|
||||
|
||||
const getLayoutedElements = (nodes, edges, options = { direction: "TB" }) => {
|
||||
const g = new Dagre.graphlib.Graph()
|
||||
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));
|
||||
|
||||
// const connectedNodes = new Set(edges.flatMap(edge => [edge.source, edge.target]));
|
||||
// const unconnectedNodes = nodes.filter(node => !connectedNodes.has(node.id));
|
||||
const connectedNodesID = new Set(edges.flatMap(edge => [edge.source, edge.target]));
|
||||
const connectedNodes = nodes.filter(node => connectedNodesID.has(node.id));
|
||||
const unconnectedNodes = nodes.filter(node => !connectedNodesID.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) => {
|
||||
let finalLayout = { edges };
|
||||
finalLayout.nodes = [
|
||||
...connectedNodes.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).
|
||||
@ -102,10 +120,31 @@ const Graph = () => {
|
||||
|
||||
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(() => {
|
||||
const fetchGraph = async () => {
|
||||
@ -120,13 +159,13 @@ const Graph = () => {
|
||||
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,
|
||||
x: nodeOffsets.unconnected.origin.x + Math.floor(idx / 15) * nodeOffsets.unconnected.scaling.x,
|
||||
y: nodeOffsets.unconnected.origin.y + Math.floor(idx % 15) * nodeOffsets.unconnected.scaling.y,
|
||||
},
|
||||
data: {
|
||||
label: pl.playlistName,
|
||||
meta: {
|
||||
name: pl.playlistName
|
||||
metadata: {
|
||||
pl
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -152,16 +191,8 @@ const Graph = () => {
|
||||
showErrorToastNotification(resp.data.message);
|
||||
return;
|
||||
}
|
||||
|
||||
fetchGraph();
|
||||
}, []);
|
||||
|
||||
const arrangeLayout = (direction) => {
|
||||
const layouted = getLayoutedElements(playlistNodes, linkEdges, { direction });
|
||||
|
||||
setNodes([...layouted.nodes]);
|
||||
setEdges([...layouted.edges]);
|
||||
}
|
||||
}, [refreshAuth, setEdges, setNodes]);
|
||||
|
||||
return (
|
||||
<div className={styles.graph_wrapper}>
|
||||
@ -169,21 +200,24 @@ const Graph = () => {
|
||||
nodes={playlistNodes}
|
||||
edges={linkEdges}
|
||||
defaultEdgeOptions={edgeOptions}
|
||||
connectionLineType="smoothstep"
|
||||
fitView
|
||||
proOptions={proOptions}
|
||||
colorMode={"light"}
|
||||
onInit={onFlowInit}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
>
|
||||
<Controls />
|
||||
<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>
|
||||
{/* <Panel position="top-right"> */}
|
||||
{/* <button onClick={() => arrangeLayout('TB')}>Arrange vertically</button> */}
|
||||
{/* <button onClick={() => arrangeLayout('LR')}>Arrange horizontally</button> */}
|
||||
{/* </Panel> */}
|
||||
</ReactFlow>
|
||||
<div className={styles.operations_wrapper}>
|
||||
test
|
||||
<Button onClickMethod={() => arrangeLayout('TB')}>Arrange</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user