Skip to content

Commit

Permalink
feat: add empty Alerts Page + mock API behind feature flag
Browse files Browse the repository at this point in the history
  • Loading branch information
flxwu committed Dec 23, 2023
1 parent d5aa809 commit 688afa5
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 4 deletions.
13 changes: 11 additions & 2 deletions jest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,24 @@ const serverTestConfig = {
testEnvironment: "jest-environment-node",
};

// To avoid the "Cannot use import statement outside a module" errors while transforming ESM.
const esModules = ["superjson"];
const transformIgnorePatterns = [`/node_modules/(?!(${esModules.join("|")})/)`];
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
// Add more setup options before each test is run
silent: false,
verbose: true,
projects: [
await createJestConfig(clientTestConfig)(),
await createJestConfig(serverTestConfig)(),
{
...(await createJestConfig(clientTestConfig)()),
transformIgnorePatterns,
},
{
...(await createJestConfig(serverTestConfig)()),
transformIgnorePatterns,
},
],
};

Expand Down
48 changes: 48 additions & 0 deletions src/__tests__/features/alerts/AlertsTable.clienttest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { AlertsTable } from "@/src/features/alerts/AlertsTable";
import { api } from "@/src/utils/api";
import "@testing-library/jest-dom";
import { render, screen, cleanup } from "@testing-library/react";

afterEach(cleanup);

jest.mock("../../../utils/api", () => ({
api: {
alerts: {
all: {
useQuery: jest.fn(),
},
},
},
}));

describe("AlertsTable", () => {
it("renders the table headers", () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
(api.alerts.all.useQuery as jest.Mock).mockReturnValue({
isLoading: false,
isError: false,
data: [
{
id: "id-1",
name: "Alert 1",
triggerAttribute: "cost",
triggerOperator: ">",
triggerValue: 10,
},
],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);

render(<AlertsTable projectId="7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" />);

const nameCol = screen.getByText(/Name/);
expect(nameCol).toBeInTheDocument();
const triggerCol = screen.getByText(/Trigger/);
expect(triggerCol).toBeInTheDocument();

const nameRow = screen.getByText(/Alert 1/);
expect(nameRow).toBeInTheDocument();
const triggerRow = screen.getByText(/cost > 10/);
expect(triggerRow).toBeInTheDocument();
});
});
46 changes: 46 additions & 0 deletions src/__tests__/server/api/routers/alerts.servertest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/** @jest-environment node */
import { pruneDatabase } from "@/src/__tests__/test-utils";
import { appRouter } from "@/src/server/api/root";
import { createInnerTRPCContext } from "@/src/server/api/trpc";
import { prisma } from "@/src/server/db";
import type { Session } from "next-auth";

describe("Alerts TRPC Router", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());

const session: Session = {
expires: "1",
user: {
id: "clgb17vnp000008jjere5g15i",
name: "John Doe",
projects: [
{
id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
role: "ADMIN",
name: "test",
},
],
featureFlags: {
templateFlag: true,
costAlerts: true,
},
admin: true,
},
};

const ctx = createInnerTRPCContext({ session });
const caller = appRouter.createCaller({ ...ctx, prisma });

test("alerts.all RPC returns an array of alerts", async () => {
// await prisma.trace.create({
// data: { ...trace, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
// });

const traces = await caller.alerts.all({
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
});
expect(traces).toBeDefined();
expect(traces).toHaveLength(1);
});
});
7 changes: 7 additions & 0 deletions src/components/layouts/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TextSelect,
UsersIcon,
Route,
AlertCircle,
} from "lucide-react";

