diff --git a/app/src/components/CopiableCode.tsx b/app/src/components/CopiableCode.tsx new file mode 100644 index 0000000..8215b78 --- /dev/null +++ b/app/src/components/CopiableCode.tsx @@ -0,0 +1,43 @@ +import { HStack, Icon, IconButton, Tooltip, Text } from "@chakra-ui/react"; +import { useCallback, useState } from "react"; +import { MdContentCopy } from "react-icons/md"; + +const CopiableCode = ({ code }: { code: string }) => { + const [copied, setCopied] = useState(false); + + const copyToClipboard = useCallback(() => { + const onCopy = async () => { + console.log("copied!"); + await navigator.clipboard.writeText(code); + setCopied(true); + }; + void onCopy(); + }, [code]); + return ( + + + {code} + + + } + size="xs" + colorScheme="white" + variant="ghost" + onClick={copyToClipboard} + onMouseLeave={() => setCopied(false)} + /> + + + ); +}; + +export default CopiableCode; diff --git a/app/src/pages/settings/index.tsx b/app/src/pages/settings/index.tsx index 8566bc4..8ca407a 100644 --- a/app/src/pages/settings/index.tsx +++ b/app/src/pages/settings/index.tsx @@ -1,4 +1,13 @@ -import { Breadcrumb, BreadcrumbItem, Text } from "@chakra-ui/react"; +import { + Breadcrumb, + BreadcrumbItem, + Text, + type TextProps, + VStack, + Input, + Button, + Divider, +} from "@chakra-ui/react"; import { useEffect, useState } from "react"; import AppShell from "~/components/nav/AppShell"; @@ -6,11 +15,15 @@ import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import { api } from "~/utils/api"; import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; +import CopiableCode from "~/components/CopiableCode"; export default function Settings() { const utils = api.useContext(); const { data: selectedOrg } = useSelectedOrg(); + const apiKey = + selectedOrg?.apiKeys?.length && selectedOrg?.apiKeys[0] ? selectedOrg?.apiKeys[0].apiKey : ""; + const updateMutation = api.organizations.update.useMutation(); const [onSaveName] = useHandledAsyncCallback(async () => { if (name && name !== selectedOrg?.name && selectedOrg?.id) { @@ -39,6 +52,61 @@ export default function Settings() { + + + + Project Settings + + + Configure your project settings. These settings only apply to {selectedOrg?.name}. + + + + + + Display Name + + setName(e.target.value)} + borderColor="gray.300" + /> + + + + + Project API Key + + Use your project API key to authenticate your requests when sending data to OpenPipe. You can set this key in your environment variables, + or use it directly in your code. + + + + + ); } + +const Subtitle = (props: TextProps) => ; diff --git a/app/src/server/api/routers/organizations.router.ts b/app/src/server/api/routers/organizations.router.ts index 195532c..1216f54 100644 --- a/app/src/server/api/routers/organizations.router.ts +++ b/app/src/server/api/routers/organizations.router.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { prisma } from "~/server/db"; +import { generateApiKey } from "~/server/utils/generateApiKey"; import { requireCanModifyOrganization, requireNothing } from "~/utils/accessControl"; export const organizationsRouter = createTRPCRouter({ @@ -41,6 +42,13 @@ export const organizationsRouter = createTRPCRouter({ role: "ADMIN", }, }), + prisma.apiKey.create({ + data: { + name: "Default API Key", + organizationId: newOrgId, + apiKey: generateApiKey(), + }, + }), ]); organizations.push(newOrg); } @@ -53,6 +61,9 @@ export const organizationsRouter = createTRPCRouter({ where: { id: input.id, }, + include: { + apiKeys: true, + } }); }), update: protectedProcedure diff --git a/app/src/server/scripts/backfillApiKeys.ts b/app/src/server/scripts/backfillApiKeys.ts new file mode 100644 index 0000000..869a21c --- /dev/null +++ b/app/src/server/scripts/backfillApiKeys.ts @@ -0,0 +1,33 @@ +import { type Prisma } from "@prisma/client"; +import { prisma } from "~/server/db"; +import { generateApiKey } from "~/server/utils/generateApiKey"; + +console.log("backfilling api keys"); + +const organizations = await prisma.organization.findMany({ + include: { + apiKeys: true, + }, +}); + +console.log(`found ${organizations.length} organizations`); + +const apiKeysToCreate: Prisma.ApiKeyCreateManyInput[] = []; + +for (const org of organizations) { + if (!org.apiKeys.length) { + apiKeysToCreate.push({ + name: "Default API Key", + organizationId: org.id, + apiKey: generateApiKey(), + }); + } +} + +console.log(`creating ${apiKeysToCreate.length} api keys`); + +await prisma.apiKey.createMany({ + data: apiKeysToCreate, +}); + +console.log("done"); \ No newline at end of file diff --git a/app/src/server/utils/generateApiKey.ts b/app/src/server/utils/generateApiKey.ts new file mode 100644 index 0000000..5ff451b --- /dev/null +++ b/app/src/server/utils/generateApiKey.ts @@ -0,0 +1,11 @@ +const KEY_LENGTH = 42; + +export const generateApiKey = () => { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let randomChars = ""; + for (let i = 0; i < KEY_LENGTH; i++) { + randomChars += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return `opc_${randomChars}`; +};