fix: scope runs detail scrolling to the output table

This commit is contained in:
CREDO23 2026-07-23 20:47:10 +02:00
parent 1e801b3ae9
commit cbf2334fb0
3 changed files with 76 additions and 85 deletions

View file

@ -2,15 +2,9 @@
import { Check, Copy, Download } from "lucide-react";
import { useMemo, useState } from "react";
import { JsonView } from "@/components/json-view";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { downloadCsv, rowsToCsv } from "@/lib/playground/csv";
@ -48,10 +42,11 @@ function ResultTable({ items }: { items: Record<string, unknown>[] }) {
const rows = items.slice(0, MAX_TABLE_ROWS);
// overscroll-contain keeps table scrolling from chaining to the page.
return (
<div className="overflow-x-auto rounded-md border border-border/60">
<Table>
<TableHeader>
<div className="max-h-[480px] overflow-auto overscroll-contain rounded-md border border-border/60">
<table className="w-full caption-bottom text-sm">
<TableHeader className="sticky top-0 z-10 bg-background">
<TableRow>
{columns.map((col) => (
<TableHead key={col} className="whitespace-nowrap">
@ -72,7 +67,7 @@ function ResultTable({ items }: { items: Record<string, unknown>[] }) {
</TableRow>
))}
</TableBody>
</Table>
</table>
</div>
);
}
@ -152,9 +147,9 @@ export function OutputViewer({ data, filenameBase }: { data: unknown; filenameBa
)}
</>
) : (
<pre className="max-h-[480px] overflow-auto rounded-md border border-border/60 bg-muted/20 p-3 text-xs">
<code>{json}</code>
</pre>
<div className="max-h-[480px] overflow-auto overscroll-contain rounded-md border border-border/60 bg-muted/20 p-3">
<JsonView src={data} collapsed={2} />
</div>
)}
</div>
);

View file

