diff --git a/apps/webapp/app/components/runs/v3/AddSmartColumnDialog.tsx b/apps/webapp/app/components/runs/v3/AddSmartColumnDialog.tsx index 39aba9f4e..234e877d7 100644 --- a/apps/webapp/app/components/runs/v3/AddSmartColumnDialog.tsx +++ b/apps/webapp/app/components/runs/v3/AddSmartColumnDialog.tsx @@ -1,4 +1,4 @@ -import { BoltIcon } from "@heroicons/react/20/solid"; +import { BoltIcon, ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid"; import { useEffect, useMemo, useState } from "react"; import { useTypedFetcher } from "remix-typedjson"; import { Button } from "~/components/primitives/Buttons"; @@ -60,6 +60,7 @@ export function AddSmartColumnDialog({ const [label, setLabel] = useState(""); const [labelEdited, setLabelEdited] = useState(false); const [displayAs, setDisplayAs] = useState("text"); + const [sampleIndex, setSampleIndex] = useState(0); useEffect(() => { if (!open) return; @@ -68,6 +69,7 @@ export function AddSmartColumnDialog({ setLabel(editing?.label ?? ""); setLabelEdited(editing !== null); setDisplayAs(editing?.displayAs ?? "text"); + setSampleIndex(0); }, [open, editing]); const sampleUrl = useMemo(() => { @@ -84,7 +86,9 @@ export function AddSmartColumnDialog({ const effectiveLabel = labelEdited ? label : labelFromPath(path); - const sampleRun = sample.data?.run ?? null; + const sampleRuns = sample.data?.runs ?? []; + const clampedIndex = sampleRuns.length > 0 ? Math.min(sampleIndex, sampleRuns.length - 1) : 0; + const sampleRun = sampleRuns[clampedIndex] ?? null; const parsed = useMemo(() => { if (!sampleRun) return undefined; @@ -195,9 +199,17 @@ export function AddSmartColumnDialog({
- - Sample — {source} of the newest run - +
+ Sample — {source} + {sampleRuns.length > 0 && ( + setSampleIndex((i) => Math.max(0, i - 1))} + onNext={() => setSampleIndex((i) => Math.min(sampleRuns.length - 1, i + 1))} + /> + )} +
{sample.state === "loading" ? ( Loading… @@ -231,12 +243,6 @@ export function AddSmartColumnDialog({ Resolves to - {sampleRun && ( - - Against {sampleRun.friendlyId} - {sampleRun.hasFinished ? "" : " · still running"} - - )}
@@ -253,6 +259,44 @@ export function AddSmartColumnDialog({ ); } +function SampleRunPicker({ + index, + total, + onPrev, + onNext, +}: { + index: number; + total: number; + onPrev: () => void; + onNext: () => void; +}) { + return ( +
+ + {index + 1}/{total} + + + +
+ ); +} + function SourceCard({ label, description, diff --git a/apps/webapp/app/components/runs/v3/SmartColumnSample.tsx b/apps/webapp/app/components/runs/v3/SmartColumnSample.tsx index 1781cf728..a361ff1c0 100644 --- a/apps/webapp/app/components/runs/v3/SmartColumnSample.tsx +++ b/apps/webapp/app/components/runs/v3/SmartColumnSample.tsx @@ -1,17 +1,14 @@ -import { useState } from "react"; import { cn } from "~/utils/cn"; /** Max children rendered per node so a large blob can't blow up the DOM. */ const MAX_CHILDREN = 200; -/** Levels auto-expanded; deeper nodes start collapsed and open on click. */ -const AUTO_OPEN_DEPTH = 2; const MAX_STRING = 80; /** - * A clickable, syntax-colored JSON tree for the smart-column sample. Only leaf - * values are selectable: clicking one fills the JSON path field via - * `onSelectPath` and highlights it. Object/array rows only expand and collapse, - * so you drill into a container and pick a leaf inside it. + * A clickable, syntax-colored JSON tree for the smart-column sample, rendered + * fully expanded. Only leaf values are selectable: clicking one fills the JSON + * path field via `onSelectPath` and highlights it. Objects and arrays are shown + * inline (not clickable) so you can see the shape and pick a leaf inside them. */ export function SmartColumnSample({ value, @@ -28,7 +25,6 @@ export function SmartColumnSample({ name={undefined} path="$" value={value} - depth={0} activePath={activePath} onSelectPath={onSelectPath} /> @@ -46,18 +42,15 @@ function JsonNode({ name, path, value, - depth, activePath, onSelectPath, }: { name: string | number | undefined; path: string; value: unknown; - depth: number; activePath: string; onSelectPath: (path: string) => void; }) { - const [open, setOpen] = useState(depth < AUTO_OPEN_DEPTH); const isObject = value !== null && typeof value === "object"; const selected = path === activePath; const keyLabel = name === undefined ? null : typeof name === "number" ? name : `"${name}"`; @@ -90,43 +83,27 @@ function JsonNode({ return (
- - {open && ( -
- {shown.map(([key, childValue]) => ( - - ))} - {entries.length > MAX_CHILDREN && ( -
… {entries.length - MAX_CHILDREN} more
- )} -
{closeBrace}
-
- )} + {openBrace} +
+
+ {shown.map(([key, childValue]) => ( + + ))} + {entries.length > MAX_CHILDREN && ( +
… {entries.length - MAX_CHILDREN} more
+ )} +
+
{closeBrace}
); } diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.smart-column-sample.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.smart-column-sample.ts index b0343d981..ba6ac6924 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.smart-column-sample.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.smart-column-sample.ts @@ -7,10 +7,14 @@ import { RunsRepository } from "~/services/runsRepository/runsRepository.server" import { $replica } from "~/db.server"; import { isFinalRunStatus } from "~/v3/taskStatus"; +/** How many recent runs the smart-column preview can page through. */ +const SAMPLE_RUN_COUNT = 10; + /** - * Newest run for the current filters, with its raw payload/metadata/output - * packets, feeding the "Add smart column" live preview. The client parses and - * resolves the JSON path; the server never parses (same rule as the list). + * The most recent runs for the current filters, with their raw + * payload/metadata/output packets, feeding the "Add smart column" preview. The + * client picks which run to sample, parses, and resolves the JSON path; the + * server never parses (same rule as the list). */ export async function loader({ request, params }: LoaderFunctionArgs) { const { project, environment } = await loadProjectEnvironmentFromRequest(request, params); @@ -42,16 +46,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) { machines: filters.machines, errorId: filters.errorId, runSelect: deriveRunSelect([], ["payload", "metadata", "output"]), - page: { size: 1 }, + page: { size: SAMPLE_RUN_COUNT }, }); - const run = runs[0]; - if (!run) { - return { run: null }; - } - return { - run: { + runs: runs.map((run) => ({ friendlyId: run.friendlyId, status: run.status, hasFinished: isFinalRunStatus(run.status), @@ -63,6 +62,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { metadataType: run.metadataType, output: run.output, outputType: run.outputType, - }, + })), }; }