Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/bright-requests-observe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@prisma/studio-core": minor
---

Add a Requests observability view with dummy request rows, trace spans, and associated logs.
4 changes: 2 additions & 2 deletions Architecture/navigation-url-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Navigation state MUST be URL-driven and managed through `useNavigation` + Nuqs.

This architecture governs:

- active Studio view (`table`, `schema`, `console`, `sql`, `stream`)
- active Studio view (`table`, `schema`, `console`, `requests`, `sql`, `stream`)
- active schema/table/stream
- active stream follow mode
- active stream aggregation-panel visibility
Expand Down Expand Up @@ -86,7 +86,7 @@ Adding a new URL key requires updating `StateKey` in `nuqs.ts` first.
When Studio is running without a database connection but with Streams enabled:

- the resolved default `view` MUST become `"stream"` instead of `"table"`
- stale database-oriented views such as `table`, `schema`, `console`, and `sql` MUST resolve back to the stream view instead of trying to render database-only UI against a disabled database session
- stale database-oriented views such as `table`, `schema`, `console`, `requests`, and `sql` MUST resolve back to the stream view instead of trying to render database-only UI against a disabled database session

When URL params are stale from a previous DB, invalid `schema`/`table` values MUST be resolved to valid current defaults.
Shared table page size and infinite-scroll mode are not derived from URL defaults; they are restored through Studio UI state and then mirrored into query behavior by `usePagination`.
Expand Down
12 changes: 12 additions & 0 deletions Architecture/non-standard-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,18 @@ It deliberately excludes:
- The storage breakdowns also need collapsible ledger-style accounting boxes whose headers surface the section totals when folded shut, plus faint shared-cap annotations that sit beside right-aligned byte values and one shared cap marker spanning both Routing and Exact cache rows, which is not a stock ShadCN pattern.
- No stock ShadCN pattern covers that descriptor-driven observability layout, especially when the UI must distinguish logical bytes from physical storage signals, separate search coverage from historical run indexes, hide unconfigured routing rows, and keep the remaining cost caveats explicit instead of inventing unavailable totals.

### Requests Trace Timeline

- Canonical component:
- [`ui/studio/views/requests/RequestsView.tsx`](ui/studio/views/requests/RequestsView.tsx)
- Closest standard ShadCN alternatives:
- `Table`
- `Badge`
- `ToggleGroup`
- Why it stays non-standard:
- The Requests view needs a dense request-log row that expands in place into a span waterfall. Standard ShadCN components cover the table, badges, and view toggle, but there is no stock ShadCN timeline primitive that can display OpenTelemetry-style nested spans with proportional start offsets and durations.
- The custom portion is limited to the timeline grid and span bars; the surrounding list and controls remain built from ShadCN primitives.

## Standardization Candidates

These are the current high-signal places where Studio is bypassing a plausible standard ShadCN component or composition pattern.
Expand Down
100 changes: 100 additions & 0 deletions Architecture/requests-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Requests View Architecture

This document is normative for the Studio Requests view.

The Requests view is a database-session view that presents request-level observability data in the existing Studio shell. The first implementation uses deterministic dummy data, but its UI contract is shaped so real request, span, and log data can replace that source later without changing navigation.

## Scope

This architecture governs:

- routing into `view=requests`
- sidebar and command-palette entry points
- request list ordering and summary columns
- in-place request expansion
- trace and log detail toggling
- dummy request/span/log data shape

## Canonical Components

- [`ui/studio/Navigation.tsx`](../ui/studio/Navigation.tsx)
- [`ui/studio/CommandPalette.tsx`](../ui/studio/CommandPalette.tsx)
- [`ui/studio/Studio.tsx`](../ui/studio/Studio.tsx)
- [`ui/studio/views/requests/RequestsView.tsx`](../ui/studio/views/requests/RequestsView.tsx)
- [`ui/hooks/use-navigation.tsx`](../ui/hooks/use-navigation.tsx)
- [`ui/hooks/use-ui-state.ts`](../ui/hooks/use-ui-state.ts)

## Non-Negotiable Rules

- Requests routing MUST use the existing URL-backed `view` param with the value `requests`.
- The left navigation MUST render Requests in the Studio block immediately after Console and before SQL while database-backed Studio views are available.
- The request list MUST render most recent requests first.
- Summary rows MUST include timestamp, service, path, message, and duration.
- Request expansion MUST happen in place under the clicked row instead of navigating away or opening a modal.
- Expanded request state and the selected detail view MUST use `useUiState` with deterministic request-scoped keys.
- Trace details MUST show proportional span durations for external requests and Prisma OpenTelemetry-style subsystem spans when dummy data includes them.
- Log details MUST handle both string messages and structured object payloads without dropping the original structured object.

## Dummy Data Contract

Until Studio is wired to a real request source, dummy rows live in the Requests view module. Each request row MUST include:

- a stable request id
- timestamp
- service
- method and path
- status
- message
- duration in milliseconds
- trace id
- zero or more trace spans
- zero or more associated log lines