@ -1,6 +1,7 @@
"use client";
import { useMemo } from "react";
import { JsonView } from "@/components/json-view";
import { Spinner } from "@/components/ui/spinner";
import { useScraperRun } from "@/hooks/use-scraper-runs";
import { OutputViewer } from "./output-viewer";
@ -69,7 +70,7 @@ export function RunDetail({ workspaceId, runId }: { workspaceId: number; runId:
{run.progress && run.progress.length > 0 && (
<div>
<h4 className="mb-1.5 text-xs font-medium text-muted-foreground">Progress</h4>
<div className="max-h-48 space-y-1 overflow-y-auto rounded-md border border-border/60 bg-background p-3 font-mono text-xs text-muted-foreground">
<div className="space-y-1 rounded-md border border-border/60 bg-background p-3 font-mono text-xs text-muted-foreground">
{run.progress.map((event, i) => (
<div key={i} className="truncate">
{progressLine(event)}
@ -81,9 +82,9 @@ export function RunDetail({ workspaceId, runId }: { workspaceId: number; runId:
<div>
<h4 className="mb-1.5 text-xs font-medium text-muted-foreground">Input</h4>
<pre className="max-h-64 overflow-auto rounded-md border border-border/60 bg-background p-3 text-xs">
<code>{JSON.stringify(run.input ?? {}, null, 2)}</code>
</pre>
<div className="rounded-md border border-border/60 bg-background p-3">
<JsonView src={run.input ?? {}} collapsed={2} />
</div>
</div>
<div>

View file

@ -2,7 +2,7 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { ChevronDown, ChevronRight, History, Info } from "lucide-react";
import { Fragment, useState } from "react";
import { useState } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
@ -13,14 +13,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { scrapersApiService } from "@/lib/apis/scrapers-api.service";
import { formatRelativeDate } from "@/lib/format-date";
import { PLAYGROUND_PLATFORMS } from "@/lib/playground/catalog";
@ -33,6 +25,12 @@ import { RunStatusBadge } from "./run-status-badge";
const PAGE_SIZE = 25;
const ALL = "__all__";
// A grid (not a <table>) keeps an expanded row's detail out of a table-cell,
// where overflow/max-height scroll containers get ignored. Tracks are fr/fixed
// (never content-based auto) so separate per-row grids stay column-aligned.
const ROW_GRID =
"grid grid-cols-[2rem_minmax(8rem,2fr)_minmax(4rem,1fr)_minmax(5rem,1fr)_4rem_5rem_5rem_minmax(6rem,1fr)] items-center gap-2 px-3";
const CAPABILITY_OPTIONS = PLAYGROUND_PLATFORMS.flatMap((platform) =>
platform.verbs.map((verb) => verb.name)
);
@ -122,63 +120,60 @@ export function RunsTable({ workspaceId }: { workspaceId: number }) {
</div>
) : (
<div className="overflow-hidden rounded-md border border-border/60">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-8" />
<TableHead>API</TableHead>
<TableHead>Origin</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Items</TableHead>
<TableHead className="text-right">Duration</TableHead>
<TableHead className="text-right">Cost</TableHead>
<TableHead className="text-right">When</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{runs.map((run) => {
const isOpen = expanded === run.id;
return (
<Fragment key={run.id}>
<TableRow
className="cursor-pointer"
onClick={() => setExpanded(isOpen ? null : run.id)}
>
<TableCell>
{isOpen ? (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
)}
</TableCell>
<TableCell className="font-mono text-xs">{run.capability}</TableCell>
<TableCell className="text-xs text-muted-foreground">{run.origin}</TableCell>
<TableCell>
<RunStatusBadge status={run.status} />
</TableCell>
<TableCell className="text-right tabular-nums">{run.item_count}</TableCell>
<TableCell className="text-right tabular-nums text-muted-foreground">
{formatDuration(run.duration_ms)}
</TableCell>
<TableCell className="text-right tabular-nums text-muted-foreground">
{formatCost(run.cost_micros)}
</TableCell>
<TableCell className="text-right text-xs text-muted-foreground">
{formatRelativeDate(run.created_at)}
</TableCell>
</TableRow>
{isOpen && (
<TableRow className="hover:bg-transparent">
<TableCell colSpan={8} className="p-0">
<RunDetail workspaceId={workspaceId} runId={run.id} />
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
</TableBody>
</Table>
<div
className={cn(
ROW_GRID,
"border-b border-border/60 bg-muted/30 py-2 text-xs font-medium text-muted-foreground"
)}
>
<span />
<span>API</span>
<span>Origin</span>
<span>Status</span>
<span className="text-right">Items</span>
<span className="text-right">Duration</span>
<span className="text-right">Cost</span>
<span className="text-right">When</span>
</div>
{runs.map((run) => {
const isOpen = expanded === run.id;
return (
<div key={run.id} className="border-b border-border/60 last:border-b-0">
<button
type="button"
onClick={() => setExpanded(isOpen ? null : run.id)}
className={cn(
ROW_GRID,
"w-full py-2.5 text-left text-sm transition-colors hover:bg-muted/50"
)}
>
{isOpen ? (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
)}
<span className="min-w-0 truncate font-mono text-xs">{run.capability}</span>
<span className="min-w-0 truncate text-xs text-muted-foreground">
{run.origin}
</span>
<span>
<RunStatusBadge status={run.status} />
</span>
<span className="text-right tabular-nums">{run.item_count}</span>
<span className="text-right tabular-nums text-muted-foreground">
{formatDuration(run.duration_ms)}
</span>
<span className="text-right tabular-nums text-muted-foreground">
{formatCost(run.cost_micros)}
</span>
<span className="text-right text-xs text-muted-foreground">
{formatRelativeDate(run.created_at)}
</span>
</button>
{isOpen && <RunDetail workspaceId={workspaceId} runId={run.id} />}
</div>
);
})}
</div>
)}