fix(ui): restore layout — same table everywhere, 50:50 split, full-height sidebar, tab styling
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 59s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s

- Sidebar wrapper gets height:100% to fill window
- Route-scoped Exchanges uses same Dashboard table (not compact ExchangeList)
- 50:50 grid split: table on left, diagram on right when route selected
- ContentTabs gets border-bottom and surface background for visibility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-28 14:22:34 +01:00
parent 7ee7076eec
commit 1e6de17084
4 changed files with 59 additions and 64 deletions

View File

@@ -1,5 +1,5 @@
.wrapper { .wrapper {
padding: 0 1.5rem; padding: 0.625rem 1.5rem 0;
padding-top: 0.75rem; border-bottom: 1px solid var(--border);
padding-bottom: 0; background: var(--surface);
} }

View File

@@ -229,7 +229,7 @@ function LayoutContent() {
return ( return (
<AppShell <AppShell
sidebar={ sidebar={
<div onClick={handleSidebarClick}> <div onClick={handleSidebarClick} style={{ height: '100%' }}>
<Sidebar apps={sidebarApps} /> <Sidebar apps={sidebarApps} />
</div> </div>
} }

View File

@@ -1,10 +1,16 @@
.threeColumn { .splitView {
display: grid; display: grid;
grid-template-columns: 280px 1fr; grid-template-columns: 1fr 1fr;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
.leftPanel {
overflow: auto;
height: 100%;
border-right: 1px solid var(--border);
}
.rightPanel { .rightPanel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1,17 +1,15 @@
import { useState, useMemo, useCallback } from 'react'; import { useState, useMemo, useCallback } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { useGlobalFilters } from '@cameleer/design-system'; import { useGlobalFilters } from '@cameleer/design-system';
import { useSearchExecutions, useExecutionDetail } from '../../api/queries/executions'; import { useExecutionDetail } from '../../api/queries/executions';
import { useDiagramByRoute } from '../../api/queries/diagrams'; import { useDiagramByRoute } from '../../api/queries/diagrams';
import { useRouteCatalog } from '../../api/queries/catalog'; import { useRouteCatalog } from '../../api/queries/catalog';
import type { ExecutionSummary } from '../../api/types';
import { ExchangeList } from './ExchangeList';
import { ExchangeHeader } from './ExchangeHeader'; import { ExchangeHeader } from './ExchangeHeader';
import { ExecutionDiagram } from '../../components/ExecutionDiagram/ExecutionDiagram'; import { ExecutionDiagram } from '../../components/ExecutionDiagram/ExecutionDiagram';
import { ProcessDiagram } from '../../components/ProcessDiagram'; import { ProcessDiagram } from '../../components/ProcessDiagram';
import styles from './ExchangesPage.module.css'; import styles from './ExchangesPage.module.css';
// Lazy-import the full-width Dashboard for the no-route-scope view // The full-width Dashboard table — used at every scope level
import Dashboard from '../Dashboard/Dashboard'; import Dashboard from '../Dashboard/Dashboard';
export default function ExchangesPage() { export default function ExchangesPage() {
@@ -19,45 +17,44 @@ export default function ExchangesPage() {
appId?: string; routeId?: string; exchangeId?: string; appId?: string; routeId?: string; exchangeId?: string;
}>(); }>();
// If no route is scoped, render the existing full-width Dashboard table // No route scoped: full-width Dashboard
if (!routeId) { if (!routeId) {
return <Dashboard />; return <Dashboard />;
} }
// Route is scoped: render 3-column layout // Route scoped: 50:50 split — Dashboard table on left, diagram on right
return ( return (
<RouteExchangeView appId={appId!} routeId={routeId} initialExchangeId={exchangeId} /> <div className={styles.splitView}>
<div className={styles.leftPanel}>
<Dashboard />
</div>
<div className={styles.rightPanel}>
<DiagramPanel appId={appId!} routeId={routeId} exchangeId={exchangeId} />
</div>
</div>
); );
} }
// ─── 3-column view when route is scoped ───────────────────────────────────── // ─── Right panel: diagram + optional execution overlay ──────────────────────
interface RouteExchangeViewProps { interface DiagramPanelProps {
appId: string; appId: string;
routeId: string; routeId: string;
initialExchangeId?: string; exchangeId?: string;
} }
function RouteExchangeView({ appId, routeId, initialExchangeId }: RouteExchangeViewProps) { function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) {
const [selectedExchangeId, setSelectedExchangeId] = useState<string | undefined>(initialExchangeId);
const { timeRange } = useGlobalFilters(); const { timeRange } = useGlobalFilters();
const timeFrom = timeRange.start.toISOString(); const timeFrom = timeRange.start.toISOString();
const timeTo = timeRange.end.toISOString(); const timeTo = timeRange.end.toISOString();
// Fetch exchanges for this route // Fetch execution detail if an exchange is selected
const { data: searchResult } = useSearchExecutions( const { data: detail } = useExecutionDetail(exchangeId ?? null);
{ timeFrom, timeTo, routeId, application: appId, sortField: 'startTime', sortDir: 'desc', offset: 0, limit: 50 },
true,
);
const exchanges: ExecutionSummary[] = searchResult?.data || [];
// Fetch execution detail for selected exchange // Fetch diagram for topology-only view
const { data: detail } = useExecutionDetail(selectedExchangeId ?? null);
// Fetch diagram for topology-only view (when no exchange selected)
const diagramQuery = useDiagramByRoute(appId, routeId); const diagramQuery = useDiagramByRoute(appId, routeId);
// Known route IDs for drill-down resolution // Known route IDs for drill-down
const { data: catalog } = useRouteCatalog(timeFrom, timeTo); const { data: catalog } = useRouteCatalog(timeFrom, timeTo);
const knownRouteIds = useMemo(() => { const knownRouteIds = useMemo(() => {
const ids = new Set<string>(); const ids = new Set<string>();
@@ -71,43 +68,35 @@ function RouteExchangeView({ appId, routeId, initialExchangeId }: RouteExchangeV
return ids; return ids;
}, [catalog]); }, [catalog]);
const handleExchangeSelect = useCallback((ex: ExecutionSummary) => { // If exchange selected: show header + ExecutionDiagram
setSelectedExchangeId(ex.executionId); if (exchangeId && detail) {
}, []); return (
<>
<ExchangeHeader detail={detail} />
<ExecutionDiagram
executionId={exchangeId}
executionDetail={detail}
knownRouteIds={knownRouteIds}
/>
</>
);
}
// No exchange: show topology-only ProcessDiagram
if (diagramQuery.data) {
return (
<ProcessDiagram
application={appId}
routeId={routeId}
diagramLayout={diagramQuery.data}
knownRouteIds={knownRouteIds}
/>
);
}
return ( return (
<div className={styles.threeColumn}> <div className={styles.emptyRight}>
<ExchangeList Loading diagram...
exchanges={exchanges}
selectedId={selectedExchangeId}
onSelect={handleExchangeSelect}
/>
<div className={styles.rightPanel}>
{selectedExchangeId && detail ? (
<>
<ExchangeHeader detail={detail} />
<ExecutionDiagram
executionId={selectedExchangeId}
executionDetail={detail}
knownRouteIds={knownRouteIds}
/>
</>
) : (
diagramQuery.data ? (
<ProcessDiagram
application={appId}
routeId={routeId}
diagramLayout={diagramQuery.data}
knownRouteIds={knownRouteIds}
/>
) : (
<div className={styles.emptyRight}>
Select an exchange to view execution details
</div>
)
)}
</div>
</div> </div>
); );
} }