Skip to content

Commit 2ae7632

Browse files
committed
run option changes from #2227, to be removed once the pr is merged
1 parent 0da6a19 commit 2ae7632

File tree

14 files changed

+1578
-383
lines changed

14 files changed

+1578
-383
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export function ClockRotateLeftIcon({ className }: { className?: string }) {
2+
return (
3+
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
4+
<path
5+
d="M4.01784 10.9999C4.27072 9.07068 5.21806 7.29972 6.68252 6.01856C8.14697 4.73741 10.0282 4.03389 11.9739 4.03971C13.9197 4.04553 15.7966 4.76028 17.2534 6.05017C18.7101 7.34006 19.6469 9.11666 19.8882 11.0474C20.1296 12.9781 19.659 14.9306 18.5645 16.5394C17.4701 18.1482 15.8268 19.303 13.9424 19.7876C12.0579 20.2722 10.0615 20.0534 8.32671 19.1721C6.59196 18.2909 5.23784 16.8076 4.51784 14.9999M4.01784 19.9999L4.01784 14.9999L9.01784 14.9999"
6+
stroke="currentColor"
7+
stroke-width="2"
8+
stroke-linecap="round"
9+
stroke-linejoin="round"
10+
/>
11+
<path d="M12 12L12 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
12+
<path d="M12 12L14 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
13+
</svg>
14+
);
15+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { Input } from "~/components/primitives/Input";
2+
import { cn } from "~/utils/cn";
3+
import React, { useRef, useState, useEffect } from "react";
4+
import { Button } from "./Buttons";
5+
6+
export interface DurationPickerProps {
7+
id?: string; // used for the hidden input for form submission
8+
name?: string; // used for the hidden input for form submission
9+
defaultValueSeconds?: number;
10+
value?: number;
11+
onChange?: (totalSeconds: number) => void;
12+
variant?: "small" | "medium";
13+
showClearButton?: boolean;
14+
}
15+
16+
export function DurationPicker({
17+
name,
18+
defaultValueSeconds: defaultValue = 0,
19+
value: controlledValue,
20+
onChange,
21+
variant = "small",
22+
showClearButton = true,
23+
}: DurationPickerProps) {
24+
// Use controlled value if provided, otherwise use default
25+
const initialValue = controlledValue ?? defaultValue;
26+
27+
const defaultHours = Math.floor(initialValue / 3600);
28+
const defaultMinutes = Math.floor((initialValue % 3600) / 60);
29+
const defaultSeconds = initialValue % 60;
30+
31+
const [hours, setHours] = useState<number>(defaultHours);
32+
const [minutes, setMinutes] = useState<number>(defaultMinutes);
33+
const [seconds, setSeconds] = useState<number>(defaultSeconds);
34+
35+
const minuteRef = useRef<HTMLInputElement>(null);
36+
const hourRef = useRef<HTMLInputElement>(null);
37+
const secondRef = useRef<HTMLInputElement>(null);
38+
39+
const totalSeconds = hours * 3600 + minutes * 60 + seconds;
40+
41+
const isEmpty = hours === 0 && minutes === 0 && seconds === 0;
42+
43+
// Sync internal state with external value changes
44+
useEffect(() => {
45+
if (controlledValue !== undefined && controlledValue !== totalSeconds) {
46+
const newHours = Math.floor(controlledValue / 3600);
47+
const newMinutes = Math.floor((controlledValue % 3600) / 60);
48+
const newSeconds = controlledValue % 60;
49+
50+
setHours(newHours);
51+
setMinutes(newMinutes);
52+
setSeconds(newSeconds);
53+
}
54+
}, [controlledValue]);
55+
56+
useEffect(() => {
57+
onChange?.(totalSeconds);
58+
}, [totalSeconds, onChange]);
59+
60+
const handleHoursChange = (e: React.ChangeEvent<HTMLInputElement>) => {
61+
const value = parseInt(e.target.value) || 0;
62+
setHours(Math.max(0, value));
63+
};
64+
65+
const handleMinutesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
66+
const value = parseInt(e.target.value) || 0;
67+
if (value >= 60) {
68+
setHours((prev) => prev + Math.floor(value / 60));
69+
setMinutes(value % 60);
70+
return;
71+
}
72+
73+
setMinutes(Math.max(0, Math.min(59, value)));
74+
};
75+
76+
const handleSecondsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
77+
const value = parseInt(e.target.value) || 0;
78+
if (value >= 60) {
79+
setMinutes((prev) => {
80+
const newMinutes = prev + Math.floor(value / 60);
81+
if (newMinutes >= 60) {
82+
setHours((prevHours) => prevHours + Math.floor(newMinutes / 60));
83+
return newMinutes % 60;
84+
}
85+
return newMinutes;
86+
});
87+
setSeconds(value % 60);
88+
return;
89+
}
90+
91+
setSeconds(Math.max(0, Math.min(59, value)));
92+
};
93+
94+
const handleKeyDown = (
95+
e: React.KeyboardEvent<HTMLInputElement>,
96+
nextRef?: React.RefObject<HTMLInputElement>,
97+
prevRef?: React.RefObject<HTMLInputElement>
98+
) => {
99+
if (e.key === "Tab") {
100+
return;
101+
}
102+
103+
if (e.key === "ArrowRight" && nextRef) {
104+
e.preventDefault();
105+
nextRef.current?.focus();
106+
nextRef.current?.select();
107+
return;
108+
}
109+
110+
if (e.key === "ArrowLeft" && prevRef) {
111+
e.preventDefault();
112+
prevRef.current?.focus();
113+
prevRef.current?.select();
114+
return;
115+
}
116+
};
117+
118+
const clearDuration = () => {
119+
setHours(0);
120+
setMinutes(0);
121+
setSeconds(0);
122+
hourRef.current?.focus();
123+
};
124+
125+
return (
126+
<div className="flex items-center gap-3">
127+
<input type="hidden" name={name} value={totalSeconds} />
128+
129+
<div className="flex items-center gap-1">
130+
<div className="group flex items-center gap-1">
131+
<Input
132+
variant={variant}
133+
ref={hourRef}
134+
className={cn(
135+
"w-10 text-center font-mono tabular-nums caret-transparent [&::-webkit-inner-spin-button]:appearance-none",
136+
isEmpty && "text-text-dimmed"
137+
)}
138+
value={hours.toString()}
139+
onChange={handleHoursChange}
140+
onKeyDown={(e) => handleKeyDown(e, minuteRef)}
141+
onFocus={(e) => e.target.select()}
142+
type="number"
143+
min={0}
144+
inputMode="numeric"
145+
/>
146+
<span className="text-sm text-text-dimmed transition-colors duration-200 group-focus-within:text-text-bright/80">
147+
h
148+
</span>
149+
</div>
150+
<div className="group flex items-center gap-1">
151+
<Input
152+
variant={variant}
153+
ref={minuteRef}
154+
className={cn(
155+
"w-10 text-center font-mono tabular-nums caret-transparent [&::-webkit-inner-spin-button]:appearance-none",
156+
isEmpty && "text-text-dimmed"
157+
)}
158+
value={minutes.toString()}
159+
onChange={handleMinutesChange}
160+
onKeyDown={(e) => handleKeyDown(e, secondRef, hourRef)}
161+
onFocus={(e) => e.target.select()}
162+
type="number"
163+
min={0}
164+
max={59}
165+
inputMode="numeric"
166+
/>
167+
<span className="text-sm text-text-dimmed transition-colors duration-200 group-focus-within:text-text-bright/80">
168+
m
169+
</span>
170+
</div>
171+
<div className="group flex items-center gap-1">
172+
<Input
173+
variant={variant}
174+
ref={secondRef}
175+
className={cn(
176+
"w-10 text-center font-mono tabular-nums caret-transparent [&::-webkit-inner-spin-button]:appearance-none",
177+
isEmpty && "text-text-dimmed"
178+
)}
179+
value={seconds.toString()}
180+
onChange={handleSecondsChange}
181+
onKeyDown={(e) => handleKeyDown(e, undefined, minuteRef)}
182+
onFocus={(e) => e.target.select()}
183+
type="number"
184+
min={0}
185+
max={59}
186+
inputMode="numeric"
187+
/>
188+
<span className="text-sm text-text-dimmed transition-colors duration-200 group-focus-within:text-text-bright/80">
189+
s
190+
</span>
191+
</div>
192+
</div>
193+
194+
{showClearButton && (
195+
<Button type="button" variant={`tertiary/${variant}`} onClick={clearDuration}>
196+
Clear
197+
</Button>
198+
)}
199+
</div>
200+
);
201+
}

