a lil abstraction, styling

This commit is contained in:
Kaushik Narayan R 2025-01-05 02:23:23 -07:00
parent f471c666e7
commit 8898fa0b9f
10 changed files with 273 additions and 135 deletions

134
package-lock.json generated
View File

@ -4041,9 +4041,9 @@
} }
}, },
"node_modules/@types/express-serve-static-core": { "node_modules/@types/express-serve-static-core": {
"version": "5.0.2", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.3.tgz",
"integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", "integrity": "sha512-JEhMNwUJt7bw728CydvYzntD0XJeTmDnvwLlbfbAhE7Tbslm/ax6bdIiUwTgeVlZTsJQPwZwKpAkyDtIjsvx3g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
@ -4137,9 +4137,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.10.2", "version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.20.0"
@ -7473,9 +7473,9 @@
} }
}, },
"node_modules/es-abstract": { "node_modules/es-abstract": {
"version": "1.23.8", "version": "1.23.9",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.8.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
"integrity": "sha512-lfab8IzDn6EpI1ibZakcgS6WsfEBiB+43cuJo+wgylx1xKXf+Sp+YR3vFuQwC/u3sxYwV8Cxe3B0DpVUu/WiJQ==", "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"array-buffer-byte-length": "^1.0.2", "array-buffer-byte-length": "^1.0.2",
@ -7489,10 +7489,11 @@
"es-define-property": "^1.0.1", "es-define-property": "^1.0.1",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0", "es-object-atoms": "^1.0.0",
"es-set-tostringtag": "^2.0.3", "es-set-tostringtag": "^2.1.0",
"es-to-primitive": "^1.3.0", "es-to-primitive": "^1.3.0",
"function.prototype.name": "^1.1.8", "function.prototype.name": "^1.1.8",
"get-intrinsic": "^1.2.6", "get-intrinsic": "^1.2.7",
"get-proto": "^1.0.0",
"get-symbol-description": "^1.1.0", "get-symbol-description": "^1.1.0",
"globalthis": "^1.0.4", "globalthis": "^1.0.4",
"gopd": "^1.2.0", "gopd": "^1.2.0",
@ -7513,11 +7514,12 @@
"object-inspect": "^1.13.3", "object-inspect": "^1.13.3",
"object-keys": "^1.1.1", "object-keys": "^1.1.1",
"object.assign": "^4.1.7", "object.assign": "^4.1.7",
"own-keys": "^1.0.0", "own-keys": "^1.0.1",
"regexp.prototype.flags": "^1.5.3", "regexp.prototype.flags": "^1.5.3",
"safe-array-concat": "^1.1.3", "safe-array-concat": "^1.1.3",
"safe-push-apply": "^1.0.0", "safe-push-apply": "^1.0.0",
"safe-regex-test": "^1.1.0", "safe-regex-test": "^1.1.0",
"set-proto": "^1.0.0",
"string.prototype.trim": "^1.2.10", "string.prototype.trim": "^1.2.10",
"string.prototype.trimend": "^1.0.9", "string.prototype.trimend": "^1.0.9",
"string.prototype.trimstart": "^1.0.8", "string.prototype.trimstart": "^1.0.8",
@ -7605,14 +7607,15 @@
} }
}, },
"node_modules/es-set-tostringtag": { "node_modules/es-set-tostringtag": {
"version": "2.0.3", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"get-intrinsic": "^1.2.4", "es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2", "has-tostringtag": "^1.0.2",
"hasown": "^2.0.1" "hasown": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -9039,21 +9042,21 @@
} }
}, },
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.2.6", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
"dunder-proto": "^1.0.0",
"es-define-property": "^1.0.1", "es-define-property": "^1.0.1",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0", "es-object-atoms": "^1.0.0",
"function-bind": "^1.1.2", "function-bind": "^1.1.2",
"get-proto": "^1.0.0",
"gopd": "^1.2.0", "gopd": "^1.2.0",
"has-symbols": "^1.1.0", "has-symbols": "^1.1.0",
"hasown": "^2.0.2", "hasown": "^2.0.2",
"math-intrinsics": "^1.0.0" "math-intrinsics": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -9077,6 +9080,19 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@ -9850,12 +9866,15 @@
"dev": true "dev": true
}, },
"node_modules/is-async-function": { "node_modules/is-async-function": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.0.tgz",
"integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", "integrity": "sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"has-tostringtag": "^1.0.0" "call-bound": "^1.0.3",
"get-proto": "^1.0.1",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -10025,12 +10044,15 @@
} }
}, },
"node_modules/is-generator-function": { "node_modules/is-generator-function": {
"version": "1.0.10", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
"integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"has-tostringtag": "^1.0.0" "call-bound": "^1.0.3",
"get-proto": "^1.0.0",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -10432,16 +10454,16 @@
} }
}, },
"node_modules/iterator.prototype": { "node_modules/iterator.prototype": {
"version": "1.1.4", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
"integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==", "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"define-data-property": "^1.1.4", "define-data-property": "^1.1.4",
"es-object-atoms": "^1.0.0", "es-object-atoms": "^1.0.0",
"get-intrinsic": "^1.2.6", "get-intrinsic": "^1.2.6",
"get-proto": "^1.0.0",
"has-symbols": "^1.1.0", "has-symbols": "^1.1.0",
"reflect.getprototypeof": "^1.0.8",
"set-function-name": "^2.0.2" "set-function-name": "^2.0.2"
}, },
"engines": { "engines": {
@ -14788,18 +14810,18 @@
} }
}, },
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.9", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
"integrity": "sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==", "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"call-bind": "^1.0.8", "call-bind": "^1.0.8",
"define-properties": "^1.2.1", "define-properties": "^1.2.1",
"dunder-proto": "^1.0.1", "es-abstract": "^1.23.9",
"es-abstract": "^1.23.6",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6", "es-object-atoms": "^1.0.0",
"gopd": "^1.2.0", "get-intrinsic": "^1.2.7",
"get-proto": "^1.0.1",
"which-builtin-type": "^1.2.1" "which-builtin-type": "^1.2.1"
}, },
"engines": { "engines": {
@ -14848,14 +14870,16 @@
"dev": true "dev": true
}, },
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.3", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
"integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"call-bind": "^1.0.7", "call-bind": "^1.0.8",
"define-properties": "^1.2.1", "define-properties": "^1.2.1",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"set-function-name": "^2.0.2" "set-function-name": "^2.0.2"
}, },
"engines": { "engines": {
@ -15277,9 +15301,9 @@
"dev": true "dev": true
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.83.0", "version": "1.83.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.83.0.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz",
"integrity": "sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==", "integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
@ -15656,6 +15680,20 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/set-proto": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
"integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
"dev": true,
"dependencies": {
"dunder-proto": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",

View File

@ -27,7 +27,7 @@ import { ReactFlowProvider } from "@xyflow/react";
// Contexts // Contexts
export const WidthContext = createContext(0); export const WidthContext = createContext(0);
export const AuthContext = createContext(false); export const AuthContext = createContext(false);
export const RefreshAuthContext = createContext<any>(null); export const RefreshAuthContext = createContext(async () => false);
function App() { function App() {
// States // States
@ -123,9 +123,8 @@ function App() {
<ToastContainer <ToastContainer
position={"bottom-center"} position={"bottom-center"}
theme={"dark"} theme={"dark"}
stacked
newestOnTop
draggable draggable
closeOnClick
/> />
</div> </div>
</ReactFlowProvider> </ReactFlowProvider>

View File

@ -1,9 +1,9 @@
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { apiRespBase, axiosInstance } from "./axiosInstance"; import { apiRespBaseType, axiosInstance } from "./axiosInstance";
import { authHealthCheckURL, authRefreshURL } from "./paths"; import { authHealthCheckURL, authRefreshURL } from "./paths";
export const apiAuthCheck = async (): Promise< export const apiAuthCheck = async (): Promise<
AxiosResponse<apiRespBase, any> AxiosResponse<apiRespBaseType, any>
> => { > => {
try { try {
const response = await axiosInstance.get(authHealthCheckURL); const response = await axiosInstance.get(authHealthCheckURL);
@ -14,7 +14,7 @@ export const apiAuthCheck = async (): Promise<
}; };
export const apiAuthRefresh = async (): Promise< export const apiAuthRefresh = async (): Promise<
AxiosResponse<apiRespBase, any> AxiosResponse<apiRespBaseType, any>
> => { > => {
try { try {
const response = await axiosInstance.get(authRefreshURL); const response = await axiosInstance.get(authRefreshURL);

View File

@ -6,11 +6,11 @@ export const axiosInstance = axios.create({
withCredentials: true, withCredentials: true,
timeout: 20000, timeout: 20000,
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
}); });
export interface apiRespBase { export interface apiRespBaseType {
message?: string, message?: string;
errors?: any[], errors?: any[];
}; }

View File

@ -1,8 +1,8 @@
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { apiRespBase, axiosInstance } from "./axiosInstance"; import { apiRespBaseType, axiosInstance } from "./axiosInstance";
import { opFetchGraphURL } from "./paths"; import { opFetchGraphURL, opUpdateUserDataURL } from "./paths";
interface fetchGraphDataType extends apiRespBase { interface fetchGraphDataType extends apiRespBaseType {
playlists?: { playlists?: {
playlistID: string; playlistID: string;
playlistName: string; playlistName: string;
@ -13,6 +13,10 @@ interface fetchGraphDataType extends apiRespBase {
}[]; }[];
} }
interface updateUserDataType extends apiRespBaseType {
removedLinks: boolean;
}
export const apiFetchGraph = async (): Promise< export const apiFetchGraph = async (): Promise<
AxiosResponse<fetchGraphDataType, any> AxiosResponse<fetchGraphDataType, any>
> => { > => {
@ -23,3 +27,14 @@ export const apiFetchGraph = async (): Promise<
return error.response; return error.response;
} }
}; };
export const apiUpdateUserData = async (): Promise<
AxiosResponse<updateUserDataType, any>
> => {
try {
const response = await axiosInstance.put(opUpdateUserDataURL);
return response;
} catch (error: any) {
return error.response;
}
};

View File

@ -7,3 +7,4 @@ export const authHealthCheckURL = "auth-health";
export const authRefreshURL = "api/auth/refresh"; export const authRefreshURL = "api/auth/refresh";
export const opFetchGraphURL = "api/operations/fetch"; export const opFetchGraphURL = "api/operations/fetch";
export const opUpdateUserDataURL = "api/operations/update";

View File

@ -0,0 +1,55 @@
import { AxiosRequestConfig, AxiosResponse } from "axios";
import { apiRespBaseType } from "../../api/axiosInstance";
import {
showErrorToastNotification,
showWarnToastNotification,
} from "../ToastNotification";
const maxRetries = 3;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// TODO: refreshAuth fn needs to be prop drilled (well, it's not really 'drilling', but still it's a single level)
// because hooks (namely, useContext) can't be used outside functional components
// so find a better way to pass refreshAuth
type APIWrapperProps<T extends apiRespBaseType> = {
apiFn(
data?: any,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T, any>>;
refreshAuth: () => Promise<boolean>;
data?: any;
config?: AxiosRequestConfig;
};
const APIWrapper = async <T extends apiRespBaseType>({
apiFn,
refreshAuth,
data,
config,
}: APIWrapperProps<T>) => {
for (let i = 1; i <= maxRetries + 1; i++) {
const apiResp = await apiFn(data, config);
if (apiResp === undefined) {
showErrorToastNotification("Please try again after sometime");
} else if (apiResp.status === 200) {
return apiResp;
} else if (apiResp.status === 401) {
showWarnToastNotification("Session expired, refreshing...");
if (!(await refreshAuth())) {
showErrorToastNotification("Session invalid.");
return;
}
} else {
showErrorToastNotification(apiResp.data.message);
}
await sleep(i * i * 1000);
}
showErrorToastNotification("Please try again after sometime");
return;
};
export default APIWrapper;

View File

@ -8,6 +8,6 @@
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
color: var(--text); color: var(--text);
box-shadow: 8px 8px var(--bg); box-shadow: 4px 4px var(--bg);
background-color: var(--bgLinkInactive); background-color: var(--bgLinkInactive);
} }

View File

@ -11,8 +11,21 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
gap: var(--mb-3); gap: var(--mb-1);
height: 100vh; height: 100vh;
width: 10vw; width: 10vw;
padding: var(--mb-3); padding: var(--mb-3);
} }
.operations_wrapper .icons {
display: flex;
flex-direction: row;
}
.divider {
display: block;
height: 1px;
width: 100%;
margin: var(--mb-2) auto;
border-top: 1px solid white;
}

View File

@ -22,7 +22,7 @@ import {
type OnDelete, type OnDelete,
type OnBeforeDelete, type OnBeforeDelete,
} from "@xyflow/react"; } from "@xyflow/react";
import Dagre, { type GraphLabel } 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";
@ -30,16 +30,19 @@ import styles from "./Graph.module.css";
import { IoIosGitNetwork } from "react-icons/io"; import { IoIosGitNetwork } from "react-icons/io";
import { WiCloudRefresh } from "react-icons/wi"; import { WiCloudRefresh } from "react-icons/wi";
import { MdOutlineLock, MdOutlineLockOpen } from "react-icons/md"; import { MdOutlineLock, MdOutlineLockOpen } from "react-icons/md";
import { AiFillSpotify } from "react-icons/ai";
import { import {
showErrorToastNotification, showErrorToastNotification,
showInfoToastNotification, showInfoToastNotification,
showWarnToastNotification,
} from "../../components/ToastNotification"; } from "../../components/ToastNotification";
import { apiFetchGraph } from "../../api/operations"; import { apiFetchGraph, apiUpdateUserData } from "../../api/operations";
import { RefreshAuthContext } from "../../App"; import { RefreshAuthContext } from "../../App";
import Button from "../../components/Button"; import Button from "../../components/Button";
import APIWrapper from "../../components/APIWrapper";
const initialNodes: Node[] = []; const initialNodes: Node[] = [];
const initialEdges: Edge[] = []; const initialEdges: Edge[] = [];
@ -67,11 +70,13 @@ const nodeOffsets = {
}, },
}; };
interface Interactive { type Interactive = {
ndDrag: boolean; ndDrag: boolean;
ndConn: boolean; ndConn: boolean;
elsSel: boolean; elsSel: boolean;
} };
type rankdirType = "TB" | "BT" | "LR" | "RL";
const initialInteractive: Interactive = { const initialInteractive: Interactive = {
ndDrag: true, ndDrag: true,
@ -147,12 +152,12 @@ const Graph = () => {
); );
type getLayoutedElementsOpts = { type getLayoutedElementsOpts = {
direction: GraphLabel["rankdir"]; direction: rankdirType;
}; };
const getLayoutedElements = ( const getLayoutedElements = (
nodes: Node[], nodes: Node[],
edges: Edge[], edges: Edge[],
options: getLayoutedElementsOpts = { direction: "TB" } options: getLayoutedElementsOpts
) => { ) => {
const g = new Dagre.graphlib.Graph(); const g = new Dagre.graphlib.Graph();
g.setDefaultEdgeLabel(() => ({})); g.setDefaultEdgeLabel(() => ({}));
@ -224,7 +229,7 @@ const Graph = () => {
return finalLayout; return finalLayout;
}; };
const arrangeLayout = (direction: GraphLabel["rankdir"]) => { const arrangeLayout = (direction: rankdirType) => {
const layouted = getLayoutedElements(playlistNodes, linkEdges, { const layouted = getLayoutedElements(playlistNodes, linkEdges, {
direction, direction,
}); });
@ -237,66 +242,59 @@ const Graph = () => {
}; };
const fetchGraph = useCallback(async () => { const fetchGraph = useCallback(async () => {
const resp = await apiFetchGraph(); const resp = await APIWrapper({ apiFn: apiFetchGraph, refreshAuth });
if (resp === undefined) { console.debug(
showErrorToastNotification("Please try again after sometime"); `graph fetched with ${resp?.data.playlists?.length} nodes and ${resp?.data.links?.length} edges`
return; );
} // place playlist nodes
if (resp.status === 200) { setPlaylistNodes(
console.debug( resp?.data.playlists?.map((pl, idx) => {
`graph fetched with ${resp.data.playlists?.length} nodes and ${resp.data.links?.length} edges` return {
); id: `${pl.playlistID}`,
// place playlist nodes position: {
setPlaylistNodes( x:
resp.data.playlists?.map((pl, idx) => { nodeOffsets.unconnected.origin.x +
return { Math.floor(idx / 15) * nodeOffsets.unconnected.scaling.x,
id: `${pl.playlistID}`, y:
position: { nodeOffsets.unconnected.origin.y +
x: Math.floor(idx % 15) * nodeOffsets.unconnected.scaling.y,
nodeOffsets.unconnected.origin.x + },
Math.floor(idx / 15) * nodeOffsets.unconnected.scaling.x, data: {
y: label: pl.playlistName,
nodeOffsets.unconnected.origin.y + metadata: {
Math.floor(idx % 15) * nodeOffsets.unconnected.scaling.y, pl,
}, },
data: { },
label: pl.playlistName, };
metadata: { }) ?? []
pl, );
}, // connect links
}, setLinkEdges(
}; resp?.data.links?.map((link, idx) => {
}) ?? [] return {
); id: `${link.from}->${link.to}`,
// connect links source: link.from,
setLinkEdges( target: link.to,
resp.data.links?.map((link, idx) => { };
return { }) ?? []
id: `${link.from}->${link.to}`, );
source: link.from, showInfoToastNotification("Graph updated.");
target: link.to,
};
}) ?? []
);
showInfoToastNotification("Graph updated.");
return;
}
if (resp.status >= 500) {
showErrorToastNotification(resp.data.message);
return;
}
if (resp.status === 401) {
await refreshAuth();
}
showErrorToastNotification(resp.data.message);
return;
}, [refreshAuth]); }, [refreshAuth]);
const onArrange = () => { const updateUserData = async () => {
arrangeLayout("TB"); const resp = await APIWrapper({
apiFn: apiUpdateUserData,
refreshAuth,
});
showInfoToastNotification("Spotify synced.");
if (resp?.data.removedLinks)
showWarnToastNotification(
"Some links with deleted playlists were removed."
);
await refreshGraph();
}; };
const onRefresh = async () => { const refreshGraph = async () => {
await fetchGraph(); await fetchGraph();
arrangeLayout("TB"); arrangeLayout("TB");
}; };
@ -304,19 +302,30 @@ const Graph = () => {
useEffect(() => { useEffect(() => {
fetchGraph(); fetchGraph();
// TODO: how to invoke async and sync fns in order correctly inside useEffect? // TODO: how to invoke async and sync fns in order correctly inside useEffect?
// onRefresh(); // refreshGraph();
}, [fetchGraph]); }, [fetchGraph]);
const toggleInteractive = () => { const disableInteractive = () => {
setInteractive({ setInteractive({
ndDrag: !interactive.ndDrag, ndDrag: false,
ndConn: !interactive.ndConn, ndConn: false,
elsSel: !interactive.elsSel, elsSel: false,
});
};
const enableInteractive = () => {
setInteractive({
ndDrag: true,
ndConn: true,
elsSel: true,
}); });
}; };
const isInteractive = () => { const isInteractive = () => {
return interactive.ndDrag && interactive.ndConn && interactive.elsSel; return interactive.ndDrag || interactive.ndConn || interactive.elsSel;
};
const toggleInteractive = () => {
isInteractive() ? disableInteractive() : enableInteractive();
}; };
return ( return (
@ -349,11 +358,11 @@ const Graph = () => {
<Background variant={BackgroundVariant.Dots} gap={36} size={3} /> <Background variant={BackgroundVariant.Dots} gap={36} size={3} />
</ReactFlow> </ReactFlow>
<div className={styles.operations_wrapper}> <div className={styles.operations_wrapper}>
<Button onClickMethod={onRefresh}> <Button onClickMethod={refreshGraph}>
<WiCloudRefresh size={36} /> <WiCloudRefresh size={36} />
Refresh Refresh Graph
</Button> </Button>
<Button onClickMethod={onArrange}> <Button onClickMethod={() => arrangeLayout("TB")}>
<IoIosGitNetwork size={36} /> <IoIosGitNetwork size={36} />
Arrange Arrange
</Button> </Button>
@ -365,6 +374,14 @@ const Graph = () => {
)} )}
{isInteractive() ? "Lock" : "Unlock"} {isInteractive() ? "Lock" : "Unlock"}
</Button> </Button>
<hr className={styles.divider} />
<Button onClickMethod={updateUserData}>
<span className={styles.icons}>
<WiCloudRefresh size={36} />
<AiFillSpotify size={36} />
</span>
Sync Spotify
</Button>
</div> </div>
</div> </div>
); );