feat(ui): drop the Lens preview header and keep setup within the page

The sample card already shows the match count, so the preview header
repeated it. On wide screens the setup no longer scrolls as a page: the
preview table and, when needed, the steps column scroll on their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yujong Lee 2026-10-04 20:17:47 -07:00
parent 284f12f522
commit fcf977b522
5 changed files with 18 additions and 53 deletions

View file

@ -189,7 +189,7 @@ export function InvestigationsView({ readOnly = false }: InvestigationsViewProps
return (
<section
aria-label="Investigations"
className={cn("flex w-full min-w-0 flex-1 flex-col", !browsing && "gap-3 p-4")}
className={cn("flex min-h-0 w-full min-w-0 flex-1 flex-col", !browsing && "gap-3 p-4")}
>
<Banners
error={bannerError}

View file

@ -171,7 +171,6 @@ describe("Investigation setup", () => {
await user.type(screen.getByRole("textbox", { name: "Check 1" }), "Find incomplete reports");
await user.click(screen.getByRole("button", { name: "Continue" }));
await waitFor(() => expect(screen.getByRole("button", { name: "Run and monitor" })).toBeEnabled());
expect(screen.getByText("1 matching run")).toBeInTheDocument();
expect(screen.getByText("Research report")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Run and monitor" }));
expect(save).toHaveBeenCalledWith(
@ -340,9 +339,9 @@ it.each(["new", "duplicate"] as const)("blocks a %s investigation until its mode
: Promise.resolve(gatewayResponse(path, {})),
);
renderWithProviders(<InvestigationSetup initial={settings} mode={mode} onClose={vi.fn()} onSave={save} />);
expect(await screen.findByText("100% of 1")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Continue" }));
await user.click(screen.getByRole("button", { name: "Continue" }));
expect(await screen.findByText("1 matching run")).toBeInTheDocument();
expect(screen.getByRole("button", { name: mode === "new" ? "Run and monitor" : "Run investigation" })).toBeDisabled();
expect(save).not.toHaveBeenCalled();
});
@ -393,14 +392,14 @@ it("appends the next preview page as the list scrolls near its end, then stops a
renderWithProviders(<InvestigationSetup mode="edit" initial={settings} onClose={vi.fn()} onSave={vi.fn()} />);
expect(await screen.findByText("Run one")).toBeVisible();
expect(screen.getByText(/Showing 1 of 2/)).toBeVisible();
expect(screen.getByRole("status")).toHaveTextContent("2 matching runs");
expect(screen.getByText("100% of 2")).toBeVisible();
const nextPageCalls = () =>
proxy.post.mock.calls.filter(([, options]) => (options?.body as { cursor: string }).cursor === "next");
expect(nextPageCalls()).toHaveLength(0);
act(() => mockAllIsIntersecting(true));
await waitFor(() => expect(nextPageCalls()).toHaveLength(1));
expect(screen.getByText("Run one")).toBeVisible();
expect(screen.getByRole("status")).toHaveTextContent("2 matching runs");
expect(screen.getByText("100% of 2")).toBeVisible();
act(() => finishSecondPage());
expect(await screen.findByText("Run two")).toBeVisible();
expect(screen.getByText("Run one")).toBeVisible();
@ -423,7 +422,7 @@ it("fetches one preview for two keystrokes inside the debounce window", async ()
await vi.advanceTimersByTimeAsync(1000);
await waitFor(() => expect(previewsFor("ab")).toHaveLength(1));
expect(previewsFor("a")).toHaveLength(0);
expect(await screen.findByText("1 matching run")).toBeVisible();
expect(await screen.findByText(/% of 1$/)).toBeVisible();
} finally {
vi.useRealTimers();
}

View file

@ -163,7 +163,7 @@ function SetupEditor({
const saveLabel = saveLabelFor(mode, repeat);
const offline = !ready && mode !== "edit";
return (
<section aria-label={TITLES[mode]} className="flex min-w-0 flex-1 flex-col">
<section aria-label={TITLES[mode]} className="flex min-h-0 min-w-0 flex-1 flex-col">
<header className="flex items-center gap-1 border-b pb-3">
<Button
variant="ghost"
@ -194,8 +194,13 @@ function SetupEditor({
The worker or trace storage is unavailable. Your draft is safe; you can start when it reconnects.
</p>
)}
<div className="grid min-w-0 flex-1 items-start gap-8 pt-6 lg:grid-cols-[minmax(0,26rem)_minmax(0,1fr)] xl:gap-10">
<SetupSteps aria-label="Investigation setup" current={step} onOpen={setStep}>
<div className="grid min-h-0 min-w-0 flex-1 items-start gap-8 pt-6 lg:grid-cols-[minmax(0,26rem)_minmax(0,1fr)] lg:grid-rows-[minmax(0,1fr)] xl:gap-10">
<SetupSteps
aria-label="Investigation setup"
current={step}
onOpen={setStep}
className="lg:-m-1 lg:max-h-full lg:overflow-y-auto lg:p-1"
>
<SetupStep id="activity" heading="Activity" description="Which runs to review" summary={<ActivitySummary />}>
<ScopeFields {...activity.scope} />
<SampleFields eligible={activity.preview.page.eligible} />
@ -232,7 +237,7 @@ function SetupEditor({
noun="run"
storageKey={FINDING_PANEL_WIDTH_KEY}
>
<MatchingActivityPreview {...activity.preview} className="min-w-0 lg:sticky lg:top-0" />
<MatchingActivityPreview {...activity.preview} className="min-w-0 lg:max-h-full" />
<Inspector.Panel label="Run details" testId="run-panel">
{(run: TraceRef) => (
<TraceEvidence

View file

@ -1,7 +1,6 @@
"use client";
import { memo, useMemo, type ComponentProps } from "react";
import { RotateCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/cva.config";
@ -72,31 +71,14 @@ export function MatchingActivityPreview({
[pickedIds, togglePick],
);
const shown = status.ready || status.stale;
const showTable = status.loading || (shown && !status.error && runs.length > 0);
return (
<section
aria-label="Matching activity"
data-slot="matching-activity-preview"
className={cn("self-start overflow-hidden rounded-lg border bg-card", className)}
className={cn("flex flex-col self-start overflow-hidden rounded-lg border bg-card", className)}
{...props}
>
<header className="flex items-start justify-between gap-3 border-b px-4 py-3">
<div className="grid min-w-0 gap-0.5">
<p className="text-sm font-semibold tabular-nums" role="status">
{status.title}
</p>
<p className="text-xs text-muted-foreground">{status.windowLabel} · Previewing is free</p>
</div>
<Button
variant="ghost"
size="icon-sm"
className="-mr-1.5 text-muted-foreground"
aria-label="Refresh matching activity"
onClick={status.refresh}
disabled={!status.ready}
>
<RotateCw className={cn("size-3.5", (status.loading || status.stale) && "motion-safe:animate-spin")} />
</Button>
</header>
{status.ready && status.error && (
<p role="alert" className="px-4 py-3 text-sm text-destructive">
{status.error.message}{" "}
@ -113,8 +95,8 @@ export function MatchingActivityPreview({
</p>
</div>
)}
{(status.loading || (shown && !status.error && runs.length > 0)) && (
<div className="flex max-h-[calc(100dvh-16rem)] min-h-0 flex-col">
{showTable && (
<div className="flex max-h-[calc(100dvh-16rem)] min-h-0 flex-1 flex-col lg:max-h-none">
<PreviewTable
traces={runs}
isLoading={status.loading}

View file

@ -5,7 +5,6 @@ import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { useFormContext, useWatch } from "react-hook-form";
import { lensKeys, lensQueries } from "../data/queries";
import { useLensApi } from "../data/LensServices";
import { durationLabel } from "../model/format";
import type { Execution, Sample } from "../model/types";
import type { TraceSummary } from "../traces/types";
import { useDebouncedValue } from "./useDebouncedValue";
@ -26,8 +25,6 @@ export interface ScopeOptions {
}
export interface PreviewStatus {
readonly title: string;
readonly windowLabel: string;
readonly ready: boolean;
/** No results yet for any search: the table shows skeleton rows. */
readonly loading: boolean;
@ -79,27 +76,11 @@ function validScope(scope: Selection): boolean {
return validWindow(scope) && validSampling;
}
function previewTitle(
state: { pending: boolean; validWindow: boolean; valid: boolean },
data: PreviewPageData | undefined,
): string {
if (!state.validWindow) return "Choose a history window between 1 hour and 365 days";
if (!state.valid) return "Complete your sampling settings to preview matches";
if (state.pending) return "Finding matching runs…";
if (!data) return "Preview unavailable";
return `${data.eligible.toLocaleString()} matching run${data.eligible === 1 ? "" : "s"}`;
}
function manualSelectedCount(selection: Selection): number {
const sampled = Math.ceil((selection.execution_ids.length * (selection.sample_percent ?? 100)) / 100);
return Math.min(sampled, selection.sample_size ?? Infinity);
}
function windowLabel(selection: Selection): string {
if (!validWindow(selection)) return "Choose a valid history window";
return `Last ${durationLabel(selection.lookback_hours ?? 24, "hours")}`;
}
function scopeOptions(selection: Selection, asOf: string, executions: readonly Execution[]): ScopeOptions {
const endMs = Date.parse(asOf) || Date.now();
return {
@ -161,8 +142,6 @@ export function useMatchingActivity(): MatchingActivity {
scope: scopeOptions(selection, asOf, executions),
preview: {
status: {
title: previewTitle({ pending, validWindow: windowValid, valid }, firstPage),
windowLabel: windowLabel(selection),
ready,
loading,
stale,