Skip to content

feat: expose all run options in the test run page #2227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d302622
Implement a new primitive UI component for picking durations
myftija Jul 3, 2025
c782336
Implement a new component to input run tags
myftija Jul 3, 2025
caa5bcf
Expose all run options in the test run page
myftija Jul 3, 2025
2a89a58
Add subtle animations when adding/removing run tags in the test page
myftija Jul 3, 2025
4da50b8
Add a new resource endpoint for fetching queues
myftija Jul 4, 2025
8493fa0
Fetch usable queues for the selected task
myftija Jul 4, 2025
67d627b
Fix width display issue in the select component
myftija Jul 4, 2025
3317686
Enable locking a run to a version from the test page
myftija Jul 4, 2025
90e903c
Disable entering max attemps <0
myftija Jul 4, 2025
2819ff9
Validate tags
myftija Jul 4, 2025
ceb9bf2
Add recent runs popover
myftija Jul 4, 2025
96b8596
Only show latest version for development environments
myftija Jul 4, 2025
2da2516
Update run options when selecting a recent run
myftija Jul 7, 2025
6080265
Rearrange the test page layout
myftija Jul 7, 2025
dddad15
Add subtle animation to the duration picker segments on focus
myftija Jul 7, 2025
794cb82
Improve queue selection dropdown styling
myftija Jul 7, 2025
d3fc160
Fix disabled state issue for the SelectTrigger component
myftija Jul 7, 2025
edcc246
Disable version selection field for dev envs
myftija Jul 7, 2025
0cb381b
Add usage hints next to the run option fields
myftija Jul 7, 2025
712bd5c
Add machine preset to the run options list
myftija Jul 7, 2025
7b266d9
Allow arbitrary queue inputs for v1 engine runs
myftija Jul 7, 2025
4987c17
Show truncated run ID instead of run numbers for recent runs
myftija Jul 7, 2025
59e95a9
Fix duplicate queue issue
myftija Jul 7, 2025
0cd9ffa
Extract common elements across the standard and scheduled test task f…
myftija Jul 7, 2025
a1cebf3
Apply values from recent runs to scheduled tasks too
myftija Jul 7, 2025
f2c55c4
Add additional run options for scheduled tasks
myftija Jul 7, 2025
a1c055b
Use a slightly smaller font size for run option labels
myftija Jul 7, 2025
d4e6a29
Disallow commas in the run tag input field
myftija Jul 7, 2025
fb7a3ee
Switch to a custom icon for recent runs button
myftija Jul 7, 2025
425c614
Flatten the load function test task result object
myftija Jul 7, 2025
101c857
Avoid redefining machine presets, use zod schema instead
myftija Jul 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/webapp/app/assets/icons/ClockRotateLeftIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function ClockRotateLeftIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
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"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path d="M12 12L12 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<path d="M12 12L14 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
);
}
201 changes: 201 additions & 0 deletions apps/webapp/app/components/primitives/DurationPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { Input } from "~/components/primitives/Input";
import { cn } from "~/utils/cn";
import React, { useRef, useState, useEffect } from "react";
import { Button } from "./Buttons";

export interface DurationPickerProps {
id?: string; // used for the hidden input for form submission
name?: string; // used for the hidden input for form submission
defaultValueSeconds?: number;
value?: number;
onChange?: (totalSeconds: number) => void;
variant?: "small" | "medium";
showClearButton?: boolean;
}

