Skip to content

Commit efe0ffd

Browse files
committed
feat(webapp): editable smart columns and display-options polish
Smart columns can now be edited in place from the Display popover. Marks smart columns with a code-bracket icon instead of a source-colored dot, shows a drop indicator while reordering columns, drops the redundant Duration cell-count label, and keeps the Display button label constant.
1 parent 90d3fbe commit efe0ffd

4 files changed

Lines changed: 99 additions & 94 deletions

File tree

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

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CodeBracketIcon } from "@heroicons/react/20/solid";
12
import { useEffect, useMemo, useState } from "react";
23
import { useTypedFetcher } from "remix-typedjson";
34
import { Button } from "~/components/primitives/Buttons";
@@ -11,26 +12,22 @@ import { Switch } from "~/components/primitives/Switch";
1112
import { useEnvironment } from "~/hooks/useEnvironment";
1213
import { useOrganization } from "~/hooks/useOrganizations";
1314
import { useProject } from "~/hooks/useProject";
14-
import { cn } from "~/utils/cn";
1515
import {
1616
SMART_COLUMN_DISPLAYS,
1717
SMART_COLUMN_SOURCES,
1818
type SmartColumnDef,
1919
type SmartColumnDisplay,
2020
type SmartColumnSource,
2121
} from "./runColumns";
22-
import {
23-
extractSmartValue,
24-
labelFromPath,
25-
parseSource,
26-
SMART_SOURCE_DOT_COLOR,
27-
} from "./smartColumnData";
22+
import { extractSmartValue, labelFromPath, parseSource } from "./smartColumnData";
2823
import type { loader as sampleLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.smart-column-sample";
2924

3025
type AddSmartColumnDialogProps = {
3126
open: boolean;
27+
/** When set, the dialog edits this existing column instead of adding a new one. */
28+
editing: SmartColumnDef | null;
3229
onOpenChange: (open: boolean) => void;
33-
onAdd: (def: SmartColumnDef) => void;
30+
onSubmit: (def: SmartColumnDef) => void;
3431
currentSearch: string;
3532
};
3633

@@ -46,8 +43,9 @@ const DISPLAY_OPTIONS = SMART_COLUMN_DISPLAYS.map((display) => ({
4643

4744
export function AddSmartColumnDialog({
4845
open,
46+
editing,
4947
onOpenChange,
50-
onAdd,
48+
onSubmit,
5149
currentSearch,
5250
}: AddSmartColumnDialogProps) {
5351
const organization = useOrganization();
@@ -61,6 +59,15 @@ export function AddSmartColumnDialog({
6159
const [labelEdited, setLabelEdited] = useState(false);
6260
const [displayAs, setDisplayAs] = useState<SmartColumnDisplay>("text");
6361

62+
useEffect(() => {
63+
if (!open) return;
64+
setSource(editing?.source ?? "metadata");
65+
setPath(editing?.path ?? "");
66+
setLabel(editing?.label ?? "");
67+
setLabelEdited(editing !== null);
68+
setDisplayAs(editing?.displayAs ?? "text");
69+
}, [open, editing]);
70+
6471
const sampleUrl = useMemo(() => {
6572
const base = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/runs/smart-column-sample`;
6673
return currentSearch ? `${base}?${currentSearch.replace(/^\?/, "")}` : base;
@@ -105,33 +112,18 @@ export function AddSmartColumnDialog({
105112
return extractSmartValue(parsed, path);
106113
}, [parsed, path]);
107114

108-
const canAdd = path.trim().length > 0;
115+
const canSubmit = path.trim().length > 0;
109116

110-
const reset = () => {
111-
setSource("metadata");
112-
setPath("");
113-
setLabel("");
114-
setLabelEdited(false);
115-
setDisplayAs("text");
116-
};
117-
118-
const handleAdd = () => {
119-
if (!canAdd) return;
120-
onAdd({ source, path: path.trim(), label: effectiveLabel.trim() || path.trim(), displayAs });
121-
reset();
117+
const handleSubmit = () => {
118+
if (!canSubmit) return;
119+
onSubmit({ source, path: path.trim(), label: effectiveLabel.trim() || path.trim(), displayAs });
122120
onOpenChange(false);
123121
};
124122

125123
return (
126-
<Dialog
127-
open={open}
128-
onOpenChange={(next) => {
129-
if (!next) reset();
130-
onOpenChange(next);
131-
}}
132-
>
124+
<Dialog open={open} onOpenChange={onOpenChange}>
133125
<DialogContent className="max-w-2xl">
134-
<DialogHeader>Add smart column</DialogHeader>
126+
<DialogHeader>{editing ? "Edit smart column" : "Add smart column"}</DialogHeader>
135127
<div className="flex flex-col gap-4 p-1">
136128
<div className="flex flex-col gap-1.5">
137129
<Label>Source</Label>
@@ -208,11 +200,7 @@ export function AddSmartColumnDialog({
208200
</div>
209201
<div className="flex flex-col gap-1.5">
210202
<Paragraph variant="extra-extra-small/dimmed/caps">Resolves to</Paragraph>
211-
<SmartColumnResolvedPreview
212-
source={source}
213-
label={effectiveLabel}
214-
resolved={resolved}
215-
/>
203+
<SmartColumnResolvedPreview label={effectiveLabel} resolved={resolved} />
216204
{sampleRun && (
217205
<Paragraph variant="extra-small" className="text-text-dimmed">
218206
Against {sampleRun.friendlyId}
@@ -239,8 +227,8 @@ export function AddSmartColumnDialog({
239227
<Button variant="tertiary/medium" onClick={() => onOpenChange(false)}>
240228
Cancel
241229
</Button>
242-
<Button variant="primary/medium" disabled={!canAdd} onClick={handleAdd}>
243-
Add column
230+
<Button variant="primary/medium" disabled={!canSubmit} onClick={handleSubmit}>
231+
{editing ? "Save changes" : "Add column"}
244232
</Button>
245233
</div>
246234
</DialogContent>
@@ -249,11 +237,9 @@ export function AddSmartColumnDialog({
249237
}
250238

251239
function SmartColumnResolvedPreview({
252-
source,
253240
label,
254241
resolved,
255242
}: {
256-
source: SmartColumnSource;
257243
label: string;
258244
resolved: ReturnType<typeof extractSmartValue> | undefined;
259245
}) {
@@ -266,8 +252,8 @@ function SmartColumnResolvedPreview({
266252

267253
return (
268254
<div className="rounded border border-grid-dimmed">
269-
<div className="flex items-center gap-1.5 border-b border-grid-dimmed px-2 py-1">
270-
<span className={cn("size-2 rounded-full", SMART_SOURCE_DOT_COLOR[source])} />
255+
<div className="flex items-center gap-1 border-b border-grid-dimmed px-2 py-1">
256+
<CodeBracketIcon className="size-3.5 flex-none text-text-dimmed" />
271257
<span className="truncate text-xs text-text-bright">{label || "Column"}</span>
272258
</div>
273259
<div className="px-2 py-1.5 text-right text-sm tabular-nums text-text-bright">{value}</div>

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

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { ArrowUturnLeftIcon, PlusIcon, ViewColumnsIcon } from "@heroicons/react/20/solid";
1+
import {
2+
ArrowUturnLeftIcon,
3+
CodeBracketIcon,
4+
PencilSquareIcon,
5+
PlusIcon,
6+
ViewColumnsIcon,
7+
} from "@heroicons/react/20/solid";
28
import { GripVerticalIcon } from "lucide-react";
39
import { useMemo, useState } from "react";
410
import { Button } from "~/components/primitives/Buttons";
@@ -18,19 +24,22 @@ import {
1824
type SmartColumnDef,
1925
} from "./runColumns";
2026
import { AddSmartColumnDialog } from "./AddSmartColumnDialog";
21-
import { SMART_SOURCE_DOT_COLOR } from "./smartColumnData";
2227

2328
function keyFor(col: ResolvedColumn): string {
2429
return col.kind === "standard" ? `std:${col.def.id}` : `smart:${col.index}`;
2530
}
2631

32+
type SmartEditTarget = { index: number; def: SmartColumnDef };
33+
2734
export function RunsDisplayOptions() {
2835
const environment = useEnvironment();
2936
const { isManagedCloud } = useFeatures();
3037
const location = useOptimisticLocation();
3138
const { values, replace } = useSearchParams();
3239
const [addOpen, setAddOpen] = useState(false);
40+
const [editing, setEditing] = useState<SmartEditTarget | null>(null);
3341
const [dragKey, setDragKey] = useState<string | null>(null);
42+
const [overKey, setOverKey] = useState<string | null>(null);
3443

3544
const runtime: RunColumnRuntime = {
3645
isManagedCloud,
@@ -47,7 +56,6 @@ export function RunsDisplayOptions() {
4756

4857
const available = availableStandardColumns(runtime);
4958
const visibleStandardCount = layout.visible.filter((c) => c.kind === "standard").length;
50-
const smartCount = layout.visible.filter((c) => c.kind === "smart").length;
5159

5260
const applyVisible = (nextVisible: ResolvedColumn[]) => {
5361
const encoded = encodeColumnLayout(nextVisible, runtime);
@@ -71,9 +79,16 @@ export function RunsDisplayOptions() {
7179
applyVisible(layout.visible.filter((c) => !(c.kind === "smart" && c.index === index)));
7280
};
7381

74-
const addSmart = (def: SmartColumnDef) => {
75-
const nextIndex = layout.smartColumns.length;
76-
applyVisible([...layout.visible, { kind: "smart", index: nextIndex, def }]);
82+
const submitSmart = (def: SmartColumnDef) => {
83+
if (editing) {
84+
applyVisible(
85+
layout.visible.map((c) =>
86+
c.kind === "smart" && c.index === editing.index ? { ...c, def } : c
87+
)
88+
);
89+
} else {
90+
applyVisible([...layout.visible, { kind: "smart", index: layout.smartColumns.length, def }]);
91+
}
7792
};
7893

7994
const reset = () => replace({ cols: undefined, sc: undefined });
@@ -89,17 +104,17 @@ export function RunsDisplayOptions() {
89104
applyVisible(arr);
90105
};
91106

107+
const endDrag = () => {
108+
setDragKey(null);
109+
setOverKey(null);
110+
};
111+
92112
return (
93113
<>
94114
<Popover>
95115
<PopoverTrigger asChild>
96116
<Button variant="secondary/small" LeadingIcon={ViewColumnsIcon} className="ml-auto">
97-
<span className="flex items-center gap-1.5">
98-
Display
99-
{smartCount > 0 && (
100-
<span className="text-xs text-text-dimmed">{smartCount} smart</span>
101-
)}
102-
</span>
117+
Display
103118
</Button>
104119
</PopoverTrigger>
105120
<PopoverContent align="end" className="w-64 p-0">
@@ -118,16 +133,23 @@ export function RunsDisplayOptions() {
118133
draggable
119134
locked={col.kind === "standard" && !!col.def.locked}
120135
dragging={dragKey === keyFor(col)}
136+
isOver={overKey === keyFor(col) && dragKey !== keyFor(col)}
121137
onDragStart={() => setDragKey(keyFor(col))}
122-
onDragEnd={() => setDragKey(null)}
138+
onDragEnter={() => setOverKey(keyFor(col))}
139+
onDragEnd={endDrag}
123140
onDrop={() => {
124141
if (dragKey) reorder(dragKey, keyFor(col));
125-
setDragKey(null);
142+
endDrag();
126143
}}
127144
onToggle={() => {
128145
if (col.kind === "smart") removeSmart(col.index);
129146
else if (!col.def.locked) hideStandard(col.def.id);
130147
}}
148+
onEdit={
149+
col.kind === "smart"
150+
? () => setEditing({ index: col.index, def: col.def })
151+
: undefined
152+
}
131153
/>
132154
))}
133155
{layout.hiddenStandard.map((def) => (
@@ -138,6 +160,7 @@ export function RunsDisplayOptions() {
138160
draggable={false}
139161
locked={false}
140162
dragging={false}
163+
isOver={false}
141164
onToggle={() => showStandard(def.id)}
142165
/>
143166
))}
@@ -164,9 +187,15 @@ export function RunsDisplayOptions() {
164187
</PopoverContent>
165188
</Popover>
166189
<AddSmartColumnDialog
167-
open={addOpen}
168-
onOpenChange={setAddOpen}
169-
onAdd={addSmart}
190+
open={addOpen || editing !== null}
191+
editing={editing?.def ?? null}
192+
onOpenChange={(next) => {
193+
if (!next) {
194+
setAddOpen(false);
195+
setEditing(null);
196+
}
197+
}}
198+
onSubmit={submitSmart}
170199
currentSearch={location.search}
171200
/>
172201
</>
@@ -179,8 +208,11 @@ function ColumnRow({
179208
draggable,
180209
locked,
181210
dragging,
211+
isOver,
182212
onToggle,
213+
onEdit,
183214
onDragStart,
215+
onDragEnter,
184216
onDragEnd,
185217
onDrop,
186218
}: {
@@ -189,45 +221,49 @@ function ColumnRow({
189221
draggable: boolean;
190222
locked: boolean;
191223
dragging: boolean;
224+
isOver: boolean;
192225
onToggle: () => void;
226+
onEdit?: () => void;
193227
onDragStart?: () => void;
228+
onDragEnter?: () => void;
194229
onDragEnd?: () => void;
195230
onDrop?: () => void;
196231
}) {
197232
const isSmart = col.kind === "smart";
198-
const label = col.def.label;
199-
const isDuration = col.kind === "standard" && col.def.id === "dur";
200233

201234
return (
202235
<div
203236
className={cn(
204-
"flex h-8 items-center gap-2 px-3 transition-colors hover:bg-charcoal-750",
237+
"relative flex h-8 items-center gap-2 px-3 transition-colors hover:bg-charcoal-750",
205238
dragging && "opacity-40"
206239
)}
207240
draggable={draggable}
208241
onDragStart={onDragStart}
242+
onDragEnter={onDragEnter}
209243
onDragEnd={onDragEnd}
210244
onDragOver={(e) => {
211245
if (draggable) e.preventDefault();
212246
}}
213247
onDrop={onDrop}
214248
>
215-
{locked ? (
216-
<Checkbox checked disabled />
217-
) : (
218-
<Checkbox checked={checked} onChange={onToggle} />
219-
)}
220-
{isSmart && (
221-
<span
222-
className={cn("size-2 flex-none rounded-full", SMART_SOURCE_DOT_COLOR[col.def.source])}
223-
/>
224-
)}
249+
{isOver && <div className="absolute inset-x-0 top-0 h-0.5 bg-blue-500" />}
250+
{locked ? <Checkbox checked disabled /> : <Checkbox checked={checked} onChange={onToggle} />}
251+
{isSmart && <CodeBracketIcon className="size-4 flex-none text-text-dimmed" />}
225252
<span
226253
className={cn("flex-1 truncate text-sm", checked ? "text-text-bright" : "text-text-dimmed")}
227254
>
228-
{label}
255+
{col.def.label}
229256
</span>
230-
{isDuration && <span className="text-xs text-text-dimmed">3 cells</span>}
257+
{onEdit && (
258+
<button
259+
type="button"
260+
onClick={onEdit}
261+
aria-label={`Edit ${col.def.label}`}
262+
className="flex size-5 items-center justify-center rounded text-text-dimmed transition-colors hover:text-text-bright focus-custom"
263+
>
264+
<PencilSquareIcon className="size-3.5" />
265+
</button>
266+
)}
231267
{draggable ? (
232268
<GripVerticalIcon className="size-4 cursor-grab text-text-dimmed active:cursor-grabbing" />
233269
) : (

0 commit comments

Comments
 (0)