chain populating, type corrections, disable buttons when loading

This commit is contained in:
Kaushik Narayan R 2025-03-14 15:06:40 -07:00
parent 1e46c087d6
commit 3a2c23e2e3
5 changed files with 75 additions and 22 deletions

View File

@ -1,6 +1,7 @@
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import { type apiRespBaseType, axiosInstance } from "./axiosInstance.ts"; import { type apiRespBaseType, axiosInstance } from "./axiosInstance.ts";
import { import {
opBackfillChainURL,
opBackfillLinkURL, opBackfillLinkURL,
opCreateLinkURL, opCreateLinkURL,
opDeleteLinkURL, opDeleteLinkURL,
@ -43,6 +44,16 @@ interface backfillLinkDataType extends apiRespBaseType {
localNum: number; localNum: number;
} }
type backfillChainBodyType = {
root: string; // playlistID
};
interface backfillChainDataType extends apiRespBaseType {
toAddNum: number;
addedNum: number;
localNum: number;
}
type pruneLinkBodyType = createLinkBodyType; type pruneLinkBodyType = createLinkBodyType;
interface pruneLinkDataType extends apiRespBaseType { interface pruneLinkDataType extends apiRespBaseType {
@ -105,6 +116,17 @@ export const apiBackfillLink = async (
} }
}; };
export const apiBackfillChain = async (
data: backfillChainBodyType
): Promise<AxiosResponse<backfillChainDataType, any>> => {
try {
const response = await axiosInstance.put(opBackfillChainURL, data);
return response;
} catch (error: any) {
return error.response;
}
};
export const apiPruneLink = async ( export const apiPruneLink = async (
data: pruneLinkBodyType data: pruneLinkBodyType
): Promise<AxiosResponse<pruneLinkDataType, any>> => { ): Promise<AxiosResponse<pruneLinkDataType, any>> => {

View File

@ -15,4 +15,5 @@ export const opUpdateUserDataURL = "api/operations/update";
export const opCreateLinkURL = "api/operations/link"; export const opCreateLinkURL = "api/operations/link";
export const opDeleteLinkURL: "api/operations/link" = opCreateLinkURL; export const opDeleteLinkURL: "api/operations/link" = opCreateLinkURL;
export const opBackfillLinkURL = "api/operations/populate/link"; export const opBackfillLinkURL = "api/operations/populate/link";
export const opBackfillChainURL = "api/operations/populate/chain";
export const opPruneLinkURL = "api/operations/prune/link"; export const opPruneLinkURL = "api/operations/prune/link";

View File

@ -14,22 +14,19 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// because hooks (namely, useContext) can't be used outside functional components // because hooks (namely, useContext) can't be used outside functional components
// so find a better way to pass refreshAuth // so find a better way to pass refreshAuth
type APIWrapperProps<T extends apiRespBaseType> = { type APIWrapperProps<B, T extends apiRespBaseType> = {
apiFn( apiFn(data?: B, config?: AxiosRequestConfig): Promise<AxiosResponse<T, any>>;
data?: any,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T, any>>;
refreshAuth: () => Promise<boolean>; refreshAuth: () => Promise<boolean>;
data?: any; data?: B;
config?: AxiosRequestConfig; config?: AxiosRequestConfig;
}; };
const APIWrapper = async <T extends apiRespBaseType>({ const APIWrapper = async <B, T extends apiRespBaseType>({
apiFn, apiFn,
refreshAuth, refreshAuth,
data, data,
config, config,
}: APIWrapperProps<T>): Promise<AxiosResponse<T, any> | null> => { }: APIWrapperProps<B, T>): Promise<AxiosResponse<T, any> | null> => {
let apiResp; let apiResp;
for (let i = 1; i <= maxRetries + 1; i++) { for (let i = 1; i <= maxRetries + 1; i++) {
apiResp = await apiFn(data, config); apiResp = await apiFn(data, config);

View File

@ -3,11 +3,13 @@ import styles from "./Button.module.css";
type ButtonProps = { type ButtonProps = {
children: React.ReactNode; children: React.ReactNode;
disabled?: boolean;
onClickMethod?: () => void; onClickMethod?: () => void;
}; };
const Button = ({ const Button = ({
children, children,
disabled = false,
onClickMethod = () => {}, onClickMethod = () => {},
}: ButtonProps): React.ReactNode => { }: ButtonProps): React.ReactNode => {
const clickHandler = (e: React.MouseEvent) => { const clickHandler = (e: React.MouseEvent) => {
@ -15,7 +17,12 @@ const Button = ({
onClickMethod(); onClickMethod();
}; };
return ( return (
<button type="button" className={styles.btn_wrapper} onClick={clickHandler}> <button
type="button"
disabled={disabled}
className={styles.btn_wrapper}
onClick={clickHandler}
>
{children} {children}
</button> </button>
); );

View File

@ -43,6 +43,7 @@ import {
import { spotifyPlaylistLinkPrefix } from "../../api/paths.ts"; import { spotifyPlaylistLinkPrefix } from "../../api/paths.ts";
import { import {
apiBackfillChain,
apiBackfillLink, apiBackfillLink,
apiCreateLink, apiCreateLink,
apiDeleteLink, apiDeleteLink,
@ -154,7 +155,6 @@ const Graph = (): React.ReactNode => {
const onFlowInit: OnInit = (_instance) => { const onFlowInit: OnInit = (_instance) => {
console.debug("flow loaded"); console.debug("flow loaded");
console.log(selectedNodeID);
}; };
// base event handling // base event handling
@ -252,14 +252,13 @@ const Graph = (): React.ReactNode => {
[refreshAuth] [refreshAuth]
); );
// backfill link
const backfillLink = async () => { const backfillLink = async () => {
if (selectedEdgeID === "") { if (selectedEdgeID === "") {
showWarnToastNotification("Select an edge!"); showWarnToastNotification("Select a link!");
return; return;
} }
const selectedEdge = linkEdges.filter((ed) => ed.id === selectedEdgeID)[0]; const selectedEdge = linkEdges.filter((ed) => ed.id === selectedEdgeID)[0];
if (!selectedEdge) throw new ReferenceError("no edge selected"); if (!selectedEdge) throw new ReferenceError("no link selected");
setLoading(true); setLoading(true);
const resp = await APIWrapper({ const resp = await APIWrapper({
apiFn: apiBackfillLink, apiFn: apiBackfillLink,
@ -280,7 +279,34 @@ const Graph = (): React.ReactNode => {
return; return;
}; };
// prune link const backfillChain = async () => {
if (selectedNodeID === "") {
showWarnToastNotification("Select a playlist!");
return;
}
const selectedNode = playlistNodes.filter(
(nd) => nd.id === selectedNodeID
)[0];
if (!selectedNode) throw new ReferenceError("no playlist selected");
setLoading(true);
const resp = await APIWrapper({
apiFn: apiBackfillChain,
data: {
root: spotifyPlaylistLinkPrefix + selectedNodeID,
},
refreshAuth,
});
setLoading(false);
if (resp?.status === 200) {
if (resp?.data.addedNum < resp?.data.toAddNum)
showWarnToastNotification(resp?.data.message);
else showSuccessToastNotification(resp?.data.message);
return;
}
return;
};
const pruneLink = async () => { const pruneLink = async () => {
if (selectedEdgeID === "") { if (selectedEdgeID === "") {
showWarnToastNotification("Select an edge!"); showWarnToastNotification("Select an edge!");
@ -531,29 +557,29 @@ const Graph = (): React.ReactNode => {
<Panel position="top-right">{loading && <SimpleLoader />}</Panel> <Panel position="top-right">{loading && <SimpleLoader />}</Panel>
</ReactFlow> </ReactFlow>
<div className={`${styles.operations_wrapper} custom_scrollbar`}> <div className={`${styles.operations_wrapper} custom_scrollbar`}>
<Button onClickMethod={backfillLink}> <Button disabled={loading} onClickMethod={backfillLink}>
<PiSupersetOf size={36} /> <PiSupersetOf size={36} />
Backfill Link Backfill Link
</Button> </Button>
<Button> <Button disabled={loading} onClickMethod={backfillChain}>
<PiSupersetOf size={36} /> <PiSupersetOf size={36} />
Backfill Chain Backfill Chain
</Button> </Button>
<hr className={styles.divider} /> <hr className={styles.divider} />
<Button onClickMethod={pruneLink}> <Button disabled={loading} onClickMethod={pruneLink}>
<PiSubsetOf size={36} /> <PiSubsetOf size={36} />
Prune Link Prune Link
</Button> </Button>
<Button> <Button disabled={loading}>
<PiSubsetOf size={36} /> <PiSubsetOf size={36} />
Prune Link Prune Link
</Button> </Button>
<hr className={styles.divider} /> <hr className={styles.divider} />
<Button onClickMethod={() => arrangeLayout("TB")}> <Button disabled={loading} onClickMethod={() => arrangeLayout("TB")}>
<IoIosGitNetwork size={36} /> <IoIosGitNetwork size={36} />
Arrange Arrange
</Button> </Button>
<Button onClickMethod={toggleInteractive}> <Button disabled={loading} onClickMethod={toggleInteractive}>
{isInteractive() ? ( {isInteractive() ? (
<MdOutlineLock size={36} /> <MdOutlineLock size={36} />
) : ( ) : (
@ -562,14 +588,14 @@ const Graph = (): React.ReactNode => {
{isInteractive() ? "Lock" : "Unlock"} {isInteractive() ? "Lock" : "Unlock"}
</Button> </Button>
<hr className={styles.divider} /> <hr className={styles.divider} />
<Button onClickMethod={updateUserData}> <Button disabled={loading} onClickMethod={updateUserData}>
<span className={styles.icons}> <span className={styles.icons}>
<WiCloudRefresh size={36} /> <WiCloudRefresh size={36} />
<AiFillSpotify size={36} /> <AiFillSpotify size={36} />
</span> </span>
Sync Spotify Sync Spotify
</Button> </Button>
<Button onClickMethod={refreshGraph}> <Button disabled={loading} onClickMethod={refreshGraph}>
<WiCloudRefresh size={36} /> <WiCloudRefresh size={36} />
Refresh Graph Refresh Graph
</Button> </Button>