diff --git a/ccw/frontend/src/components/cli-endpoints/CliEndpointFormDialog.tsx b/ccw/frontend/src/components/cli-endpoints/CliEndpointFormDialog.tsx new file mode 100644 index 00000000..7ecc9ec1 --- /dev/null +++ b/ccw/frontend/src/components/cli-endpoints/CliEndpointFormDialog.tsx @@ -0,0 +1,270 @@ +// ======================================== +// CLI Endpoint Form Dialog +// ======================================== +// Dialog for creating and editing CLI endpoints + +import { useEffect, useMemo, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/Dialog'; +import { Input } from '@/components/ui/Input'; +import { Textarea } from '@/components/ui/Textarea'; +import { Button } from '@/components/ui/Button'; +import { Label } from '@/components/ui/Label'; +import { Switch } from '@/components/ui/Switch'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/Select'; +import type { CliEndpoint } from '@/lib/api'; + +export type CliEndpointFormMode = 'create' | 'edit'; + +export interface CliEndpointSavePayload { + name: string; + type: CliEndpoint['type']; + enabled: boolean; + config: Record; +} + +export interface CliEndpointFormDialogProps { + mode: CliEndpointFormMode; + endpoint?: CliEndpoint; + open: boolean; + onClose: () => void; + onSave: (payload: CliEndpointSavePayload) => Promise; +} + +interface FormErrors { + name?: string; + type?: string; + configJson?: string; +} + +function safeStringifyConfig(config: unknown): string { + try { + return JSON.stringify(config ?? {}, null, 2); + } catch { + return '{}'; + } +} + +function parseConfigJson( + configJson: string +): { ok: true; value: Record } | { ok: false; errorKey: string } { + const trimmed = configJson.trim(); + if (!trimmed) { + return { ok: true, value: {} }; + } + + try { + const parsed = JSON.parse(trimmed) as unknown; + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return { ok: false, errorKey: 'validation.configMustBeObject' }; + } + return { ok: true, value: parsed as Record }; + } catch { + return { ok: false, errorKey: 'validation.invalidJson' }; + } +} + +export function CliEndpointFormDialog({ + mode, + endpoint, + open, + onClose, + onSave, +}: CliEndpointFormDialogProps) { + const { formatMessage } = useIntl(); + const isEditing = mode === 'edit'; + + const [name, setName] = useState(''); + const [type, setType] = useState('custom'); + const [enabled, setEnabled] = useState(true); + const [configJson, setConfigJson] = useState('{}'); + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + + const dialogTitle = useMemo(() => { + return isEditing + ? formatMessage({ id: 'cliEndpoints.dialog.editTitle' }, { id: endpoint?.id ?? '' }) + : formatMessage({ id: 'cliEndpoints.dialog.createTitle' }); + }, [formatMessage, isEditing, endpoint?.id]); + + useEffect(() => { + if (!open) return; + + if (isEditing && endpoint) { + setName(endpoint.name); + setType(endpoint.type); + setEnabled(endpoint.enabled); + setConfigJson(safeStringifyConfig(endpoint.config)); + } else { + setName(''); + setType('custom'); + setEnabled(true); + setConfigJson('{}'); + } + setErrors({}); + setIsSubmitting(false); + }, [open, isEditing, endpoint]); + + const handleSubmit = async () => { + const nextErrors: FormErrors = {}; + + if (!name.trim()) { + nextErrors.name = 'validation.nameRequired'; + } + if (!type) { + nextErrors.type = 'validation.typeRequired'; + } + + const parsedConfig = parseConfigJson(configJson); + if (!parsedConfig.ok) { + nextErrors.configJson = parsedConfig.errorKey; + } + + if (Object.keys(nextErrors).length > 0) { + setErrors(nextErrors); + return; + } + + setIsSubmitting(true); + try { + await onSave({ + name: name.trim(), + type, + enabled, + config: (parsedConfig as { ok: true; value: Record }).value, + }); + onClose(); + } catch (err) { + console.error('Failed to save CLI endpoint:', err); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + {dialogTitle} + + +
+ {isEditing && endpoint && ( +
+ + +
+ )} + +
+
+ + { + setName(e.target.value); + if (errors.name) setErrors((prev) => ({ ...prev, name: undefined })); + }} + placeholder={formatMessage({ id: 'cliEndpoints.form.namePlaceholder' })} + error={!!errors.name} + /> + {errors.name && ( +

+ {formatMessage({ id: `cliEndpoints.${errors.name}` })} +

+ )} +
+ +
+ + + {errors.type && ( +

+ {formatMessage({ id: `cliEndpoints.${errors.type}` })} +

+ )} +
+
+ +
+
+

{formatMessage({ id: 'cliEndpoints.form.enabled' })}

+

{formatMessage({ id: 'cliEndpoints.form.enabledHint' })}

+
+ +
+ +
+ +