/** * TemplateEditor -- full-page form for creating and editing config templates. * Includes name, description, monospace content editor, tag input, * and auto-detected variable table. */ import { useState } from 'react' import { X, Plus, Loader2, Sparkles } 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 { type TemplateResponse, type VariableDef, type TemplateCreateData, } from '@/lib/templatesApi' interface TemplateEditorProps { tenantId: string template?: TemplateResponse | null onSave: (data: TemplateCreateData) => Promise onCancel: () => void } const VARIABLE_TYPES = ['string', 'ip', 'integer', 'boolean', 'subnet'] as const export function TemplateEditor({ template, onSave, onCancel }: TemplateEditorProps) { const [name, setName] = useState(template?.name ?? '') const [description, setDescription] = useState(template?.description ?? '') const [content, setContent] = useState(template?.content ?? '') const [tags, setTags] = useState(template?.tags ?? []) const [tagInput, setTagInput] = useState('') const [variables, setVariables] = useState(template?.variables ?? []) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const addTag = () => { const t = tagInput.trim() if (t && !tags.includes(t)) { setTags([...tags, t]) setTagInput('') } } const removeTag = (tag: string) => { setTags(tags.filter((t) => t !== tag)) } const detectVariables = () => { // Simple regex-based detection of {{ variable }} patterns const regex = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g const found = new Set() let match while ((match = regex.exec(content)) !== null) { const varName = match[1] // Skip 'device' built-in and its properties if (varName !== 'device') { found.add(varName) } } // Also detect dot-access variables like {{ device.hostname }} // These are built-in and we skip them const existingNames = new Set(variables.map((v) => v.name)) const newVars: VariableDef[] = [...variables] for (const name of found) { if (!existingNames.has(name)) { newVars.push({ name, type: 'string', default: null, description: null }) } } setVariables(newVars) } const updateVariable = (index: number, field: keyof VariableDef, value: string | null) => { setVariables( variables.map((v, i) => i === index ? { ...v, [field]: value } : v, ), ) } const removeVariable = (index: number) => { setVariables(variables.filter((_, i) => i !== index)) } const addVariable = () => { setVariables([...variables, { name: '', type: 'string', default: null, description: null }]) } const handleSave = async () => { if (!name.trim()) { setError('Template name is required') return } if (!content.trim()) { setError('Template content is required') return } setSaving(true) setError(null) try { await onSave({ name: name.trim(), description: description.trim() || undefined, content, variables: variables.filter((v) => v.name.trim()), tags, }) } catch (err) { setError(err instanceof Error ? err.message : 'Save failed') } finally { setSaving(false) } } return (
{/* Header */}

{template ? 'Edit Template' : 'Create Template'}

{error && (
{error}
)} {/* Name */}
setName(e.target.value)} placeholder="e.g., Basic Firewall Setup" className="bg-elevated/50 border-border" />
{/* Description */}