Skip to content

Commit be7dcad

Browse files
kolbeyangclaude
andcommitted
refactor: restructure manage-signal-sheet, add size variants to Input and Button
- Split manage-signal-sheet.tsx into folder with one component per file - Group template-picker and test-results-view into subfolders with index.tsx pattern - Add xs/sm/md size prop to Input component (xs default, preserves existing behavior) - Add md size variant to Button component - Use size sm for signal name input, md for Test/Create/Run test buttons - Right-justify footer buttons, disable Test when Create is disabled - Reduce sheet width by 10%, remove selected trace chip, remove test flask icon - Rename template label to "Template" with consistent header styling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bc442bf commit be7dcad

File tree

16 files changed

+1013
-696
lines changed

16 files changed

+1013
-696
lines changed

frontend/components/signals/manage-signal-sheet.tsx

Lines changed: 0 additions & 694 deletions
This file was deleted.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use client";
2+
3+
import { X } from "lucide-react";
4+
import { useCallback, useState } from "react";
5+
6+
export default function EnumValuesInput({
7+
values,
8+
onChange,
9+
}: {
10+
values: string[] | undefined;
11+
onChange: (values: string[] | undefined) => void;
12+
}) {
13+
const [inputValue, setInputValue] = useState("");
14+
15+
const handleKeyDown = useCallback(
16+
(e: React.KeyboardEvent<HTMLInputElement>) => {
17+
if (e.key === "Enter" || e.key === ",") {
18+
e.preventDefault();
19+
const trimmed = inputValue.trim();
20+
if (trimmed && !values?.includes(trimmed)) {
21+
onChange([...(values || []), trimmed]);
22+
setInputValue("");
23+
}
24+
} else if (e.key === "Backspace" && !inputValue && values && values.length > 0) {
25+
onChange(values.slice(0, -1));
26+
}
27+
},
28+
[inputValue, values, onChange]
29+
);
30+
31+
const removeValue = useCallback(
32+
(valueToRemove: string) => {
33+
const newValues = values?.filter((v) => v !== valueToRemove);
34+
onChange(newValues && newValues.length > 0 ? newValues : undefined);
35+
},
36+
[values, onChange]
37+
);
38+
39+
return (
40+
<div className="flex flex-wrap items-center gap-1 min-h-7 px-2 py-1 border rounded-md bg-background w-full">
41+
{values?.map((value) => (
42+
<span
43+
key={value}
44+
className="inline-flex items-center gap-0.5 px-1.5 py-0.5 text-xs bg-muted text-secondary-foreground rounded"
45+
>
46+
{value}
47+
<button
48+
type="button"
49+
onClick={() => removeValue(value)}
50+
className="hover:text-destructive text-muted-foreground transition-colors"
51+
>
52+
<X className="w-3 h-3" />
53+
</button>
54+
</span>
55+
))}
56+
<input
57+
type="text"
58+
value={inputValue}
59+
onChange={(e) => setInputValue(e.target.value)}
60+
onKeyDown={handleKeyDown}
61+
placeholder={values?.length ? "" : "Add values..."}
62+
className="flex-1 min-w-16 text-xs bg-transparent outline-none placeholder:text-muted-foreground"
63+
/>
64+
</div>
65+
);
66+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"use client";
2+
3+
import { Loader2 } from "lucide-react";
4+
import { useParams } from "next/navigation";
5+
import { type PropsWithChildren, useCallback, useState } from "react";
6+
import { FormProvider, useForm, useFormContext } from "react-hook-form";
7+
8+
import { Button } from "@/components/ui/button";
9+
import { ScrollArea } from "@/components/ui/scroll-area";
10+
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
11+
import { useToast } from "@/lib/hooks/use-toast";
12+
import { type TraceRow } from "@/lib/traces/types";
13+
import { cn } from "@/lib/utils";
14+
15+
import SignalFormFields from "./signal-form-fields";
16+
import TestPanel from "./test-panel";
17+
import { getDefaultValues, type ManageSignalForm } from "./types";
18+
import useSubmitHandler from "./use-submit-handler";
19+
import useTestExecution from "./use-test-execution";
20+
21+
export type { ManageSignalForm } from "./types";
22+
export { getDefaultValues } from "./types";
23+
24+
function SubmitButton({ isLoading }: { isLoading: boolean }) {
25+
const {
26+
watch,
27+
formState: { isValid },
28+
} = useFormContext<ManageSignalForm>();
29+
const id = watch("id");
30+
31+
return (
32+
<Button type="submit" size="md" disabled={isLoading || !isValid} handleEnter>
33+
<Loader2 className={cn("hidden", isLoading && "animate-spin block")} size={16} />
34+
{id ? "Save" : "Create"}
35+
</Button>
36+
);
37+
}
38+
39+
function DrawerContent({
40+
setOpen,
41+
onSuccess,
42+
showTest,
43+
setShowTest,
44+
}: {
45+
setOpen: (open: boolean) => void;
46+
onSuccess?: (signal: ManageSignalForm) => Promise<void>;
47+
showTest: boolean;
48+
setShowTest: (show: boolean) => void;
49+
}) {
50+
const [isLoading, setIsLoading] = useState(false);
51+
const [selectedTrace, setSelectedTrace] = useState<TraceRow | null>(null);
52+
53+
const { projectId } = useParams();
54+
const { toast } = useToast();
55+
const {
56+
handleSubmit,
57+
reset,
58+
watch,
59+
getValues,
60+
formState: { isValid },
61+
} = useFormContext<ManageSignalForm>();
62+
const id = watch("id");
63+
64+
const submit = useSubmitHandler({
65+
projectId: String(projectId),
66+
toast,
67+
setOpen,
68+
reset,
69+
onSuccess,
70+
setIsLoading,
71+
});
72+
73+
const { isExecuting, testOutput, execute } = useTestExecution({
74+
getValues,
75+
projectId: String(projectId),
76+
selectedTrace,
77+
});
78+
79+
return (
80+
<div className="flex h-full overflow-hidden">
81+
{/* Left side — Form */}
82+
<form onSubmit={handleSubmit(submit)} className="flex flex-col flex-1 overflow-hidden min-w-0">
83+
<ScrollArea className="flex-1 px-5 py-4">
84+
<SignalFormFields showTemplates={!id} />
85+
</ScrollArea>
86+
87+
{/* Footer */}
88+
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t">
89+
<Button
90+
type="button"
91+
size="md"
92+
variant={showTest ? "secondary" : "outline"}
93+
disabled={isLoading || !isValid}
94+
onClick={() => setShowTest(!showTest)}
95+
>
96+
Test
97+
</Button>
98+
<SubmitButton isLoading={isLoading} />
99+
</div>
100+
</form>
101+
102+
{/* Right side — Test panel */}
103+
{showTest && (
104+
<TestPanel
105+
watch={watch}
106+
selectedTrace={selectedTrace}
107+
setSelectedTrace={setSelectedTrace}
108+
isExecuting={isExecuting}
109+
testOutput={testOutput}
110+
execute={execute}
111+
onClose={() => setShowTest(false)}
112+
/>
113+
)}
114+
</div>
115+
);
116+
}
117+
118+
export default function ManageSignalSheet({
119+
children,
120+
open,
121+
setOpen,
122+
defaultValues: initialValues,
123+
onSuccess,
124+
}: PropsWithChildren<{
125+
open: boolean;
126+
setOpen: (open: boolean) => void;
127+
defaultValues?: ManageSignalForm;
128+
onSuccess?: (signal: ManageSignalForm) => Promise<void>;
129+
}>) {
130+
const { projectId } = useParams();
131+
const [showTest, setShowTest] = useState(false);
132+
133+
const convertToFormValues = useCallback(
134+
(values: ManageSignalForm | undefined): ManageSignalForm => {
135+
if (!values) return getDefaultValues(String(projectId));
136+
return values;
137+
},
138+
[projectId]
139+
);
140+
141+
const form = useForm<ManageSignalForm>({
142+
defaultValues: convertToFormValues(initialValues),
143+
mode: "onChange",
144+
});
145+
146+
const onOpenChange = useCallback(
147+
(nextOpen: boolean) => {
148+
setOpen(nextOpen);
149+
if (nextOpen) {
150+
form.reset(convertToFormValues(initialValues));
151+
} else {
152+
form.reset(getDefaultValues(String(projectId)));
153+
setShowTest(false);
154+
}
155+
},
156+
[form, initialValues, projectId, setOpen, convertToFormValues]
157+
);
158+
159+
return (
160+
<FormProvider {...form}>
161+
<Sheet open={open} onOpenChange={onOpenChange}>
162+
<SheetTrigger asChild>{children}</SheetTrigger>
163+
<SheetContent
164+
side="right"
165+
className={cn(
166+
"sm:max-w-none! p-0 flex flex-col transition-[width] duration-300",
167+
showTest ? "w-[72vw]" : "w-[45vw]"
168+
)}
169+
>
170+
<DrawerContent setOpen={setOpen} onSuccess={onSuccess} showTest={showTest} setShowTest={setShowTest} />
171+
</SheetContent>
172+
</Sheet>
173+
</FormProvider>
174+
);
175+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"use client";
2+
3+
import { X } from "lucide-react";
4+
import { useCallback } from "react";
5+
import { Controller, useFormContext } from "react-hook-form";
6+
7+
import { SCHEMA_FIELD_TYPES } from "@/components/signals/utils";
8+
import { Button } from "@/components/ui/button";
9+
import { Input } from "@/components/ui/input";
10+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select.tsx";
11+
import { Textarea } from "@/components/ui/textarea";
12+
13+
import EnumValuesInput from "./enum-values-input";
14+
import { type ManageSignalForm } from "./types";
15+
16+
export default function SchemaFieldRow({
17+
index,
18+
onRemove,
19+
canRemove,
20+
}: {
21+
index: number;
22+
onRemove: () => void;
23+
canRemove: boolean;
24+
}) {
25+
const {
26+
control,
27+
watch,
28+
setValue,
29+
formState: { errors },
30+
} = useFormContext<ManageSignalForm>();
31+
32+
const fieldType = watch(`schemaFields.${index}.type`);
33+
const enumValues = watch(`schemaFields.${index}.enumValues`);
34+
const fieldErrors = errors.schemaFields?.[index];
35+
36+
const handleTypeChange = useCallback(
37+
(newType: string) => {
38+
setValue(`schemaFields.${index}.type`, newType as "string" | "number" | "boolean" | "enum");
39+
if (newType !== "enum") {
40+
setValue(`schemaFields.${index}.enumValues`, undefined);
41+
}
42+
},
43+
[setValue, index]
44+
);
45+
46+
const handleEnumValuesChange = useCallback(
47+
(values: string[] | undefined) => {
48+
setValue(`schemaFields.${index}.enumValues`, values);
49+
},
50+
[setValue, index]
51+
);
52+
53+
return (
54+
<div className="flex flex-col gap-1">
55+
<div className="flex gap-2 items-start">
56+
<Controller
57+
name={`schemaFields.${index}.name`}
58+
control={control}
59+
rules={{
60+
required: "Name is required",
61+
pattern: {
62+
value: /^[a-zA-Z_][a-zA-Z0-9_]*$/,
63+
message: "Name must be a valid identifier",
64+
},
65+
}}
66+
render={({ field }) => <Input {...field} placeholder="Field name" className="w-32 text-sm" />}
67+
/>
68+
{fieldType === "enum" ? (
69+
<div className="flex flex-col gap-1 w-28">
70+
<Controller
71+
name={`schemaFields.${index}.type`}
72+
control={control}
73+
render={({ field }) => (
74+
<Select value={field.value} onValueChange={handleTypeChange}>
75+
<SelectTrigger className="w-28">
76+
<SelectValue />
77+
</SelectTrigger>
78+
<SelectContent>
79+
{SCHEMA_FIELD_TYPES.map((type) => (
80+
<SelectItem key={type.value} value={type.value}>
81+
{type.label}
82+
</SelectItem>
83+
))}
84+
</SelectContent>
85+
</Select>
86+
)}
87+
/>
88+
<EnumValuesInput values={enumValues} onChange={handleEnumValuesChange} />
89+
</div>
90+
) : (
91+
<Controller
92+
name={`schemaFields.${index}.type`}
93+
control={control}
94+
render={({ field }) => (
95+
<Select value={field.value} onValueChange={handleTypeChange}>
96+
<SelectTrigger className="w-28">
97+
<SelectValue />
98+
</SelectTrigger>
99+
<SelectContent>
100+
{SCHEMA_FIELD_TYPES.map((type) => (
101+
<SelectItem key={type.value} value={type.value}>
102+
{type.label}
103+
</SelectItem>
104+
))}
105+
</SelectContent>
106+
</Select>
107+
)}
108+
/>
109+
)}
110+
<Controller
111+
name={`schemaFields.${index}.description`}
112+
control={control}
113+
render={({ field }) => (
114+
<Textarea
115+
{...field}
116+
placeholder="Description of the field"
117+
rows={0}
118+
className="flex-1 text-xs! py-1.25 min-h-7!"
119+
/>
120+
)}
121+
/>
122+
<Button type="button" variant="ghost" onClick={onRemove} disabled={!canRemove} className="py-[7px] shrink-0">
123+
<X className="w-3.5 h-3.5" />
124+
</Button>
125+
</div>
126+
{fieldErrors && (
127+
<p className="text-destructive text-xs">
128+
{fieldErrors.name?.message || fieldErrors.description?.message || fieldErrors.type?.message}
129+
</p>
130+
)}
131+
</div>
132+
);
133+
}

0 commit comments

Comments
 (0)