Show selected org

This commit is contained in:
David Corbitt
2023-08-06 23:23:20 -07:00
parent a53d70d8b2
commit 6b304f8456
23 changed files with 380 additions and 97 deletions

View File

@@ -22,6 +22,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/data"> | StaticRoute<"/data">
| DynamicRoute<"/experiments/[id]", { "id": string }> | DynamicRoute<"/experiments/[id]", { "id": string }>
| StaticRoute<"/experiments"> | StaticRoute<"/experiments">
| StaticRoute<"/home">
| StaticRoute<"/"> | StaticRoute<"/">
| StaticRoute<"/sentry-example-page"> | StaticRoute<"/sentry-example-page">
| StaticRoute<"/world-champs"> | StaticRoute<"/world-champs">

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Organization" ADD COLUMN "name" TEXT NOT NULL DEFAULT 'Project 1';

View File

@@ -200,8 +200,11 @@ model DatasetEntry {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
// TODO rename Organization to Project
model Organization { model Organization {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
name String @default("Project 1")
personalOrgUserId String? @unique @db.Uuid personalOrgUserId String? @unique @db.Uuid
PersonalOrgUser User? @relation(fields: [personalOrgUserId], references: [id], onDelete: Cascade) PersonalOrgUser User? @relation(fields: [personalOrgUserId], references: [id], onDelete: Cascade)

View File

@@ -14,6 +14,7 @@ import {
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useRef } from "react"; import { useRef } from "react";
import { BsTrash } from "react-icons/bs"; import { BsTrash } from "react-icons/bs";
import { useAppStore } from "~/state/store";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
@@ -23,6 +24,8 @@ export const DeleteButton = () => {
const utils = api.useContext(); const utils = api.useContext();
const router = useRouter(); const router = useRouter();
const closeDrawer = useAppStore((s) => s.closeDrawer);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const cancelRef = useRef<HTMLButtonElement>(null); const cancelRef = useRef<HTMLButtonElement>(null);
@@ -31,6 +34,8 @@ export const DeleteButton = () => {
await mutation.mutateAsync({ id: experiment.data.id }); await mutation.mutateAsync({ id: experiment.data.id });
await utils.experiments.list.invalidate(); await utils.experiments.list.invalidate();
await router.push({ pathname: "/experiments" }); await router.push({ pathname: "/experiments" });
closeDrawer();
onClose(); onClose();
}, [mutation, experiment.data?.id, router]); }, [mutation, experiment.data?.id, router]);

View File

@@ -7,48 +7,21 @@ import {
Image, Image,
Text, Text,
Box, Box,
type BoxProps,
Link as ChakraLink, Link as ChakraLink,
Flex, Flex,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import Head from "next/head"; import Head from "next/head";
import Link, { type LinkProps } from "next/link"; import Link from "next/link";
import { BsGithub, BsPersonCircle } from "react-icons/bs"; import { BsGithub, BsPersonCircle } from "react-icons/bs";
import { useRouter } from "next/router";
import { type IconType } from "react-icons";
import { RiDatabase2Line, RiFlaskLine } from "react-icons/ri"; import { RiDatabase2Line, RiFlaskLine } from "react-icons/ri";
import { signIn, useSession } from "next-auth/react"; import { signIn, useSession } from "next-auth/react";
import UserMenu from "./UserMenu"; import UserMenu from "./UserMenu";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
import ProjectMenu from "./ProjectMenu";
import NavSidebarOption from "./NavSidebarOption";
import IconLink from "./IconLink";
type IconLinkProps = BoxProps & LinkProps & { label?: string; icon: IconType; href: string }; const Divider = () => <Box h="1px" bgColor="gray.300" w="full" />;
const IconLink = ({ icon, label, href, color, ...props }: IconLinkProps) => {
const router = useRouter();
const isActive = href && router.pathname.startsWith(href);
return (
<Link href={href} style={{ width: "100%" }}>
<HStack
w="full"
p={4}
color={color}
as={ChakraLink}
bgColor={isActive ? "gray.200" : "transparent"}
_hover={{ bgColor: "gray.300", textDecoration: "none" }}
justifyContent="start"
cursor="pointer"
{...props}
>
<Icon as={icon} boxSize={6} mr={2} />
<Text fontWeight="bold" fontSize="sm">
{label}
</Text>
</HStack>
</Link>
);
};
const Divider = () => <Box h="1px" bgColor="gray.200" />;
const NavSidebar = () => { const NavSidebar = () => {
const user = useSession().data; const user = useSession().data;
@@ -56,22 +29,28 @@ const NavSidebar = () => {
return ( return (
<VStack <VStack
align="stretch" align="stretch"
bgColor="gray.100" bgColor="gray.50"
py={2} py={2}
px={2}
pb={0} pb={0}
height="100%" height="100%"
w={{ base: "56px", md: "200px" }} w={{ base: "56px", md: "240px" }}
overflow="hidden" overflow="hidden"
borderRightWidth={1}
borderColor="gray.300"
> >
<HStack as={Link} href="/" _hover={{ textDecoration: "none" }} spacing={0} px={4} py={2}> <HStack as={Link} href="/" _hover={{ textDecoration: "none" }} spacing={0} px={2} py={2}>
<Image src="/logo.svg" alt="" boxSize={6} mr={4} /> <Image src="/logo.svg" alt="" boxSize={6} mr={4} />
<Heading size="md" fontFamily="inconsolata, monospace"> <Heading size="md" fontFamily="inconsolata, monospace">
OpenPipe OpenPipe
</Heading> </Heading>
</HStack> </HStack>
<VStack spacing={0} align="flex-start" overflowY="auto" overflowX="hidden" flex={1}> <Divider />
<VStack align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
{user != null && ( {user != null && (
<> <>
<ProjectMenu />
<Divider />
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" /> <IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
{env.NEXT_PUBLIC_SHOW_DATA && ( {env.NEXT_PUBLIC_SHOW_DATA && (
<IconLink icon={RiDatabase2Line} label="Data" href="/data" /> <IconLink icon={RiDatabase2Line} label="Data" href="/data" />
@@ -79,29 +58,26 @@ const NavSidebar = () => {
</> </>
)} )}
{user === null && ( {user === null && (
<HStack <NavSidebarOption>
w="full" <HStack
p={4} w="full"
as={ChakraLink} p={4}
_hover={{ bgColor: "gray.300", textDecoration: "none" }} as={ChakraLink}
justifyContent="start" justifyContent="start"
cursor="pointer" onClick={() => {
onClick={() => { signIn("github").catch(console.error);
signIn("github").catch(console.error); }}
}} >
> <Icon as={BsPersonCircle} boxSize={6} mr={2} />
<Icon as={BsPersonCircle} boxSize={6} mr={2} /> <Text fontWeight="bold" fontSize="sm">
<Text fontWeight="bold" fontSize="sm"> Sign In
Sign In </Text>
</Text> </HStack>
</HStack> </NavSidebarOption>
)} )}
</VStack> </VStack>
{user ? ( {user && <UserMenu user={user} borderColor={"gray.200"} />}
<UserMenu user={user} borderColor={"gray.200"} borderTopWidth={1} borderBottomWidth={1} /> <Divider />
) : (
<Divider />
)}
<VStack spacing={0} align="center"> <VStack spacing={0} align="center">
<ChakraLink <ChakraLink
href="https://github.com/openpipe/openpipe" href="https://github.com/openpipe/openpipe"

View File

@@ -0,0 +1,23 @@
import { Icon, HStack, Text, type BoxProps } from "@chakra-ui/react";
import Link, { type LinkProps } from "next/link";
import { type IconType } from "react-icons";
import NavSidebarOption from "./NavSidebarOption";
type IconLinkProps = BoxProps & LinkProps & { label?: string; icon: IconType; href: string };
const IconLink = ({ icon, label, href, color, ...props }: IconLinkProps) => {
return (
<Link href={href} style={{ width: "100%" }}>
<NavSidebarOption activeHrefPattern={href}>
<HStack w="full" p={2} color={color} justifyContent="start" {...props}>
<Icon as={icon} boxSize={6} mr={2} />
<Text fontSize="sm">
{label}
</Text>
</HStack>
</NavSidebarOption>
</Link>
);
};
export default IconLink;

View File

@@ -0,0 +1,26 @@
import { Box, type BoxProps } from "@chakra-ui/react";
import { useRouter } from "next/router";
const NavSidebarOption = ({
activeHrefPattern,
...props
}: { activeHrefPattern?: string } & BoxProps) => {
const router = useRouter();
const isActive = activeHrefPattern && router.pathname.startsWith(activeHrefPattern);
return (
<Box
w="full"
fontWeight={isActive ? "bold" : "500"}
bgColor={isActive ? "gray.200" : "transparent"}
_hover={{ bgColor: "gray.200", textDecoration: "none" }}
justifyContent="start"
cursor="pointer"
borderRadius={4}
{...props}
>
{props.children}
</Box>
);
};
export default NavSidebarOption;

View File

@@ -0,0 +1,74 @@
import {
HStack,
VStack,
Text,
Popover,
PopoverTrigger,
PopoverContent,
Flex,
} from "@chakra-ui/react";
import { useEffect } from "react";
import Link from "next/link";
import { useAppStore } from "~/state/store";
import { api } from "~/utils/api";
import NavSidebarOption from "./NavSidebarOption";
import { useSelectedOrg } from "~/utils/hooks";
export default function ProjectMenu() {
const selectedOrgId = useAppStore((s) => s.selectedOrgId);
const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
const { data } = api.organizations.list.useQuery();
useEffect(() => {
if (data && data[0] && (!selectedOrgId || !data.find((org) => org.id === selectedOrgId))) {
setSelectedOrgId(data[0].id);
}
}, [selectedOrgId, setSelectedOrgId, data]);
const { data: selectedOrg } = useSelectedOrg();
return (
<>
<Popover placement="right">
<PopoverTrigger>
<VStack w="full" alignItems="flex-start" spacing={0}>
<Text
pl={2}
pb={2}
fontSize="xs"
fontWeight="bold"
color="gray.500"
display={{ base: "none", md: "flex" }}
>
PROJECT
</Text>
<NavSidebarOption activeHrefPattern="/home">
<Link href="/home">
<HStack w="full">
<Flex
p={1}
borderRadius={4}
backgroundColor="orange.100"
minW={{ base: 10, md: 8 }}
minH={{ base: 10, md: 8 }}
m={{ base: 0, md: 1 }}
alignItems="center"
justifyContent="center"
>
<Text>{selectedOrg?.name[0]?.toUpperCase()}</Text>
</Flex>
<Text fontSize="sm" display={{ base: "none", md: "block" }} py={1}>
{selectedOrg?.name}
</Text>
</HStack>
</Link>
</NavSidebarOption>
</VStack>
</PopoverTrigger>
<PopoverContent _focusVisible={{ boxShadow: "unset", outline: "unset" }}></PopoverContent>
</Popover>
</>
);
}

View File

@@ -14,6 +14,7 @@ import {
import { type Session } from "next-auth"; import { type Session } from "next-auth";
import { signOut } from "next-auth/react"; import { signOut } from "next-auth/react";
import { BsBoxArrowRight, BsChevronRight, BsPersonCircle } from "react-icons/bs"; import { BsBoxArrowRight, BsChevronRight, BsPersonCircle } from "react-icons/bs";
import NavSidebarOption from "./NavSidebarOption";
export default function UserMenu({ user, ...rest }: { user: Session } & StackProps) { export default function UserMenu({ user, ...rest }: { user: Session } & StackProps) {
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
@@ -27,30 +28,39 @@ export default function UserMenu({ user, ...rest }: { user: Session } & StackPro
return ( return (
<> <>
<Popover placement="right"> <Popover placement="right">
<PopoverTrigger> <VStack w="full" alignItems="flex-start" spacing={0} {...rest}>
<HStack <Text
// Weird values to make mobile look right; can clean up when we make the sidebar disappear on mobile pl={2}
px={3} pb={2}
spacing={3} fontSize="xs"
py={2} fontWeight="bold"
{...rest} color="gray.500"
cursor="pointer" display={{ base: "none", md: "flex" }}
_hover={{
bgColor: colorMode === "light" ? "gray.200" : "gray.700",
}}
> >
{profileImage} ACCOUNT
<VStack spacing={0} align="start" flex={1} flexShrink={1}> </Text>
<Text fontWeight="bold" fontSize="sm"> <PopoverTrigger>
{user.user.name} <NavSidebarOption>
</Text> <HStack
<Text color="gray.500" fontSize="xs"> // Weird values to make mobile look right; can clean up when we make the sidebar disappear on mobile
{user.user.email} py={2}
</Text> px={1}
</VStack> spacing={3}
<Icon as={BsChevronRight} boxSize={4} color="gray.500" /> >
</HStack> {profileImage}
</PopoverTrigger> <VStack spacing={0} align="start" flex={1} flexShrink={1}>
<Text fontWeight="bold" fontSize="sm">
{user.user.name}
</Text>
<Text color="gray.500" fontSize="xs">
{/* {user.user.email} */}
</Text>
</VStack>
<Icon as={BsChevronRight} boxSize={4} color="gray.500" />
</HStack>
</NavSidebarOption>
</PopoverTrigger>
</VStack>
<PopoverContent _focusVisible={{ boxShadow: "unset", outline: "unset" }} maxW="200px"> <PopoverContent _focusVisible={{ boxShadow: "unset", outline: "unset" }} maxW="200px">
<VStack align="stretch" spacing={0}> <VStack align="stretch" spacing={0}>
{/* sign out */} {/* sign out */}

View File

@@ -56,8 +56,7 @@ export default function Dataset() {
<AppShell title={dataset.data?.name}> <AppShell title={dataset.data?.name}>
<VStack h="full"> <VStack h="full">
<Flex <Flex
pl={4} px={8}
pr={8}
py={2} py={2}
w="full" w="full"
direction={{ base: "column", sm: "row" }} direction={{ base: "column", sm: "row" }}
@@ -90,7 +89,7 @@ export default function Dataset() {
</Breadcrumb> </Breadcrumb>
<DatasetHeaderButtons /> <DatasetHeaderButtons />
</Flex> </Flex>
<Box w="full" overflowX="auto" flex={1} pl={4} pr={8} pt={8} pb={16}> <Box w="full" overflowX="auto" flex={1} px={8} pt={8} pb={16}>
{datasetId && <DatasetEntriesTable />} {datasetId && <DatasetEntriesTable />}
</Box> </Box>
</VStack> </VStack>

View File

@@ -50,7 +50,7 @@ export default function DatasetsPage() {
return ( return (
<AppShell title="Data"> <AppShell title="Data">
<VStack alignItems={"flex-start"} px={4} py={2}> <VStack alignItems={"flex-start"} px={8} py={2}>
<HStack minH={8} align="center" pt={2}> <HStack minH={8} align="center" pt={2}>
<Breadcrumb flex={1}> <Breadcrumb flex={1}>
<BreadcrumbItem> <BreadcrumbItem>
@@ -60,7 +60,7 @@ export default function DatasetsPage() {
</BreadcrumbItem> </BreadcrumbItem>
</Breadcrumb> </Breadcrumb>
</HStack> </HStack>
<SimpleGrid w="full" columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={8} p="4"> <SimpleGrid w="full" columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={8} py="4">
<NewDatasetCard /> <NewDatasetCard />
{datasets.data && !datasets.isLoading ? ( {datasets.data && !datasets.isLoading ? (
datasets?.data?.map((dataset) => ( datasets?.data?.map((dataset) => (

View File

@@ -105,7 +105,8 @@ export default function Experiment() {
<AppShell title={experiment.data?.label}> <AppShell title={experiment.data?.label}>
<VStack h="full"> <VStack h="full">
<Flex <Flex
px={4} pl={8}
pr={4}
py={2} py={2}
w="full" w="full"
direction={{ base: "column", sm: "row" }} direction={{ base: "column", sm: "row" }}

View File

@@ -50,7 +50,7 @@ export default function ExperimentsPage() {
return ( return (
<AppShell title="Experiments"> <AppShell title="Experiments">
<VStack alignItems={"flex-start"} px={4} py={2}> <VStack alignItems={"flex-start"} px={8} py={2}>
<HStack minH={8} align="center" pt={2}> <HStack minH={8} align="center" pt={2}>
<Breadcrumb flex={1}> <Breadcrumb flex={1}>
<BreadcrumbItem> <BreadcrumbItem>
@@ -60,7 +60,7 @@ export default function ExperimentsPage() {
</BreadcrumbItem> </BreadcrumbItem>
</Breadcrumb> </Breadcrumb>
</HStack> </HStack>
<SimpleGrid w="full" columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={8} p="4"> <SimpleGrid w="full" columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={8} py="4">
<NewExperimentCard /> <NewExperimentCard />
{experiments.data && !experiments.isLoading ? ( {experiments.data && !experiments.isLoading ? (
experiments?.data?.map((exp) => <ExperimentCard key={exp.id} exp={exp} />) experiments?.data?.map((exp) => <ExperimentCard key={exp.id} exp={exp} />)

View File

@@ -0,0 +1,56 @@
import { Breadcrumb, BreadcrumbItem, HStack, Input } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import AppShell from "~/components/nav/AppShell";
import { api } from "~/utils/api";
import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
export default function HomePage() {
const utils = api.useContext();
const { data: selectedOrg } = useSelectedOrg();
const updateMutation = api.organizations.update.useMutation();
const [onSaveName] = useHandledAsyncCallback(async () => {
if (name && name !== selectedOrg?.name && selectedOrg?.id) {
await updateMutation.mutateAsync({
id: selectedOrg.id,
updates: { name },
});
await Promise.all([utils.organizations.get.invalidate({ id: selectedOrg.id })]);
}
}, [updateMutation, selectedOrg]);
const [name, setName] = useState(selectedOrg?.name);
useEffect(() => {
setName(selectedOrg?.name);
}, [selectedOrg?.name]);
return (
<AppShell>
<HStack
px={4}
py={2}
w="full"
direction={{ base: "column", sm: "row" }}
alignItems={{ base: "flex-start", sm: "center" }}
>
<Breadcrumb flex={1}>
<BreadcrumbItem isCurrentPage>
<Input
size="sm"
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={onSaveName}
borderWidth={1}
borderColor="transparent"
fontSize={16}
px={0}
minW={{ base: 100, lg: 300 }}
flex={1}
_hover={{ borderColor: "gray.300" }}
_focus={{ borderColor: "blue.500", outline: "none" }}
/>
</BreadcrumbItem>
</Breadcrumb>
</HStack>
</AppShell>
);
}

View File

@@ -4,7 +4,7 @@ import { type GetServerSideProps } from "next";
export const getServerSideProps: GetServerSideProps = async () => { export const getServerSideProps: GetServerSideProps = async () => {
return { return {
redirect: { redirect: {
destination: "/experiments", destination: "/home",
permanent: false, permanent: false,
}, },
}; };

View File

@@ -9,6 +9,7 @@ import { worldChampsRouter } from "./routers/worldChamps.router";
import { datasetsRouter } from "./routers/datasets.router"; import { datasetsRouter } from "./routers/datasets.router";
import { datasetEntries } from "./routers/datasetEntries.router"; import { datasetEntries } from "./routers/datasetEntries.router";
import { externalApiRouter } from "./routers/externalApi.router"; import { externalApiRouter } from "./routers/externalApi.router";
import { organizationsRouter } from "./routers/organizations.router";
/** /**
* This is the primary router for your server. * This is the primary router for your server.
@@ -25,6 +26,7 @@ export const appRouter = createTRPCRouter({
worldChamps: worldChampsRouter, worldChamps: worldChampsRouter,
datasets: datasetsRouter, datasets: datasetsRouter,
datasetEntries: datasetEntries, datasetEntries: datasetEntries,
organizations: organizationsRouter,
externalApi: externalApiRouter, externalApi: externalApiRouter,
}); });

View File

@@ -0,0 +1,71 @@
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db";
import { requireCanModifyOrganization, requireNothing } from "~/utils/accessControl";
export const organizationsRouter = createTRPCRouter({
list: protectedProcedure.query(async ({ ctx }) => {
const userId = ctx.session.user.id;
requireNothing(ctx);
if (!userId) {
return null;
}
const organizations = await prisma.organization.findMany({
where: {
organizationUsers: {
some: { userId: ctx.session.user.id },
},
},
orderBy: {
createdAt: "desc",
},
});
if (!organizations.length) {
const newOrgId = uuidv4();
const [newOrg] = await prisma.$transaction([
prisma.organization.create({
data: {
id: newOrgId,
personalOrgUserId: userId,
},
}),
prisma.organizationUser.create({
data: {
userId,
organizationId: newOrgId,
role: "ADMIN",
},
}),
]);
organizations.push(newOrg);
}
return organizations;
}),
get: protectedProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
requireNothing(ctx);
return await prisma.organization.findUnique({
where: {
id: input.id,
},
});
}),
update: protectedProcedure
.input(z.object({ id: z.string(), updates: z.object({ name: z.string() }) }))
.mutation(async ({ input, ctx }) => {
await requireCanModifyOrganization(input.id, ctx);
return await prisma.organization.update({
where: {
id: input.id,
},
data: {
name: input.updates.name,
},
});
}),
});

View File

@@ -17,7 +17,7 @@ const taskList = registeredTasks.reduce((acc, task) => {
// Run a worker to execute jobs: // Run a worker to execute jobs:
const runner = await run({ const runner = await run({
connectionString: env.DATABASE_URL, connectionString: env.DATABASE_URL,
concurrency: 50, concurrency: 10,
// Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc // Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc
noHandleSignals: false, noHandleSignals: false,
pollInterval: 1000, pollInterval: 1000,

View File

@@ -1,10 +1,7 @@
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
// import OpenAI from "openai"; // import OpenAI from "openai";
// import { OpenPipe } from "../../../../client-libs/js/openai/index";
import { OpenAI } from "openpipe"; import { OpenAI } from "openpipe";
// Set a dummy key so it doesn't fail at build time // Set a dummy key so it doesn't fail at build time
export const openai = new OpenAI.OpenPipe({ apiKey: env.OPENAI_API_KEY ?? "dummy-key" }); export const openai = new OpenAI.OpenAI({ apiKey: env.OPENAI_API_KEY ?? "dummy-key" });

View File

@@ -14,6 +14,8 @@ export type State = {
api: APIClient | null; api: APIClient | null;
setApi: (api: APIClient) => void; setApi: (api: APIClient) => void;
sharedVariantEditor: SharedVariantEditorSlice; sharedVariantEditor: SharedVariantEditorSlice;
selectedOrgId: string | null;
setSelectedOrgId: (orgId: string) => void;
}; };
export type SliceCreator<T> = StateCreator<State, [["zustand/immer", never]], [], T>; export type SliceCreator<T> = StateCreator<State, [["zustand/immer", never]], [], T>;
@@ -39,6 +41,11 @@ const useBaseStore = create<State, [["zustand/immer", never]]>(
state.drawerOpen = false; state.drawerOpen = false;
}), }),
sharedVariantEditor: createVariantEditorSlice(set, get, ...rest), sharedVariantEditor: createVariantEditorSlice(set, get, ...rest),
selectedOrgId: null,
setSelectedOrgId: (orgId: string) =>
set((state) => {
state.selectedOrgId = orgId;
}),
})), })),
); );

View File

@@ -16,6 +16,27 @@ export const requireNothing = (ctx: TRPCContext) => {
ctx.markAccessControlRun(); ctx.markAccessControlRun();
}; };
export const requireCanModifyOrganization = async (organizationId: string, ctx: TRPCContext) => {
const userId = ctx.session?.user.id;
if (!userId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const canModify = await prisma.organizationUser.findFirst({
where: {
userId,
organizationId,
role: { in: [OrganizationUserRole.ADMIN, OrganizationUserRole.MEMBER] },
},
});
if (!canModify) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
ctx.markAccessControlRun();
}
export const requireCanViewDataset = async (datasetId: string, ctx: TRPCContext) => { export const requireCanViewDataset = async (datasetId: string, ctx: TRPCContext) => {
const dataset = await prisma.dataset.findFirst({ const dataset = await prisma.dataset.findFirst({
where: { where: {

View File

@@ -2,6 +2,7 @@ import { useRouter } from "next/router";
import { type RefObject, useCallback, useEffect, useRef, useState } from "react"; import { type RefObject, useCallback, useEffect, useRef, useState } from "react";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { NumberParam, useQueryParam, withDefault } from "use-query-params"; import { NumberParam, useQueryParam, withDefault } from "use-query-params";
import { useAppStore } from "~/state/store";
export const useExperiment = () => { export const useExperiment = () => {
const router = useRouter(); const router = useRouter();
@@ -132,3 +133,11 @@ export const useScenario = (scenarioId: string) => {
}; };
export const useVisibleScenarioIds = () => useScenarios().data?.scenarios.map((s) => s.id) ?? []; export const useVisibleScenarioIds = () => useScenarios().data?.scenarios.map((s) => s.id) ?? [];
export const useSelectedOrg = () => {
const selectedOrgId = useAppStore((state) => state.selectedOrgId);
return api.organizations.get.useQuery(
{ id: selectedOrgId ?? "" },
{ enabled: !!selectedOrgId },
);
};

View File

@@ -1,15 +1,15 @@
import * as OpenAI from "openai-beta"; import * as openai from "openai-beta";
import { readEnv } from "openai-beta/core"; import { readEnv } from "openai-beta/core";
// Anything we don't override we want to pass through to openai directly // Anything we don't override we want to pass through to openai directly
export * as openai from "openai-beta"; export * as openai from "openai-beta";
interface ClientOptions extends OpenAI.ClientOptions { interface ClientOptions extends openai.ClientOptions {
openPipeApiKey?: string; openPipeApiKey?: string;
openPipeBaseUrl?: string; openPipeBaseUrl?: string;
} }
export class OpenPipe extends OpenAI.OpenAI { export class OpenAI extends openai.OpenAI {
openPipeApiKey: string; openPipeApiKey: string;
openPipeBaseUrl: string; openPipeBaseUrl: string;