export function DurationPicker({
name,
defaultValueSeconds: defaultValue = 0,
value: controlledValue,
onChange,
variant = "small",
showClearButton = true,
}: DurationPickerProps) {
// Use controlled value if provided, otherwise use default
const initialValue = controlledValue ?? defaultValue;

const defaultHours = Math.floor(initialValue / 3600);
const defaultMinutes = Math.floor((initialValue % 3600) / 60);
const defaultSeconds = initialValue % 60;

const [hours, setHours] = useState<number>(defaultHours);
const [minutes, setMinutes] = useState<number>(defaultMinutes);
const [seconds, setSeconds] = useState<number>(defaultSeconds);

const minuteRef = useRef<HTMLInputElement>(null);
const hourRef = useRef<HTMLInputElement>(null);
const secondRef = useRef<HTMLInputElement>(null);

const totalSeconds = hours * 3600 + minutes * 60 + seconds;

const isEmpty = hours === 0 && minutes === 0 && seconds === 0;

// Sync internal state with external value changes
useEffect(() => {
if (controlledValue !== undefined && controlledValue !== totalSeconds) {
const newHours = Math.floor(controlledValue / 3600);
const newMinutes = Math.floor((controlledValue % 3600) / 60);
const newSeconds = controlledValue % 60;

setHours(newHours);
setMinutes(newMinutes);
setSeconds(newSeconds);
}
}, [controlledValue]);

useEffect(() => {
onChange?.(totalSeconds);
}, [totalSeconds, onChange]);

const handleHoursChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value) || 0;
setHours(Math.max(0, value));
};

const handleMinutesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value) || 0;
if (value >= 60) {
setHours((prev) => prev + Math.floor(value / 60));
setMinutes(value % 60);
return;
}

setMinutes(Math.max(0, Math.min(59, value)));
};

const handleSecondsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value) || 0;
if (value >= 60) {
setMinutes((prev) => {
const newMinutes = prev + Math.floor(value / 60);
if (newMinutes >= 60) {
setHours((prevHours) => prevHours + Math.floor(newMinutes / 60));
return newMinutes % 60;
}
return newMinutes;
});
setSeconds(value % 60);
return;
}

setSeconds(Math.max(0, Math.min(59, value)));
};

const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>,
nextRef?: React.RefObject<HTMLInputElement>,
prevRef?: React.RefObject<HTMLInputElement>
) => {
if (e.key === "Tab") {
return;
}

if (e.key === "ArrowRight" && nextRef) {
e.preventDefault();
nextRef.current?.focus();
nextRef.current?.select();
return;
}

if (e.key === "ArrowLeft" && prevRef) {
e.preventDefault();
prevRef.current?.focus();
prevRef.current?.select();
return;
}
};

const clearDuration = () => {
setHours(0);
setMinutes(0);
setSeconds(0);
hourRef.current?.focus();
};

return (
<div className="flex items-center gap-3">
<input type="hidden" name={name} value={totalSeconds} />

<div className="flex items-center gap-1">
<div className="group flex items-center gap-1">
<Input
variant={variant}
ref={hourRef}
className={cn(
"w-10 text-center font-mono tabular-nums caret-transparent [&::-webkit-inner-spin-button]:appearance-none",
isEmpty && "text-text-dimmed"
)}
value={hours.toString()}
onChange={handleHoursChange}
onKeyDown={(e) => handleKeyDown(e, minuteRef)}
onFocus={(e) => e.target.select()}
type="number"
min={0}
inputMode="numeric"
/>
<span className="text-sm text-text-dimmed transition-colors duration-200 group-focus-within:text-text-bright/80">
h
</span>
</div>
<div className="group flex items-center gap-1">
<Input
variant={variant}
ref={minuteRef}
className={cn(
"w-10 text-center font-mono tabular-nums caret-transparent [&::-webkit-inner-spin-button]:appearance-none",
isEmpty && "text-text-dimmed"
)}
value={minutes.toString()}
onChange={handleMinutesChange}
onKeyDown={(e) => handleKeyDown(e, secondRef, hourRef)}
onFocus={(e) => e.target.select()}
type="number"
min={0}
max={59}
inputMode="numeric"
/>
<span className="text-sm text-text-dimmed transition-colors duration-200 group-focus-within:text-text-bright/80">
m
</span>
</div>
<div className="group flex items-center gap-1">
<Input
variant={variant}
ref={secondRef}
className={cn(
"w-10 text-center font-mono tabular-nums caret-transparent [&::-webkit-inner-spin-button]:appearance-none",
isEmpty && "text-text-dimmed"
)}
value={seconds.toString()}
onChange={handleSecondsChange}
onKeyDown={(e) => handleKeyDown(e, undefined, minuteRef)}
onFocus={(e) => e.target.select()}
type="number"
min={0}
max={59}
inputMode="numeric"
/>
<span className="text-sm text-text-dimmed transition-colors duration-200 group-focus-within:text-text-bright/80">
s
</span>
</div>
</div>

{showClearButton && (
<Button type="button" variant={`tertiary/${variant}`} onClick={clearDuration}>
Clear
</Button>
)}
</div>
);
}
2 changes: 1 addition & 1 deletion apps/webapp/app/components/primitives/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { InfoIconTooltip, SimpleTooltip } from "./Tooltip";

