Skip to content

Commit f943099

Browse files
committed
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.
1 parent dd8d3d4 commit f943099

3 files changed

Lines changed: 88 additions & 68 deletions

File tree

apps/webapp/app/components/runs/v3/AddSmartColumnDialog.tsx

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BoltIcon } from "@heroicons/react/20/solid";
1+
import { BoltIcon, ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
22
import { useEffect, useMemo, useState } from "react";
33
import { useTypedFetcher } from "remix-typedjson";
44
import { Button } from "~/components/primitives/Buttons";
@@ -60,6 +60,7 @@ export function AddSmartColumnDialog({
6060
const [label, setLabel] = useState("");
6161
const [labelEdited, setLabelEdited] = useState(false);
6262
const [displayAs, setDisplayAs] = useState<SmartColumnDisplay>("text");
63+
const [sampleIndex, setSampleIndex] = useState(0);
6364

6465
useEffect(() => {
6566
if (!open) return;
@@ -68,6 +69,7 @@ export function AddSmartColumnDialog({
6869
setLabel(editing?.label ?? "");
6970
setLabelEdited(editing !== null);
7071
setDisplayAs(editing?.displayAs ?? "text");
72+
setSampleIndex(0);
7173
}, [open, editing]);
7274

7375
const sampleUrl = useMemo(() => {
@@ -84,7 +86,9 @@ export function AddSmartColumnDialog({
8486

8587
const effectiveLabel = labelEdited ? label : labelFromPath(path);
8688

87-
const sampleRun = sample.data?.run ?? null;
89+
const sampleRuns = sample.data?.runs ?? [];
90+
const clampedIndex = sampleRuns.length > 0 ? Math.min(sampleIndex, sampleRuns.length - 1) : 0;
91+
const sampleRun = sampleRuns[clampedIndex] ?? null;
8892

8993
const parsed = useMemo(() => {
9094
if (!sampleRun) return undefined;
@@ -195,9 +199,17 @@ export function AddSmartColumnDialog({
195199
</div>
196200

197201
<div className="flex flex-col gap-1.5 self-start rounded-lg border border-grid-dimmed bg-background-dimmed p-3">
198-
<Paragraph variant="extra-extra-small/dimmed/caps">
199-
Sample — {source} of the newest run
200-
</Paragraph>
202+
<div className="flex items-center justify-between gap-2">
203+
<Paragraph variant="extra-extra-small/dimmed/caps">Sample — {source}</Paragraph>
204+
{sampleRuns.length > 0 && (
205+
<SampleRunPicker
206+
index={clampedIndex}
207+
total={sampleRuns.length}
208+
onPrev={() => setSampleIndex((i) => Math.max(0, i - 1))}
209+
onNext={() => setSampleIndex((i) => Math.min(sampleRuns.length - 1, i + 1))}
210+
/>
211+
)}
212+
</div>
201213
{sample.state === "loading" ? (
202214
<Paragraph variant="extra-small" className="text-text-dimmed">
203215
Loading…
@@ -231,12 +243,6 @@ export function AddSmartColumnDialog({
231243
Resolves to
232244
</Paragraph>
233245
<SmartColumnResolvedPreview label={effectiveLabel} resolved={resolved} />
234-
{sampleRun && (
235-
<Paragraph variant="extra-small" className="text-text-dimmed">
236-
Against {sampleRun.friendlyId}
237-
{sampleRun.hasFinished ? "" : " · still running"}
238-
</Paragraph>
239-
)}
240246
</div>
241247
</div>
242248
</div>
@@ -253,6 +259,44 @@ export function AddSmartColumnDialog({
253259
);
254260
}
255261

262+
function SampleRunPicker({
263+
index,
264+
total,
265+
onPrev,
266+
onNext,
267+
}: {
268+
index: number;
269+
total: number;
270+
onPrev: () => void;
271+
onNext: () => void;
272+
}) {
273+
return (
274+
<div className="flex flex-none items-center gap-1 text-xs text-text-dimmed">
275+
<span className="tabular-nums">
276+
{index + 1}/{total}
277+
</span>
278+
<button
279+
type="button"
280+
onClick={onPrev}
281+
disabled={index === 0}
282+
aria-label="Newer run"
283+
className="flex size-5 items-center justify-center rounded hover:bg-charcoal-750 disabled:opacity-30"
284+
>
285+
<ChevronLeftIcon className="size-4" />
286+
</button>
287+
<button
288+
type="button"
289+
onClick={onNext}
290+
disabled={index >= total - 1}
291+
aria-label="Older run"
292+
className="flex size-5 items-center justify-center rounded hover:bg-charcoal-750 disabled:opacity-30"
293+
>
294+
<ChevronRightIcon className="size-4" />
295+
</button>
296+
</div>
297+
);
298+
}
299+
256300
function SourceCard({
257301
label,
258302
description,

apps/webapp/app/components/runs/v3/SmartColumnSample.tsx

Lines changed: 23 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
import { useState } from "react";
21
import { cn } from "~/utils/cn";
32

43
/** Max children rendered per node so a large blob can't blow up the DOM. */
54
const MAX_CHILDREN = 200;
6-
/** Levels auto-expanded; deeper nodes start collapsed and open on click. */
7-
const AUTO_OPEN_DEPTH = 2;
85
const MAX_STRING = 80;
96

107
/**
11-
* A clickable, syntax-colored JSON tree for the smart-column sample. Only leaf
12-
* values are selectable: clicking one fills the JSON path field via
13-
* `onSelectPath` and highlights it. Object/array rows only expand and collapse,
14-
* so you drill into a container and pick a leaf inside it.
8+
* A clickable, syntax-colored JSON tree for the smart-column sample, rendered
9+
* fully expanded. Only leaf values are selectable: clicking one fills the JSON
10+
* path field via `onSelectPath` and highlights it. Objects and arrays are shown
11+
* inline (not clickable) so you can see the shape and pick a leaf inside them.
1512
*/
1613
export function SmartColumnSample({
1714
value,
@@ -28,7 +25,6 @@ export function SmartColumnSample({
2825
name={undefined}
2926
path="$"
3027
value={value}
31-
depth={0}
3228
activePath={activePath}
3329
onSelectPath={onSelectPath}
3430
/>
@@ -46,18 +42,15 @@ function JsonNode({
4642
name,
4743
path,
4844
value,
49-
depth,
5045
activePath,
5146
onSelectPath,
5247
}: {
5348
name: string | number | undefined;
5449
path: string;
5550
value: unknown;
56-
depth: number;
5751
activePath: string;
5852
onSelectPath: (path: string) => void;
5953
}) {
60-
const [open, setOpen] = useState(depth < AUTO_OPEN_DEPTH);
6154
const isObject = value !== null && typeof value === "object";
6255
const selected = path === activePath;
6356
const keyLabel = name === undefined ? null : typeof name === "number" ? name : `"${name}"`;
@@ -90,43 +83,27 @@ function JsonNode({
9083

9184
return (
9285
<div>
93-
<button
94-
type="button"
95-
onClick={() => setOpen((o) => !o)}
96-
aria-label={open ? "Collapse" : "Expand"}
97-
aria-expanded={open}
98-
className="flex w-full items-start whitespace-pre rounded px-0.5 text-left hover:bg-charcoal-750"
99-
>
100-
<span className="mr-1 w-3 shrink-0 text-text-dimmed">{open ? "▾" : "▸"}</span>
86+
<div className="whitespace-pre px-0.5">
10187
{keyLabel !== null && <span className="text-sky-300">{keyLabel}</span>}
10288
{keyLabel !== null && <span className="text-text-dimmed">: </span>}
103-
<span className="text-text-dimmed">
104-
{openBrace}
105-
{!open && `… ${closeBrace}`}
106-
{!open && entries.length > 0 && (
107-
<span className="ml-1 text-faint">{`${entries.length} ${isArray ? "items" : "keys"}`}</span>
108-
)}
109-
</span>
110-
</button>
111-
{open && (
112-
<div className="ml-[0.4rem] border-l border-grid-dimmed/50 pl-3">
113-
{shown.map(([key, childValue]) => (
114-
<JsonNode
115-
key={String(key)}
116-
name={key}
117-
path={childPath(path, key)}
118-
value={childValue}
119-
depth={depth + 1}
120-
activePath={activePath}
121-
onSelectPath={onSelectPath}
122-
/>
123-
))}
124-
{entries.length > MAX_CHILDREN && (
125-
<div className="text-text-dimmed">{entries.length - MAX_CHILDREN} more</div>
126-
)}
127-
<div className="text-text-dimmed">{closeBrace}</div>
128-
</div>
129-
)}
89+
<span className="text-text-dimmed">{openBrace}</span>
90+
</div>
91+
<div className="ml-[0.4rem] border-l border-grid-dimmed/50 pl-3">
92+
{shown.map(([key, childValue]) => (
93+
<JsonNode
94+
key={String(key)}
95+
name={key}
96+
path={childPath(path, key)}
97+
value={childValue}
98+
activePath={activePath}
99+
onSelectPath={onSelectPath}
100+
/>
101+
))}
102+
{entries.length > MAX_CHILDREN && (
103+
<div className="text-text-dimmed">{entries.length - MAX_CHILDREN} more</div>
104+
)}
105+
</div>
106+
<div className="whitespace-pre px-0.5 text-text-dimmed">{closeBrace}</div>
130107
</div>
131108
);
132109
}

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.smart-column-sample.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import { RunsRepository } from "~/services/runsRepository/runsRepository.server"
77
import { $replica } from "~/db.server";
88
import { isFinalRunStatus } from "~/v3/taskStatus";
99

10+
/** How many recent runs the smart-column preview can page through. */
11+
const SAMPLE_RUN_COUNT = 10;
12+
1013
/**
11-
* Newest run for the current filters, with its raw payload/metadata/output
12-
* packets, feeding the "Add smart column" live preview. The client parses and
13-
* resolves the JSON path; the server never parses (same rule as the list).
14+
* The most recent runs for the current filters, with their raw
15+
* payload/metadata/output packets, feeding the "Add smart column" preview. The
16+
* client picks which run to sample, parses, and resolves the JSON path; the
17+
* server never parses (same rule as the list).
1418
*/
1519
export async function loader({ request, params }: LoaderFunctionArgs) {
1620
const { project, environment } = await loadProjectEnvironmentFromRequest(request, params);
@@ -42,16 +46,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
4246
machines: filters.machines,
4347
errorId: filters.errorId,
4448
runSelect: deriveRunSelect([], ["payload", "metadata", "output"]),
45-
page: { size: 1 },
49+
page: { size: SAMPLE_RUN_COUNT },
4650
});
4751

48-
const run = runs[0];
49-
if (!run) {
50-
return { run: null };
51-
}
52-
5352
return {
54-
run: {
53+
runs: runs.map((run) => ({
5554
friendlyId: run.friendlyId,
5655
status: run.status,
5756
hasFinished: isFinalRunStatus(run.status),
@@ -63,6 +62,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
6362
metadataType: run.metadataType,
6463
output: run.output,
6564
outputType: run.outputType,
66-
},
65+
})),
6766
};
6867
}

0 commit comments

Comments
 (0)