Frontent/test/explore/1 (#552)

* refactor(MultipleBrain): separate providing and data fetching

* refactor(MultipleBrain): update useBrainApi

* feat(MultipleBrains): remove unnecessary data fetchings

* test(useBrainApi): update unit tests
This commit is contained in:
Mamadou DICKO 2023-07-07 12:56:08 +02:00 committed by GitHub
parent 0dec4f6bdd
commit 3ba2c92b50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 286 additions and 280 deletions

27
frontend/app/App.tsx Normal file
View File

@ -0,0 +1,27 @@
"use client";
import { PropsWithChildren, useEffect } from "react";
import Footer from "@/lib/components/Footer";
import { NavBar } from "@/lib/components/NavBar";
import { TrackingWrapper } from "@/lib/components/TrackingWrapper";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
// This wrapper is used to make effect calls at a high level in app rendering.
export const App = ({ children }: PropsWithChildren): JSX.Element => {
const { fetchAllBrains, fetchAndSetActiveBrain } = useBrainContext();
useEffect(() => {
void fetchAllBrains();
void fetchAndSetActiveBrain();
}, []);
return (
<>
<TrackingWrapper />
<NavBar />
<div className="flex-1">{children}</div>
<Footer />
</>
);
};

View File

@ -2,7 +2,6 @@ import { UUID } from "crypto";
import { useEffect, useState } from "react";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { getBrainFromLocalStorage } from "@/lib/context/BrainProvider/helpers/brainLocalStorage";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useAxios } from "@/lib/hooks";
@ -14,25 +13,12 @@ export const useExplore = () => {
const [isPending, setIsPending] = useState(true);
const { session } = useSupabase();
const { axiosInstance } = useAxios();
const { setActiveBrain, setDefaultBrain, currentBrainId } = useBrainContext();
const { currentBrainId } = useBrainContext();
const { getBrainDocuments } = useBrainApi();
const fetchAndSetActiveBrain = async () => {
const storedBrain = getBrainFromLocalStorage();
if (storedBrain) {
setActiveBrain({ ...storedBrain });
return storedBrain;
} else {
const defaultBrain = await setDefaultBrain();
return defaultBrain;
}
};
useEffect(() => {
const fetchDocuments = async (brainId: UUID | null) => {
setIsPending(true);
await fetchAndSetActiveBrain();
try {
if (brainId === null) {
throw new Error("Brain id not found");

View File

@ -3,13 +3,12 @@ import { Analytics } from "@vercel/analytics/react";
import { Inter } from "next/font/google";
import { cookies, headers } from "next/headers";
import Footer from "@/lib/components/Footer";
import { NavBar } from "@/lib/components/NavBar";
import { TrackingWrapper } from "@/lib/components/TrackingWrapper";
import { ToastProvider } from "@/lib/components/ui/Toast";
import { BrainConfigProvider } from "@/lib/context/BrainConfigProvider";
import { BrainProvider } from "@/lib/context/BrainProvider";
import { SupabaseProvider } from "@/lib/context/SupabaseProvider";
import { App } from "./App";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
@ -43,10 +42,9 @@ const RootLayout = async ({
<SupabaseProvider session={session}>
<BrainConfigProvider>
<BrainProvider>
<TrackingWrapper />
<NavBar />
<div className="flex-1">{children}</div>
<Footer />
<App>
<div className="flex-1">{children}</div>
</App>
</BrainProvider>
</BrainConfigProvider>
</SupabaseProvider>

View File

@ -1,39 +1,15 @@
/* eslint-disable */
"use client";
import Link from "next/link";
import { useEffect } from "react";
import Button from "@/lib/components/ui/Button";
import { Divider } from "@/lib/components/ui/Divider";
import PageHeading from "@/lib/components/ui/PageHeading";
import { getBrainFromLocalStorage } from "@/lib/context/BrainProvider/helpers/brainLocalStorage";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { Crawler } from "./components/Crawler";
import { FileUploader } from "./components/FileUploader";
const UploadPage = (): JSX.Element => {
const { setActiveBrain, setDefaultBrain } = useBrainContext();
const fetchAndSetActiveBrain = async () => {
const storedBrain = getBrainFromLocalStorage();
if (storedBrain) {
setActiveBrain({ ...storedBrain });
return storedBrain;
} else {
const defaultBrain = await setDefaultBrain();
return defaultBrain;
}
};
useEffect(() => {
const fetchBrains = async () => {
await fetchAndSetActiveBrain();
};
fetchBrains();
}, []);
return (
<main className="pt-10">
<PageHeading

View File

@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import { renderHook } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
@ -9,17 +10,28 @@ const axiosGetMock = vi.fn(() => ({
},
}));
const axiosPostMock = vi.fn(() => ({
data: {
id: "123",
name: "Test Brain",
},
}));
const axiosDeleteMock = vi.fn(() => ({}));
vi.mock("@/lib/hooks", () => ({
useAxios: vi.fn(() => ({
axiosInstance: {
get: axiosGetMock,
post: axiosPostMock,
delete: axiosDeleteMock,
},
})),
}));
describe("useBrainApi", () => {
afterEach(() => {
vi.resetAllMocks();
vi.restoreAllMocks();
});
it("should call getBrainDocuments with the correct parameters", async () => {
@ -34,4 +46,67 @@ describe("useBrainApi", () => {
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith(`/explore/?brain_id=${brainId}`);
});
it("should call createBrain with the correct parameters", async () => {
const {
result: {
current: { createBrain },
},
} = renderHook(() => useBrainApi());
const name = "Test Brain";
await createBrain(name);
expect(axiosPostMock).toHaveBeenCalledTimes(1);
expect(axiosPostMock).toHaveBeenCalledWith("/brains/", { name });
});
it("should call deleteBrain with the correct parameters", async () => {
const {
result: {
current: { deleteBrain },
},
} = renderHook(() => useBrainApi());
const id = "123";
await deleteBrain(id);
expect(axiosDeleteMock).toHaveBeenCalledTimes(1);
expect(axiosDeleteMock).toHaveBeenCalledWith(`/brains/${id}/`);
});
it("should call getDefaultBrain with the correct parameters", async () => {
const {
result: {
current: { getDefaultBrain },
},
} = renderHook(() => useBrainApi());
await getDefaultBrain();
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith("/brains/default/");
});
it("should call getBrains with the correct parameters", async () => {
const {
result: {
current: { getBrains },
},
} = renderHook(() => useBrainApi());
await getBrains();
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith("/brains/");
});
it("should call getBrain with the correct parameters", async () => {
const {
result: {
current: { getBrain },
},
} = renderHook(() => useBrainApi());
const id = "123";
await getBrain(id);
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith(`/brains/${id}/`);
});
});

View File

@ -1,5 +1,6 @@
import { AxiosInstance } from "axios";
import { Brain } from "@/lib/context/BrainProvider/types";
import { Document } from "@/lib/types/Document";
export const getBrainDocuments = async (
@ -12,3 +13,49 @@ export const getBrainDocuments = async (
return response.data.documents;
};
export const createBrain = async (
name: string,
axiosInstance: AxiosInstance
): Promise<Brain> => {
const createdBrain = (await axiosInstance.post<Brain>(`/brains/`, { name }))
.data;
return createdBrain;
};
export const getBrain = async (
brainId: string,
axiosInstance: AxiosInstance
): Promise<Brain | undefined> => {
const brain = (
await axiosInstance.get<Brain | undefined>(`/brains/${brainId}/`)
).data;
return brain;
};
export const deleteBrain = async (
brainId: string,
axiosInstance: AxiosInstance
): Promise<void> => {
await axiosInstance.delete(`/brains/${brainId}/`);
};
export const getDefaultBrain = async (
axiosInstance: AxiosInstance
): Promise<Brain | undefined> => {
const defaultBrain = (await axiosInstance.get<Brain>(`/brains/default/`))
.data;
return { id: defaultBrain.id, name: defaultBrain.name };
};
export const getBrains = async (
axiosInstance: AxiosInstance
): Promise<Brain[]> => {
const brains = (await axiosInstance.get<{ brains: Brain[] }>(`/brains/`))
.data;
return brains.brains;
};

View File

@ -1,6 +1,13 @@
import { useAxios } from "@/lib/hooks";
import { getBrainDocuments } from "./brain";
import {
createBrain,
deleteBrain,
getBrain,
getBrainDocuments,
getBrains,
getDefaultBrain,
} from "./brain";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainApi = () => {
@ -9,5 +16,10 @@ export const useBrainApi = () => {
return {
getBrainDocuments: async (brainId: string) =>
getBrainDocuments(brainId, axiosInstance),
createBrain: async (name: string) => createBrain(name, axiosInstance),
deleteBrain: async (id: string) => deleteBrain(id, axiosInstance),
getDefaultBrain: async () => getDefaultBrain(axiosInstance),
getBrains: async () => getBrains(axiosInstance),
getBrain: async (id: string) => getBrain(id, axiosInstance),
};
};

View File

@ -1,76 +0,0 @@
import { AxiosInstance } from "axios";
import { UUID } from "crypto";
import { Brain } from "../context/BrainProvider/types";
export const createBrainFromBackend = async (
axiosInstance: AxiosInstance,
name: string
): Promise<Brain | undefined> => {
try {
const createdBrain = (await axiosInstance.post<Brain>(`/brains/`, { name }))
.data;
return createdBrain;
} catch (error) {
console.error(`Error creating brain ${name}`, error);
}
};
export const getUserDefaultBrainFromBackend = async (
axiosInstance: AxiosInstance
): Promise<Brain | undefined> => {
try {
const defaultBrain = (await axiosInstance.get<Brain>(`/brains/default/`))
.data;
return { id: defaultBrain.id, name: defaultBrain.name };
} catch (error) {
console.error(`Error getting user's default brain`, error);
}
};
export const getBrainFromBE = async (
axiosInstance: AxiosInstance,
brainId: UUID
): Promise<Brain | undefined> => {
try {
const brain = (await axiosInstance.get<Brain>(`/brains/${brainId}/`)).data;
return brain;
} catch (error) {
console.error(`Error getting brain ${brainId}`, error);
throw new Error(`Error getting brain ${brainId}`);
}
};
export const deleteBrainFromBE = async (
axiosInstance: AxiosInstance,
brainId: UUID
): Promise<void> => {
try {
(await axiosInstance.delete(`/brains/${brainId}/`)).data;
} catch (error) {
console.error(`Error deleting brain ${brainId}`, error);
throw new Error(`Error deleting brain ${brainId}`);
}
};
export const getAllUserBrainsFromBE = async (
axiosInstance: AxiosInstance
): Promise<Brain[] | undefined> => {
try {
const brains = (await axiosInstance.get<{ brains: Brain[] }>(`/brains/`))
.data;
console.log("BRAINS", brains);
return brains.brains;
} catch (error) {
console.error(`Error getting brain for current user}`, error);
throw new Error(`Error getting brain for current user`);
}
};

View File

@ -1 +0,0 @@
export * from "./brains";

View File

@ -2,7 +2,7 @@
import { createContext } from "react";
import { useBrainState } from "./hooks/useBrainState";
import { useBrainProvider } from "./hooks/useBrainProvider";
import { BrainContextType } from "./types";
export const BrainContext = createContext<BrainContextType | undefined>(
@ -14,9 +14,11 @@ export const BrainProvider = ({
}: {
children: React.ReactNode;
}): JSX.Element => {
const brainState = useBrainState();
const brainProviderUtils = useBrainProvider();
return (
<BrainContext.Provider value={brainState}>{children}</BrainContext.Provider>
<BrainContext.Provider value={brainProviderUtils}>
{children}
</BrainContext.Provider>
);
};

View File

@ -1,9 +1,9 @@
import { useContext } from "react";
import { BrainStateProps } from "./useBrainState";
import { BrainContext } from "../brain-provider";
import { BrainContextType } from "../types";
export const useBrainContext = (): BrainStateProps => {
export const useBrainContext = (): BrainContextType => {
const context = useContext(BrainContext);
if (context === undefined) {

View File

@ -0,0 +1,108 @@
/* eslint-disable max-lines */
import { UUID } from "crypto";
import { useCallback, useState } from "react";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { useToast } from "@/lib/hooks";
import { useEventTracking } from "@/services/analytics/useEventTracking";
import {
getBrainFromLocalStorage,
saveBrainInLocalStorage,
} from "../helpers/brainLocalStorage";
import { Brain } from "../types";
// CAUTION: This hook should be use in BrainProvider only. You may be need `useBrainContext` instead.
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainProvider = () => {
const { publish } = useToast();
const { track } = useEventTracking();
const { createBrain, deleteBrain, getBrains, getDefaultBrain } =
useBrainApi();
const [allBrains, setAllBrains] = useState<Brain[]>([]);
const [currentBrainId, setCurrentBrainId] = useState<null | UUID>(null);
const [isFetchingBrains, setIsFetchingBrains] = useState(false);
const currentBrain = allBrains.find((brain) => brain.id === currentBrainId);
const createBrainHandler = async (
name: string
): Promise<UUID | undefined> => {
const createdBrain = await createBrain(name);
try {
setAllBrains((prevBrains) => [...prevBrains, createdBrain]);
saveBrainInLocalStorage(createdBrain);
void track("BRAIN_CREATED");
return createdBrain.id;
} catch {
publish({
variant: "danger",
text: "Error occured while creating a brain",
});
}
};
const deleteBrainHandler = async (id: UUID) => {
await deleteBrain(id);
setAllBrains((prevBrains) => prevBrains.filter((brain) => brain.id !== id));
void track("DELETE_BRAIN");
};
const fetchAllBrains = useCallback(async () => {
setIsFetchingBrains(true);
try {
const brains = await getBrains();
setAllBrains(brains);
} catch (error) {
console.error(error);
} finally {
setIsFetchingBrains(false);
}
}, []);
const setActiveBrain = useCallback(
({ id, name }: { id: UUID; name: string }) => {
const newActiveBrain = { id, name };
saveBrainInLocalStorage(newActiveBrain);
setCurrentBrainId(id);
void track("CHANGE_BRAIN");
},
[]
);
const setDefaultBrain = useCallback(async () => {
const defaultBrain = await getDefaultBrain();
if (defaultBrain !== undefined) {
saveBrainInLocalStorage(defaultBrain);
setActiveBrain({ ...defaultBrain });
} else {
console.warn("No brains found");
}
}, [setActiveBrain]);
const fetchAndSetActiveBrain = useCallback(async () => {
const storedBrain = getBrainFromLocalStorage();
if (storedBrain?.id !== undefined) {
setActiveBrain({ ...storedBrain });
} else {
await setDefaultBrain();
}
}, [setDefaultBrain, setActiveBrain]);
return {
currentBrain,
currentBrainId,
allBrains,
createBrain: createBrainHandler,
deleteBrain: deleteBrainHandler,
setActiveBrain,
fetchAllBrains,
setDefaultBrain,
fetchAndSetActiveBrain,
isFetchingBrains,
setIsFetchingBrains,
};
};

View File

@ -1,148 +0,0 @@
/* eslint-disable max-lines */
import { UUID } from "crypto";
import { useCallback, useEffect, useState } from "react";
import {
createBrainFromBackend,
deleteBrainFromBE,
getAllUserBrainsFromBE,
getBrainFromBE,
getUserDefaultBrainFromBackend,
} from "@/lib/api";
import { useAxios, useToast } from "@/lib/hooks";
import { useEventTracking } from "@/services/analytics/useEventTracking";
import {
getBrainFromLocalStorage,
saveBrainInLocalStorage,
} from "../helpers/brainLocalStorage";
import { Brain } from "../types";
export interface BrainStateProps {
currentBrain: Brain | undefined;
currentBrainId: UUID | null;
allBrains: Brain[];
createBrain: (name: string) => Promise<UUID | undefined>;
deleteBrain: (id: UUID) => Promise<void>;
setActiveBrain: ({ id, name }: { id: UUID; name: string }) => void;
getBrainWithId: (brainId: UUID) => Promise<Brain>;
fetchAllBrains: () => Promise<void>;
setDefaultBrain: () => Promise<void>;
}
export const useBrainState = (): BrainStateProps => {
const { publish } = useToast();
const [allBrains, setAllBrains] = useState<Brain[]>([]);
const [currentBrainId, setCurrentBrainId] = useState<null | UUID>(null);
const { axiosInstance } = useAxios();
const { track } = useEventTracking();
const currentBrain = allBrains.find((brain) => brain.id === currentBrainId);
// options: Record<string, string | unknown>;
const createBrain = async (name: string): Promise<UUID | undefined> => {
const createdBrain = await createBrainFromBackend(axiosInstance, name);
if (createdBrain !== undefined) {
setAllBrains((prevBrains) => [...prevBrains, createdBrain]);
saveBrainInLocalStorage(createdBrain);
void track("BRAIN_CREATED");
return createdBrain.id;
} else {
publish({
variant: "danger",
text: "Error occured while creating a brain",
});
}
};
const deleteBrain = async (id: UUID) => {
await deleteBrainFromBE(axiosInstance, id);
setAllBrains((prevBrains) => prevBrains.filter((brain) => brain.id !== id));
void track("DELETE_BRAIN");
};
const getBrainWithId = async (brainId: UUID): Promise<Brain> => {
const brain =
allBrains.find(({ id }) => id === brainId) ??
(await getBrainFromBE(axiosInstance, brainId));
if (brain === undefined) {
throw new Error(`Error finding brain ${brainId}`);
}
return brain;
};
const fetchAllBrains = useCallback(async () => {
try {
console.log("Fetching all brains for a user");
const brains = await getAllUserBrainsFromBE(axiosInstance);
console.log(brains);
setAllBrains(brains ?? []);
console.log("Fetched all brains for user");
} catch (error) {
console.error(error);
}
}, [axiosInstance]);
const setActiveBrain = useCallback(
({ id, name }: { id: UUID; name: string }) => {
const newActiveBrain = { id, name };
saveBrainInLocalStorage(newActiveBrain);
setCurrentBrainId(id);
console.log("Setting active brain", newActiveBrain);
void track("CHANGE_BRAIN");
},
[]
);
const setDefaultBrain = useCallback(async () => {
console.log("Setting default brain");
const defaultBrain = await getUserDefaultBrainFromBackend(axiosInstance);
console.log("defaultBrain", defaultBrain);
if (defaultBrain) {
saveBrainInLocalStorage(defaultBrain);
setActiveBrain({ ...defaultBrain });
} else {
console.warn("No brains found");
}
}, [axiosInstance, setActiveBrain]);
const fetchAndSetActiveBrain = useCallback(async () => {
console.log(
"Fetching and setting active brain use effect in useBrainState"
);
const storedBrain = getBrainFromLocalStorage();
if (storedBrain?.id !== undefined) {
console.log("Setting active brain from local storage");
console.log("storedBrain", storedBrain);
setActiveBrain({ ...storedBrain });
} else {
console.log("Setting default brain for first time");
await setDefaultBrain();
}
}, [setDefaultBrain, setActiveBrain]);
useEffect(() => {
void fetchAllBrains();
console.log("brainId", currentBrainId);
void fetchAndSetActiveBrain();
}, [fetchAllBrains, fetchAndSetActiveBrain, currentBrainId]);
return {
currentBrain,
currentBrainId,
allBrains,
createBrain,
deleteBrain,
setActiveBrain,
getBrainWithId,
fetchAllBrains,
setDefaultBrain,
};
};

View File

@ -2,7 +2,7 @@ import { UUID } from "crypto";
import { Document } from "@/lib/types/Document";
import { useBrainState } from "./hooks/useBrainState";
import { useBrainProvider } from "./hooks/useBrainProvider";
export type Brain = {
id: UUID;
@ -14,4 +14,4 @@ export type Brain = {
temperature?: string;
};
export type BrainContextType = ReturnType<typeof useBrainState>;
export type BrainContextType = ReturnType<typeof useBrainProvider>;