const variants = {
small: {
text: "font-sans text-sm font-normal text-text-bright leading-tight flex items-center gap-1",
text: "font-sans text-[0.8125rem] font-normal text-text-bright leading-tight flex items-center gap-1",
},
medium: {
text: "font-sans text-sm text-text-bright leading-tight flex items-center gap-1",
Expand Down
3 changes: 2 additions & 1 deletion apps/webapp/app/components/primitives/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ export function SelectTrigger({
className
)}
ref={ref}
disabled={disabled}
{...props}
/>
}
Expand Down Expand Up @@ -615,7 +616,7 @@ export function SelectPopover({
unmountOnHide={unmountOnHide}
className={cn(
"z-50 flex flex-col overflow-clip rounded border border-charcoal-700 bg-background-bright shadow-md outline-none animate-in fade-in-40",
"min-w-[max(180px,calc(var(--popover-anchor-width)+0.5rem))]",
"min-w-[max(180px,var(--popover-anchor-width))]",
"max-w-[min(480px,var(--popover-available-width))]",
"max-h-[min(600px,var(--popover-available-height))]",
"origin-[var(--popover-transform-origin)]",
Expand Down
59 changes: 56 additions & 3 deletions apps/webapp/app/components/runs/v3/RunTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ import tagLeftPath from "./tag-left.svg";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
import { ClipboardCheckIcon, ClipboardIcon, XIcon } from "lucide-react";

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

export function RunTag({ tag, to, tooltip }: { tag: string; to?: string; tooltip?: string }) {
export function RunTag({
tag,
to,
tooltip,
action = { type: "copy" },
}: {
tag: string;
action?: { type: "copy" } | { type: "delete"; onDelete: (tag: string) => void };
to?: string;
tooltip?: string;
}) {
const tagResult = useMemo(() => splitTag(tag), [tag]);
const [isHovered, setIsHovered] = useState(false);

Expand Down Expand Up @@ -57,7 +67,11 @@ export function RunTag({ tag, to, tooltip }: { tag: string; to?: string; tooltip
return (
<div className="group relative inline-flex shrink-0" onMouseLeave={() => setIsHovered(false)}>
{tagContent}
<CopyButton textToCopy={tag} isHovered={isHovered} />
{action.type === "delete" ? (
<DeleteButton tag={tag} onDelete={action.onDelete} isHovered={isHovered} />
) : (
<CopyButton textToCopy={tag} isHovered={isHovered} />
)}
</div>
);
}
Expand Down Expand Up @@ -105,6 +119,45 @@ function CopyButton({ textToCopy, isHovered }: { textToCopy: string; isHovered:
);
}

function DeleteButton({
tag,
onDelete,
isHovered,
}: {
tag: string;
onDelete: (tag: string) => void;
isHovered: boolean;
}) {
const handleDelete = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
onDelete(tag);
},
[tag, onDelete]
);

return (
<SimpleTooltip
button={
<span
onClick={handleDelete}
onMouseDown={(e) => e.stopPropagation()}
className={cn(
"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",
isHovered ? "flex" : "hidden",
"text-text-dimmed hover:border-charcoal-600 hover:bg-charcoal-700 hover:text-rose-400"
)}
>
<XIcon className="size-3.5" />
</span>
}
content="Remove tag"
disableHoverableContent
/>
);
}

/** Takes a string and turns it into a tag
*
* 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
Expand Down
Loading