diff --git a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx index 4592906..9a8779f 100644 --- a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx +++ b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx @@ -221,6 +221,8 @@ export interface FormGeneratorTableProps { groupActions?: (groupKey: string, groupRows: T[]) => React.ReactNode; initialSearchTerm?: string; initialSort?: Array<{ key: string; direction: 'asc' | 'desc' }>; + /** Pre-set column filters on mount (e.g. {workflowId: "abc"}). Reacts to prop changes. */ + initialFilters?: Record; rowDraggable?: boolean; onRowDragStart?: (e: React.DragEvent, row: T) => void; } @@ -580,6 +582,7 @@ export function FormGeneratorTable>({ groupActions, initialSearchTerm = '', initialSort, + initialFilters, rowDraggable = false, onRowDragStart, }: FormGeneratorTableProps) { @@ -724,7 +727,7 @@ export function FormGeneratorTable>({ const [filterFocused, setFilterFocused] = useState>({}); // Multi-column sorting: array of sort configs in order of priority const [sortConfigs, setSortConfigs] = useState>(initialSort ?? []); - const [filters, setFilters] = useState>({}); + const [filters, setFilters] = useState>(initialFilters || {}); const [columnWidths, setColumnWidths] = useState>({}); // Actions column width - resizable, default based on number of buttons const [actionsColumnWidth, setActionsColumnWidth] = useState(null); diff --git a/src/components/UiComponents/Tabs/Tabs.tsx b/src/components/UiComponents/Tabs/Tabs.tsx index 8b30ffd..c849dc1 100644 --- a/src/components/UiComponents/Tabs/Tabs.tsx +++ b/src/components/UiComponents/Tabs/Tabs.tsx @@ -10,17 +10,21 @@ export interface Tab { export interface TabsProps { tabs: Tab[]; defaultTabId?: string; + /** Controlled active tab. When provided, internal state is ignored. */ + activeTabId?: string; onTabChange?: (tabId: string) => void; className?: string; } -export function Tabs({ tabs, defaultTabId, onTabChange, className = '' }: TabsProps) { - const [activeTabId, setActiveTabId] = useState( +export function Tabs({ tabs, defaultTabId, activeTabId: controlledTabId, onTabChange, className = '' }: TabsProps) { + const [internalTabId, setInternalTabId] = useState( defaultTabId || tabs[0]?.id || '' ); + const activeTabId = controlledTabId ?? internalTabId; + const handleTabClick = (tabId: string) => { - setActiveTabId(tabId); + if (!controlledTabId) setInternalTabId(tabId); onTabChange?.(tabId); }; diff --git a/src/pages/AutomationsDashboardPage.tsx b/src/pages/AutomationsDashboardPage.tsx index 2ac5c5c..5279219 100644 --- a/src/pages/AutomationsDashboardPage.tsx +++ b/src/pages/AutomationsDashboardPage.tsx @@ -7,7 +7,7 @@ */ import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { FaSync, FaPlay, FaCog, FaChartBar, FaDownload, FaCheck, FaBan, FaPen, FaEye, FaTimes, FaStream, FaStop } from 'react-icons/fa'; import { FormGeneratorTable, type ColumnConfig } from '../components/FormGenerator'; import { Tabs } from '../components/UiComponents/Tabs'; @@ -15,7 +15,7 @@ import { useToast } from '../contexts/ToastContext'; import { usePrompt } from '../hooks/usePrompt'; import { useApiRequest } from '../hooks/useApi'; import { formatUnixTimestamp } from '../utils/time'; -import { updateWorkflow, executeGraph, deleteSystemWorkflow, fetchWorkspaceRuns, fetchWorkspaceRunDetail, type WorkspaceRun } from '../api/workflowApi'; +import { updateWorkflow, executeGraph, deleteSystemWorkflow, fetchWorkspaceRunDetail } from '../api/workflowApi'; import { fetchAttributes } from '../api/attributesApi'; import type { AttributeDefinition } from '../api/attributesApi'; import { resolveColumnTypes } from '../utils/columnTypeResolver'; @@ -424,7 +424,12 @@ const _RunTracingModal: React.FC<_RunTracingModalProps> = ({ run, onClose }) => // DashboardTab — Metrics + Runs table with backend pagination // =========================================================================== -const _DashboardTab: React.FC = () => { +interface _DashboardTabProps { + workflowFilter?: string | null; + onRunClick?: (runId: string) => void; +} + +const _DashboardTab: React.FC<_DashboardTabProps> = ({ workflowFilter, onRunClick }) => { const { t } = useLanguage(); const { request } = useApiRequest(); const { showError } = useToast(); @@ -491,8 +496,7 @@ const _DashboardTab: React.FC = () => { useEffect(() => { _loadMetrics(); - _loadRuns(); - }, [_loadMetrics, _loadRuns]); + }, [_loadMetrics]); const hasRunningRuns = runs.some((r) => r.status === 'running' || r.status === 'paused'); useEffect(() => { @@ -531,13 +535,19 @@ const _DashboardTab: React.FC = () => { } }, [showError, t]); + const _initialFilters = useMemo(() => { + if (!workflowFilter) return undefined; + return { workflowId: workflowFilter }; + }, [workflowFilter]); + const _rawRunColumns: ColumnConfig[] = useMemo(() => [ { - key: 'workflowLabel', + key: 'workflowId', label: t('Workflow'), width: 200, sortable: true, - formatter: (v: string, row: WorkflowRun) => v || row.workflowId || t('—'), + filterable: true, + displayField: 'workflowLabel', }, { key: 'mandateId', @@ -643,7 +653,9 @@ const _DashboardTab: React.FC = () => { )} -

{t('Letzte Runs')}

+
+

{t('Letzte Runs')}

+
data={runs} @@ -656,7 +668,9 @@ const _DashboardTab: React.FC = () => { sortable={true} selectable={true} initialSort={[{ key: 'startedAt', direction: 'desc' }]} + initialFilters={_initialFilters} apiEndpoint="/api/system/workflow-runs" + onRowClick={(row) => onRunClick?.(row.id)} customActions={[ { id: 'tracing', @@ -686,7 +700,11 @@ const _DashboardTab: React.FC = () => { // WorkflowsTab — Central workflow management across all instances // =========================================================================== -const _WorkflowsTab: React.FC = () => { +interface _WorkflowsTabProps { + onWorkflowClick?: (workflowId: string) => void; +} + +const _WorkflowsTab: React.FC<_WorkflowsTabProps> = ({ onWorkflowClick }) => { const { t } = useLanguage(); const navigate = useNavigate(); const { request } = useApiRequest(); @@ -1051,6 +1069,7 @@ const _WorkflowsTab: React.FC = () => { }, ]} onDelete={(row) => _handleDelete(row.id)} + onRowClick={(row) => onWorkflowClick?.(row.id)} hookData={_hookData} emptyMessage={t('Keine Workflows gefunden.')} /> @@ -1061,45 +1080,24 @@ const _WorkflowsTab: React.FC = () => { }; // =========================================================================== -// Workspace Tab (user-facing workflow run history) +// Workspace Tab (run detail only — no table) // =========================================================================== -const _WorkspaceTab: React.FC = () => { +interface _WorkspaceTabProps { + runId: string | null; + onBack: () => void; +} + +const _WorkspaceTab: React.FC<_WorkspaceTabProps> = ({ runId, onBack }) => { const { t } = useLanguage(); const { request } = useApiRequest(); - const navigate = useNavigate(); - const [runs, setRuns] = useState([]); - const [total, setTotal] = useState(0); - const [loading, setLoading] = useState(true); - const [scope, setScope] = useState<'mine' | 'mandate'>('mine'); - const [statusFilter, setStatusFilter] = useState(''); - const [selectedRunId, setSelectedRunId] = useState(null); const [runDetail, setRunDetail] = useState> | null>(null); const [detailLoading, setDetailLoading] = useState(false); - const _loadRuns = useCallback(async () => { - setLoading(true); - try { - const data = await fetchWorkspaceRuns(request, { - scope, - status: statusFilter || undefined, - limit: 50, - }); - setRuns(data.runs || []); - setTotal(data.total || 0); - } catch (e) { - console.error('Workspace runs load failed', e); - } finally { - setLoading(false); - } - }, [request, scope, statusFilter]); - - useEffect(() => { _loadRuns(); }, [_loadRuns]); - - const _loadDetail = useCallback(async (runId: string) => { + const _loadDetail = useCallback(async (id: string) => { setDetailLoading(true); try { - const detail = await fetchWorkspaceRunDetail(request, runId); + const detail = await fetchWorkspaceRunDetail(request, id); setRunDetail(detail); } catch (e) { console.error('Workspace run detail failed', e); @@ -1109,166 +1107,144 @@ const _WorkspaceTab: React.FC = () => { }, [request]); useEffect(() => { - if (selectedRunId) _loadDetail(selectedRunId); + if (runId) _loadDetail(runId); else setRunDetail(null); - }, [selectedRunId, _loadDetail]); + }, [runId, _loadDetail]); - if (selectedRunId && runDetail) { - const { run, steps, files, workflow } = runDetail; + if (!runId) { return ( -
- -

{run.workflowLabel || run.workflowId}

-
- {t('Status')}: {run.status} - {run.startedAt && {t('Start')}: {formatUnixTimestamp(run.startedAt)}} - {run.completedAt && {t('Ende')}: {formatUnixTimestamp(run.completedAt)}} - {workflow?.targetFeatureInstanceId && {t('Ziel-Instanz')}: {run.targetInstanceLabel || workflow.targetFeatureInstanceId}} - {(run.costTokens ?? 0) > 0 && Tokens: {run.costTokens}} -
- {run.error && ( -
- {run.error} -
- )} -

{t('Schritte')}

- {steps.length === 0 ? ( -

{t('Keine Schritte protokolliert.')}

- ) : ( -
- {steps.map((step) => ( -
- - - {step.status} - - {step.nodeType} ({step.nodeId}) - {step.durationMs != null && {step.durationMs}ms} - - {step.output && Object.keys(step.output).length > 0 && ( -
-                    {JSON.stringify(step.output, null, 2)}
-                  
- )} - {step.error &&

{step.error}

} -
- ))} -
- )} - {files.length > 0 && ( - <> -

{t('Dokumente')}

-
- {files.map((f) => ( - - - {f.fileName || f.id} - - ))} -
- - )} +
+

{t('Wähle einen Run im Dashboard aus, um die Details anzuzeigen.')}

); } + if (detailLoading || !runDetail) { + return

{t('Laden…')}

; + } + + const { run, steps, files, workflow } = runDetail; + return (
-
- - - - {total} {t('Runs')} + +

{run.workflowLabel || run.workflowId}

+
+ {t('Status')}: {run.status} + {run.startedAt && {t('Start')}: {_formatTs(run.startedAt)}} + {run.completedAt && {t('Ende')}: {_formatTs(run.completedAt)}} + {workflow?.targetFeatureInstanceId && {t('Ziel-Instanz')}: {run.targetInstanceLabel || workflow.targetFeatureInstanceId}} + {(run.costTokens ?? 0) > 0 && Tokens: {run.costTokens}}
- {loading ? ( -

{t('Laden…')}

- ) : runs.length === 0 ? ( -

{t('Keine Workflow-Runs gefunden.')}

+ {run.error && ( +
+ {run.error} +
+ )} +

{t('Schritte')}

+ {steps.length === 0 ? ( +

{t('Keine Schritte protokolliert.')}

) : ( - - - - - - - - - - - - {runs.map((run) => ( - setSelectedRunId(run.id)} - style={{ borderBottom: '1px solid var(--border-color)', cursor: 'pointer' }} - onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--bg-hover, rgba(0,0,0,0.03))')} - onMouseLeave={(e) => (e.currentTarget.style.background = '')} +
+ {steps.map((step) => ( +
+ + + {step.status} + + {step.nodeType} ({step.nodeId}) + {step.durationMs != null && {step.durationMs}ms} + + {step.output && Object.keys(step.output).length > 0 && ( +
+                  {JSON.stringify(step.output, null, 2)}
+                
+ )} + {step.error &&

{step.error}

} +
+ ))} +
+ )} + {files.length > 0 && ( + <> +

{t('Dokumente')}

+
+ {files.map((f) => ( + -
- - - - - + + {f.fileName || f.id} + ))} - -
{t('Workflow')}{t('Status')}{t('Gestartet')}{t('Ziel-Instanz')}Tokens
{run.workflowLabel || run.workflowId} - - {run.status} - - {run.startedAt ? formatUnixTimestamp(run.startedAt) : '—'}{run.targetInstanceLabel || '—'}{run.costTokens ?? 0}
+
+ )}
); }; // =========================================================================== -// Main page with Tabs +// Main page with Tabs (Workflows → Dashboard → Workspace) // =========================================================================== export const AutomationsDashboardPage: React.FC = () => { const { t } = useLanguage(); + const [searchParams] = useSearchParams(); + + const initialTab = searchParams.get('tab') || 'workflows'; + const initialRunId = searchParams.get('runId') || null; + + const [activeTab, setActiveTab] = useState(initialRunId ? 'workspace' : initialTab); + const [selectedRunId, setSelectedRunId] = useState(initialRunId); + const [workflowFilter, setWorkflowFilter] = useState(null); + + const _handleWorkflowClick = useCallback((workflowId: string) => { + setWorkflowFilter(workflowId); + setActiveTab('dashboard'); + }, []); + + useEffect(() => { + if (workflowFilter) setWorkflowFilter(null); + }, [workflowFilter]); + + const _handleRunClick = useCallback((runId: string) => { + setSelectedRunId(runId); + setActiveTab('workspace'); + }, []); + + const _handleBackFromWorkspace = useCallback(() => { + setSelectedRunId(null); + setActiveTab('dashboard'); + }, []); const tabs = useMemo(() => [ - { - id: 'dashboard', - label: t('Dashboard'), - content: <_DashboardTab />, - }, { id: 'workflows', label: t('Workflows'), - content: <_WorkflowsTab />, + content: <_WorkflowsTab onWorkflowClick={_handleWorkflowClick} />, + }, + { + id: 'dashboard', + label: t('Dashboard'), + content: <_DashboardTab workflowFilter={workflowFilter} onRunClick={_handleRunClick} />, }, { id: 'workspace', label: t('Workspace'), - content: <_WorkspaceTab />, + content: <_WorkspaceTab runId={selectedRunId} onBack={_handleBackFromWorkspace} />, }, - ], [t]); + ], [t, _handleWorkflowClick, workflowFilter, _handleRunClick, selectedRunId, _handleBackFromWorkspace]); return (

{t('Automatisierung')}

- +
); }; diff --git a/src/pages/views/trustee/TrusteeAnalyseView.tsx b/src/pages/views/trustee/TrusteeAnalyseView.tsx index 45f8a39..8b12b87 100644 --- a/src/pages/views/trustee/TrusteeAnalyseView.tsx +++ b/src/pages/views/trustee/TrusteeAnalyseView.tsx @@ -408,6 +408,9 @@ export const TrusteeAnalyseView: React.FC = () => {
{t('Budget-Excel hochladen')}
+
+ {t('Ergebnis: Excel-Bericht mit Konten-Tabelle, Uebersichts-Chart und Management-Summary.')} +
{budgetFileName ? (
📄 {budgetFileName}