feat(webapp): expand the sample tree and let it page through recent runs

The smart-column sample now renders fully expanded (no collapse), and a
run picker steps through the most recent runs so you can find one that
has the value you're after when the newest run doesn't.
This commit is contained in:
Eric Allam
2026-08-17 17:35:50 +01:00
parent 7f776d7b2b
commit de33c52325
3 changed files with 88 additions and 68 deletions
@@ -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<SmartColumnDisplay>("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({
</div>
<div className="flex flex-col gap-1.5 self-start rounded-lg border border-grid-dimmed bg-background-dimmed p-3">
<Paragraph variant="extra-extra-small/dimmed/caps">
Sample {source} of the newest run
</Paragraph>
<div className="flex items-center justify-between gap-2">
<Paragraph variant="extra-extra-small/dimmed/caps">Sample {source}</Paragraph>
{sampleRuns.length > 0 && (
<SampleRunPicker
index={clampedIndex}
total={sampleRuns.length}
onPrev={() => setSampleIndex((i) => Math.max(0, i - 1))}
onNext={() => setSampleIndex((i) => Math.min(sampleRuns.length - 1, i + 1))}
/>
)}
</div>
{sample.state === "loading" ? (
<Paragraph variant="extra-small" className="text-text-dimmed">
Loading
@@ -231,12 +243,6 @@ export function AddSmartColumnDialog({
Resolves to
</Paragraph>
<SmartColumnResolvedPreview label={effectiveLabel} resolved={resolved} />
{sampleRun && (
<Paragraph variant="extra-small" className="text-text-dimmed">
Against {sampleRun.friendlyId}
{sampleRun.hasFinished ? "" : " · still running"}
</Paragraph>
)}
</div>
</div>
</div>
@@ -253,6 +259,44 @@ export function AddSmartColumnDialog({
);
}
function SampleRunPicker({
index,
total,
onPrev,
onNext,
}: {
index: number;
total: number;
onPrev: () => void;
onNext: () => void;
}) {
return (
<div className="flex flex-none items-center gap-1 text-xs text-text-dimmed">
<span className="tabular-nums">
{index + 1}/{total}
</span>
<button
type="button"
onClick={onPrev}
disabled={index === 0}
aria-label="Newer run"
className="flex size-5 items-center justify-center rounded hover:bg-charcoal-750 disabled:opacity-30"
>
<ChevronLeftIcon className="size-4" />
</button>
<button
type="button"
onClick={onNext}
disabled={index >= total - 1}
aria-label="Older run"
className="flex size-5 items-center justify-center rounded hover:bg-charcoal-750 disabled:opacity-30"
>
<ChevronRightIcon className="size-4" />
</button>
</div>
);
}
function SourceCard({
label,
description,
@@ -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 (
<div>
<button
type="button"
onClick={() => setOpen((o) => !o)}
aria-label={open ? "Collapse" : "Expand"}
aria-expanded={open}
className="flex w-full items-start whitespace-pre rounded px-0.5 text-left hover:bg-charcoal-750"
>
<span className="mr-1 w-3 shrink-0 text-text-dimmed">{open ? "▾" : "▸"}</span>
<div className="whitespace-pre px-0.5">
{keyLabel !== null && <span className="text-sky-300">{keyLabel}</span>}
{keyLabel !== null && <span className="text-text-dimmed">: </span>}
<span className="text-text-dimmed">
{openBrace}
{!open && `${closeBrace}`}
{!open && entries.length > 0 && (
<span className="ml-1 text-faint">{`${entries.length} ${isArray ? "items" : "keys"}`}</span>
)}
</span>
</button>
{open && (
<div className="ml-[0.4rem] border-l border-grid-dimmed/50 pl-3">
{shown.map(([key, childValue]) => (
<JsonNode
key={String(key)}
name={key}
path={childPath(path, key)}
value={childValue}
depth={depth + 1}
activePath={activePath}
onSelectPath={onSelectPath}
/>
))}
{entries.length > MAX_CHILDREN && (
<div className="text-text-dimmed"> {entries.length - MAX_CHILDREN} more</div>
)}
<div className="text-text-dimmed">{closeBrace}</div>
</div>
)}
<span className="text-text-dimmed">{openBrace}</span>
</div>
<div className="ml-[0.4rem] border-l border-grid-dimmed/50 pl-3">
{shown.map(([key, childValue]) => (
<JsonNode
key={String(key)}
name={key}
path={childPath(path, key)}
value={childValue}
activePath={activePath}
onSelectPath={onSelectPath}
/>
))}
{entries.length > MAX_CHILDREN && (
<div className="text-text-dimmed"> {entries.length - MAX_CHILDREN} more</div>
)}
</div>
<div className="whitespace-pre px-0.5 text-text-dimmed">{closeBrace}</div>
</div>
);
}
@@ -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,
},
})),
};
}