fix: created feature view for automation page instead of using workflow pages, updated to use pop-up component instead of hard-code
This commit is contained in:
parent
b13e6f105c
commit
bc94e52904
10 changed files with 456 additions and 766 deletions
|
|
@ -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() {
|
|||
<Route path="workflows">
|
||||
<Route path="playground" element={<PlaygroundPage />} />
|
||||
<Route path="list" element={<WorkflowsPage />} />
|
||||
<Route path="automations" element={<AutomationsPage />} />
|
||||
<Route path="automations" element={<AutomationDefinitionsView />} />
|
||||
</Route>
|
||||
|
||||
{/* ============================================== */}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,8 +178,13 @@ const SharepointFolderPicker: React.FC<SharepointFolderPickerProps> = ({ connect
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className={styles.sharepointFolderBrowser}>
|
||||
<Popup
|
||||
isOpen={isExpanded}
|
||||
title="SharePoint Ordner auswählen"
|
||||
onClose={() => setIsExpanded(false)}
|
||||
size="large"
|
||||
closable={true}
|
||||
>
|
||||
{error && (
|
||||
<div className={styles.sharepointError}>
|
||||
<FaExclamationTriangle /> {error}
|
||||
|
|
@ -268,8 +274,7 @@ const SharepointFolderPicker: React.FC<SharepointFolderPickerProps> = ({ connect
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Popup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -715,27 +720,47 @@ export const AutomationEditor: React.FC<AutomationEditorProps> = ({
|
|||
const isFormSaving = saving || isSaving;
|
||||
|
||||
return (
|
||||
<div className={styles.editorOverlay} onClick={onCancel}>
|
||||
<div className={styles.editorContainer} onClick={e => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
<header className={styles.editorHeader}>
|
||||
<div className={styles.headerLeft}>
|
||||
<h2 className={styles.editorTitle}>{editorTitle}</h2>
|
||||
<span className={`${styles.modeBadge} ${styles[mode]}`}>
|
||||
{mode === 'template' ? (
|
||||
<><FaFileAlt style={{ marginRight: '0.375rem' }} /> Template</>
|
||||
) : (
|
||||
<><FaRocket style={{ marginRight: '0.375rem' }} /> Definition</>
|
||||
)}
|
||||
<Popup
|
||||
isOpen={true}
|
||||
title={`${editorTitle} · ${mode === 'template' ? 'Template' : 'Definition'}`}
|
||||
onClose={onCancel}
|
||||
size="fullscreen"
|
||||
closable={true}
|
||||
className={styles.editorPopup}
|
||||
footerContent={
|
||||
<div className={styles.editorFooter}>
|
||||
<div className={styles.footerLeft}>
|
||||
{jsonError && (
|
||||
<span style={{ color: 'var(--danger-color)', fontSize: '0.875rem' }}>
|
||||
<FaExclamationTriangle style={{ marginRight: '0.375rem' }} />
|
||||
JSON enthält Fehler
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button className={styles.closeButton} onClick={onCancel} title="Schliessen">
|
||||
<FaTimes />
|
||||
<div className={styles.footerRight}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={onCancel}
|
||||
disabled={isFormSaving}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={handleSave}
|
||||
disabled={isFormSaving || !!jsonError}
|
||||
>
|
||||
{isFormSaving ? (
|
||||
<>Speichern...</>
|
||||
) : (
|
||||
<><FaSave style={{ marginRight: '0.375rem' }} /> Speichern</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.editorContainer}>
|
||||
{/* Content */}
|
||||
<div className={styles.editorContent}>
|
||||
{/* Form Panel (Left) */}
|
||||
|
|
@ -1036,40 +1061,8 @@ export const AutomationEditor: React.FC<AutomationEditorProps> = ({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className={styles.editorFooter}>
|
||||
<div className={styles.footerLeft}>
|
||||
{jsonError && (
|
||||
<span style={{ color: 'var(--danger-color)', fontSize: '0.875rem' }}>
|
||||
<FaExclamationTriangle style={{ marginRight: '0.375rem' }} />
|
||||
JSON enthält Fehler
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.footerRight}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={onCancel}
|
||||
disabled={isFormSaving}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={handleSave}
|
||||
disabled={isFormSaving || !!jsonError}
|
||||
>
|
||||
{isFormSaving ? (
|
||||
<>Speichern...</>
|
||||
) : (
|
||||
<><FaSave style={{ marginRight: '0.375rem' }} /> Speichern</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, Record<string, ViewComponent>> = {
|
|||
workflows: WorkflowsPage,
|
||||
},
|
||||
automation: {
|
||||
definitions: AutomationsPage,
|
||||
templates: AutomationTemplatesPage,
|
||||
logs: () => <PlaceholderView title="Execution Logs" description="Automatisierungs-Ausführungsprotokolle" />,
|
||||
definitions: AutomationDefinitionsView,
|
||||
templates: AutomationTemplatesView,
|
||||
logs: AutomationLogsView,
|
||||
},
|
||||
codeeditor: {
|
||||
editor: CodeEditorPage,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
/**
|
||||
* AutomationsPage
|
||||
* AutomationDefinitionsView
|
||||
*
|
||||
* Page for viewing and managing workflow automations using FormGeneratorTable.
|
||||
* 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,
|
||||
|
|
@ -48,7 +42,6 @@ export const AutomationsPage: React.FC = () => {
|
|||
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<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;
|
||||
|
|
@ -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<NodeJS.Timeout | null>(null);
|
||||
const lastLogIdRef = useRef<string | null>(null);
|
||||
const logContainerRef = useRef<HTMLDivElement>(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<Automation> = {
|
||||
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<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,
|
||||
};
|
||||
|
||||
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
|
||||
: ((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)) {
|
||||
|
|
@ -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<Automation> = {
|
||||
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<string, string> = {};
|
||||
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<string, string> = {};
|
||||
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 <FaCheck className={styles.successIcon} />;
|
||||
case 'completed': return <FaCheck className={styles.successIcon} />;
|
||||
case 'error':
|
||||
case 'failed':
|
||||
return <FaExclamationCircle className={styles.errorIcon} />;
|
||||
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;
|
||||
case 'starting': return <FaSpinner className={`${styles.spinningIcon} spinning`} />;
|
||||
case 'stopped': return <FaStop className={styles.warningIcon} />;
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -541,9 +402,7 @@ export const AutomationsPage: React.FC = () => {
|
|||
<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>
|
||||
<button className={styles.secondaryButton} onClick={() => refetch()}><FaSync /> Erneut versuchen</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -557,26 +416,15 @@ export const AutomationsPage: React.FC = () => {
|
|||
<p className={styles.pageSubtitle}>Geplante und automatisierte Workflows</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => refetch()}
|
||||
disabled={loading}
|
||||
>
|
||||
<button className={styles.secondaryButton} onClick={() => refetch()} disabled={loading}>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||||
</button>
|
||||
{canCreate && (
|
||||
<>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={handleLoadTemplates}
|
||||
disabled={loadingTemplates}
|
||||
>
|
||||
<button className={styles.secondaryButton} onClick={handleLoadTemplates} disabled={loadingTemplates}>
|
||||
<FaFileAlt /> {loadingTemplates ? 'Lädt...' : 'Aus Vorlage'}
|
||||
</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={handleCreateClick}
|
||||
>
|
||||
<button className={styles.primaryButton} onClick={handleCreateClick}>
|
||||
<FaPlus /> Neue Automatisierung
|
||||
</button>
|
||||
</>
|
||||
|
|
@ -594,23 +442,11 @@ export const AutomationsPage: React.FC = () => {
|
|||
<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>
|
||||
<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>
|
||||
<button className={styles.secondaryButton} onClick={handleLoadTemplates}><FaFileAlt /> Aus Vorlage erstellen</button>
|
||||
<button className={styles.primaryButton} onClick={handleCreateClick}><FaPlus /> Manuell erstellen</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -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: <FaRocket />,
|
||||
onClick: handleExecute,
|
||||
title: 'Ausführen',
|
||||
loading: (row: any) => executingAutomations.has(row.id),
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
icon: <FaList />,
|
||||
onClick: handleShowLogs,
|
||||
title: 'Ausführungsverlauf',
|
||||
},
|
||||
{ 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,
|
||||
}}
|
||||
hookData={{ refetch, permissions, pagination, handleDelete: handleAutomationDelete, handleInlineUpdate, updateOptimistically }}
|
||||
emptyMessage="Keine Automatisierungen gefunden"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Automation Editor */}
|
||||
{showEditor && editingAutomation && (
|
||||
<AutomationEditor
|
||||
mode="definition"
|
||||
|
|
@ -683,85 +488,46 @@ export const AutomationsPage: React.FC = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
<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
|
||||
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 (
|
||||
<div key={template.id || index} className={styles.templateItem}>
|
||||
<div className={styles.templateHeader}>
|
||||
<h4 className={styles.templateTitle}>{labelText}</h4>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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}>
|
||||
|
|
@ -772,83 +538,49 @@ export const AutomationsPage: React.FC = () => {
|
|||
{executionModal.status === 'stopped' && 'Gestoppt'}
|
||||
{executionModal.status === 'error' && 'Fehler'}
|
||||
</span>
|
||||
{executionModal.workflowId && (
|
||||
<span className={styles.workflowId}>
|
||||
Workflow: <code>{executionModal.workflowId}</code>
|
||||
</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' }}
|
||||
>
|
||||
<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} ${log.status === 'error' || log.status === 'failed' ? styles.logEntryError : ''}`}>
|
||||
<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>
|
||||
)}
|
||||
{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>
|
||||
{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>
|
||||
<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.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>
|
||||
)}
|
||||
<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>
|
||||
))}
|
||||
{log.messages.map((msg, msgIndex) => <div key={msgIndex} className={styles.logHistoryMessage}>{msg}</div>)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -857,12 +589,7 @@ export const AutomationsPage: React.FC = () => {
|
|||
)}
|
||||
</div>
|
||||
<div className={styles.modalFooter}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => setLogsModal({ visible: false, automation: null })}
|
||||
>
|
||||
Schliessen
|
||||
</button>
|
||||
<button className={styles.secondaryButton} onClick={() => setLogsModal({ visible: false, automation: null })}>Schliessen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -870,5 +597,3 @@ export const AutomationsPage: React.FC = () => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutomationsPage;
|
||||
14
src/pages/views/automation/AutomationLogsView.tsx
Normal file
14
src/pages/views/automation/AutomationLogsView.tsx
Normal file
|
|
@ -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 = () => (
|
||||
<div className={styles.placeholder}>
|
||||
<h2>Execution Logs</h2>
|
||||
<p>Automatisierungs-Ausführungsprotokolle</p>
|
||||
</div>
|
||||
);
|
||||
196
src/pages/views/automation/AutomationTemplatesView.tsx
Normal file
196
src/pages/views/automation/AutomationTemplatesView.tsx
Normal file
|
|
@ -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<AutomationTemplate | null>(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 ? <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: '0.75rem', padding: '0.125rem 0.5rem', borderRadius: 10, background: 'var(--info-color, #3182ce)', color: '#fff' }}><FaLock style={{ fontSize: '0.625rem' }} /> System</span>
|
||||
: <span style={{ fontSize: '0.75rem', padding: '0.125rem 0.5rem', borderRadius: 10, background: 'var(--success-color, #38a169)', color: '#fff' }}>Instanz</span>
|
||||
},
|
||||
{ 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<AutomationTemplate>) => {
|
||||
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<boolean> => {
|
||||
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 (
|
||||
<div className={styles.adminPage}>
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>Fehler beim Laden der Vorlagen: {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}>Automation-Vorlagen</h1>
|
||||
<p className={styles.pageSubtitle}>Verwalten Sie Ihre Workflow-Vorlagen</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button className={styles.secondaryButton} onClick={() => refetch()} disabled={loading}>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||||
</button>
|
||||
{canCreate && (
|
||||
<button className={styles.primaryButton} onClick={handleCreateClick}>
|
||||
<FaPlus /> Neue Vorlage
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.tableContainer}>
|
||||
{loading && (!templates || templates.length === 0) ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Vorlagen...</span>
|
||||
</div>
|
||||
) : !templates || templates.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaFileAlt className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Keine Vorlagen vorhanden</h3>
|
||||
<p className={styles.emptyDescription}>Erstellen Sie eine neue Vorlage für Ihre Workflows.</p>
|
||||
{canCreate && (
|
||||
<button className={styles.primaryButton} onClick={handleCreateClick}>
|
||||
<FaPlus /> Vorlage erstellen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorTable
|
||||
data={templates as any[]}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/automation-templates"
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
selectable={false}
|
||||
actionButtons={[
|
||||
{ type: 'copy' as const, title: 'Duplizieren', onAction: handleDuplicate },
|
||||
{ type: 'edit' as const, onAction: handleEditClick, title: 'Bearbeiten', disabled: (row: any) => 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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showEditor && (
|
||||
<AutomationEditor
|
||||
mode="template"
|
||||
initialData={editingTemplate}
|
||||
onSave={handleEditorSave}
|
||||
onCancel={handleEditorCancel}
|
||||
saving={saving}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
7
src/pages/views/automation/index.ts
Normal file
7
src/pages/views/automation/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Automation Views Export
|
||||
*/
|
||||
|
||||
export { AutomationDefinitionsView } from './AutomationDefinitionsView';
|
||||
export { AutomationTemplatesView } from './AutomationTemplatesView';
|
||||
export { AutomationLogsView } from './AutomationLogsView';
|
||||
|
|
@ -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<AutomationTemplate | null>(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 ? <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: '0.75rem', padding: '0.125rem 0.5rem', borderRadius: 10, background: 'var(--info-color, #3182ce)', color: '#fff' }}><FaLock style={{ fontSize: '0.625rem' }} /> System</span>
|
||||
: <span style={{ fontSize: '0.75rem', padding: '0.125rem 0.5rem', borderRadius: 10, background: 'var(--success-color, #38a169)', color: '#fff' }}>Instanz</span>
|
||||
},
|
||||
{ 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<AutomationTemplate>) => {
|
||||
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<boolean> => {
|
||||
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 (
|
||||
<div className={styles.adminPage}>
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>Fehler beim Laden der Vorlagen: {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}>Automation-Vorlagen</h1>
|
||||
<p className={styles.pageSubtitle}>Verwalten Sie Ihre Workflow-Vorlagen</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => refetch()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||||
</button>
|
||||
{canCreate && (
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={handleCreateClick}
|
||||
>
|
||||
<FaPlus /> Neue Vorlage
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.tableContainer}>
|
||||
{loading && (!templates || templates.length === 0) ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Vorlagen...</span>
|
||||
</div>
|
||||
) : !templates || templates.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaFileAlt className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Keine Vorlagen vorhanden</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Erstellen Sie eine neue Vorlage für Ihre Workflows.
|
||||
</p>
|
||||
{canCreate && (
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={handleCreateClick}
|
||||
>
|
||||
<FaPlus /> Vorlage erstellen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorTable
|
||||
data={templates as any[]}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/automation-templates"
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
selectable={false}
|
||||
actionButtons={[
|
||||
{
|
||||
type: 'copy' as const,
|
||||
title: 'Duplizieren',
|
||||
onAction: handleDuplicate,
|
||||
},
|
||||
{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: 'Bearbeiten',
|
||||
disabled: (row: any) => 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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Automation Editor */}
|
||||
{showEditor && (
|
||||
<AutomationEditor
|
||||
mode="template"
|
||||
initialData={editingTemplate}
|
||||
onSave={handleEditorSave}
|
||||
onCancel={handleEditorCancel}
|
||||
saving={saving}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutomationTemplatesPage;
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
export { PlaygroundPage } from './PlaygroundPage';
|
||||
export { WorkflowsPage } from './WorkflowsPage';
|
||||
export { AutomationsPage } from './AutomationsPage';
|
||||
export { AutomationTemplatesPage } from './AutomationTemplatesPage';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue