Compare commits

..

6 Commits

Author SHA1 Message Date
Kyle Corbitt
6c060c6ea0 persist the currently-selected project 2023-08-09 16:45:54 -07:00
Kyle Corbitt
f70e73e338 Merge pull request #126 from OpenPipe/org-to-proj
Rename Organization to Project
2023-08-09 16:04:36 -07:00
Kyle Corbitt
16aa6672fc Rename Organization to Project
We'll probably need a concept of organizations at some point in the future, but in practice the way we're using these in the codebase right now is as a project, so this renames it to that to avoid confusion.
2023-08-09 16:01:13 -07:00
Kyle Corbitt
ac99c8e0f7 Merge pull request #127 from OpenPipe/pause-champs
Pause world championships
2023-08-09 15:59:15 -07:00
Kyle Corbitt
df121db78c Merge pull request #125 from OpenPipe/claude-1.1
Support Claude Instant 1.2
2023-08-09 15:58:36 -07:00
Kyle Corbitt
f09dfe18be Support Claude Instant 1.2 2023-08-09 14:43:54 -07:00
32 changed files with 337 additions and 272 deletions

View File

@@ -0,0 +1,37 @@
-- Rename Enum
ALTER TYPE "OrganizationUserRole" RENAME TO "ProjectUserRole";
-- Drop and recreate foreign keys
ALTER TABLE "ApiKey" DROP CONSTRAINT "ApiKey_organizationId_fkey";
ALTER TABLE "Dataset" DROP CONSTRAINT "Dataset_organizationId_fkey";
ALTER TABLE "Experiment" DROP CONSTRAINT "Experiment_organizationId_fkey";
ALTER TABLE "LoggedCall" DROP CONSTRAINT "LoggedCall_organizationId_fkey";
ALTER TABLE "OrganizationUser" DROP CONSTRAINT "OrganizationUser_organizationId_fkey";
ALTER TABLE "OrganizationUser" DROP CONSTRAINT "OrganizationUser_userId_fkey";
-- Rename columns
ALTER TABLE "ApiKey" RENAME COLUMN "organizationId" TO "projectId";
ALTER TABLE "Dataset" RENAME COLUMN "organizationId" TO "projectId";
ALTER TABLE "Experiment" RENAME COLUMN "organizationId" TO "projectId";
ALTER TABLE "LoggedCall" RENAME COLUMN "organizationId" TO "projectId";
ALTER TABLE "OrganizationUser" RENAME COLUMN "organizationId" TO "projectId";
ALTER TABLE "Organization" RENAME COLUMN "personalOrgUserId" TO "personalProjectUserId";
-- Rename table
ALTER TABLE "Organization" RENAME TO "Project";
ALTER TABLE "OrganizationUser" RENAME TO "ProjectUser";
-- Recreate foreign keys
ALTER TABLE "Experiment" ADD CONSTRAINT "Experiment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Dataset" ADD CONSTRAINT "Dataset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProjectUser" ADD CONSTRAINT "ProjectUser_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProjectUser" ADD CONSTRAINT "ProjectUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Rename indexes
ALTER TABLE "Project" RENAME CONSTRAINT "Organization_pkey" TO "Project_pkey";
ALTER TABLE "ProjectUser" RENAME CONSTRAINT "OrganizationUser_pkey" TO "ProjectUser_pkey";
ALTER TABLE "Project" RENAME CONSTRAINT "Organization_personalOrgUserId_fkey" TO "Project_personalProjectUserId_fkey";
ALTER INDEX "Organization_personalOrgUserId_key" RENAME TO "Project_personalProjectUserId_key";
ALTER INDEX "OrganizationUser_organizationId_userId_key" RENAME TO "ProjectUser_projectId_userId_key";

View File