export const ROUTES: Array<{
Expand Down Expand Up @@ -54,6 +55,12 @@ export const ROUTES: Array<{
pathname: `/project/[projectId]/datasets`,
icon: Database,
},
{
name: "Alerts",
pathname: "/project/[projectId]/alerts",
icon: AlertCircle,
featureFlag: "costAlerts",
},
{
name: "Settings",
pathname: "/project/[projectId]/settings",
Expand Down
67 changes: 67 additions & 0 deletions src/features/alerts/AlertsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { DataTable } from "@/src/components/table/data-table";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { NewAlertButton } from "@/src/features/alerts/NewAlertButton";
import { api } from "@/src/utils/api";
import { type RouterOutput } from "@/src/utils/types";

type RowData = {
key: {
id: string;
name: string;
};
trigger: string;
};

export function AlertsTable(props: { projectId: string }) {
const alerts = api.alerts.all.useQuery({
projectId: props.projectId,
});

const columns: LangfuseColumnDef<RowData>[] = [
{
accessorKey: "key",
header: "Name",
cell: ({ row }) => {
const key: RowData["key"] = row.getValue("key");
return key.name;
},
},
{
accessorKey: "trigger",
header: "Trigger",
},
];

const convertToTableRow = (
item: RouterOutput["alerts"]["all"][number],
): RowData => {
return {
key: { id: item.id, name: item.name },
trigger: `${item.triggerAttribute} ${item.triggerOperator} ${item.triggerValue}`,
};
};

return (
<div>
<DataTable
columns={columns}
data={
alerts.isLoading
? { isLoading: true, isError: false }
: alerts.isError
? {
isLoading: false,
isError: true,
error: alerts.error.message,
}
: {
isLoading: false,
isError: false,
data: alerts.data.map((t) => convertToTableRow(t)),
}
}
/>
<NewAlertButton projectId={props.projectId} className="mt-4" />
</div>
);
}
50 changes: 50 additions & 0 deletions src/features/alerts/NewAlertButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Button } from "@/src/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/src/components/ui/dialog";
import { DialogTrigger } from "@radix-ui/react-dialog";
import { LockIcon, PlusIcon } from "lucide-react";
import { useState } from "react";

export const NewAlertButton = (props: {
projectId: string;
className?: string;
}) => {
const [open, setOpen] = useState(false);
// TODO: create scope and check access using useHasAccess
const hasAccess = true;

return (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
<Dialog open={hasAccess && open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="secondary"
className={props.className}
disabled={!hasAccess}
>
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
{hasAccess ? (
<PlusIcon className="-ml-0.5 mr-1.5" aria-hidden="true" />
) : (
<LockIcon className="-ml-0.5 mr-1.5 h-3 w-3" aria-hidden="true" />
)}
New Alert
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-3xl">
<DialogHeader>
<DialogTitle className="mb-5">Create new Alert</DialogTitle>
</DialogHeader>
{/* TODO: Form for creating new alert
* <NewDatasetForm
projectId={props.projectId}
onFormSuccess={() => setOpen(false)}
/> */}
</DialogContent>
</Dialog>
);
};
2 changes: 1 addition & 1 deletion src/features/feature-flags/available-flags.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const availableFlags = ["templateFlag"] as const;
export const availableFlags = ["templateFlag", "costAlerts"] as const;
15 changes: 15 additions & 0 deletions src/pages/project/[projectId]/alerts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Header from "@/src/components/layouts/header";
import { AlertsTable } from "@/src/features/alerts/AlertsTable";
import { useRouter } from "next/router";

export default function Alerts() {
const router = useRouter();
const projectId = router.query.projectId as string;

return (
<div>
<Header title="Alerts" />
<AlertsTable projectId={projectId} />
</div>
);
}
2 changes: 2 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { environmentRouter } from "@/src/server/api/routers/environment";
import { usageMeteringRouter } from "@/src/features/usage-metering/server/usageMeteringRouter";
import { observationsRouter } from "@/src/server/api/routers/observations";
import { sessionRouter } from "@/src/server/api/routers/sessions";
import { alertsRouter } from "@/src/server/api/routers/alerts";

/**
* This is the primary router for your server.
Expand All @@ -20,6 +21,7 @@ import { sessionRouter } from "@/src/server/api/routers/sessions";
*/
export const appRouter = createTRPCRouter({
traces: traceRouter,
alerts: alertsRouter,
sessions: sessionRouter,
generations: generationsRouter,
scores: scoresRouter,
Expand Down
27 changes: 27 additions & 0 deletions src/server/api/routers/alerts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from "zod";

import {
createTRPCRouter,
protectedProjectProcedure,
} from "@/src/server/api/trpc";
import { paginationZod } from "@/src/utils/zod";

const AlertFilterOptions = z.object({
projectId: z.string(), // Required for protectedProjectProcedure
...paginationZod,
});

export const alertsRouter = createTRPCRouter({
all: protectedProjectProcedure.input(AlertFilterOptions).query(async () => {
const alerts = [
{
id: "1",
name: "Alert 1",
triggerAttribute: "cost",
triggerOperator: ">",
triggerValue: 100,
},
];
return Promise.resolve(alerts);
}),
});
2 changes: 1 addition & 1 deletion src/server/api/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type CreateContextOptions = {
*
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
*/
const createInnerTRPCContext = (opts: CreateContextOptions) => {
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
return {
session: opts.session,
prisma,
Expand Down

0 comments on commit 688afa5

Please sign in to comment.