Add React UI with Execution Explorer, auth, and standalone deployment
Some checks failed
CI / build (push) Failing after 1m53s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped

- Scaffold Vite + React + TypeScript frontend in ui/ with full design
  system (dark/light themes) matching the HTML mockups
- Implement Execution Explorer page: search filters, results table with
  expandable processor tree and exchange detail sidebar, pagination
- Add UI authentication: UiAuthController (login/refresh endpoints),
  JWT filter handles ui: subject prefix, CORS configuration
- Shared components: StatusPill, DurationBar, StatCard, AppBadge,
  FilterChip, Pagination — all using CSS Modules with design tokens
- API client layer: openapi-fetch with auth middleware, TanStack Query
  hooks for search/detail/snapshot queries, Zustand for state
- Standalone deployment: Nginx Dockerfile, K8s Deployment + ConfigMap +
  NodePort (30080), runtime config.js for API base URL
- Embedded mode: maven-resources-plugin copies ui/dist into JAR static
  resources, SPA forward controller for client-side routing
- CI/CD: UI build step, Docker build/push for server-ui image, K8s
  deploy step for UI, UI credential secrets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-13 13:59:22 +01:00
parent 9c2391e5d4
commit 3eb83f97d3
65 changed files with 6449 additions and 22 deletions

View File

@@ -0,0 +1,120 @@
import { useState } from 'react';
import type { ExecutionSummary } from '../../api/schema';
import { StatusPill } from '../../components/shared/StatusPill';
import { DurationBar } from '../../components/shared/DurationBar';
import { AppBadge } from '../../components/shared/AppBadge';
import { ProcessorTree } from './ProcessorTree';
import { ExchangeDetail } from './ExchangeDetail';
import styles from './ResultsTable.module.css';
interface ResultsTableProps {
results: ExecutionSummary[];
loading: boolean;
}
function formatTime(iso: string) {
return new Date(iso).toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
});
}
export function ResultsTable({ results, loading }: ResultsTableProps) {
const [expandedId, setExpandedId] = useState<string | null>(null);
if (loading && results.length === 0) {
return (
<div className={styles.tableWrap}>
<div className={styles.loadingOverlay}>Loading executions...</div>
</div>
);
}
if (results.length === 0) {
return (
<div className={styles.tableWrap}>
<div className={styles.emptyState}>No executions found matching your filters.</div>
</div>
);
}
return (
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead className={styles.thead}>
<tr>
<th className={styles.th} style={{ width: 32 }} />
<th className={styles.th}>Timestamp</th>
<th className={styles.th}>Status</th>
<th className={styles.th}>Application</th>
<th className={styles.th}>Route</th>
<th className={styles.th}>Correlation ID</th>
<th className={styles.th}>Duration</th>
<th className={styles.th}>Processors</th>
</tr>
</thead>
<tbody>
{results.map((exec) => {
const isExpanded = expandedId === exec.executionId;
return (
<ResultRow
key={exec.executionId}
exec={exec}
isExpanded={isExpanded}
onToggle={() => setExpandedId(isExpanded ? null : exec.executionId)}
/>
);
})}
</tbody>
</table>
</div>
);
}
function ResultRow({
exec,
isExpanded,
onToggle,
}: {
exec: ExecutionSummary;
isExpanded: boolean;
onToggle: () => void;
}) {
return (
<>
<tr
className={`${styles.row} ${isExpanded ? styles.expanded : ''}`}
onClick={onToggle}
>
<td className={`${styles.td} ${styles.tdExpand}`}>&rsaquo;</td>
<td className={`${styles.td} mono`}>{formatTime(exec.startTime)}</td>
<td className={styles.td}>
<StatusPill status={exec.status} />
</td>
<td className={styles.td}>
<AppBadge name={exec.agentId} />
</td>
<td className={`${styles.td} mono text-secondary`}>{exec.routeId}</td>
<td className={`${styles.td} mono text-muted ${styles.correlationId}`} title={exec.correlationId ?? ''}>
{exec.correlationId ?? '-'}
</td>
<td className={styles.td}>
<DurationBar duration={exec.duration} />
</td>
<td className={`${styles.td} mono text-muted`}>{exec.processorCount}</td>
</tr>
{isExpanded && (
<tr className={styles.detailRowVisible}>
<td className={styles.detailCell} colSpan={8}>
<div className={styles.detailContent}>
<ProcessorTree executionId={exec.executionId} />
<ExchangeDetail execution={exec} />
</div>
</td>
</tr>
)}
</>
);
}