@@ -16,8 +16,8 @@ model Experiment {
sortIndex Int @default(0)
organizationId String @db.Uuid
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
projectId String @db.Uuid
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -180,8 +180,8 @@ model Dataset {
name String
datasetEntries DatasetEntry[]
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
projectId String @db.Uuid
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -200,36 +200,35 @@ model DatasetEntry {
updatedAt DateTime @updatedAt
}
// TODO rename Organization to Project
model Organization {
id String @id @default(uuid()) @db.Uuid
name String @default("Project 1")
model Project {
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)
personalProjectUserId String? @unique @db.Uuid
personalProjectUser User? @relation(fields: [personalProjectUserId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organizationUsers OrganizationUser[]
experiments Experiment[]
datasets Dataset[]
loggedCalls LoggedCall[]
apiKeys ApiKey[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
projectUsers ProjectUser[]
experiments Experiment[]
datasets Dataset[]
loggedCalls LoggedCall[]
apiKeys ApiKey[]
}
enum OrganizationUserRole {
enum ProjectUserRole {
ADMIN
MEMBER
VIEWER
}
model OrganizationUser {
model ProjectUser {
id String @id @default(uuid()) @db.Uuid
role OrganizationUserRole
role ProjectUserRole
organizationId String @db.Uuid
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
projectId String @db.Uuid
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
userId String @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -237,7 +236,7 @@ model OrganizationUser {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, userId])
@@unique([projectId, userId])
}
model WorldChampEntrant {
@@ -265,14 +264,14 @@ model LoggedCall {
// A LoggedCall is always associated with a LoggedCallModelResponse. If this
// is a cache miss, we create a new LoggedCallModelResponse.
// If it's a cache hit, it's a pre-existing LoggedCallModelResponse.
modelResponseId String? @db.Uuid
modelResponseId String? @db.Uuid
modelResponse LoggedCallModelResponse? @relation(fields: [modelResponseId], references: [id], onDelete: Cascade)
// The responses created by this LoggedCall. Will be empty if this LoggedCall was a cache hit.
createdResponses LoggedCallModelResponse[] @relation(name: "ModelResponseOriginalCall")
organizationId String @db.Uuid
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
projectId String @db.Uuid
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
tags LoggedCallTag[]
@@ -323,11 +322,11 @@ model LoggedCallModelResponse {
}
model LoggedCallTag {
id String @id @default(uuid()) @db.Uuid
id String @id @default(uuid()) @db.Uuid
name String
value String?
loggedCallId String @db.Uuid
loggedCallId String @db.Uuid
loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
@@index([name])
@@ -340,8 +339,8 @@ model ApiKey {
name String
apiKey String @unique
organizationId String @db.Uuid
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
projectId String @db.Uuid
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -390,8 +389,8 @@ model User {
accounts Account[]
sessions Session[]
organizationUsers OrganizationUser[]
organizations Organization[]
projectUsers ProjectUser[]
projects Project[]
worldChampEntrant WorldChampEntrant?
createdAt DateTime @default(now())

View File

@@ -5,14 +5,14 @@ import { promptConstructorVersion } from "~/promptConstructor/version";
const defaultId = "11111111-1111-1111-1111-111111111111";
await prisma.organization.deleteMany({
await prisma.project.deleteMany({
where: { id: defaultId },
});
// If there's an existing org, just seed into it
const org =
(await prisma.organization.findFirst({})) ??
(await prisma.organization.create({
// If there's an existing project, just seed into it
const project =
(await prisma.project.findFirst({})) ??
(await prisma.project.create({
data: { id: defaultId },
}));
@@ -26,7 +26,7 @@ await prisma.experiment.create({
data: {
id: defaultId,
label: "Country Capitals Example",
organizationId: org.id,
projectId: project.id,
},
});

View File

@@ -7,14 +7,14 @@ import { promptConstructorVersion } from "~/promptConstructor/version";
const defaultId = "11111111-1111-1111-1111-111111111112";
await prisma.organization.deleteMany({
await prisma.project.deleteMany({
where: { id: defaultId },
});
// If there's an existing org, just seed into it
const org =
(await prisma.organization.findFirst({})) ??
(await prisma.organization.create({
// If there's an existing project, just seed into it
const project =
(await prisma.project.findFirst({})) ??
(await prisma.project.create({
data: { id: defaultId },
}));
@@ -47,7 +47,7 @@ for (const dataset of datasets) {
const oldExperiment = await prisma.experiment.findFirst({
where: {
label: experimentName,
organizationId: org.id,
projectId: project.id,
},
});
if (oldExperiment) {
@@ -60,7 +60,7 @@ for (const dataset of datasets) {
data: {
id: oldExperiment?.id ?? undefined,
label: experimentName,
organizationId: org.id,
projectId: project.id,
},
});

View File

@@ -311,9 +311,9 @@ const MODEL_RESPONSE_TEMPLATES: {
await prisma.loggedCallModelResponse.deleteMany();
const org = await prisma.organization.findFirst({
const project = await prisma.project.findFirst({
where: {
personalOrgUserId: {
personalProjectUserId: {
not: null,
},
},
@@ -322,8 +322,8 @@ const org = await prisma.organization.findFirst({
},
});
if (!org) {
console.error("No org found. Sign up to create your first org.");
if (!project) {
console.error("No project found. Sign up to create your first project.");
process.exit(1);
}
@@ -348,7 +348,7 @@ for (let i = 0; i < 1437; i++) {
id: loggedCallId,
cacheHit: false,
startTime,
organizationId: org.id,
projectId: project.id,
createdAt: startTime,
});
@@ -373,7 +373,7 @@ for (let i = 0; i < 1437; i++) {
respStatus: template.respStatus,
error: template.error,
createdAt: startTime,
cacheKey: hashRequest(org.id, template.reqPayload as JsonValue),
cacheKey: hashRequest(project.id, template.reqPayload as JsonValue),
durationMs: endTime.getTime() - startTime.getTime(),
inputTokens: template.inputTokens,
outputTokens: template.outputTokens,

View File

@@ -6,14 +6,14 @@ import { promptConstructorVersion } from "~/promptConstructor/version";
const defaultId = "11111111-1111-1111-1111-111111111112";
await prisma.organization.deleteMany({
await prisma.project.deleteMany({
where: { id: defaultId },
});
// If there's an existing org, just seed into it
const org =
(await prisma.organization.findFirst({})) ??
(await prisma.organization.create({
// If there's an existing project, just seed into it
const project =
(await prisma.project.findFirst({})) ??
(await prisma.project.create({
data: { id: defaultId },
}));
@@ -27,7 +27,7 @@ const experimentName = `Twitter Sentiment Analysis`;
const oldExperiment = await prisma.experiment.findFirst({
where: {
label: experimentName,
organizationId: org.id,
projectId: project.id,
},
});
if (oldExperiment) {
@@ -40,7 +40,7 @@ const experiment = await prisma.experiment.create({
data: {
id: oldExperiment?.id ?? undefined,
label: experimentName,
organizationId: org.id,
projectId: project.id,
},
});

View File

@@ -72,12 +72,12 @@ const CountLabel = ({ label, count }: { label: string; count: number }) => {
export const NewDatasetCard = () => {
const router = useRouter();
const selectedOrgId = useAppStore((s) => s.selectedOrgId);
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
const createMutation = api.datasets.create.useMutation();
const [createDataset, isLoading] = useHandledAsyncCallback(async () => {
const newDataset = await createMutation.mutateAsync({ organizationId: selectedOrgId ?? "" });
const newDataset = await createMutation.mutateAsync({ projectId: selectedProjectId ?? "" });
await router.push({ pathname: "/data/[id]", query: { id: newDataset.id } });
}, [createMutation, router, selectedOrgId]);
}, [createMutation, router, selectedProjectId]);
return (
<AspectRatio ratio={1.2} w="full">

View File

@@ -76,17 +76,17 @@ const CountLabel = ({ label, count }: { label: string; count: number }) => {
export const NewExperimentCard = () => {
const router = useRouter();
const selectedOrgId = useAppStore((s) => s.selectedOrgId);
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
const createMutation = api.experiments.create.useMutation();
const [createExperiment, isLoading] = useHandledAsyncCallback(async () => {
const newExperiment = await createMutation.mutateAsync({
organizationId: selectedOrgId ?? "",
projectId: selectedProjectId ?? "",
});
await router.push({
pathname: "/experiments/[id]",
query: { id: newExperiment.id },
});
}, [createMutation, router, selectedOrgId]);
}, [createMutation, router, selectedProjectId]);
return (
<AspectRatio ratio={1.2} w="full">

View File

@@ -10,15 +10,15 @@ export const useOnForkButtonPressed = () => {
const user = useSession().data;
const experiment = useExperiment();
const selectedOrgId = useAppStore((state) => state.selectedOrgId);
const selectedProjectId = useAppStore((state) => state.selectedProjectId);
const forkMutation = api.experiments.fork.useMutation();
const [onFork, isForking] = useHandledAsyncCallback(async () => {
if (!experiment.data?.id || !selectedOrgId) return;
if (!experiment.data?.id || !selectedProjectId) return;
const forkedExperimentId = await forkMutation.mutateAsync({
id: experiment.data.id,
organizationId: selectedOrgId,
projectId: selectedProjectId,
});
await router.push({ pathname: "/experiments/[id]", query: { id: forkedExperimentId } });
}, [forkMutation, experiment.data?.id, router]);

View File

@@ -1,12 +1,12 @@
import { HStack, Flex, Text } from "@chakra-ui/react";
import { useSelectedOrg } from "~/utils/hooks";
import { useSelectedProject } from "~/utils/hooks";
// Have to export only contents here instead of full BreadcrumbItem because Chakra doesn't
// recognize a BreadcrumbItem exported with this component as a valid child of Breadcrumb.
export default function ProjectBreadcrumbContents({ orgName = "" }: { orgName?: string }) {
const { data: selectedOrg } = useSelectedOrg();
export default function ProjectBreadcrumbContents({ projectName = "" }: { projectName?: string }) {
const { data: selectedProject } = useSelectedProject();
orgName = orgName || selectedOrg?.name || "";
projectName = projectName || selectedProject?.name || "";
return (
<HStack w="full">
@@ -18,10 +18,10 @@ export default function ProjectBreadcrumbContents({ orgName = "" }: { orgName?:
alignItems="center"
justifyContent="center"
>
<Text>{orgName[0]?.toUpperCase()}</Text>
<Text>{projectName[0]?.toUpperCase()}</Text>
</Flex>
<Text display={{ base: "none", md: "block" }} py={1}>
{orgName}
{projectName}
</Text>
</HStack>
);

View File

@@ -17,39 +17,42 @@ import React, { useEffect, useState } from "react";
import Link from "next/link";
import { AiFillCaretDown } from "react-icons/ai";
import { BsGear, BsPlus } from "react-icons/bs";
import { type Organization } from "@prisma/client";
import { type Project } from "@prisma/client";
import { useAppStore } from "~/state/store";
import { api } from "~/utils/api";
import NavSidebarOption from "./NavSidebarOption";
import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
import { useRouter } from "next/router";
export default function ProjectMenu() {
const router = useRouter();
const isActive = router.pathname.startsWith("/home");
const utils = api.useContext();
const selectedOrgId = useAppStore((s) => s.selectedOrgId);
const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
const setselectedProjectId = useAppStore((s) => s.setselectedProjectId);
const { data: orgs } = api.organizations.list.useQuery();
const { data: projects } = api.projects.list.useQuery();
useEffect(() => {
if (orgs && orgs[0] && (!selectedOrgId || !orgs.find((org) => org.id === selectedOrgId))) {
setSelectedOrgId(orgs[0].id);
if (
projects &&
projects[0] &&
(!selectedProjectId || !projects.find((proj) => proj.id === selectedProjectId))
) {
setselectedProjectId(projects[0].id);
}
}, [selectedOrgId, setSelectedOrgId, orgs]);
}, [selectedProjectId, setselectedProjectId, projects]);
const { data: selectedOrg } = useSelectedOrg();
const { data: selectedProject } = useSelectedProject();
const popover = useDisclosure();
const createMutation = api.organizations.create.useMutation();
const createMutation = api.projects.create.useMutation();
const [createProject, isLoading] = useHandledAsyncCallback(async () => {
const newOrg = await createMutation.mutateAsync({ name: "New Project" });
await utils.organizations.list.invalidate();
setSelectedOrgId(newOrg.id);
const newProj = await createMutation.mutateAsync({ name: "New Project" });
await utils.projects.list.invalidate();
setselectedProjectId(newProj.id);
await router.push({ pathname: "/project/settings" });
}, [createMutation, router]);
@@ -73,7 +76,7 @@ export default function ProjectMenu() {
closeOnBlur
>
<PopoverTrigger>
<HStack w="full" justifyContent="space-between" onClick={popover.onToggle}>
<HStack w="full" onClick={popover.onToggle}>
<Flex
p={1}
borderRadius={4}
@@ -83,12 +86,11 @@ export default function ProjectMenu() {
m={{ base: 0, md: 1 }}
alignItems="center"
justifyContent="center"
// onClick={sidebarExpanded ? undefined : openMenu}
>
<Text>{selectedOrg?.name[0]?.toUpperCase()}</Text>
<Text>{selectedProject?.name[0]?.toUpperCase()}</Text>
</Flex>
<Text fontSize="sm" display={{ base: "none", md: "block" }} py={1}>
{selectedOrg?.name}
<Text fontSize="sm" display={{ base: "none", md: "block" }} py={1} flex={1}>
{selectedProject?.name}
</Text>
<Icon as={AiFillCaretDown} boxSize={3} size="xs" color="gray.500" mr={2} />
</HStack>
@@ -105,11 +107,11 @@ export default function ProjectMenu() {
</Text>
<Divider />
<VStack spacing={0} w="full">
{orgs?.map((org) => (
{projects?.map((proj) => (
<ProjectOption
key={org.id}
org={org}
isActive={org.id === selectedOrgId}
key={proj.id}
proj={proj}
isActive={proj.id === selectedProjectId}
onClose={popover.onClose}
/>
))}
@@ -135,22 +137,22 @@ export default function ProjectMenu() {
}
const ProjectOption = ({
org,
proj,
isActive,
onClose,
}: {
org: Organization;
proj: Project;
isActive: boolean;
onClose: () => void;
}) => {
const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
const setselectedProjectId = useAppStore((s) => s.setselectedProjectId);
const [gearHovered, setGearHovered] = useState(false);
return (
<HStack
as={Link}
href="/experiments"
onClick={() => {
setSelectedOrgId(org.id);
setselectedProjectId(proj.id);
onClose();
}}
w="full"
@@ -159,11 +161,11 @@ const ProjectOption = ({
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
p={2}
>
<Text>{org.name}</Text>
<Text>{proj.name}</Text>
<IconButton
as={Link}
href="/project/settings"
aria-label={`Open ${org.name} settings`}
aria-label={`Open ${proj.name} settings`}
icon={<Icon as={BsGear} boxSize={5} strokeWidth={0.5} color="gray.500" />}
variant="ghost"
size="xs"

View File

@@ -16,7 +16,7 @@ import {
import { useRouter } from "next/router";
import { useRef, useState } from "react";
import { api } from "~/utils/api";
import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
export const DeleteProjectDialog = ({
isOpen,
@@ -25,20 +25,20 @@ export const DeleteProjectDialog = ({
isOpen: boolean;
onClose: () => void;
}) => {
const selectedOrg = useSelectedOrg();
const deleteMutation = api.organizations.delete.useMutation();
const selectedProject = useSelectedProject();
const deleteMutation = api.projects.delete.useMutation();
const utils = api.useContext();
const router = useRouter();
const cancelRef = useRef<HTMLButtonElement>(null);
const [onDeleteConfirm, isDeleting] = useHandledAsyncCallback(async () => {
if (!selectedOrg.data?.id) return;
await deleteMutation.mutateAsync({ id: selectedOrg.data.id });
await utils.organizations.list.invalidate();
if (!selectedProject.data?.id) return;
await deleteMutation.mutateAsync({ id: selectedProject.data.id });
await utils.projects.list.invalidate();
await router.push({ pathname: "/experiments" });
onClose();
}, [deleteMutation, selectedOrg, router]);
}, [deleteMutation, selectedProject, router]);
const [nameToDelete, setNameToDelete] = useState("");
@@ -58,10 +58,10 @@ export const DeleteProjectDialog = ({
of the project below.
</Text>
<Box bgColor="orange.100" w="full" p={2} borderRadius={4}>
<Text fontFamily="inconsolata">{selectedOrg.data?.name}</Text>
<Text fontFamily="inconsolata">{selectedProject.data?.name}</Text>
</Box>
<Input
placeholder={selectedOrg.data?.name}
placeholder={selectedProject.data?.name}
value={nameToDelete}
onChange={(e) => setNameToDelete(e.target.value)}
/>
@@ -76,7 +76,7 @@ export const DeleteProjectDialog = ({
colorScheme="red"
onClick={onDeleteConfirm}
ml={3}
isDisabled={nameToDelete !== selectedOrg.data?.name}
isDisabled={nameToDelete !== selectedProject.data?.name}
w={20}
>
{isDeleting ? <Spinner /> : "Delete"}

View File

@@ -9,7 +9,8 @@
"claude-2",
"claude-2.0",
"claude-instant-1",
"claude-instant-1.1"
"claude-instant-1.1",
"claude-instant-1.2"
]
},
"prompt": {

View File

@@ -60,7 +60,7 @@ export default function Dataset() {
<PageHeaderContainer>
<Breadcrumb>
<BreadcrumbItem>
<ProjectBreadcrumbContents orgName={dataset.data?.organization?.name} />
<ProjectBreadcrumbContents projectName={dataset.data?.project?.name} />
</BreadcrumbItem>
<BreadcrumbItem>
<Link href="/data">

View File

@@ -109,7 +109,7 @@ export default function Experiment() {
<PageHeaderContainer>
<Breadcrumb>
<BreadcrumbItem>
<ProjectBreadcrumbContents orgName={experiment.data?.organization?.name} />
<ProjectBreadcrumbContents projectName={experiment.data?.project?.name} />
</BreadcrumbItem>
<BreadcrumbItem>
<Link href="/experiments">

View File

@@ -34,17 +34,17 @@ import { useMemo } from "react";
import AppShell from "~/components/nav/AppShell";
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
import { useSelectedOrg } from "~/utils/hooks";
import { useSelectedProject } from "~/utils/hooks";
import dayjs from "~/utils/dayjs";
import { api } from "~/utils/api";
import LoggedCallTable from "~/components/dashboard/LoggedCallTable";
export default function LoggedCalls() {
const { data: selectedOrg } = useSelectedOrg();
const { data: selectedProject } = useSelectedProject();
const stats = api.dashboard.stats.useQuery(
{ organizationId: selectedOrg?.id ?? "" },
{ enabled: !!selectedOrg },
{ projectId: selectedProject?.id ?? "" },
{ enabled: !!selectedProject },
);
const data = useMemo(() => {
@@ -71,7 +71,7 @@ export default function LoggedCalls() {
</PageHeaderContainer>
<VStack px={8} pt={4} alignItems="flex-start" spacing={4}>
<Text fontSize="2xl" fontWeight="bold">
{selectedOrg?.name}
{selectedProject?.name}
</Text>
<Divider />
<VStack margin="auto" spacing={4} align="stretch" w="full">

View File

@@ -17,33 +17,35 @@ import { BsTrash } from "react-icons/bs";
import AppShell from "~/components/nav/AppShell";
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
import { api } from "~/utils/api";
import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
import CopiableCode from "~/components/CopiableCode";
import { DeleteProjectDialog } from "~/components/projectSettings/DeleteProjectDialog";
export default function Settings() {
const utils = api.useContext();
const { data: selectedOrg } = useSelectedOrg();
const { data: selectedProject } = useSelectedProject();
const apiKey =
selectedOrg?.apiKeys?.length && selectedOrg?.apiKeys[0] ? selectedOrg?.apiKeys[0].apiKey : "";
selectedProject?.apiKeys?.length && selectedProject?.apiKeys[0]
? selectedProject?.apiKeys[0].apiKey
: "";
const updateMutation = api.organizations.update.useMutation();
const updateMutation = api.projects.update.useMutation();
const [onSaveName] = useHandledAsyncCallback(async () => {
if (name && name !== selectedOrg?.name && selectedOrg?.id) {
if (name && name !== selectedProject?.name && selectedProject?.id) {
await updateMutation.mutateAsync({
id: selectedOrg.id,
id: selectedProject.id,
updates: { name },
});
await Promise.all([utils.organizations.get.invalidate({ id: selectedOrg.id })]);
await Promise.all([utils.projects.get.invalidate({ id: selectedProject.id })]);
}
}, [updateMutation, selectedOrg]);
}, [updateMutation, selectedProject]);
const [name, setName] = useState(selectedOrg?.name);
const [name, setName] = useState(selectedProject?.name);
useEffect(() => {
setName(selectedOrg?.name);
}, [selectedOrg?.name]);
setName(selectedProject?.name);
}, [selectedProject?.name]);
const deleteProjectOpen = useDisclosure();
@@ -66,7 +68,7 @@ export default function Settings() {
Project Settings
</Text>
<Text fontSize="sm">
Configure your project settings. These settings only apply to {selectedOrg?.name}.
Configure your project settings. These settings only apply to {selectedProject?.name}.
</Text>
</VStack>
<VStack
@@ -90,7 +92,7 @@ export default function Settings() {
borderColor="gray.300"
/>
<Button
isDisabled={!name || name === selectedOrg?.name}
isDisabled={!name || name === selectedProject?.name}
colorScheme="orange"
borderRadius={4}
mt={2}
@@ -113,12 +115,12 @@ export default function Settings() {
</VStack>
<CopiableCode code={apiKey} />
<Divider />
{selectedOrg?.personalOrgUserId ? (
{selectedProject?.personalProjectUserId ? (
<VStack alignItems="flex-start">
<Subtitle>Personal Project</Subtitle>
<Text fontSize="sm">
This project is {selectedOrg?.personalOrgUser?.name}'s personal project. It cannot
be deleted.
This project is {selectedProject?.personalProjectUser?.name}'s personal project.
It cannot be deleted.
</Text>
</VStack>
) : (
@@ -129,7 +131,7 @@ export default function Settings() {
</Text>
<HStack
as={Button}
isDisabled={selectedOrg?.role !== "ADMIN"}
isDisabled={selectedProject?.role !== "ADMIN"}
colorScheme="red"
variant="outline"
borderRadius={4}
@@ -137,7 +139,7 @@ export default function Settings() {
onClick={deleteProjectOpen.onOpen}
>
<Icon as={BsTrash} />
<Text>Delete {selectedOrg?.name}</Text>
<Text>Delete {selectedProject?.name}</Text>
</HStack>
</VStack>
)}

View File

@@ -9,7 +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";
import { projectsRouter } from "./routers/projects.router";
import { dashboardRouter } from "./routers/dashboard.router";
/**
@@ -27,7 +27,7 @@ export const appRouter = createTRPCRouter({
worldChamps: worldChampsRouter,
datasets: datasetsRouter,
datasetEntries: datasetEntries,
organizations: organizationsRouter,
projects: projectsRouter,
dashboard: dashboardRouter,
externalApi: externalApiRouter,
});

View File

@@ -10,7 +10,7 @@ export const dashboardRouter = createTRPCRouter({
z.object({
// TODO: actually take startDate into account
startDate: z.string().optional(),
organizationId: z.string(),
projectId: z.string(),
}),
)
.query(async ({ input }) => {
@@ -22,7 +22,7 @@ export const dashboardRouter = createTRPCRouter({
"LoggedCall.id",
"LoggedCallModelResponse.originalLoggedCallId",
)
.where("organizationId", "=", input.organizationId)
.where("projectId", "=", input.projectId)
.select(({ fn }) => [
sql<Date>`date_trunc('day', "LoggedCallModelResponse"."startTime")`.as("period"),
sql<number>`count("LoggedCall"."id")::int`.as("numQueries"),
@@ -70,7 +70,7 @@ export const dashboardRouter = createTRPCRouter({
"LoggedCall.id",
"LoggedCallModelResponse.originalLoggedCallId",
)
.where("organizationId", "=", input.organizationId)
.where("projectId", "=", input.projectId)
.select(({ fn }) => [
fn.sum(fn.coalesce("LoggedCallModelResponse.totalCost", sql<number>`0`)).as("totalCost"),
fn.count("LoggedCall.id").as("numQueries"),
@@ -79,7 +79,7 @@ export const dashboardRouter = createTRPCRouter({
const errors = await kysely
.selectFrom("LoggedCall")
.where("organizationId", "=", input.organizationId)
.where("projectId", "=", input.projectId)
.leftJoin(
"LoggedCallModelResponse",
"LoggedCall.id",

View File

@@ -3,20 +3,20 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/
import { prisma } from "~/server/db";
import {
requireCanModifyDataset,
requireCanModifyOrganization,
requireCanModifyProject,
requireCanViewDataset,
requireCanViewOrganization,
requireCanViewProject,
} from "~/utils/accessControl";
export const datasetsRouter = createTRPCRouter({
list: protectedProcedure
.input(z.object({ organizationId: z.string() }))
.input(z.object({ projectId: z.string() }))
.query(async ({ input, ctx }) => {
await requireCanViewOrganization(input.organizationId, ctx);
await requireCanViewProject(input.projectId, ctx);
const datasets = await prisma.dataset.findMany({
where: {
organizationId: input.organizationId,
projectId: input.projectId,
},
orderBy: {
createdAt: "desc",
@@ -36,26 +36,26 @@ export const datasetsRouter = createTRPCRouter({
return await prisma.dataset.findFirstOrThrow({
where: { id: input.id },
include: {
organization: true,
project: true,
},
});
}),
create: protectedProcedure
.input(z.object({ organizationId: z.string() }))
.input(z.object({ projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
await requireCanModifyOrganization(input.organizationId, ctx);
await requireCanModifyProject(input.projectId, ctx);
const numDatasets = await prisma.dataset.count({
where: {
organizationId: input.organizationId,
projectId: input.projectId,
},
});
return await prisma.dataset.create({
data: {
name: `Dataset ${numDatasets + 1}`,
organizationId: input.organizationId,
projectId: input.projectId,
},
});
}),

View File

@@ -8,9 +8,9 @@ import { generateNewCell } from "~/server/utils/generateNewCell";
import {
canModifyExperiment,
requireCanModifyExperiment,
requireCanModifyOrganization,
requireCanModifyProject,
requireCanViewExperiment,
requireCanViewOrganization,
requireCanViewProject,
} from "~/utils/accessControl";
import generateTypes from "~/modelProviders/generateTypes";
import { promptConstructorVersion } from "~/promptConstructor/version";
@@ -44,13 +44,13 @@ export const experimentsRouter = createTRPCRouter({
};
}),
list: protectedProcedure
.input(z.object({ organizationId: z.string() }))
.input(z.object({ projectId: z.string() }))
.query(async ({ input, ctx }) => {
await requireCanViewOrganization(input.organizationId, ctx);
await requireCanViewProject(input.projectId, ctx);
const experiments = await prisma.experiment.findMany({
where: {
organizationId: input.organizationId,
projectId: input.projectId,
},
orderBy: {
sortIndex: "desc",
@@ -90,7 +90,7 @@ export const experimentsRouter = createTRPCRouter({
const experiment = await prisma.experiment.findFirstOrThrow({
where: { id: input.id },
include: {
organization: true,
project: true,
},
});
@@ -108,10 +108,10 @@ export const experimentsRouter = createTRPCRouter({
}),
fork: protectedProcedure
.input(z.object({ id: z.string(), organizationId: z.string() }))
.input(z.object({ id: z.string(), projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
await requireCanViewExperiment(input.id, ctx);
await requireCanModifyOrganization(input.organizationId, ctx);
await requireCanModifyProject(input.projectId, ctx);
const [
existingExp,
@@ -264,7 +264,7 @@ export const experimentsRouter = createTRPCRouter({
id: newExperimentId,
sortIndex: maxSortIndex + 1,
label: `${existingExp.label} (forked)`,
organizationId: input.organizationId,
projectId: input.projectId,
},
}),
prisma.promptVariant.createMany({
@@ -294,9 +294,9 @@ export const experimentsRouter = createTRPCRouter({
}),
create: protectedProcedure
.input(z.object({ organizationId: z.string() }))
.input(z.object({ projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
await requireCanModifyOrganization(input.organizationId, ctx);
await requireCanModifyProject(input.projectId, ctx);
const maxSortIndex =
(
@@ -304,7 +304,7 @@ export const experimentsRouter = createTRPCRouter({
_max: {
sortIndex: true,
},
where: { organizationId: input.organizationId },
where: { projectId: input.projectId },
})
)._max?.sortIndex ?? 0;
@@ -312,7 +312,7 @@ export const experimentsRouter = createTRPCRouter({
data: {
sortIndex: maxSortIndex + 1,
label: `Experiment ${maxSortIndex + 1}`,
organizationId: input.organizationId,
projectId: input.projectId,
},
});

View File

@@ -66,7 +66,7 @@ export const externalApiRouter = createTRPCRouter({
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const reqPayload = await reqValidator.spa(input.reqPayload);
const cacheKey = hashRequest(key.organizationId, reqPayload as JsonValue);
const cacheKey = hashRequest(key.projectId, reqPayload as JsonValue);
const existingResponse = await prisma.loggedCallModelResponse.findFirst({
where: {
@@ -84,7 +84,7 @@ export const externalApiRouter = createTRPCRouter({
await prisma.loggedCall.create({
data: {
organizationId: key.organizationId,
projectId: key.projectId,
startTime: new Date(input.startTime),
cacheHit: true,
modelResponseId: existingResponse.id,
@@ -135,7 +135,7 @@ export const externalApiRouter = createTRPCRouter({
const reqPayload = await reqValidator.spa(input.reqPayload);
const respPayload = await respValidator.spa(input.respPayload);
const requestHash = hashRequest(key.organizationId, reqPayload as JsonValue);
const requestHash = hashRequest(key.projectId, reqPayload as JsonValue);
const newLoggedCallId = uuidv4();
const newModelResponseId = uuidv4();
@@ -146,7 +146,7 @@ export const externalApiRouter = createTRPCRouter({
prisma.loggedCall.create({
data: {
id: newLoggedCallId,
organizationId: key.organizationId,
projectId: key.projectId,
startTime: new Date(input.startTime),
cacheHit: false,
},

View File

@@ -5,15 +5,15 @@ import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db";
import { generateApiKey } from "~/server/utils/generateApiKey";
import userOrg from "~/server/utils/userOrg";
import userProject from "~/server/utils/userProject";
import {
requireCanModifyOrganization,
requireCanViewOrganization,
requireIsOrgAdmin,
requireCanModifyProject,
requireCanViewProject,
requireIsProjectAdmin,
requireNothing,
} from "~/utils/accessControl";
export const organizationsRouter = createTRPCRouter({
export const projectsRouter = createTRPCRouter({
list: protectedProcedure.query(async ({ ctx }) => {
const userId = ctx.session.user.id;
requireNothing(ctx);
@@ -22,9 +22,9 @@ export const organizationsRouter = createTRPCRouter({
return null;
}
const organizations = await prisma.organization.findMany({
const projects = await prisma.project.findMany({
where: {
organizationUsers: {
projectUsers: {
some: { userId: ctx.session.user.id },
},
},
@@ -33,30 +33,30 @@ export const organizationsRouter = createTRPCRouter({
},
});
if (!organizations.length) {
if (!projects.length) {
// TODO: We should move this to a separate endpoint that is called on sign up
const personalOrg = await userOrg(userId);
organizations.push(personalOrg);
const personalProject = await userProject(userId);
projects.push(personalProject);
}
return organizations;
return projects;
}),
get: protectedProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
await requireCanViewOrganization(input.id, ctx);
const [org, userRole] = await prisma.$transaction([
prisma.organization.findUnique({
await requireCanViewProject(input.id, ctx);
const [proj, userRole] = await prisma.$transaction([
prisma.project.findUnique({
where: {
id: input.id,
},
include: {
apiKeys: true,
personalOrgUser: true,
personalProjectUser: true,
},
}),
prisma.organizationUser.findFirst({
prisma.projectUser.findFirst({
where: {
userId: ctx.session.user.id,
organizationId: input.id,
projectId: input.id,
role: {
in: ["ADMIN", "MEMBER"],
},
@@ -64,20 +64,20 @@ export const organizationsRouter = createTRPCRouter({
}),
]);
if (!org) {
if (!proj) {
throw new TRPCError({ code: "NOT_FOUND" });
}
return {
...org,
...proj,
role: userRole?.role ?? null,
};
}),
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({
await requireCanModifyProject(input.id, ctx);
return await prisma.project.update({
where: {
id: input.id,
},
@@ -90,36 +90,36 @@ export const organizationsRouter = createTRPCRouter({
.input(z.object({ name: z.string() }))
.mutation(async ({ input, ctx }) => {
requireNothing(ctx);
const newOrgId = uuidv4();
const [newOrg] = await prisma.$transaction([
prisma.organization.create({
const newProjectId = uuidv4();
const [newProject] = await prisma.$transaction([
prisma.project.create({
data: {
id: newOrgId,
id: newProjectId,
name: input.name,
},
}),
prisma.organizationUser.create({
prisma.projectUser.create({
data: {
userId: ctx.session.user.id,
organizationId: newOrgId,
projectId: newProjectId,
role: "ADMIN",
},
}),
prisma.apiKey.create({
data: {
name: "Default API Key",
organizationId: newOrgId,
projectId: newProjectId,
apiKey: generateApiKey(),
},
}),
]);
return newOrg;
return newProject;
}),
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input, ctx }) => {
await requireIsOrgAdmin(input.id, ctx);
return await prisma.organization.delete({
await requireIsProjectAdmin(input.id, ctx);
return await prisma.project.delete({
where: {
id: input.id,
},

View File

@@ -9,8 +9,8 @@ import {
type OutputEvaluation,
type Dataset,
type DatasetEntry,
type Organization,
type OrganizationUser,
type Project,
type ProjectUser,
type WorldChampEntrant,
type LoggedCall,
type LoggedCallModelResponse,
@@ -43,8 +43,8 @@ interface DB {
OutputEvaluation: OutputEvaluation;
Dataset: Dataset;
DatasetEntry: DatasetEntry;
Organization: Organization;
OrganizationUser: OrganizationUser;
Project: Project;
ProjectUser: ProjectUser;
WorldChampEntrant: WorldChampEntrant;
LoggedCall: LoggedCall;
LoggedCallModelResponse: LoggedCallModelResponse;

View File

@@ -4,21 +4,21 @@ import { generateApiKey } from "~/server/utils/generateApiKey";
console.log("backfilling api keys");
const organizations = await prisma.organization.findMany({
const projects = await prisma.project.findMany({
include: {
apiKeys: true,
},
});
console.log(`found ${organizations.length} organizations`);
console.log(`found ${projects.length} projects`);
const apiKeysToCreate: Prisma.ApiKeyCreateManyInput[] = [];
for (const org of organizations) {
if (!org.apiKeys.length) {
for (const proj of projects) {
if (!proj.apiKeys.length) {
apiKeysToCreate.push({
name: "Default API Key",
organizationId: org.id,
projectId: proj.id,
apiKey: generateApiKey(),
});
}

View File

@@ -6,7 +6,7 @@ const projectId = "1234";
// Find all calls in the last 24 hours
const responses = await prisma.loggedCall.findMany({
where: {
organizationId: projectId,
projectId: projectId,
startTime: {
gt: dayjs()
.subtract(24 * 3600)
@@ -24,7 +24,7 @@ const responses = await prisma.loggedCall.findMany({
// Find all calls in the last 24 hours with promptId 'hello-world'
const helloWorld = await prisma.loggedCall.findMany({
where: {
organizationId: projectId,
projectId: projectId,
startTime: {
gt: dayjs()
.subtract(24 * 3600)
@@ -52,7 +52,7 @@ const totalSpent = await prisma.loggedCallModelResponse.aggregate({
},
where: {
originalLoggedCall: {
organizationId: projectId,
projectId: projectId,
},
startTime: {
gt: dayjs()

View File

@@ -24,9 +24,9 @@ function sortKeys(obj: JsonValue): JsonValue {
return sortedObj;
}
export function hashRequest(organizationId: string, reqPayload: JsonValue): string {
export function hashRequest(projectId: string, reqPayload: JsonValue): string {
const obj = {
organizationId,
projectId,
reqPayload,
};
return hashObject(obj);

View File

@@ -1,15 +1,15 @@
import { prisma } from "~/server/db";
import { generateApiKey } from "./generateApiKey";
export default async function userOrg(userId: string) {
return await prisma.organization.upsert({
export default async function userProject(userId: string) {
return await prisma.project.upsert({
where: {
personalOrgUserId: userId,
personalProjectUserId: userId,
},
update: {},
create: {
personalOrgUserId: userId,
organizationUsers: {
personalProjectUserId: userId,
projectUsers: {
create: {
userId: userId,
role: "ADMIN",

13
app/src/state/persist.ts Normal file
View File

@@ -0,0 +1,13 @@
import { PersistOptions } from "zustand/middleware/persist";
import { State } from "./store";
export const stateToPersist = {
selectedProjectId: null as string | null,
};
export const persistOptions: PersistOptions<State, typeof stateToPersist> = {
name: "persisted-app-store",
partialize: (state) => ({
selectedProjectId: state.selectedProjectId,
}),
};

View File

@@ -1,11 +1,13 @@
import { type StateCreator, create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { persist } from "zustand/middleware";
import { createSelectors } from "./createSelectors";
import {
type SharedVariantEditorSlice,
createVariantEditorSlice,
} from "./sharedVariantEditor.slice";
import { type APIClient } from "~/utils/api";
import { persistOptions, stateToPersist } from "./persist";
export type State = {
drawerOpen: boolean;
@@ -14,8 +16,8 @@ export type State = {
api: APIClient | null;
setApi: (api: APIClient) => void;
sharedVariantEditor: SharedVariantEditorSlice;
selectedOrgId: string | null;
setSelectedOrgId: (orgId: string) => void;
selectedProjectId: string | null;
setselectedProjectId: (id: string) => void;
};
export type SliceCreator<T> = StateCreator<State, [["zustand/immer", never]], [], T>;
@@ -23,30 +25,36 @@ export type SliceCreator<T> = StateCreator<State, [["zustand/immer", never]], []
export type SetFn = Parameters<SliceCreator<unknown>>[0];
export type GetFn = Parameters<SliceCreator<unknown>>[1];
const useBaseStore = create<State, [["zustand/immer", never]]>(
immer((set, get, ...rest) => ({
api: null,
setApi: (api) =>
set((state) => {
state.api = api;
}),
const useBaseStore = create<
State,
[["zustand/persist", typeof stateToPersist], ["zustand/immer", never]]
>(
persist(
immer((set, get, ...rest) => ({
api: null,
setApi: (api) =>
set((state) => {
state.api = api;
}),
drawerOpen: false,
openDrawer: () =>
set((state) => {
state.drawerOpen = true;
}),
closeDrawer: () =>
set((state) => {
state.drawerOpen = false;
}),
sharedVariantEditor: createVariantEditorSlice(set, get, ...rest),
selectedOrgId: null,
setSelectedOrgId: (orgId: string) =>
set((state) => {
state.selectedOrgId = orgId;
}),
})),
drawerOpen: false,
openDrawer: () =>
set((state) => {
state.drawerOpen = true;
}),
closeDrawer: () =>
set((state) => {
state.drawerOpen = false;
}),
sharedVariantEditor: createVariantEditorSlice(set, get, ...rest),
selectedProjectId: null,
setselectedProjectId: (id: string) =>
set((state) => {
state.selectedProjectId = id;
}),
})),
persistOptions,
),
);
export const useAppStore = createSelectors(useBaseStore);

View File

@@ -1,4 +1,4 @@
import { OrganizationUserRole } from "@prisma/client";
import { ProjectUserRole } from "@prisma/client";
import { TRPCError } from "@trpc/server";
import { type TRPCContext } from "~/server/api/trpc";
import { prisma } from "~/server/db";
@@ -16,16 +16,16 @@ export const requireNothing = (ctx: TRPCContext) => {
ctx.markAccessControlRun();
};
export const requireIsOrgAdmin = async (organizationId: string, ctx: TRPCContext) => {
export const requireIsProjectAdmin = async (projectId: string, ctx: TRPCContext) => {
const userId = ctx.session?.user.id;
if (!userId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const isAdmin = await prisma.organizationUser.findFirst({
const isAdmin = await prisma.projectUser.findFirst({
where: {
userId,
organizationId,
projectId,
role: "ADMIN",
},
});
@@ -37,16 +37,16 @@ export const requireIsOrgAdmin = async (organizationId: string, ctx: TRPCContext
ctx.markAccessControlRun();
};
export const requireCanViewOrganization = async (organizationId: string, ctx: TRPCContext) => {
export const requireCanViewProject = async (projectId: string, ctx: TRPCContext) => {
const userId = ctx.session?.user.id;
if (!userId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const canView = await prisma.organizationUser.findFirst({
const canView = await prisma.projectUser.findFirst({
where: {
userId,
organizationId,
projectId,
},
});
@@ -57,17 +57,17 @@ export const requireCanViewOrganization = async (organizationId: string, ctx: TR
ctx.markAccessControlRun();
};
export const requireCanModifyOrganization = async (organizationId: string, ctx: TRPCContext) => {
export const requireCanModifyProject = async (projectId: string, ctx: TRPCContext) => {
const userId = ctx.session?.user.id;
if (!userId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const canModify = await prisma.organizationUser.findFirst({
const canModify = await prisma.projectUser.findFirst({
where: {
userId,
organizationId,
role: { in: [OrganizationUserRole.ADMIN, OrganizationUserRole.MEMBER] },
projectId,
role: { in: [ProjectUserRole.ADMIN, ProjectUserRole.MEMBER] },
},
});
@@ -82,10 +82,10 @@ export const requireCanViewDataset = async (datasetId: string, ctx: TRPCContext)
const dataset = await prisma.dataset.findFirst({
where: {
id: datasetId,
organization: {
organizationUsers: {
project: {
projectUsers: {
some: {
role: { in: [OrganizationUserRole.ADMIN, OrganizationUserRole.MEMBER] },
role: { in: [ProjectUserRole.ADMIN, ProjectUserRole.MEMBER] },
userId: ctx.session?.user.id,
},
},
@@ -120,10 +120,10 @@ export const canModifyExperiment = async (experimentId: string, userId: string)
prisma.experiment.findFirst({
where: {
id: experimentId,
organization: {
organizationUsers: {
project: {
projectUsers: {
some: {
role: { in: [OrganizationUserRole.ADMIN, OrganizationUserRole.MEMBER] },
role: { in: [ProjectUserRole.ADMIN, ProjectUserRole.MEMBER] },
userId,
},
},

View File

@@ -5,10 +5,10 @@ import { NumberParam, useQueryParam, withDefault } from "use-query-params";
import { useAppStore } from "~/state/store";
export const useExperiments = () => {
const selectedOrgId = useAppStore((state) => state.selectedOrgId);
const selectedProjectId = useAppStore((state) => state.selectedProjectId);
return api.experiments.list.useQuery(
{ organizationId: selectedOrgId ?? "" },
{ enabled: !!selectedOrgId },
{ projectId: selectedProjectId ?? "" },
{ enabled: !!selectedProjectId },
);
};
@@ -27,10 +27,10 @@ export const useExperimentAccess = () => {
};
export const useDatasets = () => {
const selectedOrgId = useAppStore((state) => state.selectedOrgId);
const selectedProjectId = useAppStore((state) => state.selectedProjectId);
return api.datasets.list.useQuery(
{ organizationId: selectedOrgId ?? "" },
{ enabled: !!selectedOrgId },
{ projectId: selectedProjectId ?? "" },
{ enabled: !!selectedProjectId },
);
};
@@ -150,7 +150,10 @@ 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 });
export const useSelectedProject = () => {
const selectedProjectId = useAppStore((state) => state.selectedProjectId);
return api.projects.get.useQuery(
{ id: selectedProjectId ?? "" },
{ enabled: !!selectedProjectId },
);
};