diff --git a/flowsint-app/src/api/template-service.ts b/flowsint-app/src/api/template-service.ts index 148cb17..79e18a1 100644 --- a/flowsint-app/src/api/template-service.ts +++ b/flowsint-app/src/api/template-service.ts @@ -37,6 +37,7 @@ export interface TestTemplateResponse { duration_ms: number status_code?: number url: string + raw_results?: Record } export interface GenerateTemplateResponse { diff --git a/flowsint-app/src/components/templates/index.ts b/flowsint-app/src/components/templates/index.ts index 0b6c14d..f048b6e 100644 --- a/flowsint-app/src/components/templates/index.ts +++ b/flowsint-app/src/components/templates/index.ts @@ -1,6 +1,9 @@ export { YamlEditor } from './yaml-editor' export { JsonViewer } from './json-viewer' export { TemplateEditor } from './template-editor' +export { TemplateEditorHeader } from './template-editor-header' +export { TemplateTestPanel } from './template-test-panel' +export type { TestResult } from './template-test-panel' export { AIChatPanel } from './ai-chat-panel' export type { AIChatPanelHandle } from './ai-chat-panel' export { templateSchema, defaultTemplate } from './template-schema' diff --git a/flowsint-app/src/components/templates/template-editor-header.tsx b/flowsint-app/src/components/templates/template-editor-header.tsx new file mode 100644 index 0000000..27fce78 --- /dev/null +++ b/flowsint-app/src/components/templates/template-editor-header.tsx @@ -0,0 +1,233 @@ +import { + Copy, + Check, + AlertCircle, + Trash2, + ChevronRight, + Loader2, + CheckCircle2, + XCircle, + Keyboard, + Sparkles, + Save +} from 'lucide-react' +import type { editor } from 'monaco-editor' + +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' + +interface TemplateEditorHeaderProps { + isEditMode: boolean + templateName: string + hasChanges: boolean + hasErrors: boolean + totalErrors: number + validationErrors: string[] + editorErrors: editor.IMarkerData[] + isSaving: boolean + copied: boolean + deletePending: boolean + onSave: () => void + onDelete: () => void + onCopy: () => void + onGenerateClick: () => void + onNavigateBack: () => void +} + +export function TemplateEditorHeader({ + isEditMode, + templateName, + hasChanges, + hasErrors, + totalErrors, + validationErrors, + editorErrors, + isSaving, + copied, + deletePending, + onSave, + onDelete, + onCopy, + onGenerateClick, + onNavigateBack +}: TemplateEditorHeaderProps) { + return ( +
+ {/* Left: Breadcrumb + Status */} +
+ + + {isEditMode ? templateName : 'New Template'} + + {isEditMode ? ( + hasChanges ? ( + + Unsaved + + ) : ( + !hasErrors && ( + + + Saved + + ) + ) + ) : ( + + Draft + + )} +
+ + {/* Right: Toolbar */} +
+ {/* Validate (popover with errors) */} + + + + + +
+

+ {hasErrors ? 'Validation Errors' : 'Validation Passed'} +

+
+ {hasErrors ? ( +
+ {validationErrors.map((error, i) => ( +
+ + {error} +
+ ))} + {editorErrors + .filter((e) => e.severity >= 8) + .map((error, i) => ( +
+ + + Line {error.startLineNumber}: {error.message} + +
+ ))} +
+ ) : ( +
+

+ All required fields are present and valid. +

+
+ )} +
+
+ +
+ + {/* Generate */} + + + + + Focus AI assistant + + +
+ + {/* Copy */} + + + + + Copy YAML + + + {/* Save */} + + + + + + + ⌘S + + + + + {/* Delete */} + {isEditMode && ( + <> +
+ + + + + Delete template + + + )} +
+
+ ) +} diff --git a/flowsint-app/src/components/templates/template-editor.tsx b/flowsint-app/src/components/templates/template-editor.tsx index 6e5eb59..4e6ec8c 100644 --- a/flowsint-app/src/components/templates/template-editor.tsx +++ b/flowsint-app/src/components/templates/template-editor.tsx @@ -4,61 +4,31 @@ import { useNavigate } from '@tanstack/react-router' import { parse as parseYaml, stringify as stringifyYaml } from 'yaml' import { toast } from 'sonner' import { - Copy, - Check, - AlertCircle, - Trash2, - ChevronRight, - Play, FileCode2, FlaskConical, - Loader2, CheckCircle2, - XCircle, - Keyboard, - Sparkles, - Save + XCircle } from 'lucide-react' import type { editor } from 'monaco-editor' -import { Button } from '@/components/ui/button' -import { Badge } from '@/components/ui/badge' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { TooltipProvider } from '@/components/ui/tooltip' import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable' import { useConfirm } from '@/components/use-confirm-dialog' import { YamlEditor } from './yaml-editor' -import { JsonViewer } from './json-viewer' import { AIChatPanel, type AIChatPanelHandle } from './ai-chat-panel' import { defaultTemplate, templateSchema, type TemplateData } from './template-schema' +import { TemplateEditorHeader } from './template-editor-header' +import { TemplateTestPanel, type TestResult } from './template-test-panel' import { templateService } from '@/api/template-service' -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - interface TemplateEditorProps { templateId?: string initialContent?: TemplateData importedYaml?: string } -interface TestResult { - success: boolean - data?: unknown - error?: string - duration?: number - url?: string -} - -// --------------------------------------------------------------------------- -// Validation -// --------------------------------------------------------------------------- - function validateTemplate(content: string): { valid: boolean errors: string[] @@ -114,10 +84,6 @@ function validateTemplate(content: string): { return { valid: errors.length === 0, errors, data: parsed } } -// --------------------------------------------------------------------------- -// Main Component -// --------------------------------------------------------------------------- - export function TemplateEditor({ templateId, initialContent, importedYaml }: TemplateEditorProps) { const navigate = useNavigate() const queryClient = useQueryClient() @@ -130,7 +96,6 @@ export function TemplateEditor({ templateId, initialContent, importedYaml }: Tem [initialContent, importedYaml] ) - // ── Editor state ────────────────────────────────────────────────────────── const [content, setContent] = useState(initialYaml) const [savedContent, setSavedContent] = useState(initialYaml) const [copied, setCopied] = useState(false) @@ -138,17 +103,14 @@ export function TemplateEditor({ templateId, initialContent, importedYaml }: Tem const [validationResult, setValidationResult] = useState(() => validateTemplate(initialYaml)) const [activeTab, setActiveTab] = useState<'editor' | 'test'>('editor') - // ── Test state ──────────────────────────────────────────────────────────── const [testInput, setTestInput] = useState('') const [testParams, setTestParams] = useState>({}) const [testResult, setTestResult] = useState(null) const [isTesting, setIsTesting] = useState(false) - // ── Refs ────────────────────────────────────────────────────────────────── const chatRef = useRef(null) const contentBeingSavedRef = useRef(null) - // ── Derived state ───────────────────────────────────────────────────────── const hasChanges = content !== savedContent const hasErrors = !validationResult.valid || editorErrors.some((e) => e.severity >= 8) const templateName = validationResult.data?.name || 'Untitled' @@ -158,8 +120,6 @@ export function TemplateEditor({ templateId, initialContent, importedYaml }: Tem const stateRef = useRef({ hasErrors, hasChanges, data: validationResult.data, content }) stateRef.current = { hasErrors, hasChanges, data: validationResult.data, content } - // ── Mutations ───────────────────────────────────────────────────────────── - const createMutation = useMutation({ mutationFn: (data: TemplateData) => templateService.create({ @@ -209,8 +169,6 @@ export function TemplateEditor({ templateId, initialContent, importedYaml }: Tem const isSaving = createMutation.isPending || updateMutation.isPending - // ── Handlers ────────────────────────────────────────────────────────────── - const handleChange = useCallback((value: string) => { setContent(value) setValidationResult(validateTemplate(value)) @@ -279,10 +237,12 @@ export function TemplateEditor({ templateId, initialContent, importedYaml }: Tem setTestResult({ success: response.success, data: response.data, + raw_results: response.raw_results, error: response.error, duration: response.duration_ms, url: response.url }) + console.log(response) } catch (error) { setTestResult({ success: false, @@ -294,7 +254,7 @@ export function TemplateEditor({ templateId, initialContent, importedYaml }: Tem } }, [isEditMode, templateId, testInput, validationResult.data]) - const buildPreviewUrl = () => { + const buildPreviewUrl = useCallback(() => { if (!validationResult.data?.request?.url) return '' const inputKey = validationResult.data.input?.key || 'value' let url = validationResult.data.request.url.replace( @@ -315,9 +275,7 @@ export function TemplateEditor({ templateId, initialContent, importedYaml }: Tem url += (url.includes('?') ? '&' : '?') + searchParams.toString() } return url - } - - // ── Keyboard shortcuts ──────────────────────────────────────────────────── + }, [validationResult.data, testInput, paramKeys, templateParams, testParams]) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -330,197 +288,34 @@ export function TemplateEditor({ templateId, initialContent, importedYaml }: Tem return () => window.removeEventListener('keydown', handleKeyDown) }, [handleSave]) - // ── Derived ─────────────────────────────────────────────────────────────── - const totalErrors = validationResult.errors.length + editorErrors.filter((e) => e.severity >= 8).length - // ── Render ──────────────────────────────────────────────────────────────── - return (
- {/* ── Header ──────────────────────────────────────────────── */} -
- {/* Left: Breadcrumb + Status */} -
- - - {isEditMode ? templateName : 'New Template'} + { + setActiveTab('editor') + setTimeout(() => chatRef.current?.focusInput(), 50) + }} + onNavigateBack={() => navigate({ to: '/dashboard/enrichers' })} + /> - {isEditMode ? ( - hasChanges ? ( - - Unsaved - - ) : ( - !hasErrors && ( - - - Saved - - ) - ) - ) : ( - - Draft - - )} -
- - {/* Right: Toolbar */} -
- {/* Validate (popover with errors) */} - - - - - -
-

