link backfill and prune

This commit is contained in:
Kaushik Narayan R 2025-01-08 02:04:50 -07:00
parent c9938aca35
commit 4baedc3e60
4 changed files with 123 additions and 17 deletions

View File

@ -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<fetchGraphDataType, any>
> => {
@ -53,7 +73,7 @@ export const apiUpdateUserData = async (): Promise<
export const apiCreateLink = async (
data: createLinkBodyType
): Promise<AxiosResponse<apiRespBaseType, any>> => {
): Promise<AxiosResponse<createLinkDataType, any>> => {
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<AxiosResponse<apiRespBaseType, any>> => {
): Promise<AxiosResponse<deleteLinkDataType, any>> => {
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<AxiosResponse<backfillLinkDataType, any>> => {
try {
const response = await axiosInstance.put(opBackfillLinkURL, data);
return response;
} catch (error: any) {
return error.response;
}
};
export const apiPruneLink = async (
data: pruneLinkBodyType
): Promise<AxiosResponse<pruneLinkDataType, any>> => {
try {
const response = await axiosInstance.put(opPruneLinkURL, data);
return response;
} catch (error: any) {
return error.response;

View File

@ -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";

View File

@ -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();

View File

@ -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<Node[]>(initialNodes);
const [linkEdges, setLinkEdges] = useState<Edge[]>(initialEdges);
const [selectedEdgeID, setSelectedEdgeID] = useState<Edge["id"]>("");
const [interactive, setInteractive] =
useState<Interactive>(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 = () => {
<Background variant={BackgroundVariant.Dots} gap={36} size={3} />
</ReactFlow>
<div className={`${styles.operations_wrapper} custom_scrollbar`}>
<Button onClickMethod={refreshGraph}>
<WiCloudRefresh size={36} />
Refresh Graph
<Button onClickMethod={backfillLink}>
<PiSupersetOf size={36} />
Backfill Link
</Button>
<Button onClickMethod={pruneLink}>
<PiSubsetOf size={36} />
Prune Link
</Button>
<Button onClickMethod={() => arrangeLayout("TB")}>
<IoIosGitNetwork size={36} />
@ -442,6 +499,10 @@ const Graph = () => {
</span>
Sync Spotify
</Button>
<Button onClickMethod={refreshGraph}>
<WiCloudRefresh size={36} />
Refresh Graph
</Button>
</div>
</div>
);