Show selected org
This commit is contained in:
1
app/@types/nextjs-routes.d.ts
vendored
1
app/@types/nextjs-routes.d.ts
vendored
@@ -22,6 +22,7 @@ declare module "nextjs-routes" {
|
||||
| StaticRoute<"/data">
|
||||
| DynamicRoute<"/experiments/[id]", { "id": string }>
|
||||
| StaticRoute<"/experiments">
|
||||
| StaticRoute<"/home">
|
||||
| StaticRoute<"/">
|
||||
| StaticRoute<"/sentry-example-page">
|
||||
| StaticRoute<"/world-champs">
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Organization" ADD COLUMN "name" TEXT NOT NULL DEFAULT 'Project 1';
|
||||
@@ -200,8 +200,11 @@ model DatasetEntry {
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// TODO rename Organization to Project
|
||||
model Organization {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
name String @default("Project 1")
|
||||
|
||||
personalOrgUserId String? @unique @db.Uuid
|
||||
PersonalOrgUser User? @relation(fields: [personalOrgUserId], references: [id], onDelete: Cascade)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { useRouter } from "next/router";
|
||||
import { useRef } from "react";
|
||||
import { BsTrash } from "react-icons/bs";
|
||||
import { useAppStore } from "~/state/store";
|
||||
import { api } from "~/utils/api";
|
||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||
|
||||
@@ -23,6 +24,8 @@ export const DeleteButton = () => {
|
||||
const utils = api.useContext();
|
||||
const router = useRouter();
|
||||
|
||||
const closeDrawer = useAppStore((s) => s.closeDrawer);
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -31,6 +34,8 @@ export const DeleteButton = () => {
|
||||
await mutation.mutateAsync({ id: experiment.data.id });
|
||||
await utils.experiments.list.invalidate();
|
||||
await router.push({ pathname: "/experiments" });
|
||||
closeDrawer();
|
||||
|
||||
onClose();
|
||||
}, [mutation, experiment.data?.id, router]);
|
||||
|
||||
|
||||
@@ -7,48 +7,21 @@ import {
|
||||
Image,
|
||||
Text,
|
||||
Box,
|
||||
type BoxProps,
|
||||
Link as ChakraLink,
|
||||
Flex,
|
||||
} from "@chakra-ui/react";
|
||||
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 { useRouter } from "next/router";
|
||||
import { type IconType } from "react-icons";
|
||||
import { RiDatabase2Line, RiFlaskLine } from "react-icons/ri";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import UserMenu from "./UserMenu";
|
||||
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 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 Divider = () => <Box h="1px" bgColor="gray.300" w="full" />;
|
||||
|
||||
const NavSidebar = () => {
|
||||
const user = useSession().data;
|
||||
@@ -56,22 +29,28 @@ const NavSidebar = () => {
|
||||
return (
|
||||
<VStack
|
||||
align="stretch"
|
||||
bgColor="gray.100"
|
||||
bgColor="gray.50"
|
||||
py={2}
|
||||
px={2}
|
||||
pb={0}
|
||||
height="100%"
|
||||
w={{ base: "56px", md: "200px" }}
|
||||
w={{ base: "56px", md: "240px" }}
|
||||
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} />
|
||||
<Heading size="md" fontFamily="inconsolata, monospace">
|
||||
OpenPipe
|
||||
</Heading>
|
||||
</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 && (
|
||||
<>
|
||||
<ProjectMenu />
|
||||
<Divider />
|
||||
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
||||
{env.NEXT_PUBLIC_SHOW_DATA && (
|
||||
<IconLink icon={RiDatabase2Line} label="Data" href="/data" />
|
||||
@@ -79,13 +58,12 @@ const NavSidebar = () => {
|
||||
</>
|
||||
)}
|
||||
{user === null && (
|
||||
<NavSidebarOption>
|
||||
<HStack
|
||||
w="full"
|
||||
p={4}
|
||||
as={ChakraLink}
|
||||
_hover={{ bgColor: "gray.300", textDecoration: "none" }}
|
||||
justifyContent="start"
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
signIn("github").catch(console.error);
|
||||
}}
|
||||
@@ -95,13 +73,11 @@ const NavSidebar = () => {
|
||||
Sign In
|
||||
</Text>
|
||||
</HStack>
|
||||
</NavSidebarOption>
|
||||
)}
|
||||
</VStack>
|
||||
{user ? (
|
||||
<UserMenu user={user} borderColor={"gray.200"} borderTopWidth={1} borderBottomWidth={1} />
|
||||
) : (
|
||||
{user && <UserMenu user={user} borderColor={"gray.200"} />}
|
||||
<Divider />
|
||||
)}
|
||||
<VStack spacing={0} align="center">
|
||||
<ChakraLink
|
||||
href="https://github.com/openpipe/openpipe"
|
||||
|
||||
23
app/src/components/nav/IconLink.tsx
Normal file
23
app/src/components/nav/IconLink.tsx
Normal 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;
|
||||
26
app/src/components/nav/NavSidebarOption.tsx
Normal file
26
app/src/components/nav/NavSidebarOption.tsx
Normal 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;
|
||||
74
app/src/components/nav/ProjectMenu.tsx
Normal file
74
app/src/components/nav/ProjectMenu.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { type Session } from "next-auth";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { BsBoxArrowRight, BsChevronRight, BsPersonCircle } from "react-icons/bs";
|
||||
import NavSidebarOption from "./NavSidebarOption";
|
||||
|
||||
export default function UserMenu({ user, ...rest }: { user: Session } & StackProps) {
|
||||
const { colorMode } = useColorMode();
|
||||
@@ -27,17 +28,24 @@ export default function UserMenu({ user, ...rest }: { user: Session } & StackPro
|
||||
return (
|
||||
<>
|
||||
<Popover placement="right">
|
||||
<VStack w="full" alignItems="flex-start" spacing={0} {...rest}>
|
||||
<Text
|
||||
pl={2}
|
||||
pb={2}
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
color="gray.500"
|
||||
display={{ base: "none", md: "flex" }}
|
||||
>
|
||||
ACCOUNT
|
||||
</Text>
|
||||
<PopoverTrigger>
|
||||
<NavSidebarOption>
|
||||
<HStack
|
||||
// Weird values to make mobile look right; can clean up when we make the sidebar disappear on mobile
|
||||
px={3}
|
||||
spacing={3}
|
||||
py={2}
|
||||
{...rest}
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
bgColor: colorMode === "light" ? "gray.200" : "gray.700",
|
||||
}}
|
||||
px={1}
|
||||
spacing={3}
|
||||
>
|
||||
{profileImage}
|
||||
<VStack spacing={0} align="start" flex={1} flexShrink={1}>
|
||||
@@ -45,12 +53,14 @@ export default function UserMenu({ user, ...rest }: { user: Session } & StackPro
|
||||
{user.user.name}
|
||||
</Text>
|
||||
<Text color="gray.500" fontSize="xs">
|
||||
{user.user.email}
|
||||
{/* {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">
|
||||
<VStack align="stretch" spacing={0}>
|
||||
{/* sign out */}
|
||||
|
||||
@@ -56,8 +56,7 @@ export default function Dataset() {
|
||||
<AppShell title={dataset.data?.name}>
|
||||
<VStack h="full">
|
||||
<Flex
|
||||
pl={4}
|
||||
pr={8}
|
||||
px={8}
|
||||
py={2}
|
||||
w="full"
|
||||
direction={{ base: "column", sm: "row" }}
|
||||
@@ -90,7 +89,7 @@ export default function Dataset() {
|
||||
</Breadcrumb>
|
||||
<DatasetHeaderButtons />
|
||||
</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 />}
|
||||
</Box>
|
||||
</VStack>
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function DatasetsPage() {
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<Breadcrumb flex={1}>
|
||||
<BreadcrumbItem>
|
||||
@@ -60,7 +60,7 @@ export default function DatasetsPage() {
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</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 />
|
||||
{datasets.data && !datasets.isLoading ? (
|
||||
datasets?.data?.map((dataset) => (
|
||||
|
||||
@@ -105,7 +105,8 @@ export default function Experiment() {
|
||||
<AppShell title={experiment.data?.label}>
|
||||
<VStack h="full">
|
||||
<Flex
|
||||
px={4}
|
||||
pl={8}
|
||||
pr={4}
|
||||
py={2}
|
||||
w="full"
|
||||
direction={{ base: "column", sm: "row" }}
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function ExperimentsPage() {
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<Breadcrumb flex={1}>
|
||||
<BreadcrumbItem>
|
||||
@@ -60,7 +60,7 @@ export default function ExperimentsPage() {
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</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 />
|
||||
{experiments.data && !experiments.isLoading ? (
|
||||
experiments?.data?.map((exp) => <ExperimentCard key={exp.id} exp={exp} />)
|
||||
|
||||
56
app/src/pages/home/index.tsx
Normal file
56
app/src/pages/home/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { type GetServerSideProps } from "next";
|
||||
export const getServerSideProps: GetServerSideProps = async () => {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/experiments",
|
||||
destination: "/home",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { worldChampsRouter } from "./routers/worldChamps.router";
|
||||
import { datasetsRouter } from "./routers/datasets.router";
|
||||
import { datasetEntries } from "./routers/datasetEntries.router";
|
||||
import { externalApiRouter } from "./routers/externalApi.router";
|
||||
import { organizationsRouter } from "./routers/organizations.router";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@@ -25,6 +26,7 @@ export const appRouter = createTRPCRouter({
|
||||
worldChamps: worldChampsRouter,
|
||||
datasets: datasetsRouter,
|
||||
datasetEntries: datasetEntries,
|
||||
organizations: organizationsRouter,
|
||||
externalApi: externalApiRouter,
|
||||
});
|
||||
|
||||
|
||||
71
app/src/server/api/routers/organizations.router.ts
Normal file
71
app/src/server/api/routers/organizations.router.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -17,7 +17,7 @@ const taskList = registeredTasks.reduce((acc, task) => {
|
||||
// Run a worker to execute jobs:
|
||||
const runner = await run({
|
||||
connectionString: env.DATABASE_URL,
|
||||
concurrency: 50,
|
||||
concurrency: 10,
|
||||
// Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc
|
||||
noHandleSignals: false,
|
||||
pollInterval: 1000,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { env } from "~/env.mjs";
|
||||
|
||||
// import OpenAI from "openai";
|
||||
|
||||
// import { OpenPipe } from "../../../../client-libs/js/openai/index";
|
||||
|
||||
import { OpenAI } from "openpipe";
|
||||
|
||||
// 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" });
|
||||
|
||||
@@ -14,6 +14,8 @@ export type State = {
|
||||
api: APIClient | null;
|
||||
setApi: (api: APIClient) => void;
|
||||
sharedVariantEditor: SharedVariantEditorSlice;
|
||||
selectedOrgId: string | null;
|
||||
setSelectedOrgId: (orgId: string) => void;
|
||||
};
|
||||
|
||||
export type SliceCreator<T> = StateCreator<State, [["zustand/immer", never]], [], T>;
|
||||
@@ -39,6 +41,11 @@ const useBaseStore = create<State, [["zustand/immer", never]]>(
|
||||
state.drawerOpen = false;
|
||||
}),
|
||||
sharedVariantEditor: createVariantEditorSlice(set, get, ...rest),
|
||||
selectedOrgId: null,
|
||||
setSelectedOrgId: (orgId: string) =>
|
||||
set((state) => {
|
||||
state.selectedOrgId = orgId;
|
||||
}),
|
||||
})),
|
||||
);
|
||||
|
||||
|
||||
@@ -16,6 +16,27 @@ export const requireNothing = (ctx: TRPCContext) => {
|
||||
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) => {
|
||||
const dataset = await prisma.dataset.findFirst({
|
||||
where: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useRouter } from "next/router";
|
||||
import { type RefObject, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { api } from "~/utils/api";
|
||||
import { NumberParam, useQueryParam, withDefault } from "use-query-params";
|
||||
import { useAppStore } from "~/state/store";
|
||||
|
||||
export const useExperiment = () => {
|
||||
const router = useRouter();
|
||||
@@ -132,3 +133,11 @@ export const useScenario = (scenarioId: string) => {
|
||||
};
|
||||
|
||||
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 },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as OpenAI from "openai-beta";
|
||||
import * as openai from "openai-beta";
|
||||
import { readEnv } from "openai-beta/core";
|
||||
|
||||
// Anything we don't override we want to pass through to openai directly
|
||||
export * as openai from "openai-beta";
|
||||
|
||||
interface ClientOptions extends OpenAI.ClientOptions {
|
||||
interface ClientOptions extends openai.ClientOptions {
|
||||
openPipeApiKey?: string;
|
||||
openPipeBaseUrl?: string;
|
||||
}
|
||||
|
||||
export class OpenPipe extends OpenAI.OpenAI {
|
||||
export class OpenAI extends openai.OpenAI {
|
||||
openPipeApiKey: string;
|
||||
openPipeBaseUrl: string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user