- {hasErrors ? 'Validation Errors' : 'Validation Passed'} -

-
- {hasErrors ? ( -
- {validationResult.errors.map((error, i) => ( -
- - {error} -
- ))} - {editorErrors - .filter((e) => e.severity >= 8) - .map((error, i) => ( -
- - - Line {error.startLineNumber}: {error.message} - -
- ))} -
- ) : ( -
-

- All required fields are present and valid. -

-
- )} -
-
- -
- - {/* Generate */} - - - - - Focus AI assistant - - -
- - {/* Copy */} - - - - - Copy YAML - - - {/* Save */} - - - - - - - ⌘S - - - - - {/* Delete */} - {isEditMode && ( - <> -
- - - - - Delete template - - - )} -
-
- - {/* ── Tab Bar ─────────────────────────────────────────────── */} + {/* Tab Bar */}
- {/* ── Tab Content ─────────────────────────────────────────── */} + {/* Tab Content */}
- {/* ── Editor Tab ───────────────────────────────────────── */} {activeTab === 'editor' && (
- {/* Left: Code Editor */}
@@ -580,198 +373,28 @@ export function TemplateEditor({ templateId, initialContent, importedYaml }: Tem
- - {/* Resize Handle */} - - {/* Right: AI Chat */} - +
)} - - {/* ── Test Tab ─────────────────────────────────────────── */} {activeTab === 'test' && ( -
- {/* Test Input */} -
-
-

Test your template

-

- Enter a test value to simulate the enricher request. -

-
- -
-
- - setTestInput(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleTest()} - /> -
- - {paramKeys.length > 0 && ( -
- -
- - - - - - - - - {paramKeys.map((paramKey, idx) => { - const defaultValue = templateParams[paramKey] ?? '' - return ( - - - - - ) - })} - -
- Key - - Value -
- {paramKey} - - - setTestParams((prev) => ({ - ...prev, - [paramKey]: e.target.value - })) - } - className="h-7 text-xs border-0 bg-transparent focus-visible:ring-1 focus-visible:ring-offset-0" - /> -
-
-
- )} - - {validationResult.data?.request?.url && ( -
- - - {buildPreviewUrl()} - -
- )} - - -
- - {hasErrors && ( -
-

- Fix validation errors before testing. -

-
- )} -
- - {/* Test Result */} -
-
-
-

Response

- {testResult?.duration !== undefined && testResult.duration > 0 && ( - - {testResult.duration}ms - - )} -
- - {!testResult ? ( -
-
- -

Run a test to see the response here.

-
-
- ) : ( -
-
-
- {testResult.success ? ( - <> - - - Request Successful - - - ) : ( - <> - - Request Failed - - )} -
- {testResult.error && ( -

{testResult.error}

- )} -
- {testResult.data != null && ( -
- -
- -
-
- )} -
- )} -
-
-
+ )}
diff --git a/flowsint-app/src/components/templates/template-test-panel.tsx b/flowsint-app/src/components/templates/template-test-panel.tsx new file mode 100644 index 0000000..8896e3b --- /dev/null +++ b/flowsint-app/src/components/templates/template-test-panel.tsx @@ -0,0 +1,241 @@ +import { Play, FlaskConical, Loader2, CheckCircle2, XCircle } from 'lucide-react' + +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable' + +import { JsonViewer } from './json-viewer' +import type { TemplateData } from './template-schema' + +export interface TestResult { + success: boolean + data?: unknown + error?: string + duration?: number + url?: string + raw_results?: Record +} + +interface TemplateTestPanelProps { + testInput: string + testParams: Record + testResult: TestResult | null + isTesting: boolean + hasErrors: boolean + validationData: TemplateData | undefined + paramKeys: string[] + templateParams: Record + onTestInputChange: (value: string) => void + onTestParamsChange: (params: Record) => void + onRunTest: () => void + buildPreviewUrl: () => string +} + +export function TemplateTestPanel({ + testInput, + testParams, + testResult, + isTesting, + hasErrors, + validationData, + paramKeys, + templateParams, + onTestInputChange, + onTestParamsChange, + onRunTest, + buildPreviewUrl +}: TemplateTestPanelProps) { + return ( + + {/* Test Input */} + +
+
+