Each span MUST include:

- stable span id
- display name
- service
- kind (`request`, `framework`, `external`, or `prisma`)
- start offset in milliseconds
- duration in milliseconds
- nesting depth
- status

Each log line MUST include:

- stable log id
- timestamp
- level
- service
- message as either a string or structured object

## UI State Contract

The expanded request id MUST be stored through `useUiState` using:

- `requests:expanded-request`

The active detail view for each expanded request MUST be stored through `useUiState` using:

- `requests:${requestId}:detail-view`

These keys keep request-detail interaction consistent with the existing Studio UI state architecture and avoid introducing component-local shared view state.

## Forbidden Patterns

- Do not introduce a second routing system for requests.
- Do not store expanded request state in module globals.
- Do not replace structured log objects with stringified summaries as the source data.
- Do not open trace or log details in a modal or side panel for the default row click behavior.

## Testing Requirements

Requests view changes MUST include tests for:

- `view=requests` routing in `Studio`
- sidebar placement directly under Console
- newest-first dummy request list rendering with the required summary columns
- in-place row expansion
- trace detail rendering for external and Prisma spans
- log detail rendering for both string and structured object payloads
1 change: 1 addition & 0 deletions Architecture/ui-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ The following are valid examples of UI state and where they belong:
- Scoped by active schema plus the current visualized table set so returning to the same schema graph restores dragged positions without leaking across schemas.
- Includes the stored ELK baseline positions and reset-layout request token used by the header action.
- Command-palette `x more...` handoff into table browsing: the same navigation table-name search `useUiState` entry, not a second command-palette-specific table-filter store
- Requests view expanded request id and selected request detail view: `uiLocalStateCollection` via `useUiState`

If new UI state is shared across components, it MUST be assigned to one of these stores (or a new TanStack DB collection added in Studio context).
Container-level fullscreen controls are now host-owned rather than Studio-owned, so they MUST NOT be reintroduced as implicit package-level shared UI state unless the architecture is updated first.
Expand Down
6 changes: 6 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ In table view it surfaces context-aware actions like row search, AI filtering, i
Table navigation stays intentionally short by showing only the first 3 tables by default and the top 3 matches while filtering. If more tables exist, the palette shows an `x more...` entry that hands off into the existing sidebar table search so users keep one consistent table-filtering UI.
`Search rows` and `Filter with AI` work in two modes: typing the command name keeps them as focus actions for the existing toolbar inputs, while free text turns them into direct `Search rows: ...` and `Filter with AI: ...` actions that execute immediately. Keyboard selection stays active from the moment the palette opens, so arrow keys can move through results before any typing, and the list auto-scrolls the active result into view as you move into lower sections.

## Request Observability

The Requests view gives Studio a request-centered observability surface for inspecting dummy request data in the same shell as tables, SQL, and Console.
Each request row shows timestamp, service, path, message, and duration with newest requests first, then expands in place to show an OpenTelemetry-style trace timeline or the log lines associated with that request.
The trace view highlights external calls and Prisma subsystem spans, while the logs view preserves both plain string messages and structured JSON objects for detailed inspection.

## Column Controls and Metadata

Columns support drag-and-drop reordering, resizing, sorting, and pinning to keep important fields anchored during wide-table review.
Expand Down
6 changes: 5 additions & 1 deletion ui/studio/CommandPalette.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface NavigationMockValue {
schemaParam: string;
setSchemaParam: () => Promise<URLSearchParams>;
setTableParam: () => Promise<URLSearchParams>;
viewParam: "table" | "schema" | "console" | "sql";
viewParam: "table" | "schema" | "console" | "requests" | "sql";
}

