diff --git a/src/App.tsx b/src/App.tsx index 87a11d2..7a058cd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,7 +40,8 @@ import StorePage from './pages/Store'; import { FeatureViewPage } from './pages/FeatureView'; import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage, AdminLogsPage } from './pages/admin'; import { AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin/wizards'; -import { PlaygroundPage, WorkflowsPage, AutomationsPage } from './pages/workflows'; +import { PlaygroundPage, WorkflowsPage } from './pages/workflows'; +import { AutomationDefinitionsView } from './pages/views/automation'; import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata'; import { BillingDataView, BillingAdmin } from './pages/billing'; function App() { @@ -106,7 +107,7 @@ function App() { } /> } /> - } /> + } /> {/* ============================================== */} diff --git a/src/components/AutomationEditor/AutomationEditor.module.css b/src/components/AutomationEditor/AutomationEditor.module.css index 1edfbba..9143aaa 100644 --- a/src/components/AutomationEditor/AutomationEditor.module.css +++ b/src/components/AutomationEditor/AutomationEditor.module.css @@ -4,6 +4,7 @@ * Full-screen editor with form on left and actions panel on right */ +/* Used when AutomationEditor had custom overlay - kept for reference, Popup is used now */ .editorOverlay { position: fixed; top: 0; @@ -18,16 +19,22 @@ padding: 1rem; } +/* Popup customisation for fullscreen editor - fill content area */ +.editorPopup :global([class*="content"]) { + padding: 0; + display: flex; + flex-direction: column; + min-height: 0; +} + .editorContainer { background: var(--surface-color, #ffffff); border-radius: 12px; width: 100%; - max-width: 1400px; - height: 90vh; - max-height: 900px; + flex: 1; + min-height: 0; display: flex; flex-direction: column; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); overflow: hidden; } diff --git a/src/components/AutomationEditor/AutomationEditor.tsx b/src/components/AutomationEditor/AutomationEditor.tsx index 255e404..56524d9 100644 --- a/src/components/AutomationEditor/AutomationEditor.tsx +++ b/src/components/AutomationEditor/AutomationEditor.tsx @@ -11,7 +11,8 @@ */ import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'; -import { FaTimes, FaSave, FaChevronLeft, FaChevronRight, FaRocket, FaFileAlt, FaCode, FaExclamationTriangle, FaMagic, FaFolder, FaFolderOpen, FaArrowUp, FaSpinner } from 'react-icons/fa'; +import { FaSave, FaChevronLeft, FaChevronRight, FaCode, FaExclamationTriangle, FaMagic, FaFolder, FaFolderOpen, FaArrowUp, FaSpinner } from 'react-icons/fa'; +import { Popup } from '../UiComponents/Popup'; import { ActionsPanel } from '../ActionsPanel'; import { ProviderMultiSelect } from '../ProviderSelector'; import { useToast } from '../../contexts/ToastContext'; @@ -177,99 +178,103 @@ const SharepointFolderPicker: React.FC = ({ connect - {isExpanded && ( -
- {error && ( -
- {error} -
- )} - - {/* Site Selection */} -
- - {isLoadingSites ? ( -
- Lade Sites... -
- ) : ( - - )} + setIsExpanded(false)} + size="large" + closable={true} + > + {error && ( +
+ {error}
- - {/* Folder Browser */} - {selectedSite && ( -
-
- {selectedSite.siteName} - {currentPath && / {currentPath}} -
- -
- {currentPath && ( -
- .. -
- )} - - {isLoadingFolders ? ( -
- Lade Ordner... -
- ) : folderOptions.length === 0 ? ( -
- Keine Unterordner -
- ) : ( - folderOptions.map(folder => ( -
- - handleFolderNavigate(folder)} - > - {folder.folderName} - - -
- )) - )} -
- - + )} + + {/* Site Selection */} +
+ + {isLoadingSites ? ( +
+ Lade Sites...
+ ) : ( + )}
- )} + + {/* Folder Browser */} + {selectedSite && ( +
+
+ {selectedSite.siteName} + {currentPath && / {currentPath}} +
+ +
+ {currentPath && ( +
+ .. +
+ )} + + {isLoadingFolders ? ( +
+ Lade Ordner... +
+ ) : folderOptions.length === 0 ? ( +
+ Keine Unterordner +
+ ) : ( + folderOptions.map(folder => ( +
+ + handleFolderNavigate(folder)} + > + {folder.folderName} + + +
+ )) + )} +
+ + +
+ )} +
); }; @@ -715,27 +720,47 @@ export const AutomationEditor: React.FC = ({ const isFormSaving = saving || isSaving; return ( -
-
e.stopPropagation()}> - {/* Header */} -
-
-

{editorTitle}

- - {mode === 'template' ? ( - <> Template - ) : ( - <> Definition - )} - + +
+ {jsonError && ( + + + JSON enthält Fehler + + )}
-
- +
-
- +
+ } + > +
{/* Content */}
{/* Form Panel (Left) */} @@ -1036,40 +1061,8 @@ export const AutomationEditor: React.FC = ({ )}
- - {/* Footer */} -
-
- {jsonError && ( - - - JSON enthält Fehler - - )} -
-
- - -
-
-
+ ); }; diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx index b11384f..c0a29bc 100644 --- a/src/pages/FeatureView.tsx +++ b/src/pages/FeatureView.tsx @@ -30,8 +30,8 @@ import { RealEstatePekView, RealEstateInstanceRolesPlaceholder } from './views/r // Chat Playground Views (reusing existing workflow pages) import { PlaygroundPage, WorkflowsPage } from './workflows'; -// Automation Views (reusing existing workflow pages) -import { AutomationsPage, AutomationTemplatesPage } from './workflows'; +// Automation Views +import { AutomationDefinitionsView, AutomationTemplatesView, AutomationLogsView } from './views/automation'; // CodeEditor Views import { CodeEditorPage, CodeEditorWorkflowsPage } from './views/codeeditor'; @@ -129,9 +129,9 @@ const VIEW_COMPONENTS: Record> = { workflows: WorkflowsPage, }, automation: { - definitions: AutomationsPage, - templates: AutomationTemplatesPage, - logs: () => , + definitions: AutomationDefinitionsView, + templates: AutomationTemplatesView, + logs: AutomationLogsView, }, codeeditor: { editor: CodeEditorPage, diff --git a/src/pages/workflows/AutomationsPage.tsx b/src/pages/views/automation/AutomationDefinitionsView.tsx similarity index 58% rename from src/pages/workflows/AutomationsPage.tsx rename to src/pages/views/automation/AutomationDefinitionsView.tsx index 2784328..3a4d492 100644 --- a/src/pages/workflows/AutomationsPage.tsx +++ b/src/pages/views/automation/AutomationDefinitionsView.tsx @@ -1,21 +1,19 @@ /** - * AutomationsPage - * - * Page for viewing and managing workflow automations using FormGeneratorTable. + * AutomationDefinitionsView + * + * View for viewing and managing workflow automation definitions. * Includes template selection, execution modal with live logs, and execution history. */ import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; -import { useAutomations, useAutomationOperations, AutomationTemplate, Automation } from '../../hooks/useAutomations'; -import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; -import { AutomationEditor } from '../../components/AutomationEditor'; +import { useAutomations, useAutomationOperations, AutomationTemplate, Automation } from '../../../hooks/useAutomations'; +import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable'; +import { AutomationEditor } from '../../../components/AutomationEditor'; import { FaSync, FaRobot, FaRocket, FaPlus, FaFileAlt, FaStop, FaList, FaTimes, FaCheck, FaExclamationCircle, FaSpinner } from 'react-icons/fa'; -import { useToast } from '../../contexts/ToastContext'; -import { useApiRequest } from '../../hooks/useApi'; -import { useFeatureStore } from '../../stores/featureStore'; -import styles from '../admin/Admin.module.css'; - - +import { useToast } from '../../../contexts/ToastContext'; +import { useApiRequest } from '../../../hooks/useApi'; +import { useFeatureStore } from '../../../stores/featureStore'; +import styles from '../../admin/Admin.module.css'; interface WorkflowLog { id: string; @@ -25,17 +23,13 @@ interface WorkflowLog { progress?: number; } -export const AutomationsPage: React.FC = () => { - // Get mandate and feature instance from store (first chatbot instance or first available) +export const AutomationDefinitionsView: React.FC = () => { const { getAllInstances } = useFeatureStore(); const instances = getAllInstances(); - - // Find first chatbot instance, or fall back to first available instance const chatbotInstance = instances.find(i => i.featureCode === 'chatbot') || instances[0]; const mandateId = chatbotInstance?.mandateId; const featureInstanceId = chatbotInstance?.id; - - // Data hook + const { data: automations, attributes, @@ -47,8 +41,7 @@ export const AutomationsPage: React.FC = () => { fetchAutomationById, updateOptimistically, } = useAutomations(); - - // Operations hook + const { handleAutomationCreate, handleAutomationUpdate, @@ -63,17 +56,12 @@ export const AutomationsPage: React.FC = () => { const { showSuccess, showError, showInfo } = useToast(); const { request } = useApiRequest(); - // Editor states const [showEditor, setShowEditor] = useState(false); const [editingAutomation, setEditingAutomation] = useState(null); const [editorSaving, setEditorSaving] = useState(false); - - // Template selection states const [showTemplateModal, setShowTemplateModal] = useState(false); const [templates, setTemplates] = useState([]); const [loadingTemplates, setLoadingTemplates] = useState(false); - - // Execution modal state const [executionModal, setExecutionModal] = useState<{ visible: boolean; automationId: string | null; @@ -89,8 +77,6 @@ export const AutomationsPage: React.FC = () => { status: 'starting', logs: [], }); - - // Logs modal state const [logsModal, setLogsModal] = useState<{ visible: boolean; automation: Automation | null; @@ -99,41 +85,24 @@ export const AutomationsPage: React.FC = () => { automation: null, }); - // Refs for polling const pollIntervalRef = useRef(null); const lastLogIdRef = useRef(null); const logContainerRef = useRef(null); - // Initial fetch - useEffect(() => { - refetch(); + useEffect(() => { refetch(); }, []); + useEffect(() => () => { + if (pollIntervalRef.current) clearInterval(pollIntervalRef.current); }, []); - - // Cleanup polling on unmount useEffect(() => { - return () => { - if (pollIntervalRef.current) { - clearInterval(pollIntervalRef.current); - } - }; - }, []); - - // Auto-scroll logs - useEffect(() => { - if (logContainerRef.current) { - logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; - } + if (logContainerRef.current) logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; }, [executionModal.logs]); - // Generate columns from attributes - exclude internal fields, add enriched display columns const columns = useMemo(() => { const hiddenColumns = [ 'id', 'mandateId', 'featureInstanceId', '_createdBy', '_createdAt', '_modifiedAt', 'template', 'executionLogs', 'placeholders', - // Hide enriched fields from attribute list (added manually below) '_createdByUserName', 'mandateName', 'featureInstanceName', ]; - const attrColumns = (attributes || []) .filter(attr => !hiddenColumns.includes(attr.name)) .map(attr => ({ @@ -147,63 +116,45 @@ export const AutomationsPage: React.FC = () => { minWidth: attr.minWidth || 100, maxWidth: attr.maxWidth || 400, })); - - // Add enriched display columns (from backend enrichment) const enrichedColumns = [ { key: 'mandateName', label: 'Mandant', type: 'text' as any, sortable: true, filterable: true, searchable: true, width: 150, minWidth: 100, maxWidth: 250 }, { key: 'featureInstanceName', label: 'Feature-Instanz', type: 'text' as any, sortable: true, filterable: true, searchable: true, width: 160, minWidth: 100, maxWidth: 250 }, { key: '_createdByUserName', label: 'Erstellt von', type: 'text' as any, sortable: true, filterable: true, searchable: true, width: 150, minWidth: 100, maxWidth: 250 }, ]; - return [...attrColumns, ...enrichedColumns]; }, [attributes]); - // Check permissions const canCreate = permissions?.create !== 'n'; const canUpdate = permissions?.update !== 'n'; const canDelete = permissions?.delete !== 'n'; - // Handle edit click - open editor with automation data const handleEditClick = async (automation: Automation) => { const fullAutomation = await fetchAutomationById(automation.id); setEditingAutomation(fullAutomation as Automation || automation); setShowEditor(true); }; - // Handle create click - open editor for new automation const handleCreateClick = () => { - // Pre-fill with context - const newAutomation: Partial = { - mandateId: mandateId, - featureInstanceId: featureInstanceId, + setEditingAutomation({ + mandateId: mandateId!, + featureInstanceId: featureInstanceId!, label: '', schedule: '0 22 * * *', active: false, placeholders: {}, - }; - setEditingAutomation(newAutomation as Automation); + } as Automation); setShowEditor(true); }; - // Handle editor save const handleEditorSave = async (data: Partial) => { - // Validate context if (!mandateId || !featureInstanceId) { showError('Fehler: Kein aktiver Mandant oder Feature-Instanz gefunden'); return; } - setEditorSaving(true); try { - // Add required context fields - const saveData = { - ...data, - mandateId: mandateId, - featureInstanceId: featureInstanceId, - }; - + const saveData = { ...data, mandateId, featureInstanceId }; if (editingAutomation?.id) { - // Update existing - include id in payload for backend validation saveData.id = editingAutomation.id; const success = await handleAutomationUpdate(editingAutomation.id, saveData as any); if (success) { @@ -213,7 +164,6 @@ export const AutomationsPage: React.FC = () => { await refetch(); } } else { - // Create new const result = await handleAutomationCreate(saveData as any); if (result) { showSuccess('Automatisierung erstellt'); @@ -229,13 +179,11 @@ export const AutomationsPage: React.FC = () => { } }; - // Handle editor cancel const handleEditorCancel = () => { setShowEditor(false); setEditingAutomation(null); }; - // Handle delete single automation (confirmation handled by DeleteActionButton) const handleDelete = async (automation: Automation) => { const success = await handleAutomationDelete(automation.id); if (success) { @@ -244,7 +192,6 @@ export const AutomationsPage: React.FC = () => { } }; - // Handle duplicate automation const handleDuplicate = async (automation: Automation) => { try { await request({ url: `/api/automations/${automation.id}/duplicate`, method: 'post' }); @@ -255,7 +202,6 @@ export const AutomationsPage: React.FC = () => { } }; - // Load templates const handleLoadTemplates = async () => { setLoadingTemplates(true); try { @@ -273,33 +219,22 @@ export const AutomationsPage: React.FC = () => { } }; - // Handle template selection - open editor with template data pre-filled const handleTemplateSelect = (template: AutomationTemplate) => { setShowTemplateModal(false); - - // Validate context - mandateId and featureInstanceId are required if (!mandateId || !featureInstanceId) { showError('Fehler: Kein aktiver Mandant oder Feature-Instanz gefunden'); return; } - - // Get label from template (can be multilingual) let templateLabel = 'Neue Automatisierung'; if (template.label) { - if (typeof template.label === 'string') { - templateLabel = template.label; - } else if (typeof template.label === 'object') { - // TextMultilingual - use German or English - templateLabel = (template.label as any).de || (template.label as any).en || 'Neue Automatisierung'; - } + templateLabel = typeof template.label === 'string' + ? template.label + : (template.label as any).de || (template.label as any).en || 'Neue Automatisierung'; } else if (template.overview) { - // TextMultilingual - use German or English - templateLabel = typeof template.overview === 'string' - ? template.overview + templateLabel = typeof template.overview === 'string' + ? template.overview : ((template.overview as any).de || (template.overview as any).en || 'Neue Automatisierung'); } - - // Convert placeholder values to strings (backend expects Dict[str, str]) const convertedPlaceholders: Record = {}; const templateParams = (template as any).parameters || {}; for (const [key, value] of Object.entries(templateParams)) { @@ -311,95 +246,64 @@ export const AutomationsPage: React.FC = () => { convertedPlaceholders[key] = String(value); } } - - // Pre-fill form with template data and open editor for user to customize const prefillData: Partial = { - mandateId: mandateId, - featureInstanceId: featureInstanceId, + mandateId, + featureInstanceId, label: templateLabel, - template: typeof template.template === 'string' - ? template.template - : JSON.stringify(template.template, null, 2), + template: typeof template.template === 'string' ? template.template : JSON.stringify(template.template, null, 2), placeholders: convertedPlaceholders, active: false, schedule: '0 22 * * *', }; - - // Open editor with pre-filled data (no id = create mode) setEditingAutomation(prefillData as Automation); setShowEditor(true); }; - // Poll workflow logs const pollWorkflowLogs = useCallback(async (workflowId: string) => { try { - // Include mandate context header for RBAC (featureInstanceId is not needed for workflows) const contextHeaders: Record = {}; if (mandateId) contextHeaders['X-Mandate-Id'] = mandateId; - const response = await request({ url: `/api/workflows/${workflowId}/logs`, method: 'get', params: lastLogIdRef.current ? { afterId: lastLogIdRef.current } : {}, additionalConfig: { headers: contextHeaders }, }); - const logs: WorkflowLog[] = response?.items || response || []; - if (logs.length > 0) { setExecutionModal(prev => { - // Deduplicate logs by ID const existingIds = new Set(prev.logs.map(l => l.id)); const newLogs = logs.filter(l => !existingIds.has(l.id)); - - return { - ...prev, - logs: [...prev.logs, ...newLogs], - }; + return { ...prev, logs: [...prev.logs, ...newLogs] }; }); lastLogIdRef.current = logs[logs.length - 1].id; } - - // Check workflow status const statusResponse = await request({ url: `/api/workflows/${workflowId}`, method: 'get', additionalConfig: { headers: contextHeaders }, }); - const workflowStatus = statusResponse?.status; - if (workflowStatus === 'completed' || workflowStatus === 'stopped' || workflowStatus === 'error' || workflowStatus === 'failed') { - // Stop polling if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } - setExecutionModal(prev => ({ ...prev, - status: workflowStatus === 'completed' ? 'completed' : - workflowStatus === 'error' || workflowStatus === 'failed' ? 'error' : 'stopped', + status: workflowStatus === 'completed' ? 'completed' : workflowStatus === 'error' || workflowStatus === 'failed' ? 'error' : 'stopped', })); - - if (workflowStatus === 'completed') { - showSuccess('Automatisierung erfolgreich abgeschlossen'); - } else if (workflowStatus === 'error' || workflowStatus === 'failed') { - showError('Automatisierung fehlgeschlagen'); - } else { - showInfo('Automatisierung gestoppt'); - } - + if (workflowStatus === 'completed') showSuccess('Automatisierung erfolgreich abgeschlossen'); + else if (workflowStatus === 'error' || workflowStatus === 'failed') showError('Automatisierung fehlgeschlagen'); + else showInfo('Automatisierung gestoppt'); refetch(); } } catch (err) { console.error('Error polling workflow logs:', err); } - }, [request, refetch, showSuccess, showError, showInfo, mandateId, featureInstanceId]); + }, [request, refetch, showSuccess, showError, showInfo, mandateId]); - // Handle execute automation with modal const handleExecute = async (automation: Automation) => { - // Reset and show modal lastLogIdRef.current = null; setExecutionModal({ visible: true, @@ -407,78 +311,49 @@ export const AutomationsPage: React.FC = () => { automationLabel: automation.label, workflowId: null, status: 'starting', - logs: [{ - id: 'init', - timestamp: Date.now() / 1000, - message: 'Automatisierung wird gestartet...', - }], + logs: [{ id: 'init', timestamp: Date.now() / 1000, message: 'Automatisierung wird gestartet...' }], }); - try { const result = await handleAutomationExecute(automation.id); const workflowId = result?.id; - if (workflowId) { setExecutionModal(prev => ({ ...prev, workflowId, status: 'running', - logs: [...prev.logs, { - id: 'started', - timestamp: Date.now() / 1000, - message: `Workflow ${workflowId} gestartet`, - status: 'running', - }], + logs: [...prev.logs, { id: 'started', timestamp: Date.now() / 1000, message: `Workflow ${workflowId} gestartet`, status: 'running' }], })); - - // Start polling - pollIntervalRef.current = setInterval(() => { - pollWorkflowLogs(workflowId); - }, 2000); + pollIntervalRef.current = setInterval(() => pollWorkflowLogs(workflowId), 2000); } } catch (err: any) { setExecutionModal(prev => ({ ...prev, status: 'error', - logs: [...prev.logs, { - id: 'error', - timestamp: Date.now() / 1000, - message: `Fehler: ${err.message || 'Unbekannter Fehler'}`, - status: 'error', - }], + logs: [...prev.logs, { id: 'error', timestamp: Date.now() / 1000, message: `Fehler: ${err.message || 'Unbekannter Fehler'}`, status: 'error' }], })); showError(`Fehler beim Ausführen: ${err.message}`); } }; - // Handle stop workflow const handleStopWorkflow = async () => { if (!executionModal.workflowId) return; - try { const stopHeaders: Record = {}; if (mandateId) stopHeaders['X-Mandate-Id'] = mandateId; - await request({ url: `/api/workflows/${executionModal.workflowId}/stop`, method: 'post', additionalConfig: { headers: stopHeaders }, }); - setExecutionModal(prev => ({ ...prev, - logs: [...prev.logs, { - id: 'stopping', - timestamp: Date.now() / 1000, - message: 'Workflow wird gestoppt...', - }], + logs: [...prev.logs, { id: 'stopping', timestamp: Date.now() / 1000, message: 'Workflow wird gestoppt...' }], })); } catch (err: any) { showError(`Fehler beim Stoppen: ${err.message}`); } }; - // Close execution modal const closeExecutionModal = () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); @@ -494,44 +369,30 @@ export const AutomationsPage: React.FC = () => { }); }; - // Show logs modal const handleShowLogs = async (automation: Automation) => { const fullAutomation = await fetchAutomationById(automation.id); - setLogsModal({ - visible: true, - automation: fullAutomation as Automation || automation, - }); + setLogsModal({ visible: true, automation: fullAutomation as Automation || automation }); }; - // Format timestamp const formatTimestamp = (timestamp: number) => { if (!timestamp) return ''; - const date = new Date(timestamp * 1000); - return date.toLocaleString('de-DE'); + return new Date(timestamp * 1000).toLocaleString('de-DE'); }; - // Format time only const formatTime = (timestamp: number) => { if (!timestamp) return ''; - const date = new Date(timestamp * 1000); - return date.toLocaleTimeString('de-DE'); + return new Date(timestamp * 1000).toLocaleTimeString('de-DE'); }; - // Get status icon const getStatusIcon = (status: string) => { switch (status) { - case 'completed': - return ; + case 'completed': return ; case 'error': - case 'failed': - return ; + case 'failed': return ; case 'running': - case 'starting': - return ; - case 'stopped': - return ; - default: - return null; + case 'starting': return ; + case 'stopped': return ; + default: return null; } }; @@ -541,9 +402,7 @@ export const AutomationsPage: React.FC = () => {
⚠️

Fehler beim Laden der Automatisierungen: {error}

- +
); @@ -557,26 +416,15 @@ export const AutomationsPage: React.FC = () => {

Geplante und automatisierte Workflows

- {canCreate && ( <> - - @@ -594,23 +442,11 @@ export const AutomationsPage: React.FC = () => {

Keine Automatisierungen vorhanden

-

- Erstellen Sie eine neue Automatisierung, um Workflows zeitgesteuert auszuführen. -

+

Erstellen Sie eine neue Automatisierung, um Workflows zeitgesteuert auszuführen.

{canCreate && (
- - + +
)}
@@ -627,52 +463,21 @@ export const AutomationsPage: React.FC = () => { sortable={true} selectable={false} actionButtons={[ - ...(canCreate ? [{ - type: 'copy' as const, - title: 'Duplizieren', - onAction: handleDuplicate, - }] : []), - ...(canUpdate ? [{ - type: 'edit' as const, - onAction: handleEditClick, - title: 'Bearbeiten', - }] : []), - ...(canDelete ? [{ - type: 'delete' as const, - title: 'Löschen', - loading: (row: any) => deletingAutomations.has(row.id), - }] : []), + ...(canCreate ? [{ type: 'copy' as const, title: 'Duplizieren', onAction: handleDuplicate }] : []), + ...(canUpdate ? [{ type: 'edit' as const, onAction: handleEditClick, title: 'Bearbeiten' }] : []), + ...(canDelete ? [{ type: 'delete' as const, title: 'Löschen', loading: (row: any) => deletingAutomations.has(row.id) }] : []), ]} customActions={[ - { - id: 'execute', - icon: , - onClick: handleExecute, - title: 'Ausführen', - loading: (row: any) => executingAutomations.has(row.id), - }, - { - id: 'logs', - icon: , - onClick: handleShowLogs, - title: 'Ausführungsverlauf', - }, + { id: 'execute', icon: , onClick: handleExecute, title: 'Ausführen', loading: (row: any) => executingAutomations.has(row.id) }, + { id: 'logs', icon: , onClick: handleShowLogs, title: 'Ausführungsverlauf' }, ]} onDelete={handleDelete} - hookData={{ - refetch, - permissions, - pagination, - handleDelete: handleAutomationDelete, - handleInlineUpdate, - updateOptimistically, - }} + hookData={{ refetch, permissions, pagination, handleDelete: handleAutomationDelete, handleInlineUpdate, updateOptimistically }} emptyMessage="Keine Automatisierungen gefunden" /> )}
- {/* Automation Editor */} {showEditor && editingAutomation && ( { /> )} - {/* Template Selection Modal */} {showTemplateModal && (
setShowTemplateModal(false)}>
e.stopPropagation()}>

Vorlage auswählen

- +
{templates.map((template, index) => { - // Get label from TextMultilingual - const labelText = template.label - ? (typeof template.label === 'string' - ? template.label - : (template.label as any).de || (template.label as any).en || `Vorlage ${index + 1}`) - : `Vorlage ${index + 1}`; - - // Get overview from TextMultilingual - const overviewText = template.overview - ? (typeof template.overview === 'string' - ? template.overview - : (template.overview as any).de || (template.overview as any).en || '') - : ''; - - // Try to parse template JSON for additional info + const labelText = template.label ? (typeof template.label === 'string' ? template.label : (template.label as any).de || (template.label as any).en || `Vorlage ${index + 1}`) : `Vorlage ${index + 1}`; + const overviewText = template.overview ? (typeof template.overview === 'string' ? template.overview : (template.overview as any).de || (template.overview as any).en || '') : ''; let parsedTemplate: any = null; try { - if (template.template) { - parsedTemplate = typeof template.template === 'string' - ? JSON.parse(template.template) - : template.template; - } - } catch { /* ignore parse errors */ } - - const description = overviewText - || parsedTemplate?.overview - || parsedTemplate?.tasks?.[0]?.objective - || 'Keine Beschreibung'; - + if (template.template) parsedTemplate = typeof template.template === 'string' ? JSON.parse(template.template) : template.template; + } catch { /* ignore */ } + const description = overviewText || parsedTemplate?.overview || parsedTemplate?.tasks?.[0]?.objective || 'Keine Beschreibung'; return (
-
-

{labelText}

-
+

{labelText}

{description}

- +
); })}
- +
)} - {/* Execution Modal */} {executionModal.visible && (
e.stopPropagation()} style={{ maxWidth: '700px' }}>
-

- {getStatusIcon(executionModal.status)} Ausführung: {executionModal.automationLabel} -

- +

{getStatusIcon(executionModal.status)} Ausführung: {executionModal.automationLabel}

+
@@ -772,83 +538,49 @@ export const AutomationsPage: React.FC = () => { {executionModal.status === 'stopped' && 'Gestoppt'} {executionModal.status === 'error' && 'Fehler'} - {executionModal.workflowId && ( - - Workflow: {executionModal.workflowId} - - )} + {executionModal.workflowId && Workflow: {executionModal.workflowId}}
-
+
{executionModal.logs.map((log, index) => (
[{formatTime(log.timestamp)}] {log.status && {log.status}:} {log.message} - {log.progress !== undefined && log.progress !== null && log.progress < 1 && ( - ({Math.round(log.progress * 100)}%) - )} + {log.progress !== undefined && log.progress !== null && log.progress < 1 && ({Math.round(log.progress * 100)}%)}
))}
- {executionModal.status === 'running' && ( - - )} - + {executionModal.status === 'running' && } +
)} - {/* Logs History Modal */} {logsModal.visible && logsModal.automation && (
setLogsModal({ visible: false, automation: null })}>
e.stopPropagation()} style={{ maxWidth: '700px' }}>
-

- Ausführungsverlauf: {logsModal.automation.label} -

- +

Ausführungsverlauf: {logsModal.automation.label}

+
{(!logsModal.automation.executionLogs || logsModal.automation.executionLogs.length === 0) ? ( -
-

Keine Ausführungen vorhanden

-
+

Keine Ausführungen vorhanden

) : (
{[...logsModal.automation.executionLogs].reverse().map((log, index) => (
{formatTimestamp(log.timestamp)} - - {log.status || 'Unbekannt'} - - {log.workflowId && ( - - Workflow: {log.workflowId} - - )} + {log.status || 'Unbekannt'} + {log.workflowId && Workflow: {log.workflowId}}
{log.messages && log.messages.length > 0 && (
- {log.messages.map((msg, msgIndex) => ( -
{msg}
- ))} + {log.messages.map((msg, msgIndex) =>
{msg}
)}
)}
@@ -857,12 +589,7 @@ export const AutomationsPage: React.FC = () => { )}
- +
@@ -870,5 +597,3 @@ export const AutomationsPage: React.FC = () => {
); }; - -export default AutomationsPage; diff --git a/src/pages/views/automation/AutomationLogsView.tsx b/src/pages/views/automation/AutomationLogsView.tsx new file mode 100644 index 0000000..355fae8 --- /dev/null +++ b/src/pages/views/automation/AutomationLogsView.tsx @@ -0,0 +1,14 @@ +/** + * AutomationLogsView + * + * Placeholder view for automation execution logs. + */ +import React from 'react'; +import styles from '../../FeatureView.module.css'; + +export const AutomationLogsView: React.FC = () => ( +
+

Execution Logs

+

Automatisierungs-Ausführungsprotokolle

+
+); diff --git a/src/pages/views/automation/AutomationTemplatesView.tsx b/src/pages/views/automation/AutomationTemplatesView.tsx new file mode 100644 index 0000000..5aa4cf4 --- /dev/null +++ b/src/pages/views/automation/AutomationTemplatesView.tsx @@ -0,0 +1,196 @@ +/** + * AutomationTemplatesView + * + * View for managing automation templates (CRUD). + * System templates (isSystem=true) are read-only for non-SysAdmin, with duplicate option. + * Instance templates can be managed by instance admins/editors. + */ + +import React, { useState, useMemo, useEffect } from 'react'; +import { useAutomationTemplates, type AutomationTemplate } from '../../../hooks/useAutomations'; +import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable'; +import { AutomationEditor } from '../../../components/AutomationEditor'; +import { FaSync, FaPlus, FaFileAlt, FaLock } from 'react-icons/fa'; +import { useToast } from '../../../contexts/ToastContext'; +import { useCurrentUser } from '../../../hooks/useUsers'; +import styles from '../../admin/Admin.module.css'; + +export const AutomationTemplatesView: React.FC = () => { + const { + templates, + attributes, + loading, + error, + permissions, + refetch, + createTemplate, + updateTemplate, + deleteTemplate, + duplicateTemplate, + getTemplate, + } = useAutomationTemplates(); + const { user: currentUser } = useCurrentUser(); + const isSysAdmin = currentUser?.isSysAdmin || false; + const { showSuccess, showError } = useToast(); + + const [showEditor, setShowEditor] = useState(false); + const [editingTemplate, setEditingTemplate] = useState(null); + const [saving, setSaving] = useState(false); + + useEffect(() => { refetch(); }, []); + + const canCreate = permissions?.create !== 'n'; + const canUpdate = permissions?.update !== 'n'; + const canDelete = permissions?.delete !== 'n'; + + const columns = useMemo(() => [ + { key: 'label', label: 'Label', type: 'string' as const, sortable: true, searchable: true, width: 200 }, + { key: 'overview', label: 'Beschreibung', type: 'string' as const, width: 300 }, + { key: 'isSystem', label: 'Typ', type: 'boolean' as const, width: 100, formatter: (value: any) => + value ? System + : Instanz + }, + { key: '_createdByUserName', label: 'Erstellt von', type: 'string' as const, width: 150 }, + ], []); + + const handleEditClick = async (template: AutomationTemplate) => { + const fullTemplate = await getTemplate(template.id); + setEditingTemplate(fullTemplate || template); + setShowEditor(true); + }; + + const handleCreateClick = () => { + setEditingTemplate(null); + setShowEditor(true); + }; + + const handleEditorSave = async (data: Partial) => { + setSaving(true); + try { + if (editingTemplate) { + await updateTemplate(editingTemplate.id, data); + showSuccess('Vorlage aktualisiert'); + } else { + await createTemplate(data as any); + showSuccess('Vorlage erstellt'); + } + setShowEditor(false); + setEditingTemplate(null); + await refetch(); + } catch (err: any) { + showError(`Fehler: ${err.message}`); + } finally { + setSaving(false); + } + }; + + const handleEditorCancel = () => { + setShowEditor(false); + setEditingTemplate(null); + }; + + const handleDelete = async (templateId: string): Promise => { + try { + await deleteTemplate(templateId); + showSuccess('Vorlage gelöscht'); + return true; + } catch (err: any) { + showError(`Fehler: ${err.message}`); + return false; + } + }; + + const handleDuplicate = async (template: AutomationTemplate) => { + try { + await duplicateTemplate(template.id); + showSuccess('Vorlage dupliziert'); + await refetch(); + } catch (err: any) { + showError(`Fehler beim Duplizieren: ${err.message}`); + } + }; + + if (error) { + return ( +
+
+ ⚠️ +

Fehler beim Laden der Vorlagen: {error}

+ +
+
+ ); + } + + return ( +
+
+
+

Automation-Vorlagen

+

Verwalten Sie Ihre Workflow-Vorlagen

+
+
+ + {canCreate && ( + + )} +
+
+ +
+ {loading && (!templates || templates.length === 0) ? ( +
+
+ Lade Vorlagen... +
+ ) : !templates || templates.length === 0 ? ( +
+ +

Keine Vorlagen vorhanden

+

Erstellen Sie eine neue Vorlage für Ihre Workflows.

+ {canCreate && ( + + )} +
+ ) : ( + row.isSystem && !isSysAdmin ? { disabled: true, message: 'System-Vorlagen können nur vom SysAdmin bearbeitet werden' } : !canUpdate ? { disabled: true, message: 'Keine Berechtigung' } : false }, + { type: 'delete' as const, title: 'Löschen', disabled: (row: any) => row.isSystem && !isSysAdmin ? { disabled: true, message: 'System-Vorlagen können nur vom SysAdmin gelöscht werden' } : !canDelete ? { disabled: true, message: 'Keine Berechtigung' } : false }, + ]} + onDelete={(template) => handleDelete(template.id)} + hookData={{ refetch, handleDelete, attributes }} + emptyMessage="Keine Vorlagen gefunden" + /> + )} +
+ + {showEditor && ( + + )} +
+ ); +}; diff --git a/src/pages/views/automation/index.ts b/src/pages/views/automation/index.ts new file mode 100644 index 0000000..855bc6e --- /dev/null +++ b/src/pages/views/automation/index.ts @@ -0,0 +1,7 @@ +/** + * Automation Views Export + */ + +export { AutomationDefinitionsView } from './AutomationDefinitionsView'; +export { AutomationTemplatesView } from './AutomationTemplatesView'; +export { AutomationLogsView } from './AutomationLogsView'; diff --git a/src/pages/workflows/AutomationTemplatesPage.tsx b/src/pages/workflows/AutomationTemplatesPage.tsx deleted file mode 100644 index d199d1d..0000000 --- a/src/pages/workflows/AutomationTemplatesPage.tsx +++ /dev/null @@ -1,252 +0,0 @@ -/** - * AutomationTemplatesPage - * - * Page for managing automation templates (CRUD). - * System templates (isSystem=true) are read-only for non-SysAdmin, with duplicate option. - * Instance templates can be managed by instance admins/editors. - */ - -import React, { useState, useMemo, useEffect } from 'react'; -import { useAutomationTemplates, type AutomationTemplate } from '../../hooks/useAutomations'; -import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; -import { AutomationEditor } from '../../components/AutomationEditor'; -import { FaSync, FaPlus, FaFileAlt, FaLock } from 'react-icons/fa'; -import { useToast } from '../../contexts/ToastContext'; -import { useCurrentUser } from '../../hooks/useUsers'; -import styles from '../admin/Admin.module.css'; - -export const AutomationTemplatesPage: React.FC = () => { - const { - templates, - attributes, - loading, - error, - permissions, - refetch, - createTemplate, - updateTemplate, - deleteTemplate, - duplicateTemplate, - getTemplate, - } = useAutomationTemplates(); - const { user: currentUser } = useCurrentUser(); - const isSysAdmin = currentUser?.isSysAdmin || false; - - const { showSuccess, showError } = useToast(); - - // Editor states - const [showEditor, setShowEditor] = useState(false); - const [editingTemplate, setEditingTemplate] = useState(null); - const [saving, setSaving] = useState(false); - - // Initial fetch - useEffect(() => { - refetch(); - }, []); - - // Check permissions - const canCreate = permissions?.create !== 'n'; - const canUpdate = permissions?.update !== 'n'; - const canDelete = permissions?.delete !== 'n'; - - // Table columns - FormGeneratorTable auto-renders TextMultilingual in user language - const columns = useMemo(() => [ - { key: 'label', label: 'Label', type: 'string' as const, sortable: true, searchable: true, width: 200 }, - { key: 'overview', label: 'Beschreibung', type: 'string' as const, width: 300 }, - { key: 'isSystem', label: 'Typ', type: 'boolean' as const, width: 100, formatter: (value: any) => - value ? System - : Instanz - }, - { key: '_createdByUserName', label: 'Erstellt von', type: 'string' as const, width: 150 }, - ], []); - - // Handle edit click - open editor with template data - const handleEditClick = async (template: AutomationTemplate) => { - // Fetch full template data - const fullTemplate = await getTemplate(template.id); - setEditingTemplate(fullTemplate || template); - setShowEditor(true); - }; - - // Handle create click - open editor for new template - const handleCreateClick = () => { - setEditingTemplate(null); - setShowEditor(true); - }; - - // Handle editor save - const handleEditorSave = async (data: Partial) => { - setSaving(true); - try { - if (editingTemplate) { - await updateTemplate(editingTemplate.id, data); - showSuccess('Vorlage aktualisiert'); - } else { - await createTemplate(data as any); - showSuccess('Vorlage erstellt'); - } - setShowEditor(false); - setEditingTemplate(null); - await refetch(); - } catch (err: any) { - showError(`Fehler: ${err.message}`); - } finally { - setSaving(false); - } - }; - - // Handle editor cancel - const handleEditorCancel = () => { - setShowEditor(false); - setEditingTemplate(null); - }; - - // Handle delete by ID (used by DeleteActionButton via hookData) - const handleDelete = async (templateId: string): Promise => { - try { - await deleteTemplate(templateId); - showSuccess('Vorlage gelöscht'); - return true; - } catch (err: any) { - showError(`Fehler: ${err.message}`); - return false; - } - }; - - // Handle duplicate - const handleDuplicate = async (template: AutomationTemplate) => { - try { - await duplicateTemplate(template.id); - showSuccess('Vorlage dupliziert'); - await refetch(); - } catch (err: any) { - showError(`Fehler beim Duplizieren: ${err.message}`); - } - }; - - if (error) { - return ( -
-
- ⚠️ -

Fehler beim Laden der Vorlagen: {error}

- -
-
- ); - } - - return ( -
-
-
-

Automation-Vorlagen

-

Verwalten Sie Ihre Workflow-Vorlagen

-
-
- - {canCreate && ( - - )} -
-
- -
- {loading && (!templates || templates.length === 0) ? ( -
-
- Lade Vorlagen... -
- ) : !templates || templates.length === 0 ? ( -
- -

Keine Vorlagen vorhanden

-

- Erstellen Sie eine neue Vorlage für Ihre Workflows. -

- {canCreate && ( - - )} -
- ) : ( - row.isSystem && !isSysAdmin - ? { disabled: true, message: 'System-Vorlagen können nur vom SysAdmin bearbeitet werden' } - : !canUpdate - ? { disabled: true, message: 'Keine Berechtigung' } - : false, - }, - { - type: 'delete' as const, - title: 'Löschen', - disabled: (row: any) => row.isSystem && !isSysAdmin - ? { disabled: true, message: 'System-Vorlagen können nur vom SysAdmin gelöscht werden' } - : !canDelete - ? { disabled: true, message: 'Keine Berechtigung' } - : false, - }, - ]} - onDelete={(template) => handleDelete(template.id)} - hookData={{ - refetch, - handleDelete, - attributes, - }} - emptyMessage="Keine Vorlagen gefunden" - /> - )} -
- - {/* Automation Editor */} - {showEditor && ( - - )} -
- ); -}; - -export default AutomationTemplatesPage; diff --git a/src/pages/workflows/index.ts b/src/pages/workflows/index.ts index 2c60e26..43bc009 100644 --- a/src/pages/workflows/index.ts +++ b/src/pages/workflows/index.ts @@ -1,4 +1,3 @@ export { PlaygroundPage } from './PlaygroundPage'; export { WorkflowsPage } from './WorkflowsPage'; -export { AutomationsPage } from './AutomationsPage'; -export { AutomationTemplatesPage } from './AutomationTemplatesPage'; +