Skip to content

Commit

Permalink
feat: add delete run, thread and trace button (#600)
Browse files Browse the repository at this point in the history
  • Loading branch information
hughcrt authored Oct 10, 2024
1 parent d29b783 commit 2c4c4e0
Show file tree
Hide file tree
Showing 10 changed files with 356 additions and 111 deletions.
41 changes: 21 additions & 20 deletions e2e/logs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import { expect, test } from "@playwright/test"
import { expect, test } from "@playwright/test";

test.describe.configure({ mode: "serial" })
test.describe.configure({ mode: "serial" });

let publicLogUrl: string
let publicLogUrl: string;

test("make a log public", async ({ page, context }) => {
await context.grantPermissions(["clipboard-read", "clipboard-write"])
await context.grantPermissions(["clipboard-read", "clipboard-write"]);

await page.goto("/logs")
await page.goto("/logs");

await page.waitForLoadState("networkidle")
await page.waitForLoadState("networkidle");

await page.getByText("xyzTESTxyz").click()
await page.getByText("xyzTESTxyz").click();

await page.getByTestId("make-log-public-switch").click()
await page.getByTestId("selected-run-menu").click();
await page.getByTestId("toggle-run-visibility").click();

publicLogUrl = await page.evaluate(() => {
const urlParams = new URLSearchParams(window.location.search)
const selected = urlParams.get("selected")
return `${window.location.origin}/logs/${selected}`
})
})
const urlParams = new URLSearchParams(window.location.search);
const selected = urlParams.get("selected");
return `${window.location.origin}/logs/${selected}`;
});
});

test("unauthenticated user can access public log URL", async ({ browser }) => {
const context = await browser.newContext()
const page = await context.newPage()
const context = await browser.newContext();
const page = await context.newPage();

await page.goto(publicLogUrl)
await page.waitForLoadState("networkidle")
await page.goto(publicLogUrl);
await page.waitForLoadState("networkidle");

await expect(page.getByText("xyzTESTxyz")).toBeVisible()
await expect(page.getByText("xyzTESTxyz")).toBeVisible();

await context.close()
})
await context.close();
});
6 changes: 2 additions & 4 deletions packages/backend/src/api/v1/checklists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ checklists.get(
checkAccess("checklists", "read"),
async (ctx: Context) => {
const { projectId } = ctx.state;
const paramsSchema = z.object({ id: z.string().uuid() });
const { id } = paramsSchema.parse(ctx.params);
const { id } = z.object({ id: z.string().uuid() }).parse(ctx.params);

const [check] = await sql`
select
Expand Down Expand Up @@ -104,8 +103,7 @@ checklists.delete(
checkAccess("checklists", "delete"),
async (ctx: Context) => {
const { projectId } = ctx.state;
const paramsSchema = z.object({ id: z.string().uuid() });
const { id } = paramsSchema.parse(ctx.params);
const { id } = z.object({ id: z.string().uuid() }).parse(ctx.params);

await sql`
delete from
Expand Down
37 changes: 37 additions & 0 deletions packages/backend/src/api/v1/runs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,4 +1024,41 @@ runs.get("/:id/feedback", async (ctx) => {
ctx.body = row.feedback;
});

/**
* @openapi
* /api/v1/runs/{id}:
* delete:
* summary: Delete a run
* description: Delete a specific run by its ID. This action is irreversible.
* tags: [Runs]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 204:
* description: Run successfully deleted
* 403:
* description: Forbidden - User doesn't have permission to delete runs
* 404:
* description: Run not found
*/
runs.delete("/:id", checkAccess("logs", "delete"), async (ctx: Context) => {
const { id } = z.object({ id: z.string().uuid() }).parse(ctx.params);
const { projectId } = ctx.state;

await sql`
delete
from run
where
id = ${id}
and project_id = ${projectId}
returning id
`;

ctx.status = 200;
});

export default runs;
4 changes: 2 additions & 2 deletions packages/frontend/components/SmartViewer/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,9 @@ function ImageMessage({ data, compact }) {
return <ProtectedText key={index}>{item.text}</ProtectedText>;
} else if (item.type === "image_url") {
return compact ? (
<MiniatureImage src={item.imageUrl.url} />
<MiniatureImage key={index} src={item.imageUrl.url} />
) : (
<ResponsiveImage src={item.imageUrl.url} />
<ResponsiveImage key={index} src={item.imageUrl.url} />
);
}
return null;
Expand Down
78 changes: 63 additions & 15 deletions packages/frontend/components/blocks/RunChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@ import { useCallback, useMemo, useState } from "react";

import { BubbleMessage } from "@/components/SmartViewer/Message";

import { useProjectSWR, useRun } from "@/utils/dataHooks";
import { useProjectSWR, useRun, useUser } from "@/utils/dataHooks";
import errorHandler from "@/utils/errors";
import { formatDateTime } from "@/utils/format";
import {
Button,
ActionIcon,
Card,
Group,
Loader,
Menu,
Pagination,
Stack,
Text,
Title,
} from "@mantine/core";
import { IconNeedleThread } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { IconDots, IconNeedleThread, IconTrash } from "@tabler/icons-react";
import Router, { useRouter } from "next/router";
import { parseAsString, useQueryState } from "nuqs";
import { hasAccess } from "shared";
import { mutate } from "swr";
import AppUserAvatar from "./AppUserAvatar";
import Feedbacks from "./Feedbacks";
Expand Down Expand Up @@ -189,7 +194,12 @@ function RunsChat({ runs, mutateLogs }) {
);
}

export function ChatReplay({ run, mutateLogs }) {
export function ChatReplay({ run, mutateLogs, deleteRun }) {
const [_, setSelectedRunId] = useQueryState<string | undefined>(
"selected",
parseAsString,
);

const { data: runs, isLoading: loading } = useProjectSWR(
run.id && `/runs?type=chat&parentRunId=${run.id}`,
);
Expand All @@ -202,19 +212,57 @@ export function ChatReplay({ run, mutateLogs }) {
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
});

const { user: currentUser } = useUser();

async function handleDeleteThread() {
modals.openConfirmModal({
title: "Delete Thread",
children: (
<Text size="sm">
Are you sure you want to delete this Thread? This action will
permanently remove the Thread and all its children. This cannot be
undone.
</Text>
),
labels: { confirm: "Delete", cancel: "Cancel" },
confirmProps: { color: "red" },
onConfirm: async () => {
await errorHandler(deleteRun(run.id));
setSelectedRunId(null);
mutateLogs();
},
});
}

return (
<Stack>
<Button
variant="outline"
ml="auto"
w="fit-content"
onClick={() => {
Router.push(`/traces/${run.id}`);
}}
rightSection={<IconNeedleThread size="16" />}
>
View trace
</Button>
<Group justify="right">
<Menu>
<Menu.Target>
<ActionIcon variant="default">
<IconDots size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconNeedleThread size={16} />}
onClick={() => {
Router.push(`/traces/${run.id}`);
}}
>
View Trace
</Menu.Item>
{hasAccess(currentUser.role, "logs", "delete") && (
<Menu.Item
leftSection={<IconTrash size={16} color="red" />}
onClick={handleDeleteThread}
>
<Text c="red">Delete Thread</Text>
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
</Group>

<Card withBorder radius="md">
<Stack gap="xs">
Expand Down
Loading

0 comments on commit 2c4c4e0

Please sign in to comment.