From 4baedc3e60bf10e9d119f8b59fd71db7e8db144b Mon Sep 17 00:00:00 2001 From: Kaushik Narayan R Date: Wed, 8 Jan 2025 02:04:50 -0700 Subject: [PATCH] link backfill and prune --- src/api/operations.ts | 48 ++++++++++++++++++-- src/api/paths.ts | 7 ++- src/components/Button/index.tsx | 6 +-- src/pages/Graph/index.tsx | 79 +++++++++++++++++++++++++++++---- 4 files changed, 123 insertions(+), 17 deletions(-) diff --git a/src/api/operations.ts b/src/api/operations.ts index 14561f3..b05ed1b 100644 --- a/src/api/operations.ts +++ b/src/api/operations.ts @@ -1,9 +1,11 @@ import { AxiosResponse } from "axios"; import { apiRespBaseType, axiosInstance } from "./axiosInstance"; import { + opBackfillLinkURL, opCreateLinkURL, opDeleteLinkURL, opFetchGraphURL, + opPruneLinkURL, opUpdateUserDataURL, } from "./paths"; @@ -27,8 +29,26 @@ type createLinkBodyType = { to: string; // playlistID }; +interface createLinkDataType extends apiRespBaseType {} + type deleteLinkBodyType = createLinkBodyType; +interface deleteLinkDataType extends apiRespBaseType {} + +type backfillLinkBodyType = createLinkBodyType; + +interface backfillLinkDataType extends apiRespBaseType { + added?: number; + local?: number; +} + +type pruneLinkBodyType = createLinkBodyType; + +interface pruneLinkDataType extends apiRespBaseType { + added?: number; + local?: number; +} + export const apiFetchGraph = async (): Promise< AxiosResponse > => { @@ -53,7 +73,7 @@ export const apiUpdateUserData = async (): Promise< export const apiCreateLink = async ( data: createLinkBodyType -): Promise> => { +): Promise> => { try { const response = await axiosInstance.post(opCreateLinkURL, data); return response; @@ -64,9 +84,31 @@ export const apiCreateLink = async ( export const apiDeleteLink = async ( data: deleteLinkBodyType -): Promise> => { +): Promise> => { try { - const response = await axiosInstance.delete(opDeleteLinkURL, { data }); + const response = await axiosInstance.delete(opDeleteLinkURL, { data }); // axios delete method doesn't take body + return response; + } catch (error: any) { + return error.response; + } +}; + +export const apiBackfillLink = async ( + data: backfillLinkBodyType +): Promise> => { + try { + const response = await axiosInstance.put(opBackfillLinkURL, data); + return response; + } catch (error: any) { + return error.response; + } +}; + +export const apiPruneLink = async ( + data: pruneLinkBodyType +): Promise> => { + try { + const response = await axiosInstance.put(opPruneLinkURL, data); return response; } catch (error: any) { return error.response; diff --git a/src/api/paths.ts b/src/api/paths.ts index 38cb629..5dbc039 100644 --- a/src/api/paths.ts +++ b/src/api/paths.ts @@ -1,7 +1,8 @@ export const backendDomain = process.env.REACT_APP_API_BASE_URL + "/"; +export const spotifyPlaylistLinkPrefix = "https://open.spotify.com/playlist/"; -export const authLoginURL = backendDomain + "api/auth/login" -export const authLogoutURL = backendDomain + "api/auth/logout" +export const authLoginURL = backendDomain + "api/auth/login"; +export const authLogoutURL = backendDomain + "api/auth/logout"; export const authHealthCheckURL = "auth-health"; export const authRefreshURL = "api/auth/refresh"; @@ -10,3 +11,5 @@ export const opFetchGraphURL = "api/operations/fetch"; export const opUpdateUserDataURL = "api/operations/update"; export const opCreateLinkURL = "api/operations/link"; export const opDeleteLinkURL = opCreateLinkURL; +export const opBackfillLinkURL = "api/operations/populate/link"; +export const opPruneLinkURL = "api/operations/prune/link"; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 6800baa..f0c3239 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -3,10 +3,10 @@ import styles from "./Button.module.css"; type ButtonProps = { children: React.ReactNode; - onClickMethod: () => void; -} + onClickMethod?: () => void; +}; -const Button = ({ children, onClickMethod }: ButtonProps) => { +const Button = ({ children, onClickMethod = () => {} }: ButtonProps) => { const clickHandler = (e: React.MouseEvent) => { e.preventDefault(); onClickMethod(); diff --git a/src/pages/Graph/index.tsx b/src/pages/Graph/index.tsx index e8330fc..981beb5 100644 --- a/src/pages/Graph/index.tsx +++ b/src/pages/Graph/index.tsx @@ -13,9 +13,9 @@ import { BackgroundVariant, type DefaultEdgeOptions, type ProOptions, - type ReactFlowInstance, type Node, type Edge, + type OnInit, type OnNodesChange, type OnEdgesChange, type OnSelectionChangeFunc, @@ -31,6 +31,7 @@ import { IoIosGitNetwork } from "react-icons/io"; import { WiCloudRefresh } from "react-icons/wi"; import { MdOutlineLock, MdOutlineLockOpen } from "react-icons/md"; import { AiFillSpotify } from "react-icons/ai"; +import { PiSupersetOf, PiSubsetOf } from "react-icons/pi"; import { showErrorToastNotification, @@ -39,10 +40,13 @@ import { showWarnToastNotification, } from "../../components/ToastNotification"; +import { spotifyPlaylistLinkPrefix } from "../../api/paths"; import { + apiBackfillLink, apiCreateLink, apiDeleteLink, apiFetchGraph, + apiPruneLink, apiUpdateUserData, } from "../../api/operations"; @@ -127,10 +131,11 @@ const Graph = () => { const flowInstance = useReactFlow(); const [playlistNodes, setPlaylistNodes] = useState(initialNodes); const [linkEdges, setLinkEdges] = useState(initialEdges); + const [selectedEdgeID, setSelectedEdgeID] = useState(""); const [interactive, setInteractive] = useState(initialInteractive); - const onFlowInit = (_instance: ReactFlowInstance) => { + const onFlowInit: OnInit = (_instance) => { console.debug("flow loaded"); }; @@ -146,10 +151,11 @@ const Graph = () => { const onFlowSelectionChange: OnSelectionChangeFunc = useCallback( ({ nodes, edges }) => { - const newSelectedID = edges[0]?.id ?? ""; + const selectedID = edges[0]?.id ?? ""; + setSelectedEdgeID(selectedID); setLinkEdges((eds) => eds.map((ed) => - ed.id === newSelectedID + ed.id === selectedID ? { ...ed, ...selectedEdgeOptions } : { ...ed, ...edgeOptions } ) @@ -197,7 +203,6 @@ const Graph = () => { `deleted connection: ${edges[0].source} -> ${edges[0].target}` ); // call API to delete link - const spotifyPlaylistLinkPrefix = "https://open.spotify.com/playlist/"; const resp = await APIWrapper({ apiFn: apiDeleteLink, data: { @@ -215,6 +220,54 @@ const Graph = () => { [refreshAuth] ); + // backfill link + const backfillLink = async () => { + if (selectedEdgeID === "") { + showWarnToastNotification("Select an edge!"); + return; + } + const selectedEdge = linkEdges.filter((ed) => ed.id === selectedEdgeID)[0]; + + const resp = await APIWrapper({ + apiFn: apiBackfillLink, + data: { + from: spotifyPlaylistLinkPrefix + selectedEdge.source, + to: spotifyPlaylistLinkPrefix + selectedEdge.target, + }, + refreshAuth, + }); + + if (resp?.status === 200) { + showSuccessToastNotification(resp?.data.message); + return; + } + return; + }; + + // prune link + const pruneLink = async () => { + if (selectedEdgeID === "") { + showWarnToastNotification("Select an edge!"); + return; + } + const selectedEdge = linkEdges.filter((ed) => ed.id === selectedEdgeID)[0]; + + const resp = await APIWrapper({ + apiFn: apiPruneLink, + data: { + from: spotifyPlaylistLinkPrefix + selectedEdge.source, + to: spotifyPlaylistLinkPrefix + selectedEdge.target, + }, + refreshAuth, + }); + + if (resp?.status === 200) { + showSuccessToastNotification(resp?.data.message); + return; + } + return; + }; + type getLayoutedElementsOpts = { direction: rankdirType; }; @@ -405,7 +458,7 @@ const Graph = () => { nodesDraggable={interactive.ndDrag} nodesConnectable={interactive.ndConn} elementsSelectable={interactive.elsSel} - deleteKeyCode={["Delete", "Backspace"]} + deleteKeyCode={["Delete"]} multiSelectionKeyCode={null} onInit={onFlowInit} onBeforeDelete={onFlowBeforeDelete} @@ -418,9 +471,13 @@ const Graph = () => {
- + +
);