349 lines
11 KiB
TypeScript
349 lines
11 KiB
TypeScript
/**
|
|
* GraphicalEditorWorkflowsPage
|
|
* List of saved workflows with FormGeneratorTable.
|
|
* Shows: label, active, isRunning, stuckAt, createdAt, lastStartedAt, runCount.
|
|
* Filter: Alle | Aktiv | Inaktiv.
|
|
* Actions: Edit, Delete, Aktivieren/Deaktivieren, Ausführen (nur bei manuellem Trigger).
|
|
*/
|
|
|
|
import React, { useState, useCallback, useEffect } from 'react';
|
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
import { FaPlay, FaSync, FaCheck, FaBan, FaPen } from 'react-icons/fa';
|
|
import { usePrompt } from '../../../hooks/usePrompt';
|
|
import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator';
|
|
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
|
import { useApiRequest } from '../../../hooks/useApi';
|
|
import {
|
|
fetchWorkflows,
|
|
deleteWorkflow,
|
|
executeGraph,
|
|
updateWorkflow,
|
|
type Automation2Workflow,
|
|
} from '../../../api/workflowApi';
|
|
import { useToast } from '../../../contexts/ToastContext';
|
|
import { formatUnixTimestamp } from '../../../utils/time';
|
|
import styles from '../../../pages/admin/Admin.module.css';
|
|
|
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
|
|
|
function formatTs(ts?: number): string {
|
|
if (ts == null || ts <= 0) return '—';
|
|
const sec = ts < 1e12 ? ts : ts / 1000;
|
|
const { time } = formatUnixTimestamp(sec, undefined, {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
return time;
|
|
}
|
|
|
|
export const GraphicalEditorWorkflowsPage: React.FC = () => {
|
|
const { t } = useLanguage();
|
|
|
|
const instanceId = useInstanceId();
|
|
const { mandateId } = useParams<{ mandateId: string }>();
|
|
const { request } = useApiRequest();
|
|
const navigate = useNavigate();
|
|
const { showSuccess, showError } = useToast();
|
|
const { prompt: promptInput, PromptDialog } = usePrompt();
|
|
|
|
const [workflows, setWorkflows] = useState<Automation2Workflow[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [executingId, setExecutingId] = useState<string | null>(null);
|
|
const [togglingId, setTogglingId] = useState<string | null>(null);
|
|
const [activeFilter, setActiveFilter] = useState<'all' | 'active' | 'inactive'>('all');
|
|
|
|
const [paginationMeta, setPaginationMeta] = useState<any>(null);
|
|
|
|
const load = useCallback(async (paginationParams?: any) => {
|
|
if (!instanceId) return;
|
|
setLoading(true);
|
|
try {
|
|
const active = activeFilter === 'active' ? true : activeFilter === 'inactive' ? false : undefined;
|
|
const result = await fetchWorkflows(request, instanceId, { active, pagination: paginationParams });
|
|
if (result && typeof result === 'object' && 'items' in result && !Array.isArray(result)) {
|
|
setWorkflows((result as any).items);
|
|
setPaginationMeta((result as any).pagination);
|
|
} else {
|
|
setWorkflows(result as Automation2Workflow[]);
|
|
setPaginationMeta(null);
|
|
}
|
|
} catch (e) {
|
|
console.error('[graphicalEditor] load workflows failed', e);
|
|
showError(t('Fehler beim Laden der Workflows'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [instanceId, request, showError, activeFilter, t]);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [load]);
|
|
|
|
const handleDelete = useCallback(
|
|
async (workflowId: string): Promise<boolean> => {
|
|
if (!instanceId) return false;
|
|
try {
|
|
await deleteWorkflow(request, instanceId, workflowId);
|
|
showSuccess(t('Workflow gelöscht'));
|
|
await load();
|
|
return true;
|
|
} catch (e: any) {
|
|
showError(t('Fehler: {msg}', { msg: e?.message || t('Löschen fehlgeschlagen') }));
|
|
return false;
|
|
}
|
|
},
|
|
[instanceId, request, showSuccess, showError, load, t]
|
|
);
|
|
|
|
const handleEdit = useCallback(
|
|
(row: Automation2Workflow) => {
|
|
if (!mandateId || !instanceId) return;
|
|
navigate(`/mandates/${mandateId}/graphicalEditor/${instanceId}/editor?workflowId=${row.id}`);
|
|
},
|
|
[mandateId, instanceId, navigate]
|
|
);
|
|
|
|
const hasManualTrigger = useCallback((row: Automation2Workflow): boolean => {
|
|
const invs = row.invocations || [];
|
|
return invs.some((i) => i.enabled && (i.kind === 'manual' || i.kind === 'api'));
|
|
}, []);
|
|
|
|
const handleToggleActive = useCallback(
|
|
async (row: Automation2Workflow) => {
|
|
if (!instanceId) return;
|
|
const next = !(row.active !== false);
|
|
setTogglingId(row.id);
|
|
try {
|
|
await updateWorkflow(request, instanceId, row.id, { active: next });
|
|
showSuccess(next ? t('Workflow aktiviert') : t('Workflow deaktiviert'));
|
|
await load();
|
|
} catch (e: any) {
|
|
showError(t('Fehler: {msg}', { msg: e?.message || t('Status-Update fehlgeschlagen') }));
|
|
} finally {
|
|
setTogglingId(null);
|
|
}
|
|
},
|
|
[instanceId, request, showSuccess, showError, load, t]
|
|
);
|
|
|
|
const handleRename = useCallback(
|
|
async (row: Automation2Workflow) => {
|
|
if (!instanceId) return;
|
|
const newLabel = await promptInput(t('Neuer Name:'), {
|
|
title: t('Workflow umbenennen'),
|
|
defaultValue: row.label,
|
|
placeholder: t('Workflow-Name'),
|
|
});
|
|
if (!newLabel || newLabel.trim() === row.label) return;
|
|
try {
|
|
await updateWorkflow(request, instanceId, row.id, { label: newLabel.trim() });
|
|
showSuccess(t('Workflow umbenannt'));
|
|
await load();
|
|
} catch (e: any) {
|
|
showError(t('Fehler: {msg}', { msg: e?.message || t('Umbenennen fehlgeschlagen') }));
|
|
}
|
|
},
|
|
[instanceId, request, promptInput, showSuccess, showError, load, t]
|
|
);
|
|
|
|
const handleExecute = useCallback(
|
|
async (row: Automation2Workflow) => {
|
|
if (!instanceId) return;
|
|
setExecutingId(row.id);
|
|
try {
|
|
const invs = row.invocations || [];
|
|
const primary =
|
|
invs.find((i) => i.enabled && i.kind === 'manual') ||
|
|
invs.find((i) => i.enabled && (i.kind === 'form' || i.kind === 'api'));
|
|
const result = await executeGraph(request, instanceId, row.graph!, row.id, {
|
|
...(primary ? { entryPointId: primary.id } : {}),
|
|
});
|
|
if (result?.success) {
|
|
if (result?.paused) {
|
|
showSuccess(t('Workflow gestartet und bei Human Task pausiert. Öffne Workflows & Tasks.'));
|
|
} else {
|
|
showSuccess(t('Workflow ausgeführt'));
|
|
}
|
|
await load();
|
|
} else {
|
|
showError(result?.error || t('Ausführung fehlgeschlagen'));
|
|
}
|
|
} catch (e: any) {
|
|
showError(t('Fehler: {msg}', { msg: e?.message || t('Ausführung fehlgeschlagen') }));
|
|
} finally {
|
|
setExecutingId(null);
|
|
}
|
|
},
|
|
[instanceId, request, showSuccess, showError, load, t]
|
|
);
|
|
|
|
const columns: ColumnConfig[] = [
|
|
{ key: 'label', label: t('Workflow'), type: 'string', width: 200, sortable: true },
|
|
{
|
|
key: 'active',
|
|
label: t('Aktiv (Spalte)'),
|
|
type: 'boolean',
|
|
width: 80,
|
|
formatter: (value: boolean) =>
|
|
value !== false ? (
|
|
<span style={{ color: 'var(--success-color, #28a745)', fontWeight: 600 }}>Ja</span>
|
|
) : (
|
|
<span style={{ color: 'var(--text-secondary, #666)' }}>Nein</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'isRunning',
|
|
label: t('läuft'),
|
|
type: 'boolean',
|
|
width: 80,
|
|
formatter: (value: boolean) =>
|
|
value ? (
|
|
<span style={{ color: 'var(--success-color, #28a745)', fontWeight: 600 }}>{t('Ja')}</span>
|
|
) : (
|
|
<span style={{ color: 'var(--text-secondary, #666)' }}>Nein</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'stuckAtNodeLabel',
|
|
label: t('steht bei'),
|
|
type: 'string',
|
|
width: 160,
|
|
formatter: (value: string, row: Automation2Workflow) =>
|
|
row.isRunning && (value || row.stuckAtNodeId)
|
|
? value || row.stuckAtNodeId || '—'
|
|
: '—',
|
|
},
|
|
{
|
|
key: 'createdAt',
|
|
label: t('Erstellt'),
|
|
type: 'number',
|
|
width: 140,
|
|
formatter: (v: number) => formatTs(v),
|
|
},
|
|
{
|
|
key: 'lastStartedAt',
|
|
label: t('zuletzt gestartet'),
|
|
type: 'number',
|
|
width: 160,
|
|
formatter: (v: number) => formatTs(v),
|
|
},
|
|
{
|
|
key: 'runCount',
|
|
label: t('Läufe'),
|
|
type: 'number',
|
|
width: 80,
|
|
formatter: (v: number) => (v != null ? String(v) : '0'),
|
|
},
|
|
];
|
|
|
|
const hookData = {
|
|
refetch: load,
|
|
handleDelete: (id: string) => handleDelete(id),
|
|
pagination: paginationMeta,
|
|
};
|
|
|
|
if (!instanceId) {
|
|
return (
|
|
<div className={styles.adminPage}>
|
|
<p>{t('Keine Feature-Instanz gefunden')}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
|
<div className={styles.pageHeader}>
|
|
<div>
|
|
<p className={styles.pageSubtitle}>
|
|
{t('Workflows verwalten, ausführen und bearbeiten')}
|
|
</p>
|
|
</div>
|
|
<div className={styles.headerActions} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
{(['all', 'active', 'inactive'] as const).map((f) => (
|
|
<button
|
|
key={f}
|
|
className={activeFilter === f ? styles.primaryButton : styles.secondaryButton}
|
|
onClick={() => setActiveFilter(f)}
|
|
disabled={loading}
|
|
>
|
|
{f === 'all' ? t('Alle') : f === 'active' ? t('Aktiv') : t('Inaktiv')}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
className={styles.secondaryButton}
|
|
onClick={() => load()}
|
|
disabled={loading}
|
|
>
|
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.tableContainer}>
|
|
<FormGeneratorTable<Automation2Workflow>
|
|
data={workflows}
|
|
columns={columns}
|
|
loading={loading}
|
|
pagination={true}
|
|
pageSize={25}
|
|
searchable={true}
|
|
filterable={true}
|
|
sortable={true}
|
|
selectable={true}
|
|
actionButtons={[
|
|
{
|
|
type: 'edit',
|
|
title: t('bearbeiten'),
|
|
onAction: handleEdit,
|
|
},
|
|
{
|
|
type: 'delete',
|
|
title: t('löschen'),
|
|
},
|
|
]}
|
|
customActions={[
|
|
{
|
|
id: 'rename',
|
|
icon: <FaPen />,
|
|
title: t('umbenennen'),
|
|
onClick: (row) => handleRename(row),
|
|
},
|
|
{
|
|
id: 'activate',
|
|
icon: <FaCheck />,
|
|
title: t('aktivieren'),
|
|
onClick: (row) => handleToggleActive(row),
|
|
loading: (row) => togglingId === row.id,
|
|
visible: (row) => row.active === false,
|
|
},
|
|
{
|
|
id: 'deactivate',
|
|
icon: <FaBan />,
|
|
title: t('deaktivieren'),
|
|
onClick: (row) => handleToggleActive(row),
|
|
loading: (row) => togglingId === row.id,
|
|
visible: (row) => row.active !== false,
|
|
},
|
|
{
|
|
id: 'execute',
|
|
icon: <FaPlay />,
|
|
title: t('ausführen'),
|
|
onClick: (row) => handleExecute(row),
|
|
loading: (row) => executingId === row.id,
|
|
visible: (row) => hasManualTrigger(row),
|
|
},
|
|
]}
|
|
onDelete={(row) => handleDelete(row.id)}
|
|
hookData={hookData}
|
|
emptyMessage={t('Keine Workflows gefunden. Erstelle einen.')}
|
|
/>
|
|
</div>
|
|
<PromptDialog />
|
|
</div>
|
|
);
|
|
};
|