Skip to content

Commit 58b75d1

Browse files
committed
[TOOL-2804] Insight Playground: Render Select for Parameters with enum schema + Show Examples (#5805)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR enhances the `ToolTipLabel` and `SelectTrigger` components, adding new props for customization. It also introduces a new `ParameterInput` component to streamline input handling for parameters with enums, improving the overall user experience in forms. ### Detailed summary - Added `contentClassName` prop to `ToolTipLabel` for custom styling. - Updated `SelectTrigger` to accept `chevronClassName` prop for customization. - Introduced `ParameterInput` component to handle parameter inputs with enums. - Enhanced form error handling and display logic in `RequestConfigSection` and `ParameterSection`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent d7d9415 commit 58b75d1

File tree

3 files changed

+157
-25
lines changed

3 files changed

+157
-25
lines changed

apps/dashboard/src/@/components/ui/select.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ const SelectValue = SelectPrimitive.Value;
1414

1515
const SelectTrigger = React.forwardRef<
1616
React.ElementRef<typeof SelectPrimitive.Trigger>,
17-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
18-
>(({ className, children, ...props }, ref) => (
17+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
18+
chevronClassName?: string;
19+
}
20+
>(({ className, children, chevronClassName, ...props }, ref) => (
1921
<SelectPrimitive.Trigger
2022
ref={ref}
2123
className={cn(
@@ -26,7 +28,7 @@ const SelectTrigger = React.forwardRef<
2628
>
2729
{children}
2830
<SelectPrimitive.Icon asChild>
29-
<ChevronDown className="h-4 w-4 opacity-50" />
31+
<ChevronDown className={cn("h-4 w-4 opacity-50", chevronClassName)} />
3032
</SelectPrimitive.Icon>
3133
</SelectPrimitive.Trigger>
3234
));

apps/dashboard/src/@/components/ui/tooltip.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function ToolTipLabel(props: {
3535
rightIcon?: React.ReactNode;
3636
leftIcon?: React.ReactNode;
3737
hoverable?: boolean;
38+
contentClassName?: string;
3839
}) {
3940
if (!props.label) {
4041
return props.children;
@@ -48,7 +49,10 @@ export function ToolTipLabel(props: {
4849
</TooltipTrigger>
4950
<TooltipContent
5051
sideOffset={10}
51-
className="max-w-[400px] whitespace-normal leading-relaxed"
52+
className={cn(
53+
"max-w-[400px] whitespace-normal leading-relaxed",
54+
props.contentClassName,
55+
)}
5256
>
5357
<div className="flex items-center gap-1.5 p-2 text-sm">
5458
{props.leftIcon}

apps/dashboard/src/app/team/[team_slug]/[project_slug]/insight/[blueprint_slug]/blueprint-playground.client.tsx

Lines changed: 147 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import { Button } from "@/components/ui/button";
99
import { CodeClient } from "@/components/ui/code/code.client";
1010
import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form";
1111
import { Input } from "@/components/ui/input";
12+
import {
13+
Select,
14+
SelectContent,
15+
SelectItem,
16+
SelectTrigger,
17+
SelectValue,
18+
} from "@/components/ui/select";
1219
import { Separator } from "@/components/ui/separator";
1320
import { ToolTipLabel } from "@/components/ui/tooltip";
1421
import { cn } from "@/lib/utils";
@@ -27,7 +34,11 @@ import {
2734
import Link from "next/link";
2835
import type { OpenAPIV3 } from "openapi-types";
2936
import { useEffect, useMemo, useState } from "react";
30-
import { type UseFormReturn, useForm } from "react-hook-form";
37+
import {
38+
type ControllerRenderProps,
39+
type UseFormReturn,
40+
useForm,
41+
} from "react-hook-form";
3142
import { z } from "zod";
3243
import { useTrack } from "../../../../../../hooks/analytics/useTrack";
3344
import { getVercelEnv } from "../../../../../../lib/vercel-utils";
@@ -412,14 +423,20 @@ function RequestConfigSection(props: {
412423
supportedChainIds: number[];
413424
}) {
414425
const pathVariables = props.parameters.filter((param) => param.in === "path");
415-
416426
const queryParams = props.parameters.filter((param) => param.in === "query");
427+
const showError =
428+
!props.form.formState.isValid &&
429+
props.form.formState.isDirty &&
430+
props.form.formState.isSubmitted;
417431

418432
return (
419433
<div className="flex grow flex-col overflow-hidden">
420-
<div className="flex min-h-[60px] items-center gap-2 border-b p-4 text-sm">
421-
<ArrowUpRightIcon className="size-5" />
422-
Request
434+
<div className="flex min-h-[60px] items-center justify-between gap-2 border-b p-4 text-sm">
435+
<div className="flex items-center gap-2">
436+
<ArrowUpRightIcon className="size-5" />
437+
Request
438+
</div>
439+
{showError && <Badge variant="destructive">Invalid Request</Badge>}
423440
</div>
424441

425442
<ScrollShadow className="flex-1" scrollableClassName="max-h-full">
@@ -474,7 +491,25 @@ function ParameterSection(props: {
474491
? param.schema.description
475492
: undefined;
476493

494+
const example =
495+
param.schema && "type" in param.schema
496+
? param.schema.example
497+
: undefined;
498+
const exampleToShow =
499+
typeof example === "string" || typeof example === "number"
500+
? example
501+
: undefined;
502+
503+
const showTip = description !== undefined || example !== undefined;
504+
477505
const hasError = !!props.form.formState.errors[param.name];
506+
507+
const placeholder = url.includes(`{${param.name}}`)
508+
? `{${param.name}}`
509+
: url.includes(`:${param.name}`)
510+
? `:${param.name}`
511+
: "Value";
512+
478513
return (
479514
<FormField
480515
key={param.name}
@@ -530,23 +565,39 @@ function ParameterSection(props: {
530565
/>
531566
) : (
532567
<>
533-
<Input
534-
{...field}
535-
className={cn(
536-
"h-auto truncate rounded-none border-0 bg-transparent py-3 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0",
537-
description && "lg:pr-10",
538-
hasError && "text-destructive-text",
539-
)}
540-
placeholder={
541-
url.includes(`{${param.name}}`)
542-
? `{${param.name}}`
543-
: url.includes(`:${param.name}`)
544-
? `:${param.name}`
545-
: "Value"
546-
}
568+
<ParameterInput
569+
param={param}
570+
field={field}
571+
showTip={showTip}
572+
hasError={hasError}
573+
placeholder={placeholder}
547574
/>
548-
{description && (
549-
<ToolTipLabel label={description}>
575+
576+
{showTip && (
577+
<ToolTipLabel
578+
hoverable
579+
contentClassName="max-w-[100vw] break-all"
580+
label={
581+
<div className="flex flex-col gap-2">
582+
{description && (
583+
<p className="text-foreground">
584+
{description}
585+
</p>
586+
)}
587+
588+
{exampleToShow !== undefined && (
589+
<div>
590+
<p className="mb-1 text-muted-foreground">
591+
Example:{" "}
592+
<span className="font-mono">
593+
{exampleToShow}
594+
</span>
595+
</p>
596+
</div>
597+
)}
598+
</div>
599+
}
600+
>
550601
<Button
551602
asChild
552603
variant="ghost"
@@ -573,6 +624,66 @@ function ParameterSection(props: {
573624
);
574625
}
575626

627+
function ParameterInput(props: {
628+
param: OpenAPIV3.ParameterObject;
629+
field: ControllerRenderProps<
630+
{
631+
[x: string]: string | number;
632+
},
633+
string
634+
>;
635+
showTip: boolean;
636+
hasError: boolean;
637+
placeholder: string;
638+
}) {
639+
const { param, field, showTip, hasError, placeholder } = props;
640+
641+
if (param.schema && "type" in param.schema && param.schema.enum) {
642+
const { value, onChange, ...restField } = field;
643+
return (
644+
<Select
645+
{...restField}
646+
value={value.toString()}
647+
onValueChange={(v) => {
648+
onChange({ target: { value: v } });
649+
}}
650+
>
651+
<SelectTrigger
652+
className={cn(
653+
"border-none bg-transparent pr-10 font-mono focus:ring-0 focus:ring-offset-0",
654+
value === "" && "text-muted-foreground",
655+
)}
656+
chevronClassName="hidden"
657+
>
658+
<SelectValue placeholder="Select" />
659+
</SelectTrigger>
660+
661+
<SelectContent className="font-mono">
662+
{param.schema.enum.map((val) => {
663+
return (
664+
<SelectItem value={val} key={val}>
665+
{val}
666+
</SelectItem>
667+
);
668+
})}
669+
</SelectContent>
670+
</Select>
671+
);
672+
}
673+
674+
return (
675+
<Input
676+
{...field}
677+
className={cn(
678+
"h-auto truncate rounded-none border-0 bg-transparent py-3 font-mono text-sm focus-visible:ring-0 focus-visible:ring-offset-0",
679+
showTip && "lg:pr-10",
680+
hasError && "text-destructive-text",
681+
)}
682+
placeholder={placeholder}
683+
/>
684+
);
685+
}
686+
576687
function formatMilliseconds(ms: number) {
577688
if (ms < 1000) {
578689
return `${Math.round(ms)}ms`;
@@ -677,6 +788,21 @@ function openAPIV3ParamToZodFormSchema(param: BlueprintParameter) {
677788
return;
678789
}
679790

791+
// if enum values
792+
const enumValues = param.schema.enum;
793+
if (enumValues) {
794+
const enumSchema = z.enum(
795+
// @ts-expect-error - Its correct
796+
enumValues,
797+
);
798+
799+
if (param.required) {
800+
return enumSchema;
801+
}
802+
803+
return enumSchema.or(z.literal(""));
804+
}
805+
680806
switch (param.schema.type) {
681807
case "integer": {
682808
const intSchema = z.coerce

0 commit comments

Comments
 (0)