mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-16 08:25:18 +02:00
feat: user defined custom tools as part of workflow execution (#94)
* feat: add custom tools functionality * Show tools in nodes * integrate tool calling with pipeline engine
This commit is contained in:
parent
cc2d3e70d2
commit
3e55af9256
65 changed files with 5483 additions and 6673 deletions
242
ui/src/components/http/create-credential-dialog.tsx
Normal file
242
ui/src/components/http/create-credential-dialog.tsx
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
"use client";
|
||||
|
||||
import { AlertCircle, Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createCredentialApiV1CredentialsPost } from "@/client";
|
||||
import { CredentialResponse, WebhookCredentialType } from "@/client/types.gen";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
interface CreateCredentialDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onCreated?: (credential: CredentialResponse) => void;
|
||||
}
|
||||
|
||||
interface CredentialField {
|
||||
key: string;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
isSecret?: boolean;
|
||||
}
|
||||
|
||||
const getCredentialDataFields = (type: WebhookCredentialType): CredentialField[] => {
|
||||
switch (type) {
|
||||
case "api_key":
|
||||
return [
|
||||
{ key: "header_name", label: "Header Name", placeholder: "X-API-Key" },
|
||||
{ key: "api_key", label: "API Key", placeholder: "your-api-key", isSecret: true },
|
||||
];
|
||||
case "bearer_token":
|
||||
return [
|
||||
{ key: "token", label: "Token", placeholder: "your-bearer-token", isSecret: true },
|
||||
];
|
||||
case "basic_auth":
|
||||
return [
|
||||
{ key: "username", label: "Username", placeholder: "username" },
|
||||
{ key: "password", label: "Password", placeholder: "password", isSecret: true },
|
||||
];
|
||||
case "custom_header":
|
||||
return [
|
||||
{ key: "header_name", label: "Header Name", placeholder: "X-Custom-Header" },
|
||||
{ key: "header_value", label: "Header Value", placeholder: "header-value", isSecret: true },
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export function CreateCredentialDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onCreated,
|
||||
}: CreateCredentialDialogProps) {
|
||||
const { getAccessToken } = useAuth();
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [credentialType, setCredentialType] = useState<WebhookCredentialType>("bearer_token");
|
||||
const [credentialData, setCredentialData] = useState<Record<string, string>>({});
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!name.trim()) return;
|
||||
|
||||
setIsCreating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const accessToken = await getAccessToken();
|
||||
const response = await createCredentialApiV1CredentialsPost({
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
body: {
|
||||
name,
|
||||
description: description || undefined,
|
||||
credential_type: credentialType,
|
||||
credential_data: credentialData,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
const errorDetail = (response.error as { detail?: string })?.detail
|
||||
|| "Failed to create credential";
|
||||
setError(errorDetail);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
onCreated?.(response.data);
|
||||
handleClose();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to create credential:", err);
|
||||
setError(
|
||||
err instanceof Error ? err.message : "An unexpected error occurred"
|
||||
);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onOpenChange(false);
|
||||
// Reset form
|
||||
setName("");
|
||||
setDescription("");
|
||||
setCredentialType("bearer_token");
|
||||
setCredentialData({});
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
if (!newOpen) {
|
||||
setError(null);
|
||||
}
|
||||
onOpenChange(newOpen);
|
||||
};
|
||||
|
||||
const fields = getCredentialDataFields(credentialType);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Credential</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new credential for authentication.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-start gap-2 p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
|
||||
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="cred-name">Name *</Label>
|
||||
<Input
|
||||
id="cred-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My API Key"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="cred-description">Description</Label>
|
||||
<Input
|
||||
id="cred-description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Optional description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Credential Type</Label>
|
||||
<Select
|
||||
value={credentialType}
|
||||
onValueChange={(v) => {
|
||||
setCredentialType(v as WebhookCredentialType);
|
||||
setCredentialData({});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bearer_token">Bearer Token</SelectItem>
|
||||
<SelectItem value="api_key">API Key</SelectItem>
|
||||
<SelectItem value="basic_auth">Basic Auth</SelectItem>
|
||||
<SelectItem value="custom_header">Custom Header</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{fields.map((field) => (
|
||||
<div key={field.key} className="grid gap-2">
|
||||
<Label htmlFor={`cred-${field.key}`}>{field.label}</Label>
|
||||
<Input
|
||||
id={`cred-${field.key}`}
|
||||
type={field.isSecret ? "password" : "text"}
|
||||
value={credentialData[field.key] || ""}
|
||||
onChange={(e) =>
|
||||
setCredentialData((prev) => ({
|
||||
...prev,
|
||||
[field.key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isCreating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={!name.trim() || isCreating}
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
"Create"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
140
ui/src/components/http/credential-selector.tsx
Normal file
140
ui/src/components/http/credential-selector.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"use client";
|
||||
|
||||
import { Loader2, PlusIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { listCredentialsApiV1CredentialsGet } from "@/client";
|
||||
import { CredentialResponse } from "@/client/types.gen";
|
||||
import { CreateCredentialDialog } from "@/components/http/create-credential-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
interface CredentialSelectorProps {
|
||||
value: string;
|
||||
onChange: (uuid: string) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
export function CredentialSelector({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
placeholder = "No authentication",
|
||||
label = "Credential",
|
||||
description = "Select a credential for authentication, or leave empty for no auth.",
|
||||
showLabel = true,
|
||||
}: CredentialSelectorProps) {
|
||||
const { getAccessToken } = useAuth();
|
||||
|
||||
const [credentials, setCredentials] = useState<CredentialResponse[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||
|
||||
const fetchCredentials = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const accessToken = await getAccessToken();
|
||||
const response = await listCredentialsApiV1CredentialsGet({
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (response.error) {
|
||||
console.error("Failed to fetch credentials:", response.error);
|
||||
setCredentials([]);
|
||||
return;
|
||||
}
|
||||
if (response.data) {
|
||||
setCredentials(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch credentials:", error);
|
||||
setCredentials([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCredentials();
|
||||
}, [fetchCredentials]);
|
||||
|
||||
const handleCredentialCreated = async (credential: CredentialResponse) => {
|
||||
await fetchCredentials();
|
||||
onChange(credential.uuid);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
{showLabel && (
|
||||
<>
|
||||
<Label>{label}</Label>
|
||||
{description && (
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{description}
|
||||
</Label>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={value || "none"}
|
||||
onValueChange={(v) => onChange(v === "none" ? "" : v)}
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder={placeholder} />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{placeholder}</SelectItem>
|
||||
{credentials.map((cred) => (
|
||||
<SelectItem key={cred.uuid} value={cred.uuid}>
|
||||
{cred.name} ({cred.credential_type})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setIsAddDialogOpen(true)}
|
||||
title="Add new credential"
|
||||
disabled={disabled}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{credentials.length === 0 && !loading && (
|
||||
<div className="p-3 border rounded-md bg-muted/20">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No credentials found. Click the + button to create one.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreateCredentialDialog
|
||||
open={isAddDialogOpen}
|
||||
onOpenChange={setIsAddDialogOpen}
|
||||
onCreated={handleCredentialCreated}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
ui/src/components/http/http-method-selector.tsx
Normal file
44
ui/src/components/http/http-method-selector.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
|
||||
interface HttpMethodSelectorProps {
|
||||
value: HttpMethod;
|
||||
onChange: (method: HttpMethod) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const HTTP_METHODS: HttpMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
||||
|
||||
export function HttpMethodSelector({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}: HttpMethodSelectorProps) {
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(v) => onChange(v as HttpMethod)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{HTTP_METHODS.map((method) => (
|
||||
<SelectItem key={method} value={method}>
|
||||
{method}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
5
ui/src/components/http/index.ts
Normal file
5
ui/src/components/http/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export { CreateCredentialDialog } from "./create-credential-dialog";
|
||||
export { CredentialSelector } from "./credential-selector";
|
||||
export { type HttpMethod, HttpMethodSelector } from "./http-method-selector";
|
||||
export { KeyValueEditor, type KeyValueItem } from "./key-value-editor";
|
||||
export { ParameterEditor, type ParameterType,type ToolParameter } from "./parameter-editor";
|
||||
85
ui/src/components/http/key-value-editor.tsx
Normal file
85
ui/src/components/http/key-value-editor.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"use client";
|
||||
|
||||
import { PlusIcon, Trash2Icon } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export interface KeyValueItem {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface KeyValueEditorProps {
|
||||
items: KeyValueItem[];
|
||||
onChange: (items: KeyValueItem[]) => void;
|
||||
keyPlaceholder?: string;
|
||||
valuePlaceholder?: string;
|
||||
addButtonText?: string;
|
||||
emptyMessage?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function KeyValueEditor({
|
||||
items,
|
||||
onChange,
|
||||
keyPlaceholder = "Key",
|
||||
valuePlaceholder = "Value",
|
||||
addButtonText = "Add",
|
||||
disabled = false,
|
||||
}: KeyValueEditorProps) {
|
||||
const addItem = () => {
|
||||
onChange([...items, { key: "", value: "" }]);
|
||||
};
|
||||
|
||||
const updateItem = (index: number, field: "key" | "value", value: string) => {
|
||||
const newItems = [...items];
|
||||
newItems[index] = { ...newItems[index], [field]: value };
|
||||
onChange(newItems);
|
||||
};
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
onChange(items.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{items.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<Input
|
||||
placeholder={keyPlaceholder}
|
||||
value={item.key}
|
||||
onChange={(e) => updateItem(index, "key", e.target.value)}
|
||||
className="flex-1"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Input
|
||||
placeholder={valuePlaceholder}
|
||||
value={item.value}
|
||||
onChange={(e) => updateItem(index, "value", e.target.value)}
|
||||
className="flex-1"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => removeItem(index)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Trash2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addItem}
|
||||
className="w-fit"
|
||||
disabled={disabled}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-1" /> {addButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
167
ui/src/components/http/parameter-editor.tsx
Normal file
167
ui/src/components/http/parameter-editor.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
"use client";
|
||||
|
||||
import { PlusIcon, Trash2Icon } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
export type ParameterType = "string" | "number" | "boolean";
|
||||
|
||||
export interface ToolParameter {
|
||||
name: string;
|
||||
type: ParameterType;
|
||||
description: string;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
interface ParameterEditorProps {
|
||||
parameters: ToolParameter[];
|
||||
onChange: (parameters: ToolParameter[]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ParameterEditor({
|
||||
parameters,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}: ParameterEditorProps) {
|
||||
const addParameter = () => {
|
||||
onChange([
|
||||
...parameters,
|
||||
{ name: "", type: "string", description: "", required: true },
|
||||
]);
|
||||
};
|
||||
|
||||
const updateParameter = (
|
||||
index: number,
|
||||
field: keyof ToolParameter,
|
||||
value: string | boolean
|
||||
) => {
|
||||
const newParams = [...parameters];
|
||||
newParams[index] = { ...newParams[index], [field]: value };
|
||||
onChange(newParams);
|
||||
};
|
||||
|
||||
const removeParameter = (index: number) => {
|
||||
onChange(parameters.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{parameters.length === 0 && (
|
||||
<div className="text-sm text-muted-foreground py-4 text-center border border-dashed rounded-md">
|
||||
No parameters defined. Add a parameter to specify what data this tool needs.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{parameters.map((param, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border rounded-lg p-4 space-y-3 bg-muted/20"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
Parameter {index + 1}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeParameter(index)}
|
||||
disabled={disabled}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<Trash2Icon className="h-4 w-4 text-muted-foreground hover:text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Name</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Name of the parameter, like "order_id" or "customer_name"
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="e.g., customer_name"
|
||||
value={param.name}
|
||||
onChange={(e) =>
|
||||
updateParameter(index, "name", e.target.value)
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Type</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Type of the parameter, like "string" or "number" or "boolean"
|
||||
</Label>
|
||||
<Select
|
||||
value={param.type}
|
||||
onValueChange={(value: ParameterType) =>
|
||||
updateParameter(index, "type", value)
|
||||
}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="string">String</SelectItem>
|
||||
<SelectItem value="number">Number</SelectItem>
|
||||
<SelectItem value="boolean">Boolean</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Description</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Description of the parameter, which makes it easy for LLM to understand, like "The ID of the Customer to fetch Order Details"
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Describe what this parameter is for..."
|
||||
value={param.description}
|
||||
onChange={(e) =>
|
||||
updateParameter(index, "description", e.target.value)
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id={`required-${index}`}
|
||||
checked={param.required}
|
||||
onCheckedChange={(checked) =>
|
||||
updateParameter(index, "required", checked)
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Label htmlFor={`required-${index}`} className="text-sm">
|
||||
Required
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addParameter}
|
||||
className="w-fit"
|
||||
disabled={disabled}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-1" /> Add Parameter
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue