diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 90dd21f93..946c79864 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -29,6 +29,7 @@ "@tabler/icons-react": "^2.18.0", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.4", + "@types/axios": "^0.14.0", "accordion": "^3.0.2", "ace-builds": "^1.16.0", "add": "^2.0.6", @@ -142,6 +143,9 @@ } }, "node_modules/@babel/compat-data": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", "version": "7.22.9", "license": "MIT", "engines": { diff --git a/src/frontend/package.json b/src/frontend/package.json index 323a9a7be..a5c4d61eb 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -24,6 +24,7 @@ "@tabler/icons-react": "^2.18.0", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.4", + "@types/axios": "^0.14.0", "accordion": "^3.0.2", "ace-builds": "^1.16.0", "add": "^2.0.6", diff --git a/src/frontend/src/components/ui/accordion.tsx b/src/frontend/src/components/ui/accordion.tsx index 4add6c8ac..27dbba93b 100644 --- a/src/frontend/src/components/ui/accordion.tsx +++ b/src/frontend/src/components/ui/accordion.tsx @@ -56,4 +56,4 @@ const AccordionContent = React.forwardRef< )); AccordionContent.displayName = AccordionPrimitive.Content.displayName; -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }; diff --git a/src/frontend/src/components/ui/card.tsx b/src/frontend/src/components/ui/card.tsx index 585a81a76..c904310cd 100644 --- a/src/frontend/src/components/ui/card.tsx +++ b/src/frontend/src/components/ui/card.tsx @@ -77,9 +77,9 @@ CardFooter.displayName = "CardFooter"; export { Card, - CardHeader, - CardFooter, - CardTitle, - CardDescription, CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, }; diff --git a/src/frontend/src/components/ui/dialog.tsx b/src/frontend/src/components/ui/dialog.tsx index 82641c063..57dda0fef 100644 --- a/src/frontend/src/components/ui/dialog.tsx +++ b/src/frontend/src/components/ui/dialog.tsx @@ -116,10 +116,10 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, - DialogTrigger, DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, }; diff --git a/src/frontend/src/components/ui/dropdown-menu.tsx b/src/frontend/src/components/ui/dropdown-menu.tsx index c61e09fc1..f9a0d87d3 100644 --- a/src/frontend/src/components/ui/dropdown-menu.tsx +++ b/src/frontend/src/components/ui/dropdown-menu.tsx @@ -182,18 +182,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; export { DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, DropdownMenuCheckboxItem, - DropdownMenuRadioItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, - DropdownMenuRadioGroup, + DropdownMenuTrigger, }; diff --git a/src/frontend/src/components/ui/menubar.tsx b/src/frontend/src/components/ui/menubar.tsx index 276f5fc5b..512bb6e63 100644 --- a/src/frontend/src/components/ui/menubar.tsx +++ b/src/frontend/src/components/ui/menubar.tsx @@ -218,19 +218,19 @@ MenubarShortcut.displayname = "MenubarShortcut"; export { Menubar, - MenubarMenu, - MenubarTrigger, - MenubarContent, - MenubarItem, - MenubarSeparator, - MenubarLabel, MenubarCheckboxItem, + MenubarContent, + MenubarGroup, + MenubarItem, + MenubarLabel, + MenubarMenu, + MenubarPortal, MenubarRadioGroup, MenubarRadioItem, - MenubarPortal, + MenubarSeparator, + MenubarShortcut, + MenubarSub, MenubarSubContent, MenubarSubTrigger, - MenubarGroup, - MenubarSub, - MenubarShortcut, + MenubarTrigger, }; diff --git a/src/frontend/src/components/ui/table.tsx b/src/frontend/src/components/ui/table.tsx index 80a74f378..eb1e3bda6 100644 --- a/src/frontend/src/components/ui/table.tsx +++ b/src/frontend/src/components/ui/table.tsx @@ -103,11 +103,11 @@ TableCaption.displayName = "TableCaption"; export { Table, - TableHeader, TableBody, + TableCaption, + TableCell, TableFooter, TableHead, + TableHeader, TableRow, - TableCell, - TableCaption, }; diff --git a/src/frontend/src/components/ui/tabs.tsx b/src/frontend/src/components/ui/tabs.tsx index 317de722c..a1f365ba6 100644 --- a/src/frontend/src/components/ui/tabs.tsx +++ b/src/frontend/src/components/ui/tabs.tsx @@ -51,4 +51,4 @@ const TabsContent = React.forwardRef< )); TabsContent.displayName = TabsPrimitive.Content.displayName; -export { Tabs, TabsList, TabsTrigger, TabsContent }; +export { Tabs, TabsContent, TabsList, TabsTrigger }; diff --git a/src/frontend/src/components/ui/tooltip.tsx b/src/frontend/src/components/ui/tooltip.tsx index c54245a53..3d31ab66d 100644 --- a/src/frontend/src/components/ui/tooltip.tsx +++ b/src/frontend/src/components/ui/tooltip.tsx @@ -28,4 +28,4 @@ const TooltipContent = React.forwardRef< )); TooltipContent.displayName = TooltipPrimitive.Content.displayName; -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; diff --git a/src/frontend/src/contexts/typesContext.tsx b/src/frontend/src/contexts/typesContext.tsx index ff4cc1c29..d4523bf60 100644 --- a/src/frontend/src/contexts/typesContext.tsx +++ b/src/frontend/src/contexts/typesContext.tsx @@ -71,18 +71,7 @@ export function TypesProvider({ children }: { children: ReactNode }) { // Clear the interval if successful. clearInterval(intervalId); } catch (error) { - retryCount++; - // On error, double the delay for the next attempt up to a maximum. - delay = Math.min(30000, delay * 2); - // Log errors but don't do anything else - the function will try again on the next interval. - console.error(error); - // Clear the old interval and start a new one with the new delay. - if (retryCount <= maxRetryCount) { - clearInterval(intervalId); - intervalId = setInterval(getTypes, delay); - } else { - console.error("Max retry attempts reached. Stopping retries."); - } + console.error("An error has occurred while fetching types."); } } diff --git a/src/frontend/src/controllers/API/api.tsx b/src/frontend/src/controllers/API/api.tsx new file mode 100644 index 000000000..8dd9eac9f --- /dev/null +++ b/src/frontend/src/controllers/API/api.tsx @@ -0,0 +1,58 @@ +import axios, { AxiosError, AxiosInstance } from "axios"; +import { useContext, useEffect, useRef } from "react"; +import { alertContext } from "../../contexts/alertContext"; + +// Create a new Axios instance +const api: AxiosInstance = axios.create({ + baseURL: "", +}); + +function ApiInterceptor() { + const retryCounts = useRef([]); + const { setErrorData } = useContext(alertContext); + + useEffect(() => { + const interceptor = api.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + let retryCount = 0; + + while (retryCount < 4) { + await sleep(5000); // Sleep for 5 seconds + retryCount++; + try { + const response = await axios.request(error.config); + return response; + } catch (error) { + if (retryCount === 3) { + setErrorData({ + title: "There was an error on web connection, please: ", + list: [ + "Refresh the page", + "Use a new flow tab", + "Check if the backend is up", + "Endpoint: " + error.config.url, + ], + }); + return Promise.reject(error); + } + } + } + } + ); + + return () => { + // Clean up the interceptor when the component unmounts + api.interceptors.response.eject(interceptor); + }; + }, [retryCounts]); + + return null; +} + +// Function to sleep for a given duration in milliseconds +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export { ApiInterceptor, api }; diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index 7668fea0a..6d24e7d09 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -1,5 +1,6 @@ -import axios, { AxiosResponse } from "axios"; +import { AxiosResponse } from "axios"; import { ReactFlowJsonObject } from "reactflow"; +import { api } from "../../controllers/API/api"; import { APIObjectType, sendAllProps } from "../../types/api/index"; import { FlowStyleType, FlowType } from "../../types/flow"; import { @@ -17,16 +18,14 @@ import { * @returns {Promise>} A promise that resolves to an AxiosResponse containing all the objects. */ export async function getAll(): Promise> { - return await axios.get(`/api/v1/all`); + return await api.get(`/api/v1/all`); } const GITHUB_API_URL = "https://api.github.com"; export async function getRepoStars(owner, repo) { try { - const response = await axios.get( - `${GITHUB_API_URL}/repos/${owner}/${repo}` - ); + const response = await api.get(`${GITHUB_API_URL}/repos/${owner}/${repo}`); return response.data.stargazers_count; } catch (error) { console.error("Error fetching repository data:", error); @@ -41,13 +40,13 @@ export async function getRepoStars(owner, repo) { * @returns {AxiosResponse} The API response. */ export async function sendAll(data: sendAllProps) { - return await axios.post(`/api/v1/predict`, data); + return await api.post(`/api/v1/predict`, data); } export async function postValidateCode( code: string ): Promise> { - return await axios.post("/api/v1/validate/code", { code }); + return await api.post("/api/v1/validate/code", { code }); } /** @@ -62,7 +61,7 @@ export async function postValidatePrompt( template: string, frontend_node: APIClassType ): Promise> { - return await axios.post("/api/v1/validate/prompt", { + return await api.post("/api/v1/validate/prompt", { name: name, template: template, frontend_node: frontend_node, @@ -77,14 +76,14 @@ export async function postValidatePrompt( export async function getExamples(): Promise { const url = "https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples?ref=main"; - const response = await axios.get(url); + const response = await api.get(url); const jsonFiles = response.data.filter((file: any) => { return file.name.endsWith(".json"); }); const contentsPromises = jsonFiles.map(async (file: any) => { - const contentResponse = await axios.get(file.download_url); + const contentResponse = await api.get(file.download_url); return contentResponse.data; }); @@ -106,11 +105,12 @@ export async function saveFlowToDatabase(newFlow: { style?: FlowStyleType; }): Promise { try { - const response = await axios.post("/api/v1/flows/", { + const response = await api.post("/api/v1/flows/", { name: newFlow.name, data: newFlow.data, description: newFlow.description, }); + if (response.status !== 201) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -131,7 +131,7 @@ export async function updateFlowInDatabase( updatedFlow: FlowType ): Promise { try { - const response = await axios.patch(`/api/v1/flows/${updatedFlow.id}`, { + const response = await api.patch(`/api/v1/flows/${updatedFlow.id}`, { name: updatedFlow.name, data: updatedFlow.data, description: updatedFlow.description, @@ -155,7 +155,7 @@ export async function updateFlowInDatabase( */ export async function readFlowsFromDatabase() { try { - const response = await axios.get("/api/v1/flows/"); + const response = await api.get("/api/v1/flows/"); if (response.status !== 200) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -168,7 +168,7 @@ export async function readFlowsFromDatabase() { export async function downloadFlowsFromDatabase() { try { - const response = await axios.get("/api/v1/flows/download/"); + const response = await api.get("/api/v1/flows/download/"); if (response.status !== 200) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -181,7 +181,7 @@ export async function downloadFlowsFromDatabase() { export async function uploadFlowsToDatabase(flows) { try { - const response = await axios.post(`/api/v1/flows/upload/`, flows); + const response = await api.post(`/api/v1/flows/upload/`, flows); if (response.status !== 201) { throw new Error(`HTTP error! status: ${response.status}`); @@ -202,7 +202,7 @@ export async function uploadFlowsToDatabase(flows) { */ export async function deleteFlowFromDatabase(flowId: string) { try { - const response = await axios.delete(`/api/v1/flows/${flowId}`); + const response = await api.delete(`/api/v1/flows/${flowId}`); if (response.status !== 200) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -222,7 +222,7 @@ export async function deleteFlowFromDatabase(flowId: string) { */ export async function getFlowFromDatabase(flowId: number) { try { - const response = await axios.get(`/api/v1/flows/${flowId}`); + const response = await api.get(`/api/v1/flows/${flowId}`); if (response.status !== 200) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -241,7 +241,7 @@ export async function getFlowFromDatabase(flowId: number) { */ export async function getFlowStylesFromDatabase() { try { - const response = await axios.get("/api/v1/flow_styles/"); + const response = await api.get("/api/v1/flow_styles/"); if (response.status !== 200) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -261,7 +261,7 @@ export async function getFlowStylesFromDatabase() { */ export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) { try { - const response = await axios.post("/api/v1/flow_styles/", flowStyle, { + const response = await api.post("/api/v1/flow_styles/", flowStyle, { headers: { accept: "application/json", "Content-Type": "application/json", @@ -284,7 +284,7 @@ export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) { * @returns {Promise>} A promise that resolves to an AxiosResponse containing the version information. */ export async function getVersion() { - const respnose = await axios.get("/api/v1/version"); + const respnose = await api.get("/api/v1/version"); return respnose.data; } @@ -294,7 +294,7 @@ export async function getVersion() { * @returns {Promise>} A promise that resolves to an AxiosResponse containing the health status. */ export async function getHealth() { - return await axios.get("/health"); // Health is the only endpoint that doesn't require /api/v1 + return await api.get("/health"); // Health is the only endpoint that doesn't require /api/v1 } /** @@ -306,7 +306,7 @@ export async function getHealth() { export async function getBuildStatus( flowId: string ): Promise { - return await axios.get(`/api/v1/build/${flowId}/status`); + return await api.get(`/api/v1/build/${flowId}/status`); } //docs for postbuildinit @@ -319,7 +319,7 @@ export async function getBuildStatus( export async function postBuildInit( flow: FlowType ): Promise> { - return await axios.post(`/api/v1/build/init/${flow.id}`, flow); + return await api.post(`/api/v1/build/init/${flow.id}`, flow); } // fetch(`/upload/${id}`, { @@ -337,5 +337,5 @@ export async function uploadFile( ): Promise> { const formData = new FormData(); formData.append("file", file); - return await axios.post(`/api/v1/upload/${id}`, formData); + return await api.post(`/api/v1/upload/${id}`, formData); } diff --git a/src/frontend/src/index.tsx b/src/frontend/src/index.tsx index 794fcbf32..f78c9da94 100644 --- a/src/frontend/src/index.tsx +++ b/src/frontend/src/index.tsx @@ -4,6 +4,7 @@ import App from "./App"; import ContextWrapper from "./contexts"; import reportWebVitals from "./reportWebVitals"; +import { ApiInterceptor } from "./controllers/API/api"; import "./index.css"; const root = ReactDOM.createRoot( @@ -13,6 +14,7 @@ root.render( + );