feat: add user openai api key input

This commit is contained in:
mamadoudicko 2023-07-28 11:49:00 +02:00
parent 2972092900
commit 73e7e093c6
9 changed files with 262 additions and 4 deletions

View File

@ -5,7 +5,12 @@ import Button from "@/lib/components/ui/Button";
import { useApiKeyConfig } from "./hooks/useApiKeyConfig";
export const ApiKeyConfig = (): JSX.Element => {
const { apiKey, handleCopyClick, handleCreateClick } = useApiKeyConfig();
const {
apiKey,
handleCopyClick,
handleCreateClick,
} = useApiKeyConfig();
return (
<>

View File

@ -6,6 +6,7 @@ import { useEventTracking } from "@/services/analytics/useEventTracking";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useApiKeyConfig = () => {
const [apiKey, setApiKey] = useState("");
const [openAiApiKey, setOpenAiApiKey] = useState("");
const { track } = useEventTracking();
const { createApiKey } = useAuthApi();
@ -38,5 +39,7 @@ export const useApiKeyConfig = () => {
handleCreateClick,
apiKey,
handleCopyClick,
openAiApiKey,
setOpenAiApiKey,
};
};

View File

@ -1,12 +1,25 @@
/* eslint-disable max-lines */
"use client";
import Button from "@/lib/components/ui/Button";
import { Divider } from "@/lib/components/ui/Divider";
import Field from "@/lib/components/ui/Field";
import { useApiKeyConfig } from "./hooks/useApiKeyConfig";
export const ApiKeyConfig = (): JSX.Element => {
const { apiKey, handleCopyClick, handleCreateClick } = useApiKeyConfig();
const {
apiKey,
handleCopyClick,
handleCreateClick,
openAiApiKey,
setOpenAiApiKey,
changeOpenAiApiKey,
changeOpenAiApiKeyRequestPending,
userIdentity,
removeOpenAiApiKey,
hasOpenAiApiKey,
} = useApiKeyConfig();
return (
<>
@ -36,6 +49,42 @@ export const ApiKeyConfig = (): JSX.Element => {
</div>
)}
</div>
<Divider text="OpenAI Key" className="mt-4 mb-4" />
<form
onSubmit={(event) => {
event.preventDefault();
void changeOpenAiApiKey();
}}
>
<Field
name="openAiApiKey"
placeholder="Open AI Key"
className="w-full"
value={openAiApiKey ?? ""}
data-testid="open-ai-api-key"
onChange={(e) => setOpenAiApiKey(e.target.value)}
/>
<div className="mt-4 flex flex-row justify-between">
{hasOpenAiApiKey && (
<Button
isLoading={changeOpenAiApiKeyRequestPending}
variant="secondary"
onClick={() => void removeOpenAiApiKey()}
>
Remove Key
</Button>
)}
<Button
data-testid="save-open-ai-api-key"
isLoading={changeOpenAiApiKeyRequestPending}
disabled={openAiApiKey === userIdentity?.openai_api_key}
>
Save Key
</Button>
</div>
</form>
</>
);
};

View File

@ -22,8 +22,11 @@ describe("ApiKeyConfig", () => {
});
it("should render ApiConfig Component", () => {
const { getByText } = render(<ApiKeyConfig />);
const { getByText, getByTestId } = render(<ApiKeyConfig />);
expect(getByText("API Key Config")).toBeDefined();
expect(getByText("OpenAI Key")).toBeDefined();
expect(getByTestId("open-ai-api-key")).toBeDefined();
expect(getByTestId("save-open-ai-api-key")).toBeDefined();
});
it("renders 'Create New Key' button when apiKey is empty", () => {

View File

@ -6,6 +6,12 @@ import { useApiKeyConfig } from "../useApiKeyConfig";
const createApiKeyMock = vi.fn(() => "dummyApiKey");
const trackMock = vi.fn((props: unknown) => ({ props }));
const mockUseSupabase = vi.fn(() => ({
session: {
user: {},
},
}));
const useAuthApiMock = vi.fn(() => ({
createApiKey: () => createApiKeyMock(),
}));
@ -20,6 +26,31 @@ vi.mock("@/lib/api/auth/useAuthApi", () => ({
vi.mock("@/services/analytics/useEventTracking", () => ({
useEventTracking: () => useEventTrackingMock(),
}));
vi.mock("@/lib/context/SupabaseProvider", () => ({
useSupabase: () => mockUseSupabase(),
}));
vi.mock("@/lib/hooks", async () => {
const actual = await vi.importActual<typeof import("@/lib/hooks")>(
"@/lib/hooks"
);
return {
...actual,
useAxios: () => ({
axiosInstance: {
put: vi.fn(() => ({})),
get: vi.fn(() => ({})),
},
}),
};
});
vi.mock("@/lib/context/BrainConfigProvider", () => ({
useBrainConfig: () => ({
config: {},
}),
}));
describe("useApiKeyConfig", () => {
afterEach(() => {

View File

@ -1,13 +1,32 @@
import { useState } from "react";
/* eslint-disable max-lines */
import { useEffect, useState } from "react";
import { useAuthApi } from "@/lib/api/auth/useAuthApi";
import { useUserApi } from "@/lib/api/user/useUserApi";
import { UserIdentity } from "@/lib/api/user/user";
import { useToast } from "@/lib/hooks";
import { useEventTracking } from "@/services/analytics/useEventTracking";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useApiKeyConfig = () => {
const [apiKey, setApiKey] = useState("");
const [openAiApiKey, setOpenAiApiKey] = useState<string | null>();
const [
changeOpenAiApiKeyRequestPending,
setChangeOpenAiApiKeyRequestPending,
] = useState(false);
const { updateUserIdentity, getUserIdentity } = useUserApi();
const { track } = useEventTracking();
const { createApiKey } = useAuthApi();
const { publish } = useToast();
const [userIdentity, setUserIdentity] = useState<UserIdentity>();
const fetchUserIdentity = async () => {
setUserIdentity(await getUserIdentity());
};
useEffect(() => {
void fetchUserIdentity();
}, []);
const handleCreateClick = async () => {
try {
@ -34,9 +53,65 @@ export const useApiKeyConfig = () => {
}
};
const changeOpenAiApiKey = async () => {
try {
setChangeOpenAiApiKeyRequestPending(true);
await updateUserIdentity({
openai_api_key: openAiApiKey,
});
void fetchUserIdentity();
publish({
variant: "success",
text: "OpenAI API Key updated",
});
} catch (error) {
console.error(error);
} finally {
setChangeOpenAiApiKeyRequestPending(false);
}
};
const removeOpenAiApiKey = async () => {
try {
setChangeOpenAiApiKeyRequestPending(true);
await updateUserIdentity({
openai_api_key: null,
});
publish({
variant: "success",
text: "OpenAI API Key removed",
});
void fetchUserIdentity();
} catch (error) {
console.error(error);
} finally {
setChangeOpenAiApiKeyRequestPending(false);
}
};
useEffect(() => {
if (userIdentity?.openai_api_key !== undefined) {
setOpenAiApiKey(userIdentity.openai_api_key);
}
}, [userIdentity]);
const hasOpenAiApiKey =
userIdentity?.openai_api_key !== null &&
userIdentity?.openai_api_key !== undefined &&
userIdentity.openai_api_key !== "";
return {
handleCreateClick,
apiKey,
handleCopyClick,
openAiApiKey,
setOpenAiApiKey,
changeOpenAiApiKey,
changeOpenAiApiKeyRequestPending,
userIdentity,
removeOpenAiApiKey,
hasOpenAiApiKey,
};
};

View File

@ -0,0 +1,48 @@
import { renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { useUserApi } from "../useUserApi";
import { UserIdentityUpdatableProperties } from "../user";
const axiosPutMock = vi.fn(() => ({}));
const axiosGetMock = vi.fn(() => ({}));
vi.mock("@/lib/hooks", () => ({
useAxios: () => ({
axiosInstance: {
put: axiosPutMock,
get: axiosGetMock,
},
}),
}));
describe("useUserApi", () => {
it("should call updateUserIdentity with the correct parameters", async () => {
const {
result: {
current: { updateUserIdentity },
},
} = renderHook(() => useUserApi());
const userUpdatableProperties: UserIdentityUpdatableProperties = {
openai_api_key: "sk-xxx",
};
await updateUserIdentity(userUpdatableProperties);
expect(axiosPutMock).toHaveBeenCalledTimes(1);
expect(axiosPutMock).toHaveBeenCalledWith(
`/user/identity`,
userUpdatableProperties
);
});
it("should call getUserIdentity with the correct parameters", async () => {
const {
result: {
current: { getUserIdentity },
},
} = renderHook(() => useUserApi());
await getUserIdentity();
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith(`/user/identity`);
});
});

View File

@ -0,0 +1,19 @@
import { useAxios } from "@/lib/hooks";
import {
getUserIdentity,
updateUserIdentity,
UserIdentityUpdatableProperties,
} from "./user";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useUserApi = () => {
const { axiosInstance } = useAxios();
return {
updateUserIdentity: async (
userIdentityUpdatableProperties: UserIdentityUpdatableProperties
) => updateUserIdentity(userIdentityUpdatableProperties, axiosInstance),
getUserIdentity: async () => getUserIdentity(axiosInstance),
};
};

View File

@ -0,0 +1,25 @@
import { AxiosInstance } from "axios";
import { UUID } from "crypto";
export type UserIdentityUpdatableProperties = {
openai_api_key?: string | null;
};
export type UserIdentity = {
openai_api_key?: string | null;
user_id: UUID;
};
export const updateUserIdentity = async (
userUpdatableProperties: UserIdentityUpdatableProperties,
axiosInstance: AxiosInstance
): Promise<UserIdentity> =>
axiosInstance.put(`/user/identity`, userUpdatableProperties);
export const getUserIdentity = async (
axiosInstance: AxiosInstance
): Promise<UserIdentity> => {
const { data } = await axiosInstance.get<UserIdentity>(`/user/identity`);
return data;
};