Skip to content

Commit f86fc8d

Browse files
committed
feat(webapp): redesign the add-smart-column modal
Wider two-column layout with the sample/preview pinned beside the form. Source is now radio cards with a description each and defaults to payload; display options are pills; and the display-only note is an info box at the top instead of a warning at the bottom.
1 parent 918ef18 commit f86fc8d

1 file changed

Lines changed: 131 additions & 87 deletions

File tree

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

Lines changed: 131 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@ import { Dialog, DialogContent, DialogHeader } from "~/components/primitives/Dia
77
import { Input } from "~/components/primitives/Input";
88
import { Label } from "~/components/primitives/Label";
99
import { Paragraph } from "~/components/primitives/Paragraph";
10-
import SegmentedControl from "~/components/primitives/SegmentedControl";
11-
import { Switch } from "~/components/primitives/Switch";
1210
import { useEnvironment } from "~/hooks/useEnvironment";
1311
import { useOrganization } from "~/hooks/useOrganizations";
1412
import { useProject } from "~/hooks/useProject";
13+
import { cn } from "~/utils/cn";
1514
import {
1615
SMART_COLUMN_DISPLAYS,
17-
SMART_COLUMN_SOURCES,
1816
type SmartColumnDef,
1917
type SmartColumnDisplay,
2018
type SmartColumnSource,
@@ -31,16 +29,19 @@ type AddSmartColumnDialogProps = {
3129
currentSearch: string;
3230
};
3331

34-
const SOURCE_OPTIONS = SMART_COLUMN_SOURCES.map((source) => ({
35-
label: source.charAt(0).toUpperCase() + source.slice(1),
36-
value: source,
37-
}));
32+
const SOURCE_CARDS: { value: SmartColumnSource; label: string; description: string }[] = [
33+
{ value: "payload", label: "Payload", description: "What you triggered the run with." },
34+
{ value: "metadata", label: "Metadata", description: "What the run writes while it runs." },
35+
{ value: "output", label: "Output", description: "What the run returned." },
36+
];
3837

3938
const DISPLAY_OPTIONS = SMART_COLUMN_DISPLAYS.map((display) => ({
4039
label: display.charAt(0).toUpperCase() + display.slice(1),
4140
value: display,
4241
}));
4342

43+
const DEFAULT_SOURCE: SmartColumnSource = "payload";
44+
4445
export function AddSmartColumnDialog({
4546
open,
4647
editing,
@@ -53,15 +54,15 @@ export function AddSmartColumnDialog({
5354
const environment = useEnvironment();
5455
const sample = useTypedFetcher<typeof sampleLoader>();
5556

56-
const [source, setSource] = useState<SmartColumnSource>("metadata");
57+
const [source, setSource] = useState<SmartColumnSource>(DEFAULT_SOURCE);
5758
const [path, setPath] = useState("");
5859
const [label, setLabel] = useState("");
5960
const [labelEdited, setLabelEdited] = useState(false);
6061
const [displayAs, setDisplayAs] = useState<SmartColumnDisplay>("text");
6162

6263
useEffect(() => {
6364
if (!open) return;
64-
setSource(editing?.source ?? "metadata");
65+
setSource(editing?.source ?? DEFAULT_SOURCE);
6566
setPath(editing?.path ?? "");
6667
setLabel(editing?.label ?? "");
6768
setLabelEdited(editing !== null);
@@ -122,84 +123,101 @@ export function AddSmartColumnDialog({
122123

123124
return (
124125
<Dialog open={open} onOpenChange={onOpenChange}>
125-
<DialogContent className="max-w-2xl">
126+
<DialogContent className="sm:max-w-[820px]!">
126127
<DialogHeader>{editing ? "Edit smart column" : "Add smart column"}</DialogHeader>
127-
<div className="flex flex-col gap-4 p-1">
128-
<div className="flex flex-col gap-1.5">
129-
<Label>Source</Label>
130-
<SegmentedControl
131-
name="smart-column-source"
132-
value={source}
133-
options={SOURCE_OPTIONS}
134-
onChange={(value: string) => setSource(value as SmartColumnSource)}
135-
fullWidth
136-
/>
137-
<Paragraph variant="extra-small" className="text-text-dimmed">
138-
Metadata is what the run writes about itself while it runs, so it has a value before
139-
the run ends. Payload is what you triggered it with; output is what it returned.
140-
</Paragraph>
141-
</div>
128+
<div className="flex flex-col gap-5 p-1">
129+
<Callout variant="info">
130+
Display only. A smart column shows you a value from a run, but you can't sort or filter
131+
the list by it. To narrow the list, use tags or the query editor.
132+
</Callout>
142133

143-
<div className="grid grid-cols-2 gap-4">
144-
<div className="flex flex-col gap-1.5">
145-
<Label>JSON path</Label>
146-
<Input
147-
value={path}
148-
onChange={(e) => setPath(e.target.value)}
149-
placeholder="$.failed"
150-
spellCheck={false}
151-
/>
152-
<Paragraph variant="extra-small" className="text-text-dimmed">
153-
Dot and bracket notation, e.g. <code>$.failed</code> or{" "}
154-
<code>$.suites[0].name</code>.
155-
</Paragraph>
156-
</div>
157-
<div className="flex flex-col gap-1.5">
158-
<Label>Column label</Label>
159-
<Input
160-
value={effectiveLabel}
161-
onChange={(e) => {
162-
setLabel(e.target.value);
163-
setLabelEdited(true);
164-
}}
165-
placeholder={labelFromPath(path)}
166-
/>
167-
<Paragraph variant="extra-small" className="text-text-dimmed">
168-
Defaults to the last part of the path. Rename it to anything you like.
169-
</Paragraph>
170-
</div>
171-
</div>
134+
<div className="grid grid-cols-1 gap-6 md:grid-cols-[1fr_300px]">
135+
<div className="flex flex-col gap-5">
136+
<div className="flex flex-col gap-1.5">
137+
<Label>Source</Label>
138+
<div className="grid grid-cols-3 gap-2">
139+
{SOURCE_CARDS.map((card) => (
140+
<SourceCard
141+
key={card.value}
142+
label={card.label}
143+
description={card.description}
144+
selected={source === card.value}
145+
onSelect={() => setSource(card.value)}
146+
/>
147+
))}
148+
</div>
149+
</div>
172150

173-
<div className="flex flex-col gap-1.5">
174-
<Label>Display as</Label>
175-
<SegmentedControl
176-
name="smart-column-display"
177-
value={displayAs}
178-
options={DISPLAY_OPTIONS}
179-
onChange={(value: string) => setDisplayAs(value as SmartColumnDisplay)}
180-
fullWidth
181-
/>
182-
<Paragraph variant="extra-small" className="text-text-dimmed">
183-
Number right-aligns the column and uses tabular figures. Anything that doesn't parse
184-
falls back to text.
185-
</Paragraph>
186-
</div>
151+
<div className="grid grid-cols-2 gap-4">
152+
<div className="flex flex-col gap-1.5">
153+
<Label>JSON path</Label>
154+
<Input
155+
value={path}
156+
onChange={(e) => setPath(e.target.value)}
157+
placeholder="$.order.total"
158+
spellCheck={false}
159+
/>
160+
<Paragraph variant="extra-small" className="text-text-dimmed">
161+
Dot and bracket notation, e.g. <code>$.order.total</code> or{" "}
162+
<code>$.items[0].sku</code>.
163+
</Paragraph>
164+
</div>
165+
<div className="flex flex-col gap-1.5">
166+
<Label>Column label</Label>
167+
<Input
168+
value={effectiveLabel}
169+
onChange={(e) => {
170+
setLabel(e.target.value);
171+
setLabelEdited(true);
172+
}}
173+
placeholder={labelFromPath(path)}
174+
/>
175+
<Paragraph variant="extra-small" className="text-text-dimmed">
176+
Defaults to the last part of the path.
177+
</Paragraph>
178+
</div>
179+
</div>
180+
181+
<div className="flex flex-col gap-1.5">
182+
<Label>Display as</Label>
183+
<div className="flex flex-wrap gap-2">
184+
{DISPLAY_OPTIONS.map((option) => (
185+
<button
186+
key={option.value}
187+
type="button"
188+
onClick={() => setDisplayAs(option.value)}
189+
className={cn(
190+
"rounded-full border px-3.5 py-1 text-sm transition",
191+
displayAs === option.value
192+
? "border-blue-500 bg-blue-500/10 text-text-bright"
193+
: "border-grid-bright text-text-dimmed hover:text-text-bright"
194+
)}
195+
>
196+
{option.label}
197+
</button>
198+
))}
199+
</div>
200+
<Paragraph variant="extra-small" className="text-text-dimmed">
201+
Number right-aligns the column and uses tabular figures. Anything that doesn't
202+
parse falls back to text.
203+
</Paragraph>
204+
</div>
205+
</div>
187206

188-
<div className="grid grid-cols-2 gap-4 rounded border border-grid-dimmed p-3">
189-
<div className="flex min-w-0 flex-col gap-1.5">
207+
<div className="flex flex-col gap-1.5 self-start rounded-lg border border-grid-dimmed bg-background-dimmed p-3">
190208
<Paragraph variant="extra-extra-small/dimmed/caps">
191209
Sample — {source} of the newest run
192210
</Paragraph>
193-
<pre className="max-h-40 overflow-auto rounded bg-background-dimmed p-2 text-xs text-text-dimmed">
211+
<pre className="max-h-44 overflow-auto rounded bg-charcoal-900 p-2 text-xs text-text-dimmed">
194212
{sample.state === "loading"
195213
? "Loading…"
196214
: sampleRun
197215
? sampleJson
198216
: "// no runs to sample"}
199217
</pre>
200-
</div>
201-
<div className="flex flex-col gap-1.5">
202-
<Paragraph variant="extra-extra-small/dimmed/caps">Resolves to</Paragraph>
218+
<Paragraph variant="extra-extra-small/dimmed/caps" className="mt-2">
219+
Resolves to
220+
</Paragraph>
203221
<SmartColumnResolvedPreview label={effectiveLabel} resolved={resolved} />
204222
{sampleRun && (
205223
<Paragraph variant="extra-small" className="text-text-dimmed">
@@ -209,19 +227,6 @@ export function AddSmartColumnDialog({
209227
)}
210228
</div>
211229
</div>
212-
213-
<div className="flex items-center gap-6 rounded border border-grid-dimmed px-3 py-2 opacity-60">
214-
<Switch variant="small" label="Sort by this column" disabled checked={false} />
215-
<Switch variant="small" label="Add to filters" disabled checked={false} />
216-
<Paragraph variant="extra-small" className="ml-auto text-text-dimmed">
217-
Both off, and not switchable
218-
</Paragraph>
219-
</div>
220-
221-
<Callout variant="warning">
222-
Display only. A smart column shows you a value, but you can't sort or filter the list by
223-
it. To narrow the list, use tags or the query editor.
224-
</Callout>
225230
</div>
226231
<div className="flex items-center justify-end gap-2 border-t border-grid-dimmed p-3">
227232
<Button variant="tertiary/medium" onClick={() => onOpenChange(false)}>
@@ -236,6 +241,45 @@ export function AddSmartColumnDialog({
236241
);
237242
}
238243

244+
function SourceCard({
245+
label,
246+
description,
247+
selected,
248+
onSelect,
249+
}: {
250+
label: string;
251+
description: string;
252+
selected: boolean;
253+
onSelect: () => void;
254+
}) {
255+
return (
256+
<button
257+
type="button"
258+
onClick={onSelect}
259+
aria-pressed={selected}
260+
className={cn(
261+
"flex flex-col gap-1 rounded-lg border p-2.5 text-left transition",
262+
selected
263+
? "border-blue-500 bg-blue-500/10"
264+
: "border-grid-bright bg-background-dimmed hover:border-text-dimmed"
265+
)}
266+
>
267+
<span className="flex items-center gap-1.5 text-sm font-medium text-text-bright">
268+
<span
269+
className={cn(
270+
"grid size-3.5 flex-none place-items-center rounded-full border",
271+
selected ? "border-blue-500" : "border-text-dimmed"
272+
)}
273+
>
274+
{selected && <span className="size-1.5 rounded-full bg-blue-500" />}
275+
</span>
276+
{label}
277+
</span>
278+
<span className="text-xs text-text-dimmed">{description}</span>
279+
</button>
280+
);
281+
}
282+
239283
function SmartColumnResolvedPreview({
240284
label,
241285
resolved,

0 commit comments

Comments
 (0)