Test your template

+

+ Enter a test value to simulate the enricher request. +

+
+ +
+
+ + onTestInputChange(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && onRunTest()} + /> +
+ + {paramKeys.length > 0 && ( +
+ +
+ + + + + + + + + {paramKeys.map((paramKey, idx) => { + const defaultValue = templateParams[paramKey] ?? '' + return ( + + + + + ) + })} + +
+ Key + + Value +
+ {paramKey} + + + onTestParamsChange({ + ...testParams, + [paramKey]: e.target.value + }) + } + className="h-7 text-xs border-0 bg-transparent focus-visible:ring-1 focus-visible:ring-offset-0" + /> +
+
+
+ )} + + {validationData?.request?.url && ( +
+ + + {buildPreviewUrl()} + +
+ )} + + +
+ + {hasErrors && ( +
+

Fix validation errors before testing.

+
+ )} +
+
+ + {/* Test Result */} + +
+
+
+ {testResult?.duration !== undefined && testResult.duration > 0 && ( + + {testResult.duration}ms + + )} +
+ + {!testResult ? ( +
+
+ +

Run a test to see the response here.

+
+
+ ) : ( +
+
+
+ {testResult.success ? ( + <> + + Request Successful + + ) : ( + <> + + Request Failed + + )} +
+ {testResult.error && ( +

{testResult.error}

+ )} +
+ {(testResult?.data?.raw_results != null || testResult?.data?.results != null) && ( + + {testResult?.data?.raw_results != null && ( + <> + +
+ +
+ +
+
+
+ {testResult?.data?.results != null && ( + + )} + + )} + {testResult?.data?.results != null && ( + +
+ +
+ +
+
+
+ )} +
+ )} +
+ )} +
+
+
+
+ ) +}