interface IntrospectionMockValue {
Expand Down Expand Up @@ -150,8 +150,11 @@ vi.mock("../hooks/use-ui-state", async () => {

vi.mock("./context", () => ({
useStudio: () => ({
hasDatabase: true,
isDarkMode,
isNavigationOpen,
navigationWidth: 192,
setNavigationWidth: vi.fn(),
setThemeMode: setThemeModeMock,
themeMode,
toggleNavigation: toggleNavigationMock,
Expand Down Expand Up @@ -352,6 +355,7 @@ describe("Studio command palette", () => {
expect(document.body.textContent).not.toContain("incidents");
expect(document.body.textContent).toContain("Visualizer");
expect(document.body.textContent).toContain("Console");
expect(document.body.textContent).toContain("Requests");
expect(document.body.textContent).toContain("SQL");
expect(document.body.textContent).toContain("Studio theme");
expect(document.body.textContent).toContain("Light");
Expand Down
16 changes: 15 additions & 1 deletion ui/studio/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FileCode2,
GalleryVerticalEnd,
Laptop,
ListTree,
Moon,
Search,
Sun,
Expand Down Expand Up @@ -164,7 +165,9 @@ function AppearanceCommandItem(props: {
value={value}
className={cn(
"justify-between gap-3",
disabled ? "text-muted-foreground/55" : "text-foreground hover:bg-secondary/85",
disabled
? "text-muted-foreground/55"
: "text-foreground hover:bg-secondary/85",
)}
>
<span className="flex min-w-0 items-center gap-2.5">
Expand Down Expand Up @@ -508,6 +511,17 @@ function StudioCommandPalette() {
},
section: "views",
},
{
disabled: false,
icon: ListTree,
id: "view:requests",
keywords: ["requests", "logs", "traces", "observability"],
label: "Requests",
onSelect: () => {
window.location.hash = createUrl({ viewParam: "requests" });
},
section: "views",
},
{
disabled: false,
icon: FileCode2,
Expand Down
33 changes: 32 additions & 1 deletion ui/studio/Navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface NavigationMockValue {
setSchemaParam: () => Promise<URLSearchParams>;
setTableParam: () => Promise<URLSearchParams>;
streamParam: string | null;
viewParam: "table" | "schema" | "console" | "sql" | "stream";
viewParam: "table" | "schema" | "console" | "requests" | "sql" | "stream";
}

interface IntrospectionMockValue {
Expand Down Expand Up @@ -476,6 +476,37 @@ describe("Navigation", () => {
container.remove();
});

it("renders Requests directly under Console in the Studio menu", () => {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);

act(() => {
root.render(<Navigation />);
});

const studioLinks = [
...container.querySelectorAll<HTMLAnchorElement>(
'nav[aria-label="Studio"] a',
),
].map((link) => ({
href: link.getAttribute("href"),
text: link.textContent?.trim(),
}));

expect(studioLinks).toEqual([
{ href: "#viewParam=schema", text: "Visualizer" },
{ href: "#viewParam=console", text: "Console" },
{ href: "#viewParam=requests", text: "Requests" },
{ href: "#viewParam=sql", text: "SQL" },
]);

act(() => {
root.unmount();
});
container.remove();
});

it("closes table search on blur when the search input is empty", () => {
const container = document.createElement("div");
document.body.appendChild(container);
Expand Down
9 changes: 9 additions & 0 deletions ui/studio/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,15 @@ export function Navigation({ className }: NavigationProps) {
Console
</a>
</Navigation.Item>
<Navigation.Item
asChild
isActive={viewParam === "requests"}
className={navigationItemClasses}
>
<a href={createUrl({ viewParam: "requests" })} className="w-full">
Requests
</a>
</Navigation.Item>
<Navigation.Item
asChild
isActive={viewParam === "sql"}
Expand Down
46 changes: 45 additions & 1 deletion ui/studio/Studio.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type NavigationMockValue = {
metadata: {
activeTable: undefined;
};
viewParam: "table" | "stream";
viewParam: "table" | "stream" | "requests";
};

type IntrospectionMockValue = {
Expand Down Expand Up @@ -83,6 +83,10 @@ vi.mock("./views/console/ConsoleView", () => ({
ConsoleView: () => <div>Console view</div>,
}));

vi.mock("./views/requests/RequestsView", () => ({
RequestsView: () => <div>Requests view</div>,
}));

vi.mock("./views/schema/SchemaView", () => ({
SchemaView: () => <div>Schema view</div>,
}));
Expand Down Expand Up @@ -252,6 +256,46 @@ describe("Studio", () => {
container.remove();
});

it("renders the requests view when navigation selects requests", () => {
useNavigationMock.mockReturnValue({
metadata: {
activeTable: undefined,
},
viewParam: "requests",
});

const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);

act(() => {
root.render(
<Studio
adapter={
{
delete: vi.fn(),
introspect: vi.fn(),
insert: vi.fn(),
query: vi.fn(),
raw: vi.fn(),
update: vi.fn(),
} as unknown as Adapter
}
/>,
);
});

expect(container.textContent).toContain("Requests view");
expect(container.textContent).not.toContain(
"Could not load schema metadata",
);

act(() => {
root.unmount();
});
container.remove();
});

it("renders a database-unavailable placeholder for database views when the session has no database", () => {
useStudioMock.mockReturnValue({
hasDatabase: false,
Expand Down
2 changes: 2 additions & 0 deletions ui/studio/Studio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { IntrospectionStatusNotice } from "./IntrospectionStatusNotice";
import { Navigation } from "./Navigation";
import { StudioHeader } from "./StudioHeader";
import { ConsoleView } from "./views/console/ConsoleView";
import { RequestsView } from "./views/requests/RequestsView";
import { SchemaView } from "./views/schema/SchemaView";
import { SqlView } from "./views/sql/SqlView";
import { StreamView } from "./views/stream/StreamView";
Expand Down Expand Up @@ -113,6 +114,7 @@ const views: Record<string, (props: ViewProps) => JSX.Element | null> = {
table: ActiveTableView,
stream: StreamView,
console: ConsoleView,
requests: RequestsView,
sql: SqlView,
default: BasicView,
};
Expand Down
Loading