fixes before document generation refactory styles
This commit is contained in:
parent
8cecf3b320
commit
70459d57e3
4 changed files with 152 additions and 166 deletions
|
|
@ -221,6 +221,8 @@ export interface FormGeneratorTableProps<T = any> {
|
|||
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<string, any>;
|
||||
rowDraggable?: boolean;
|
||||
onRowDragStart?: (e: React.DragEvent<HTMLTableRowElement>, row: T) => void;
|
||||
}
|
||||
|
|
@ -580,6 +582,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
|||
groupActions,
|
||||
initialSearchTerm = '',
|
||||
initialSort,
|
||||
initialFilters,
|
||||
rowDraggable = false,
|
||||
onRowDragStart,
|
||||
}: FormGeneratorTableProps<T>) {
|
||||
|
|
@ -724,7 +727,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
|||
const [filterFocused, setFilterFocused] = useState<Record<string, boolean>>({});
|
||||
// Multi-column sorting: array of sort configs in order of priority
|
||||
const [sortConfigs, setSortConfigs] = useState<Array<{ key: string; direction: 'asc' | 'desc' }>>(initialSort ?? []);
|
||||
const [filters, setFilters] = useState<Record<string, any>>({});
|
||||
const [filters, setFilters] = useState<Record<string, any>>(initialFilters || {});
|
||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
||||
// Actions column width - resizable, default based on number of buttons
|
||||
const [actionsColumnWidth, setActionsColumnWidth] = useState<number | null>(null);
|
||||
|
|
|
|||
|
|
@ -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<string>(
|
||||
export function Tabs({ tabs, defaultTabId, activeTabId: controlledTabId, onTabChange, className = '' }: TabsProps) {
|
||||
const [internalTabId, setInternalTabId] = useState<string>(
|
||||
defaultTabId || tabs[0]?.id || ''
|
||||
);
|
||||
|
||||
const activeTabId = controlledTabId ?? internalTabId;
|
||||
|
||||
const handleTabClick = (tabId: string) => {
|
||||
setActiveTabId(tabId);
|
||||
if (!controlledTabId) setInternalTabId(tabId);
|
||||
onTabChange?.(tabId);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<h3 style={{ fontSize: '0.95rem', fontWeight: 600, marginBottom: 8, flexShrink: 0 }}>{t('Letzte Runs')}</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8, flexShrink: 0 }}>
|
||||
<h3 style={{ fontSize: '0.95rem', fontWeight: 600, margin: 0 }}>{t('Letzte Runs')}</h3>
|
||||
</div>
|
||||
<div className={styles.tableContainer}>
|
||||
<FormGeneratorTable<WorkflowRun>
|
||||
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<WorkspaceRun[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [scope, setScope] = useState<'mine' | 'mandate'>('mine');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [selectedRunId, setSelectedRunId] = useState<string | null>(null);
|
||||
const [runDetail, setRunDetail] = useState<Awaited<ReturnType<typeof fetchWorkspaceRunDetail>> | 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 (
|
||||
<div style={{ padding: '1rem' }}>
|
||||
<button type="button" className="btn-link" onClick={() => setSelectedRunId(null)} style={{ marginBottom: '1rem' }}>
|
||||
← {t('Zurück zur Liste')}
|
||||
</button>
|
||||
<h3 style={{ margin: '0.5rem 0' }}>{run.workflowLabel || run.workflowId}</h3>
|
||||
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', fontSize: '0.85rem', color: 'var(--text-secondary)', marginBottom: '1rem' }}>
|
||||
<span><strong>{t('Status')}:</strong> {run.status}</span>
|
||||
{run.startedAt && <span><strong>{t('Start')}:</strong> {formatUnixTimestamp(run.startedAt)}</span>}
|
||||
{run.completedAt && <span><strong>{t('Ende')}:</strong> {formatUnixTimestamp(run.completedAt)}</span>}
|
||||
{workflow?.targetFeatureInstanceId && <span><strong>{t('Ziel-Instanz')}:</strong> {run.targetInstanceLabel || workflow.targetFeatureInstanceId}</span>}
|
||||
{(run.costTokens ?? 0) > 0 && <span><strong>Tokens:</strong> {run.costTokens}</span>}
|
||||
</div>
|
||||
{run.error && (
|
||||
<div style={{ padding: '0.5rem', background: 'rgba(220,53,69,0.1)', borderRadius: 6, marginBottom: '1rem', color: 'var(--danger-color)' }}>
|
||||
{run.error}
|
||||
</div>
|
||||
)}
|
||||
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Schritte')}</h4>
|
||||
{steps.length === 0 ? (
|
||||
<p style={{ color: 'var(--text-secondary)' }}>{t('Keine Schritte protokolliert.')}</p>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{steps.map((step) => (
|
||||
<details key={step.id} style={{ border: '1px solid var(--border-color)', borderRadius: 6, padding: '0.5rem' }}>
|
||||
<summary style={{ cursor: 'pointer', fontWeight: 500 }}>
|
||||
<span style={{ marginRight: '0.5rem', fontSize: '0.75rem', padding: '2px 6px', borderRadius: 4, background: step.status === 'completed' ? 'rgba(40,167,69,0.15)' : step.status === 'failed' ? 'rgba(220,53,69,0.15)' : 'rgba(0,123,255,0.15)', color: step.status === 'completed' ? 'var(--success-color)' : step.status === 'failed' ? 'var(--danger-color)' : 'var(--primary-color)' }}>
|
||||
{step.status}
|
||||
</span>
|
||||
{step.nodeType} ({step.nodeId})
|
||||
{step.durationMs != null && <span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{step.durationMs}ms</span>}
|
||||
</summary>
|
||||
{step.output && Object.keys(step.output).length > 0 && (
|
||||
<pre style={{ fontSize: '0.75rem', maxHeight: 300, overflow: 'auto', marginTop: '0.5rem', background: 'var(--bg-secondary)', padding: '0.5rem', borderRadius: 4 }}>
|
||||
{JSON.stringify(step.output, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
{step.error && <p style={{ color: 'var(--danger-color)', marginTop: '0.25rem' }}>{step.error}</p>}
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<>
|
||||
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Dokumente')}</h4>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
{files.map((f) => (
|
||||
<a
|
||||
key={f.id}
|
||||
href={`/api/files/${f.id}/download`}
|
||||
download
|
||||
style={{ padding: '0.5rem 1rem', border: '1px solid var(--border-color)', borderRadius: 6, textDecoration: 'none', color: 'var(--primary-color)', fontSize: '0.85rem' }}
|
||||
>
|
||||
<FaDownload style={{ marginRight: 4 }} />
|
||||
{f.fileName || f.id}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div style={{ padding: '1rem', color: 'var(--text-secondary)' }}>
|
||||
<p>{t('Wähle einen Run im Dashboard aus, um die Details anzuzeigen.')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (detailLoading || !runDetail) {
|
||||
return <div style={{ padding: '1rem' }}><p>{t('Laden…')}</p></div>;
|
||||
}
|
||||
|
||||
const { run, steps, files, workflow } = runDetail;
|
||||
|
||||
return (
|
||||
<div style={{ padding: '1rem' }}>
|
||||
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<select value={scope} onChange={(e) => setScope(e.target.value as 'mine' | 'mandate')} style={{ padding: '0.3rem 0.5rem' }}>
|
||||
<option value="mine">{t('Meine Runs')}</option>
|
||||
<option value="mandate">{t('Alle zugänglichen')}</option>
|
||||
</select>
|
||||
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} style={{ padding: '0.3rem 0.5rem' }}>
|
||||
<option value="">{t('Alle Status')}</option>
|
||||
<option value="completed">{t('Abgeschlossen')}</option>
|
||||
<option value="running">{t('Läuft')}</option>
|
||||
<option value="failed">{t('Fehlgeschlagen')}</option>
|
||||
<option value="paused">{t('Pausiert')}</option>
|
||||
</select>
|
||||
<button type="button" onClick={_loadRuns} style={{ padding: '0.3rem 0.8rem' }}>
|
||||
<FaSync style={{ marginRight: 4 }} /> {t('Aktualisieren')}
|
||||
</button>
|
||||
<span style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>{total} {t('Runs')}</span>
|
||||
<button type="button" className={styles.secondaryButton} onClick={onBack} style={{ marginBottom: '1rem' }}>
|
||||
← {t('Zurück zum Dashboard')}
|
||||
</button>
|
||||
<h3 style={{ margin: '0.5rem 0' }}>{run.workflowLabel || run.workflowId}</h3>
|
||||
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', fontSize: '0.85rem', color: 'var(--text-secondary)', marginBottom: '1rem' }}>
|
||||
<span><strong>{t('Status')}:</strong> {run.status}</span>
|
||||
{run.startedAt && <span><strong>{t('Start')}:</strong> {_formatTs(run.startedAt)}</span>}
|
||||
{run.completedAt && <span><strong>{t('Ende')}:</strong> {_formatTs(run.completedAt)}</span>}
|
||||
{workflow?.targetFeatureInstanceId && <span><strong>{t('Ziel-Instanz')}:</strong> {run.targetInstanceLabel || workflow.targetFeatureInstanceId}</span>}
|
||||
{(run.costTokens ?? 0) > 0 && <span><strong>Tokens:</strong> {run.costTokens}</span>}
|
||||
</div>
|
||||
{loading ? (
|
||||
<p>{t('Laden…')}</p>
|
||||
) : runs.length === 0 ? (
|
||||
<p style={{ color: 'var(--text-secondary)' }}>{t('Keine Workflow-Runs gefunden.')}</p>
|
||||
{run.error && (
|
||||
<div style={{ padding: '0.5rem', background: 'rgba(220,53,69,0.1)', borderRadius: 6, marginBottom: '1rem', color: 'var(--danger-color)' }}>
|
||||
{run.error}
|
||||
</div>
|
||||
)}
|
||||
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Schritte')}</h4>
|
||||
{steps.length === 0 ? (
|
||||
<p style={{ color: 'var(--text-secondary)' }}>{t('Keine Schritte protokolliert.')}</p>
|
||||
) : (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid var(--border-color)', textAlign: 'left' }}>
|
||||
<th style={{ padding: '0.5rem' }}>{t('Workflow')}</th>
|
||||
<th style={{ padding: '0.5rem' }}>{t('Status')}</th>
|
||||
<th style={{ padding: '0.5rem' }}>{t('Gestartet')}</th>
|
||||
<th style={{ padding: '0.5rem' }}>{t('Ziel-Instanz')}</th>
|
||||
<th style={{ padding: '0.5rem' }}>Tokens</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{runs.map((run) => (
|
||||
<tr
|
||||
key={run.id}
|
||||
onClick={() => 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 = '')}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{steps.map((step) => (
|
||||
<details key={step.id} style={{ border: '1px solid var(--border-color)', borderRadius: 6, padding: '0.5rem' }}>
|
||||
<summary style={{ cursor: 'pointer', fontWeight: 500 }}>
|
||||
<span style={{ marginRight: '0.5rem', fontSize: '0.75rem', padding: '2px 6px', borderRadius: 4, background: step.status === 'completed' ? 'rgba(40,167,69,0.15)' : step.status === 'failed' ? 'rgba(220,53,69,0.15)' : 'rgba(0,123,255,0.15)', color: step.status === 'completed' ? 'var(--success-color)' : step.status === 'failed' ? 'var(--danger-color)' : 'var(--primary-color)' }}>
|
||||
{step.status}
|
||||
</span>
|
||||
{step.nodeType} ({step.nodeId})
|
||||
{step.durationMs != null && <span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{step.durationMs}ms</span>}
|
||||
</summary>
|
||||
{step.output && Object.keys(step.output).length > 0 && (
|
||||
<pre style={{ fontSize: '0.75rem', maxHeight: 300, overflow: 'auto', marginTop: '0.5rem', background: 'var(--bg-secondary)', padding: '0.5rem', borderRadius: 4 }}>
|
||||
{JSON.stringify(step.output, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
{step.error && <p style={{ color: 'var(--danger-color)', marginTop: '0.25rem' }}>{step.error}</p>}
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<>
|
||||
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Dokumente')}</h4>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
{files.map((f) => (
|
||||
<a
|
||||
key={f.id}
|
||||
href={`/api/files/${f.id}/download`}
|
||||
download
|
||||
style={{ padding: '0.5rem 1rem', border: '1px solid var(--border-color)', borderRadius: 6, textDecoration: 'none', color: 'var(--primary-color)', fontSize: '0.85rem' }}
|
||||
>
|
||||
<td style={{ padding: '0.5rem' }}>{run.workflowLabel || run.workflowId}</td>
|
||||
<td style={{ padding: '0.5rem' }}>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 10, fontSize: '0.75rem', fontWeight: 600, background: run.status === 'completed' ? 'rgba(40,167,69,0.15)' : run.status === 'failed' ? 'rgba(220,53,69,0.15)' : run.status === 'running' ? 'rgba(0,123,255,0.15)' : 'rgba(255,193,7,0.15)', color: run.status === 'completed' ? 'var(--success-color)' : run.status === 'failed' ? 'var(--danger-color)' : run.status === 'running' ? 'var(--primary-color)' : 'var(--warning-color)' }}>
|
||||
{run.status}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '0.5rem' }}>{run.startedAt ? formatUnixTimestamp(run.startedAt) : '—'}</td>
|
||||
<td style={{ padding: '0.5rem' }}>{run.targetInstanceLabel || '—'}</td>
|
||||
<td style={{ padding: '0.5rem' }}>{run.costTokens ?? 0}</td>
|
||||
</tr>
|
||||
<FaDownload style={{ marginRight: 4 }} />
|
||||
{f.fileName || f.id}
|
||||
</a>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ===========================================================================
|
||||
// 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<string>(initialRunId ? 'workspace' : initialTab);
|
||||
const [selectedRunId, setSelectedRunId] = useState<string | null>(initialRunId);
|
||||
const [workflowFilter, setWorkflowFilter] = useState<string | null>(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 (
|
||||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||
<h1 className={styles.pageTitle} style={{ flexShrink: 0 }}>{t('Automatisierung')}</h1>
|
||||
<Tabs tabs={tabs} defaultTabId="dashboard" />
|
||||
<Tabs tabs={tabs} activeTabId={activeTab} onTabChange={setActiveTab} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -408,6 +408,9 @@ export const TrusteeAnalyseView: React.FC = () => {
|
|||
<div style={{ fontWeight: 600, marginBottom: '0.5rem', fontSize: '0.875rem' }}>
|
||||
{t('Budget-Excel hochladen')}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary, #666)', marginBottom: '0.5rem' }}>
|
||||
{t('Ergebnis: Excel-Bericht mit Konten-Tabelle, Uebersichts-Chart und Management-Summary.')}
|
||||
</div>
|
||||
{budgetFileName ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span style={{ fontSize: '0.875rem' }}>📄 {budgetFileName}</span>
|
||||
|
|
|
|||
Loading…
Reference in a new issue