ERP-node/frontend/components/dataflow/node-editor/panels/properties/RestAPISourceProperties.tsx

249 lines
7.7 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
import { RestAPISourceNodeData } from "@/types/node-editor";
import { Globe, Plus, Trash2 } from "lucide-react";
interface RestAPISourcePropertiesProps {
nodeId: string;
data: RestAPISourceNodeData;
}
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
const AUTH_TYPES = [
{ value: "none", label: "인증 없음" },
{ value: "bearer", label: "Bearer Token" },
{ value: "basic", label: "Basic Auth" },
{ value: "apikey", label: "API Key" },
];
export function RestAPISourceProperties({ nodeId, data }: RestAPISourcePropertiesProps) {
const { updateNode } = useFlowEditorStore();
const [displayName, setDisplayName] = useState(data.displayName || "");
const [url, setUrl] = useState(data.url || "");
const [method, setMethod] = useState(data.method || "GET");
const [headers, setHeaders] = useState(data.headers || {});
const [newHeaderKey, setNewHeaderKey] = useState("");
const [newHeaderValue, setNewHeaderValue] = useState("");
const [body, setBody] = useState(JSON.stringify(data.body || {}, null, 2));
const [authType, setAuthType] = useState(data.authentication?.type || "none");
const [authToken, setAuthToken] = useState(data.authentication?.token || "");
const [timeout, setTimeout] = useState(data.timeout?.toString() || "30000");
const [responseMapping, setResponseMapping] = useState(data.responseMapping || "");
useEffect(() => {
setDisplayName(data.displayName || "");
setUrl(data.url || "");
setMethod(data.method || "GET");
setHeaders(data.headers || {});
setBody(JSON.stringify(data.body || {}, null, 2));
setAuthType(data.authentication?.type || "none");
setAuthToken(data.authentication?.token || "");
setTimeout(data.timeout?.toString() || "30000");
setResponseMapping(data.responseMapping || "");
}, [data]);
const handleApply = () => {
let parsedBody = {};
try {
parsedBody = body.trim() ? JSON.parse(body) : {};
} catch (e) {
alert("Body JSON 형식이 올바르지 않습니다.");
return;
}
updateNode(nodeId, {
displayName,
url,
method: method as any,
headers,
body: parsedBody,
authentication: {
type: authType as any,
token: authToken || undefined,
},
timeout: parseInt(timeout) || 30000,
responseMapping,
});
};
const addHeader = () => {
if (newHeaderKey.trim() && newHeaderValue.trim()) {
setHeaders({ ...headers, [newHeaderKey.trim()]: newHeaderValue.trim() });
setNewHeaderKey("");
setNewHeaderValue("");
}
};
const removeHeader = (key: string) => {
const newHeaders = { ...headers };
delete newHeaders[key];
setHeaders(newHeaders);
};
return (
<div className="space-y-4 p-4">
<div className="flex items-center gap-2 rounded-md bg-teal-50 p-2">
<Globe className="h-4 w-4 text-teal-600" />
<span className="font-semibold text-teal-600">REST API </span>
</div>
<div>
<Label htmlFor="displayName" className="text-xs">
</Label>
<Input
id="displayName"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="노드에 표시될 이름"
className="mt-1 text-sm"
/>
</div>
<div>
<Label htmlFor="url" className="text-xs">
API URL
</Label>
<Input
id="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://api.example.com/data"
className="mt-1 text-sm"
/>
</div>
<div>
<Label className="text-xs">HTTP </Label>
<Select value={method} onValueChange={setMethod}>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{HTTP_METHODS.map((m) => (
<SelectItem key={m} value={m}>
{m}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs"></Label>
<div className="mt-1 space-y-2">
<div className="flex gap-2">
<Input
value={newHeaderKey}
onChange={(e) => setNewHeaderKey(e.target.value)}
placeholder="Key"
className="text-sm"
/>
<Input
value={newHeaderValue}
onChange={(e) => setNewHeaderValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addHeader()}
placeholder="Value"
className="text-sm"
/>
<Button size="sm" onClick={addHeader}>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="max-h-[100px] space-y-1 overflow-y-auto">
{Object.entries(headers).map(([key, value]) => (
<div key={key} className="flex items-center justify-between rounded bg-teal-50 px-2 py-1">
<span className="text-xs">
<span className="font-medium">{key}:</span> {value}
</span>
<Button variant="ghost" size="sm" onClick={() => removeHeader(key)}>
<Trash2 className="h-3 w-3 text-red-500" />
</Button>
</div>
))}
</div>
</div>
</div>
{(method === "POST" || method === "PUT" || method === "PATCH") && (
<div>
<Label htmlFor="body" className="text-xs">
Body (JSON)
</Label>
<Textarea
id="body"
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder='{"key": "value"}'
className="mt-1 font-mono text-sm"
rows={5}
/>
</div>
)}
<div>
<Label className="text-xs"></Label>
<Select value={authType} onValueChange={setAuthType}>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{AUTH_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
{authType !== "none" && (
<Input
value={authToken}
onChange={(e) => setAuthToken(e.target.value)}
placeholder="토큰/키 입력"
className="mt-2 text-sm"
type="password"
/>
)}
</div>
<div>
<Label htmlFor="timeout" className="text-xs">
(ms)
</Label>
<Input
id="timeout"
type="number"
value={timeout}
onChange={(e) => setTimeout(e.target.value)}
className="mt-1 text-sm"
/>
</div>
<div>
<Label htmlFor="responseMapping" className="text-xs">
(JSON )
</Label>
<Input
id="responseMapping"
value={responseMapping}
onChange={(e) => setResponseMapping(e.target.value)}
placeholder="예: data.items"
className="mt-1 text-sm"
/>
</div>
<Button onClick={handleApply} className="w-full">
</Button>
</div>
);
}