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">
|
| 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">
|
||||||
|
|||||||
@@ -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
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
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 { 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 */}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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" }}
|
||||||
|
|||||||
@@ -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} />)
|
||||||
|
|||||||
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 () => {
|
export const getServerSideProps: GetServerSideProps = async () => {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/experiments",
|
destination: "/home",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
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:
|
// 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,
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user