848 lines
30 KiB
TypeScript
848 lines
30 KiB
TypeScript
/**
|
||
* AutomationsPage
|
||
*
|
||
* Page for viewing and managing workflow automations using FormGeneratorTable.
|
||
* 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 { 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';
|
||
|
||
|
||
|
||
interface WorkflowLog {
|
||
id: string;
|
||
timestamp: number;
|
||
message: string;
|
||
status?: string;
|
||
progress?: number;
|
||
}
|
||
|
||
export const AutomationsPage: React.FC = () => {
|
||
// Get mandate and feature instance from store (first chatbot instance or first available)
|
||
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,
|
||
permissions,
|
||
pagination,
|
||
loading,
|
||
error,
|
||
refetch,
|
||
fetchAutomationById,
|
||
updateOptimistically,
|
||
} = useAutomations();
|
||
|
||
// Operations hook
|
||
const {
|
||
handleAutomationCreate,
|
||
handleAutomationUpdate,
|
||
handleAutomationDelete,
|
||
handleAutomationExecute,
|
||
handleInlineUpdate,
|
||
fetchTemplates,
|
||
deletingAutomations,
|
||
executingAutomations,
|
||
} = useAutomationOperations();
|
||
|
||
const { showSuccess, showError, showInfo } = useToast();
|
||
const { request } = useApiRequest();
|
||
|
||
// Editor states
|
||
const [showEditor, setShowEditor] = useState(false);
|
||
const [editingAutomation, setEditingAutomation] = useState<Automation | null>(null);
|
||
const [editorSaving, setEditorSaving] = useState(false);
|
||
|
||
// Template selection states
|
||
const [showTemplateModal, setShowTemplateModal] = useState(false);
|
||
const [templates, setTemplates] = useState<AutomationTemplate[]>([]);
|
||
const [loadingTemplates, setLoadingTemplates] = useState(false);
|
||
|
||
// Execution modal state
|
||
const [executionModal, setExecutionModal] = useState<{
|
||
visible: boolean;
|
||
automationId: string | null;
|
||
automationLabel: string;
|
||
workflowId: string | null;
|
||
status: 'starting' | 'running' | 'completed' | 'stopped' | 'error';
|
||
logs: WorkflowLog[];
|
||
}>({
|
||
visible: false,
|
||
automationId: null,
|
||
automationLabel: '',
|
||
workflowId: null,
|
||
status: 'starting',
|
||
logs: [],
|
||
});
|
||
|
||
// Logs modal state
|
||
const [logsModal, setLogsModal] = useState<{
|
||
visible: boolean;
|
||
automation: Automation | null;
|
||
}>({
|
||
visible: false,
|
||
automation: null,
|
||
});
|
||
|
||
// Refs for polling
|
||
const pollIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||
const lastLogIdRef = useRef<string | null>(null);
|
||
const logContainerRef = useRef<HTMLDivElement>(null);
|
||
|
||
// Initial fetch
|
||
useEffect(() => {
|
||
refetch();
|
||
}, []);
|
||
|
||
// 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;
|
||
}
|
||
}, [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 => ({
|
||
key: attr.name,
|
||
label: attr.label || attr.name,
|
||
type: attr.type as any,
|
||
sortable: attr.sortable !== false,
|
||
filterable: attr.filterable !== false,
|
||
searchable: attr.searchable !== false,
|
||
width: attr.width || 150,
|
||
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<Automation> = {
|
||
mandateId: mandateId,
|
||
featureInstanceId: featureInstanceId,
|
||
label: '',
|
||
schedule: '0 22 * * *',
|
||
active: false,
|
||
placeholders: {},
|
||
};
|
||
setEditingAutomation(newAutomation as Automation);
|
||
setShowEditor(true);
|
||
};
|
||
|
||
// Handle editor save
|
||
const handleEditorSave = async (data: Partial<Automation>) => {
|
||
// 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,
|
||
};
|
||
|
||
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) {
|
||
showSuccess('Automatisierung aktualisiert');
|
||
setShowEditor(false);
|
||
setEditingAutomation(null);
|
||
await refetch();
|
||
}
|
||
} else {
|
||
// Create new
|
||
const result = await handleAutomationCreate(saveData as any);
|
||
if (result) {
|
||
showSuccess('Automatisierung erstellt');
|
||
setShowEditor(false);
|
||
setEditingAutomation(null);
|
||
await refetch();
|
||
}
|
||
}
|
||
} catch (err: any) {
|
||
showError(`Fehler: ${err.message}`);
|
||
} finally {
|
||
setEditorSaving(false);
|
||
}
|
||
};
|
||
|
||
// 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) {
|
||
showSuccess('Automatisierung gelöscht');
|
||
await refetch();
|
||
}
|
||
};
|
||
|
||
// Load templates
|
||
const handleLoadTemplates = async () => {
|
||
setLoadingTemplates(true);
|
||
try {
|
||
const loadedTemplates = await fetchTemplates();
|
||
setTemplates(loadedTemplates);
|
||
if (loadedTemplates.length === 0) {
|
||
showInfo('Keine Vorlagen verfügbar');
|
||
} else {
|
||
setShowTemplateModal(true);
|
||
}
|
||
} catch (err) {
|
||
showError('Fehler beim Laden der Vorlagen');
|
||
} finally {
|
||
setLoadingTemplates(false);
|
||
}
|
||
};
|
||
|
||
// 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';
|
||
}
|
||
} else if (template.overview) {
|
||
// TextMultilingual - use German or English
|
||
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<string, string> = {};
|
||
const templateParams = (template as any).parameters || {};
|
||
for (const [key, value] of Object.entries(templateParams)) {
|
||
if (value === null || value === undefined) {
|
||
convertedPlaceholders[key] = '';
|
||
} else if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
|
||
convertedPlaceholders[key] = JSON.stringify(value);
|
||
} else {
|
||
convertedPlaceholders[key] = String(value);
|
||
}
|
||
}
|
||
|
||
// Pre-fill form with template data and open editor for user to customize
|
||
const prefillData: Partial<Automation> = {
|
||
mandateId: mandateId,
|
||
featureInstanceId: featureInstanceId,
|
||
label: templateLabel,
|
||
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 {
|
||
const response = await request({
|
||
url: `/api/workflows/${workflowId}/logs`,
|
||
method: 'get',
|
||
params: lastLogIdRef.current ? { afterId: lastLogIdRef.current } : {},
|
||
});
|
||
|
||
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],
|
||
};
|
||
});
|
||
lastLogIdRef.current = logs[logs.length - 1].id;
|
||
}
|
||
|
||
// Check workflow status
|
||
const statusResponse = await request({
|
||
url: `/api/workflows/${workflowId}`,
|
||
method: 'get',
|
||
});
|
||
|
||
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',
|
||
}));
|
||
|
||
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]);
|
||
|
||
// Handle execute automation with modal
|
||
const handleExecute = async (automation: Automation) => {
|
||
// Reset and show modal
|
||
lastLogIdRef.current = null;
|
||
setExecutionModal({
|
||
visible: true,
|
||
automationId: automation.id,
|
||
automationLabel: automation.label,
|
||
workflowId: null,
|
||
status: 'starting',
|
||
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',
|
||
}],
|
||
}));
|
||
|
||
// Start polling
|
||
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',
|
||
}],
|
||
}));
|
||
showError(`Fehler beim Ausführen: ${err.message}`);
|
||
}
|
||
};
|
||
|
||
// Handle stop workflow
|
||
const handleStopWorkflow = async () => {
|
||
if (!executionModal.workflowId) return;
|
||
|
||
try {
|
||
await request({
|
||
url: `/api/workflows/${executionModal.workflowId}/stop`,
|
||
method: 'post',
|
||
});
|
||
|
||
setExecutionModal(prev => ({
|
||
...prev,
|
||
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);
|
||
pollIntervalRef.current = null;
|
||
}
|
||
setExecutionModal({
|
||
visible: false,
|
||
automationId: null,
|
||
automationLabel: '',
|
||
workflowId: null,
|
||
status: 'starting',
|
||
logs: [],
|
||
});
|
||
};
|
||
|
||
// Show logs modal
|
||
const handleShowLogs = async (automation: Automation) => {
|
||
const fullAutomation = await fetchAutomationById(automation.id);
|
||
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');
|
||
};
|
||
|
||
// Format time only
|
||
const formatTime = (timestamp: number) => {
|
||
if (!timestamp) return '';
|
||
const date = new Date(timestamp * 1000);
|
||
return date.toLocaleTimeString('de-DE');
|
||
};
|
||
|
||
// Get status icon
|
||
const getStatusIcon = (status: string) => {
|
||
switch (status) {
|
||
case 'completed':
|
||
return <FaCheck className={styles.successIcon} />;
|
||
case 'error':
|
||
case 'failed':
|
||
return <FaExclamationCircle className={styles.errorIcon} />;
|
||
case 'running':
|
||
case 'starting':
|
||
return <FaSpinner className={`${styles.spinningIcon} spinning`} />;
|
||
case 'stopped':
|
||
return <FaStop className={styles.warningIcon} />;
|
||
default:
|
||
return null;
|
||
}
|
||
};
|
||
|
||
if (error) {
|
||
return (
|
||
<div className={styles.adminPage}>
|
||
<div className={styles.errorContainer}>
|
||
<span className={styles.errorIcon}>⚠️</span>
|
||
<p className={styles.errorMessage}>Fehler beim Laden der Automatisierungen: {error}</p>
|
||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
||
<FaSync /> Erneut versuchen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={styles.adminPage}>
|
||
<div className={styles.pageHeader}>
|
||
<div>
|
||
<h1 className={styles.pageTitle}>Automatisierungen</h1>
|
||
<p className={styles.pageSubtitle}>Geplante und automatisierte Workflows</p>
|
||
</div>
|
||
<div className={styles.headerActions}>
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={() => refetch()}
|
||
disabled={loading}
|
||
>
|
||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||
</button>
|
||
{canCreate && (
|
||
<>
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={handleLoadTemplates}
|
||
disabled={loadingTemplates}
|
||
>
|
||
<FaFileAlt /> {loadingTemplates ? 'Lädt...' : 'Aus Vorlage'}
|
||
</button>
|
||
<button
|
||
className={styles.primaryButton}
|
||
onClick={handleCreateClick}
|
||
>
|
||
<FaPlus /> Neue Automatisierung
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className={styles.tableContainer}>
|
||
{loading && (!automations || automations.length === 0) ? (
|
||
<div className={styles.loadingContainer}>
|
||
<div className={styles.spinner} />
|
||
<span>Lade Automatisierungen...</span>
|
||
</div>
|
||
) : !automations || automations.length === 0 ? (
|
||
<div className={styles.emptyState}>
|
||
<FaRobot className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>Keine Automatisierungen vorhanden</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Erstellen Sie eine neue Automatisierung, um Workflows zeitgesteuert auszuführen.
|
||
</p>
|
||
{canCreate && (
|
||
<div className={styles.emptyActions}>
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={handleLoadTemplates}
|
||
>
|
||
<FaFileAlt /> Aus Vorlage erstellen
|
||
</button>
|
||
<button
|
||
className={styles.primaryButton}
|
||
onClick={handleCreateClick}
|
||
>
|
||
<FaPlus /> Manuell erstellen
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<FormGeneratorTable
|
||
data={automations as any[]}
|
||
columns={columns}
|
||
apiEndpoint="/api/automations"
|
||
loading={loading}
|
||
pagination={true}
|
||
pageSize={25}
|
||
searchable={true}
|
||
filterable={true}
|
||
sortable={true}
|
||
selectable={false}
|
||
actionButtons={[
|
||
...(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: <FaRocket />,
|
||
onClick: handleExecute,
|
||
title: 'Ausführen',
|
||
loading: (row: any) => executingAutomations.has(row.id),
|
||
},
|
||
{
|
||
id: 'logs',
|
||
icon: <FaList />,
|
||
onClick: handleShowLogs,
|
||
title: 'Ausführungsverlauf',
|
||
},
|
||
]}
|
||
onDelete={handleDelete}
|
||
hookData={{
|
||
refetch,
|
||
permissions,
|
||
pagination,
|
||
handleDelete: handleAutomationDelete,
|
||
handleInlineUpdate,
|
||
updateOptimistically,
|
||
}}
|
||
emptyMessage="Keine Automatisierungen gefunden"
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
{/* Automation Editor */}
|
||
{showEditor && editingAutomation && (
|
||
<AutomationEditor
|
||
mode="definition"
|
||
initialData={editingAutomation}
|
||
onSave={handleEditorSave}
|
||
onCancel={handleEditorCancel}
|
||
saving={editorSaving}
|
||
/>
|
||
)}
|
||
|
||
{/* Template Selection Modal */}
|
||
{showTemplateModal && (
|
||
<div className={styles.modalOverlay} onClick={() => setShowTemplateModal(false)}>
|
||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||
<div className={styles.modalHeader}>
|
||
<h2 className={styles.modalTitle}>Vorlage auswählen</h2>
|
||
<button className={styles.modalClose} onClick={() => setShowTemplateModal(false)}>
|
||
<FaTimes />
|
||
</button>
|
||
</div>
|
||
<div className={styles.modalContent}>
|
||
<div className={styles.templateList}>
|
||
{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
|
||
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';
|
||
|
||
return (
|
||
<div key={template.id || index} className={styles.templateItem}>
|
||
<div className={styles.templateHeader}>
|
||
<h4 className={styles.templateTitle}>{labelText}</h4>
|
||
</div>
|
||
<p className={styles.templateDescription}>{description}</p>
|
||
<button
|
||
className={styles.primaryButton}
|
||
onClick={() => handleTemplateSelect(template)}
|
||
>
|
||
<FaCheck /> Verwenden
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
<div className={styles.modalFooter}>
|
||
<button className={styles.secondaryButton} onClick={() => setShowTemplateModal(false)}>
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Execution Modal */}
|
||
{executionModal.visible && (
|
||
<div className={styles.modalOverlay} onClick={closeExecutionModal}>
|
||
<div className={styles.modal} onClick={e => e.stopPropagation()} style={{ maxWidth: '700px' }}>
|
||
<div className={styles.modalHeader}>
|
||
<h2 className={styles.modalTitle}>
|
||
{getStatusIcon(executionModal.status)} Ausführung: {executionModal.automationLabel}
|
||
</h2>
|
||
<button className={styles.modalClose} onClick={closeExecutionModal}>
|
||
<FaTimes />
|
||
</button>
|
||
</div>
|
||
<div className={styles.modalContent}>
|
||
<div className={styles.executionStatus}>
|
||
<span className={`${styles.statusBadge} ${styles[executionModal.status]}`}>
|
||
{executionModal.status === 'starting' && 'Wird gestartet...'}
|
||
{executionModal.status === 'running' && 'Läuft...'}
|
||
{executionModal.status === 'completed' && 'Abgeschlossen'}
|
||
{executionModal.status === 'stopped' && 'Gestoppt'}
|
||
{executionModal.status === 'error' && 'Fehler'}
|
||
</span>
|
||
{executionModal.workflowId && (
|
||
<span className={styles.workflowId}>
|
||
Workflow: <code>{executionModal.workflowId}</code>
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div
|
||
ref={logContainerRef}
|
||
className={styles.executionLogs}
|
||
style={{ maxHeight: '400px', overflowY: 'auto', fontFamily: 'monospace', fontSize: '0.875rem' }}
|
||
>
|
||
{executionModal.logs.map((log, index) => (
|
||
<div key={log.id || index} className={styles.logEntry}>
|
||
<span className={styles.logTime}>[{formatTime(log.timestamp)}]</span>
|
||
{log.status && <span className={styles.logStatus}><strong>{log.status}:</strong></span>}
|
||
<span className={styles.logMessage}>{log.message}</span>
|
||
{log.progress !== undefined && log.progress !== null && log.progress < 1 && (
|
||
<span className={styles.logProgress}>({Math.round(log.progress * 100)}%)</span>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className={styles.modalFooter}>
|
||
{executionModal.status === 'running' && (
|
||
<button className={styles.dangerButton} onClick={handleStopWorkflow}>
|
||
<FaStop /> Stoppen
|
||
</button>
|
||
)}
|
||
<button className={styles.secondaryButton} onClick={closeExecutionModal}>
|
||
Schliessen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Logs History Modal */}
|
||
{logsModal.visible && logsModal.automation && (
|
||
<div className={styles.modalOverlay} onClick={() => setLogsModal({ visible: false, automation: null })}>
|
||
<div className={styles.modal} onClick={e => e.stopPropagation()} style={{ maxWidth: '700px' }}>
|
||
<div className={styles.modalHeader}>
|
||
<h2 className={styles.modalTitle}>
|
||
Ausführungsverlauf: {logsModal.automation.label}
|
||
</h2>
|
||
<button
|
||
className={styles.modalClose}
|
||
onClick={() => setLogsModal({ visible: false, automation: null })}
|
||
>
|
||
<FaTimes />
|
||
</button>
|
||
</div>
|
||
<div className={styles.modalContent}>
|
||
{(!logsModal.automation.executionLogs || logsModal.automation.executionLogs.length === 0) ? (
|
||
<div className={styles.emptyState}>
|
||
<p>Keine Ausführungen vorhanden</p>
|
||
</div>
|
||
) : (
|
||
<div className={styles.logsHistory}>
|
||
{[...logsModal.automation.executionLogs].reverse().map((log, index) => (
|
||
<div key={index} className={`${styles.logHistoryItem} ${styles[log.status || 'unknown']}`}>
|
||
<div className={styles.logHistoryHeader}>
|
||
<span className={styles.logHistoryDate}>{formatTimestamp(log.timestamp)}</span>
|
||
<span className={`${styles.statusBadge} ${styles[log.status || 'unknown']}`}>
|
||
{log.status || 'Unbekannt'}
|
||
</span>
|
||
{log.workflowId && (
|
||
<span className={styles.workflowId}>
|
||
Workflow: <code>{log.workflowId}</code>
|
||
</span>
|
||
)}
|
||
</div>
|
||
{log.messages && log.messages.length > 0 && (
|
||
<div className={styles.logHistoryMessages}>
|
||
{log.messages.map((msg, msgIndex) => (
|
||
<div key={msgIndex} className={styles.logHistoryMessage}>{msg}</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className={styles.modalFooter}>
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={() => setLogsModal({ visible: false, automation: null })}
|
||
>
|
||
Schliessen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AutomationsPage;
|