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;
|
groupActions?: (groupKey: string, groupRows: T[]) => React.ReactNode;
|
||||||
initialSearchTerm?: string;
|
initialSearchTerm?: string;
|
||||||
initialSort?: Array<{ key: string; direction: 'asc' | 'desc' }>;
|
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;
|
rowDraggable?: boolean;
|
||||||
onRowDragStart?: (e: React.DragEvent<HTMLTableRowElement>, row: T) => void;
|
onRowDragStart?: (e: React.DragEvent<HTMLTableRowElement>, row: T) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -580,6 +582,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
groupActions,
|
groupActions,
|
||||||
initialSearchTerm = '',
|
initialSearchTerm = '',
|
||||||
initialSort,
|
initialSort,
|
||||||
|
initialFilters,
|
||||||
rowDraggable = false,
|
rowDraggable = false,
|
||||||
onRowDragStart,
|
onRowDragStart,
|
||||||
}: FormGeneratorTableProps<T>) {
|
}: FormGeneratorTableProps<T>) {
|
||||||
|
|
@ -724,7 +727,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
const [filterFocused, setFilterFocused] = useState<Record<string, boolean>>({});
|
const [filterFocused, setFilterFocused] = useState<Record<string, boolean>>({});
|
||||||
// Multi-column sorting: array of sort configs in order of priority
|
// Multi-column sorting: array of sort configs in order of priority
|
||||||
const [sortConfigs, setSortConfigs] = useState<Array<{ key: string; direction: 'asc' | 'desc' }>>(initialSort ?? []);
|
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>>({});
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
||||||
// Actions column width - resizable, default based on number of buttons
|
// Actions column width - resizable, default based on number of buttons
|
||||||
const [actionsColumnWidth, setActionsColumnWidth] = useState<number | null>(null);
|
const [actionsColumnWidth, setActionsColumnWidth] = useState<number | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,21 @@ export interface Tab {
|
||||||
export interface TabsProps {
|
export interface TabsProps {
|
||||||
tabs: Tab[];
|
tabs: Tab[];
|
||||||
defaultTabId?: string;
|
defaultTabId?: string;
|
||||||
|
/** Controlled active tab. When provided, internal state is ignored. */
|
||||||
|
activeTabId?: string;
|
||||||
onTabChange?: (tabId: string) => void;
|
onTabChange?: (tabId: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tabs({ tabs, defaultTabId, onTabChange, className = '' }: TabsProps) {
|
export function Tabs({ tabs, defaultTabId, activeTabId: controlledTabId, onTabChange, className = '' }: TabsProps) {
|
||||||
const [activeTabId, setActiveTabId] = useState<string>(
|
const [internalTabId, setInternalTabId] = useState<string>(
|
||||||
defaultTabId || tabs[0]?.id || ''
|
defaultTabId || tabs[0]?.id || ''
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const activeTabId = controlledTabId ?? internalTabId;
|
||||||
|
|
||||||
const handleTabClick = (tabId: string) => {
|
const handleTabClick = (tabId: string) => {
|
||||||
setActiveTabId(tabId);
|
if (!controlledTabId) setInternalTabId(tabId);
|
||||||
onTabChange?.(tabId);
|
onTabChange?.(tabId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
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 { FaSync, FaPlay, FaCog, FaChartBar, FaDownload, FaCheck, FaBan, FaPen, FaEye, FaTimes, FaStream, FaStop } from 'react-icons/fa';
|
||||||
import { FormGeneratorTable, type ColumnConfig } from '../components/FormGenerator';
|
import { FormGeneratorTable, type ColumnConfig } from '../components/FormGenerator';
|
||||||
import { Tabs } from '../components/UiComponents/Tabs';
|
import { Tabs } from '../components/UiComponents/Tabs';
|
||||||
|
|
@ -15,7 +15,7 @@ import { useToast } from '../contexts/ToastContext';
|
||||||
import { usePrompt } from '../hooks/usePrompt';
|
import { usePrompt } from '../hooks/usePrompt';
|
||||||
import { useApiRequest } from '../hooks/useApi';
|
import { useApiRequest } from '../hooks/useApi';
|
||||||
import { formatUnixTimestamp } from '../utils/time';
|
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 { fetchAttributes } from '../api/attributesApi';
|
||||||
import type { AttributeDefinition } from '../api/attributesApi';
|
import type { AttributeDefinition } from '../api/attributesApi';
|
||||||
import { resolveColumnTypes } from '../utils/columnTypeResolver';
|
import { resolveColumnTypes } from '../utils/columnTypeResolver';
|
||||||
|
|
@ -424,7 +424,12 @@ const _RunTracingModal: React.FC<_RunTracingModalProps> = ({ run, onClose }) =>
|
||||||
// DashboardTab — Metrics + Runs table with backend pagination
|
// 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 { t } = useLanguage();
|
||||||
const { request } = useApiRequest();
|
const { request } = useApiRequest();
|
||||||
const { showError } = useToast();
|
const { showError } = useToast();
|
||||||
|
|
@ -491,8 +496,7 @@ const _DashboardTab: React.FC = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
_loadMetrics();
|
_loadMetrics();
|
||||||
_loadRuns();
|
}, [_loadMetrics]);
|
||||||
}, [_loadMetrics, _loadRuns]);
|
|
||||||
|
|
||||||
const hasRunningRuns = runs.some((r) => r.status === 'running' || r.status === 'paused');
|
const hasRunningRuns = runs.some((r) => r.status === 'running' || r.status === 'paused');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -531,13 +535,19 @@ const _DashboardTab: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [showError, t]);
|
}, [showError, t]);
|
||||||
|
|
||||||
|
const _initialFilters = useMemo(() => {
|
||||||
|
if (!workflowFilter) return undefined;
|
||||||
|
return { workflowId: workflowFilter };
|
||||||
|
}, [workflowFilter]);
|
||||||
|
|
||||||
const _rawRunColumns: ColumnConfig[] = useMemo(() => [
|
const _rawRunColumns: ColumnConfig[] = useMemo(() => [
|
||||||
{
|
{
|
||||||
key: 'workflowLabel',
|
key: 'workflowId',
|
||||||
label: t('Workflow'),
|
label: t('Workflow'),
|
||||||
width: 200,
|
width: 200,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: (v: string, row: WorkflowRun) => v || row.workflowId || t('—'),
|
filterable: true,
|
||||||
|
displayField: 'workflowLabel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'mandateId',
|
key: 'mandateId',
|
||||||
|
|
@ -643,7 +653,9 @@ const _DashboardTab: React.FC = () => {
|
||||||
</div>
|
</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}>
|
<div className={styles.tableContainer}>
|
||||||
<FormGeneratorTable<WorkflowRun>
|
<FormGeneratorTable<WorkflowRun>
|
||||||
data={runs}
|
data={runs}
|
||||||
|
|
@ -656,7 +668,9 @@ const _DashboardTab: React.FC = () => {
|
||||||
sortable={true}
|
sortable={true}
|
||||||
selectable={true}
|
selectable={true}
|
||||||
initialSort={[{ key: 'startedAt', direction: 'desc' }]}
|
initialSort={[{ key: 'startedAt', direction: 'desc' }]}
|
||||||
|
initialFilters={_initialFilters}
|
||||||
apiEndpoint="/api/system/workflow-runs"
|
apiEndpoint="/api/system/workflow-runs"
|
||||||
|
onRowClick={(row) => onRunClick?.(row.id)}
|
||||||
customActions={[
|
customActions={[
|
||||||
{
|
{
|
||||||
id: 'tracing',
|
id: 'tracing',
|
||||||
|
|
@ -686,7 +700,11 @@ const _DashboardTab: React.FC = () => {
|
||||||
// WorkflowsTab — Central workflow management across all instances
|
// 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 { t } = useLanguage();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { request } = useApiRequest();
|
const { request } = useApiRequest();
|
||||||
|
|
@ -1051,6 +1069,7 @@ const _WorkflowsTab: React.FC = () => {
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onDelete={(row) => _handleDelete(row.id)}
|
onDelete={(row) => _handleDelete(row.id)}
|
||||||
|
onRowClick={(row) => onWorkflowClick?.(row.id)}
|
||||||
hookData={_hookData}
|
hookData={_hookData}
|
||||||
emptyMessage={t('Keine Workflows gefunden.')}
|
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 { t } = useLanguage();
|
||||||
const { request } = useApiRequest();
|
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 [runDetail, setRunDetail] = useState<Awaited<ReturnType<typeof fetchWorkspaceRunDetail>> | null>(null);
|
||||||
const [detailLoading, setDetailLoading] = useState(false);
|
const [detailLoading, setDetailLoading] = useState(false);
|
||||||
|
|
||||||
const _loadRuns = useCallback(async () => {
|
const _loadDetail = useCallback(async (id: string) => {
|
||||||
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) => {
|
|
||||||
setDetailLoading(true);
|
setDetailLoading(true);
|
||||||
try {
|
try {
|
||||||
const detail = await fetchWorkspaceRunDetail(request, runId);
|
const detail = await fetchWorkspaceRunDetail(request, id);
|
||||||
setRunDetail(detail);
|
setRunDetail(detail);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Workspace run detail failed', e);
|
console.error('Workspace run detail failed', e);
|
||||||
|
|
@ -1109,22 +1107,34 @@ const _WorkspaceTab: React.FC = () => {
|
||||||
}, [request]);
|
}, [request]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedRunId) _loadDetail(selectedRunId);
|
if (runId) _loadDetail(runId);
|
||||||
else setRunDetail(null);
|
else setRunDetail(null);
|
||||||
}, [selectedRunId, _loadDetail]);
|
}, [runId, _loadDetail]);
|
||||||
|
|
||||||
|
if (!runId) {
|
||||||
|
return (
|
||||||
|
<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>;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedRunId && runDetail) {
|
|
||||||
const { run, steps, files, workflow } = runDetail;
|
const { run, steps, files, workflow } = runDetail;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '1rem' }}>
|
<div style={{ padding: '1rem' }}>
|
||||||
<button type="button" className="btn-link" onClick={() => setSelectedRunId(null)} style={{ marginBottom: '1rem' }}>
|
<button type="button" className={styles.secondaryButton} onClick={onBack} style={{ marginBottom: '1rem' }}>
|
||||||
← {t('Zurück zur Liste')}
|
← {t('Zurück zum Dashboard')}
|
||||||
</button>
|
</button>
|
||||||
<h3 style={{ margin: '0.5rem 0' }}>{run.workflowLabel || run.workflowId}</h3>
|
<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' }}>
|
<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>
|
<span><strong>{t('Status')}:</strong> {run.status}</span>
|
||||||
{run.startedAt && <span><strong>{t('Start')}:</strong> {formatUnixTimestamp(run.startedAt)}</span>}
|
{run.startedAt && <span><strong>{t('Start')}:</strong> {_formatTs(run.startedAt)}</span>}
|
||||||
{run.completedAt && <span><strong>{t('Ende')}:</strong> {formatUnixTimestamp(run.completedAt)}</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>}
|
{workflow?.targetFeatureInstanceId && <span><strong>{t('Ziel-Instanz')}:</strong> {run.targetInstanceLabel || workflow.targetFeatureInstanceId}</span>}
|
||||||
{(run.costTokens ?? 0) > 0 && <span><strong>Tokens:</strong> {run.costTokens}</span>}
|
{(run.costTokens ?? 0) > 0 && <span><strong>Tokens:</strong> {run.costTokens}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1177,98 +1187,64 @@ const _WorkspaceTab: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
{loading ? (
|
|
||||||
<p>{t('Laden…')}</p>
|
|
||||||
) : runs.length === 0 ? (
|
|
||||||
<p style={{ color: 'var(--text-secondary)' }}>{t('Keine Workflow-Runs gefunden.')}</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 = '')}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Main page with Tabs
|
// Main page with Tabs (Workflows → Dashboard → Workspace)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
export const AutomationsDashboardPage: React.FC = () => {
|
export const AutomationsDashboardPage: React.FC = () => {
|
||||||
const { t } = useLanguage();
|
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(() => [
|
const tabs = useMemo(() => [
|
||||||
{
|
|
||||||
id: 'dashboard',
|
|
||||||
label: t('Dashboard'),
|
|
||||||
content: <_DashboardTab />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'workflows',
|
id: 'workflows',
|
||||||
label: t('Workflows'),
|
label: t('Workflows'),
|
||||||
content: <_WorkflowsTab />,
|
content: <_WorkflowsTab onWorkflowClick={_handleWorkflowClick} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dashboard',
|
||||||
|
label: t('Dashboard'),
|
||||||
|
content: <_DashboardTab workflowFilter={workflowFilter} onRunClick={_handleRunClick} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'workspace',
|
id: 'workspace',
|
||||||
label: t('Workspace'),
|
label: t('Workspace'),
|
||||||
content: <_WorkspaceTab />,
|
content: <_WorkspaceTab runId={selectedRunId} onBack={_handleBackFromWorkspace} />,
|
||||||
},
|
},
|
||||||
], [t]);
|
], [t, _handleWorkflowClick, workflowFilter, _handleRunClick, selectedRunId, _handleBackFromWorkspace]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||||
<h1 className={styles.pageTitle} style={{ flexShrink: 0 }}>{t('Automatisierung')}</h1>
|
<h1 className={styles.pageTitle} style={{ flexShrink: 0 }}>{t('Automatisierung')}</h1>
|
||||||
<Tabs tabs={tabs} defaultTabId="dashboard" />
|
<Tabs tabs={tabs} activeTabId={activeTab} onTabChange={setActiveTab} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -408,6 +408,9 @@ export const TrusteeAnalyseView: React.FC = () => {
|
||||||
<div style={{ fontWeight: 600, marginBottom: '0.5rem', fontSize: '0.875rem' }}>
|
<div style={{ fontWeight: 600, marginBottom: '0.5rem', fontSize: '0.875rem' }}>
|
||||||
{t('Budget-Excel hochladen')}
|
{t('Budget-Excel hochladen')}
|
||||||
</div>
|
</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 ? (
|
{budgetFileName ? (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
<span style={{ fontSize: '0.875rem' }}>📄 {budgetFileName}</span>
|
<span style={{ fontSize: '0.875rem' }}>📄 {budgetFileName}</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue