Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions ui/litellm-dashboard/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1109,21 +1109,11 @@
"count": 1
}
},
"src/app/(dashboard)/memory/_components/MemoryDetailDrawer.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/(dashboard)/memory/_components/MemoryEditModal.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/(dashboard)/memory/_components/MemoryView.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab.test.tsx": {
"react/display-name": {
"count": 1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { describe, expect, it, vi } from "vitest";

import { MemoryRow } from "@/components/networking";

import { MemoryDetailDrawer } from "./MemoryDetailDrawer";

const makeMemory = (overrides: Partial<MemoryRow> = {}): MemoryRow => ({
memory_id: "mem-1",
key: "user:profile",
value: "The user prefers concise answers.",
metadata: null,
user_id: "user-42",
team_id: "team-7",
created_at: "2024-05-01T12:00:00Z",
updated_at: "2024-05-02T12:00:00Z",
created_by: "alice",
updated_by: "bob",
...overrides,
});

describe("MemoryDetailDrawer", () => {
it("renders nothing until a row is selected", () => {
render(<MemoryDetailDrawer row={null} onClose={vi.fn()} />);

expect(screen.queryByText("Memory ID")).not.toBeInTheDocument();
expect(screen.queryByText("Value")).not.toBeInTheDocument();
});

it("shows the selected row's key, identifiers and value", () => {
render(<MemoryDetailDrawer row={makeMemory()} onClose={vi.fn()} />);

expect(screen.getByText("user:profile")).toBeInTheDocument();
expect(screen.getByText("Memory ID")).toBeInTheDocument();
expect(screen.getByText("mem-1")).toBeInTheDocument();
expect(screen.getByText("User ID")).toBeInTheDocument();
expect(screen.getByText("user-42")).toBeInTheDocument();
expect(screen.getByText("Team ID")).toBeInTheDocument();
expect(screen.getByText("team-7")).toBeInTheDocument();
expect(screen.getByText("Value")).toBeInTheDocument();
expect(screen.getByText("The user prefers concise answers.")).toBeInTheDocument();
});

it("falls back to a dash for a memory with no owning user or team", () => {
render(<MemoryDetailDrawer row={makeMemory({ user_id: null, team_id: null })} onClose={vi.fn()} />);

expect(screen.getAllByText("-")).toHaveLength(2);
expect(screen.queryByText("user-42")).not.toBeInTheDocument();
});

it("omits the metadata block when the row carries no metadata", () => {
render(<MemoryDetailDrawer row={makeMemory({ metadata: null })} onClose={vi.fn()} />);

expect(screen.queryByText("Metadata")).not.toBeInTheDocument();
});

it("pretty-prints metadata as JSON when present", () => {
render(<MemoryDetailDrawer row={makeMemory({ metadata: { tags: ["example"] } })} onClose={vi.fn()} />);

expect(screen.getByText("Metadata")).toBeInTheDocument();
expect(screen.getByText('{ "tags": [ "example" ] }')).toBeInTheDocument();
});

it("attributes the created and updated timestamps to their actors", () => {
render(<MemoryDetailDrawer row={makeMemory()} onClose={vi.fn()} />);

expect(screen.getByText(/^Created .* by alice$/)).toBeInTheDocument();
expect(screen.getByText(/^Updated .* by bob$/)).toBeInTheDocument();
});

it("renders an em dash for a timestamp the backend did not send", () => {
render(<MemoryDetailDrawer row={makeMemory({ created_at: undefined, created_by: undefined })} onClose={vi.fn()} />);

expect(screen.getByText("Created —")).toBeInTheDocument();
});

it("closes through the close control", async () => {
const user = userEvent.setup();
const onClose = vi.fn();
render(<MemoryDetailDrawer row={makeMemory()} onClose={onClose} />);

await user.click(screen.getByRole("button", { name: /close/i }));

expect(onClose).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
"use client";

import { Drawer, Space, Typography } from "antd";
import React from "react";

import { MemoryRow } from "@/components/networking";

const { Text, Paragraph } = Typography;
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";

interface MemoryDetailDrawerProps {
row: MemoryRow | null;
onClose: () => void;
}

const CODE_CLASS = "rounded-sm border border-border bg-muted px-1 py-0.5 font-mono text-xs text-foreground";
const BLOCK_CLASS = "mt-1 rounded-md bg-muted p-3 font-mono whitespace-pre-wrap text-foreground";
const LABEL_CLASS = "text-sm font-semibold text-foreground";

function formatTimestamp(ts?: string): string {
if (!ts) return "—";
try {
Expand All @@ -24,90 +26,61 @@ function formatTimestamp(ts?: string): string {

export function MemoryDetailDrawer({ row, onClose }: MemoryDetailDrawerProps) {
return (
<Drawer
<Sheet
open={!!row}
onClose={onClose}
title={
row ? (
<Space>
<Text code>{row.key}</Text>
</Space>
) : (
"Memory"
)
}
width={720}
destroyOnClose
onOpenChange={(open) => {
if (!open) onClose();
}}
>
{row && (
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
<Space size="large" wrap>
<div>
<Text strong style={{ display: "block" }}>
Memory ID
</Text>
<Text code style={{ fontSize: 12 }}>
{row.memory_id}
</Text>
<SheetContent className="overflow-y-auto data-[side=right]:w-full data-[side=right]:max-w-full data-[side=right]:sm:w-[720px] data-[side=right]:sm:max-w-full">
<SheetHeader className="border-b">
Comment thread
greptile-apps[bot] marked this conversation as resolved.
<SheetTitle>{row ? <code className={CODE_CLASS}>{row.key}</code> : "Memory"}</SheetTitle>
</SheetHeader>
{row && (
<div className="flex flex-col gap-4 px-4 pb-4">
<div className="flex flex-wrap gap-x-8 gap-y-3">
<div>
<span className={`block ${LABEL_CLASS}`}>Memory ID</span>
<code className={CODE_CLASS}>{row.memory_id}</code>
</div>
<div>
<span className={`block ${LABEL_CLASS}`}>User ID</span>
<span className={row.user_id ? "text-sm text-foreground" : "text-sm text-muted-foreground"}>
{row.user_id ?? "-"}
</span>
</div>
<div>
<span className={`block ${LABEL_CLASS}`}>Team ID</span>
<span className={row.team_id ? "text-sm text-foreground" : "text-sm text-muted-foreground"}>
{row.team_id ?? "-"}
</span>
</div>
</div>
<div>
<Text strong style={{ display: "block" }}>
User ID
</Text>
<Text type={row.user_id ? undefined : "secondary"}>{row.user_id ?? "-"}</Text>
<span className={LABEL_CLASS}>Value</span>
<p className={`${BLOCK_CLASS} text-[13px]`}>{row.value}</p>
</div>
<div>
<Text strong style={{ display: "block" }}>
Team ID
</Text>
<Text type={row.team_id ? undefined : "secondary"}>{row.team_id ?? "-"}</Text>
{row.metadata !== undefined && row.metadata !== null && (
<div>
<span className={LABEL_CLASS}>Metadata</span>
<p className={`${BLOCK_CLASS} text-xs`}>{JSON.stringify(row.metadata, null, 2)}</p>
</div>
)}
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span>
Created {formatTimestamp(row.created_at)}
{row.created_by ? ` by ${row.created_by}` : ""}
</span>
<span aria-hidden="true">·</span>
<span>
Updated {formatTimestamp(row.updated_at)}
{row.updated_by ? ` by ${row.updated_by}` : ""}
</span>
</div>
</Space>
<div>
<Text strong>Value</Text>
<Paragraph
style={{
background: "#fafafa",
padding: 12,
borderRadius: 6,
whiteSpace: "pre-wrap",
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
fontSize: 13,
}}
>
{row.value}
</Paragraph>
</div>
{row.metadata !== undefined && row.metadata !== null && (
<div>
<Text strong>Metadata</Text>
<Paragraph
style={{
background: "#fafafa",
padding: 12,
borderRadius: 6,
whiteSpace: "pre-wrap",
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
fontSize: 12,
}}
>
{JSON.stringify(row.metadata, null, 2)}
</Paragraph>
</div>
)}
<Space split={<Text type="secondary">·</Text>} wrap size="small" style={{ color: "rgba(0,0,0,0.45)" }}>
<Text type="secondary">
Created {formatTimestamp(row.created_at)}
{row.created_by ? ` by ${row.created_by}` : ""}
</Text>
<Text type="secondary">
Updated {formatTimestamp(row.updated_at)}
{row.updated_by ? ` by ${row.updated_by}` : ""}
</Text>
</Space>
</Space>
)}
</Drawer>
)}
</SheetContent>
</Sheet>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render } from "@testing-library/react";
import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { describe, expect, it, vi } from "vitest";

Expand All @@ -12,6 +13,7 @@ interface CapturedTableProps {
rowCount: number;
data: MemoryRow[];
hasActiveSearch: boolean;
onViewClick: (row: MemoryRow) => void;
}

const captured = vi.hoisted(() => ({ current: null as CapturedTableProps | null }));
Expand Down Expand Up @@ -42,4 +44,47 @@ describe("MemoryView", () => {
expect(captured.current?.rowCount).toBe(0);
expect(captured.current?.hasActiveSearch).toBe(false);
});

it("heads the page with the Memory title and the /v1/memory scope note", () => {
renderView(null);

expect(screen.getByRole("heading", { name: "Memory" })).toBeInTheDocument();
expect(screen.getByText("/v1/memory")).toBeInTheDocument();
expect(screen.getByText(/Scoped to memories visible to your user \/ team \(admins see all\)/)).toBeInTheDocument();
});

it("opens the create modal from the New memory button", async () => {
const user = userEvent.setup();
renderView(null);

expect(screen.queryByText("Create memory")).not.toBeInTheDocument();

await user.click(screen.getByRole("button", { name: /new memory/i }));

expect(await screen.findByText("Create memory")).toBeInTheDocument();
});

it("opens the detail drawer for the row the table hands back, and closes it again", async () => {
const user = userEvent.setup();
renderView(null);

expect(screen.queryByText("Memory ID")).not.toBeInTheDocument();

const row: MemoryRow = {
memory_id: "mem-drawer",
key: "user:profile",
value: "remembered",
metadata: null,
user_id: null,
team_id: null,
};
act(() => captured.current?.onViewClick(row));

expect(await screen.findByText("Memory ID")).toBeInTheDocument();
expect(screen.getByText("mem-drawer")).toBeInTheDocument();

await user.click(screen.getByRole("button", { name: /close/i }));

expect(screen.queryByText("mem-drawer")).not.toBeInTheDocument();
});
});
Loading
Loading