apps/webapp/app/components/primitives/Label.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { InfoIconTooltip, SimpleTooltip } from "./Tooltip";
44

55
const variants = {
66
small: {
7-
text: "font-sans text-sm font-normal text-text-bright leading-tight flex items-center gap-1",
7+
text: "font-sans text-[0.8125rem] font-normal text-text-bright leading-tight flex items-center gap-1",
88
},
99
medium: {
1010
text: "font-sans text-sm text-text-bright leading-tight flex items-center gap-1",

apps/webapp/app/components/primitives/Select.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ export function SelectTrigger({
327327
className
328328
)}
329329
ref={ref}
330+
disabled={disabled}
330331
{...props}
331332
/>
332333
}
@@ -615,7 +616,7 @@ export function SelectPopover({
615616
unmountOnHide={unmountOnHide}
616617
className={cn(
617618
"z-50 flex flex-col overflow-clip rounded border border-charcoal-700 bg-background-bright shadow-md outline-none animate-in fade-in-40",
618-
"min-w-[max(180px,calc(var(--popover-anchor-width)+0.5rem))]",
619+
"min-w-[max(180px,var(--popover-anchor-width))]",
619620
"max-w-[min(480px,var(--popover-available-width))]",
620621
"max-h-[min(600px,var(--popover-available-height))]",
621622
"origin-[var(--popover-transform-origin)]",

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

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@ import tagLeftPath from "./tag-left.svg";
33
import { SimpleTooltip } from "~/components/primitives/Tooltip";
44
import { Link } from "@remix-run/react";
55
import { cn } from "~/utils/cn";
6-
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
6+
import { ClipboardCheckIcon, ClipboardIcon, XIcon } from "lucide-react";
77

88
type Tag = string | { key: string; value: string };
99

10-
export function RunTag({ tag, to, tooltip }: { tag: string; to?: string; tooltip?: string }) {
10+
export function RunTag({
11+
tag,
12+
to,
13+
tooltip,
14+
action = { type: "copy" },
15+
}: {
16+
tag: string;
17+
action?: { type: "copy" } | { type: "delete"; onDelete: (tag: string) => void };
18+
to?: string;
19+
tooltip?: string;
20+
}) {
1121
const tagResult = useMemo(() => splitTag(tag), [tag]);
1222
const [isHovered, setIsHovered] = useState(false);
1323

@@ -57,7 +67,11 @@ export function RunTag({ tag, to, tooltip }: { tag: string; to?: string; tooltip
5767
return (
5868
<div className="group relative inline-flex shrink-0" onMouseLeave={() => setIsHovered(false)}>
5969
{tagContent}
60-
<CopyButton textToCopy={tag} isHovered={isHovered} />
70+
{action.type === "delete" ? (
71+
<DeleteButton tag={tag} onDelete={action.onDelete} isHovered={isHovered} />
72+
) : (
73+
<CopyButton textToCopy={tag} isHovered={isHovered} />
74+
)}
6175
</div>
6276
);
6377
}
@@ -105,6 +119,45 @@ function CopyButton({ textToCopy, isHovered }: { textToCopy: string; isHovered:
105119
);
106120
}
107121

122+
function DeleteButton({
123+
tag,
124+
onDelete,
125+
isHovered,
126+
}: {
127+
tag: string;
128+
onDelete: (tag: string) => void;
129+
isHovered: boolean;
130+
}) {
131+
const handleDelete = useCallback(
132+
(e: React.MouseEvent) => {
133+
e.preventDefault();
134+
e.stopPropagation();
135+
onDelete(tag);
136+
},
137+
[tag, onDelete]
138+
);
139+
140+
return (
141+
<SimpleTooltip
142+
button={
143+
<span
144+
onClick={handleDelete}
145+
onMouseDown={(e) => e.stopPropagation()}
146+
className={cn(
147+
"absolute -right-6 top-0 z-10 size-6 items-center justify-center rounded-r-sm border-y border-r border-charcoal-650 bg-charcoal-750",
148+
isHovered ? "flex" : "hidden",
149+
"text-text-dimmed hover:border-charcoal-600 hover:bg-charcoal-700 hover:text-rose-400"
150+
)}
151+
>
152+
<XIcon className="size-3.5" />
153+
</span>
154+
}
155+
content="Remove tag"
156+
disableHoverableContent
157+
/>
158+
);
159+
}
160+
108161
/** Takes a string and turns it into a tag
109162
*
110163
* If the string has 12 or fewer alpha characters followed by an underscore or colon then we return an object with a key and value

0 commit comments

Comments
 (0)