feat: upgrade design system to v0.1.19, use onNavigate/fillHeight, add SonarQube workflow
All checks were successful
CI / build (push) Successful in 1m36s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 2m10s
CI / deploy (push) Successful in 50s
CI / deploy-feature (push) Has been skipped

- Use Sidebar onNavigate callback instead of display:contents click interception
- Use DataTable fillHeight prop instead of manual scroll wrapper divs
- Fix DataTable scroll/pagination by adding overflow:hidden to content container
- Fix left panel in split view to use flex column instead of overflow:auto
- Make error tab stack trace scrollable for large traces
- Add nightly SonarQube workflow with manual trigger support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-28 16:57:12 +01:00
parent f59423bc91
commit 27249c2440
9 changed files with 130 additions and 93 deletions

View File

@@ -447,6 +447,11 @@
overflow-y: auto;
}
.errorStackWrap pre {
max-height: 50vh;
overflow-y: auto;
}
.errorStackLabel {
font-size: 10px;
font-weight: 600;

View File

@@ -38,7 +38,9 @@ export function ErrorTab({ processor, executionDetail }: ErrorTabProps) {
{errorStackTrace && (
<>
<div className={styles.errorStackLabel}>Stack Trace</div>
<CodeBlock content={errorStackTrace} copyable />
<div className={styles.errorStackWrap}>
<CodeBlock content={errorStackTrace} copyable />
</div>
</>
)}
</div>

View File

@@ -222,36 +222,31 @@ function LayoutContent() {
navigate(`${baseParts.join('/')}?text=${encodeURIComponent(query)}`);
}, [navigate, scope.appId, scope.routeId]);
// Intercept Sidebar's internal <Link> navigation to re-route through current tab
const handleSidebarClick = useCallback((e: React.MouseEvent) => {
const anchor = (e.target as HTMLElement).closest('a[href]');
if (!anchor) return;
const href = anchor.getAttribute('href') || '';
// Intercept /apps/:appId and /apps/:appId/:routeId links
const appMatch = href.match(/^\/apps\/([^/]+)(?:\/(.+))?$/);
// Translate Sidebar's internal paths to our URL structure
const handleSidebarNavigate = useCallback((path: string) => {
// /apps/:appId and /apps/:appId/:routeId → current tab
const appMatch = path.match(/^\/apps\/([^/]+)(?:\/(.+))?$/);
if (appMatch) {
e.preventDefault();
const [, sAppId, sRouteId] = appMatch;
navigate(sRouteId ? `/${scope.tab}/${sAppId}/${sRouteId}` : `/${scope.tab}/${sAppId}`);
return;
}
// Intercept /agents/* links — redirect to runtime tab
const agentMatch = href.match(/^\/agents\/([^/]+)(?:\/(.+))?$/);
// /agents/:appId/:instanceId → runtime tab
const agentMatch = path.match(/^\/agents\/([^/]+)(?:\/(.+))?$/);
if (agentMatch) {
e.preventDefault();
const [, sAppId, sInstanceId] = agentMatch;
navigate(sInstanceId ? `/runtime/${sAppId}/${sInstanceId}` : `/runtime/${sAppId}`);
return;
}
navigate(path);
}, [navigate, scope.tab]);
return (
<AppShell
sidebar={
<div onClick={handleSidebarClick} style={{ display: 'contents' }}>
<Sidebar apps={sidebarApps} />
</div>
<Sidebar apps={sidebarApps} onNavigate={handleSidebarNavigate} />
}
>
<TopBar

View File

@@ -5,25 +5,8 @@
flex: 1;
min-height: 0;
min-width: 0;
background: var(--bg-body);
}
/* Table section — stretches to fill and scrolls internally */
.tableSection {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
background: var(--bg-surface);
overflow: hidden;
}
.tableScroll {
flex: 1;
min-height: 0;
overflow-y: auto;
background: var(--bg-body);
}
.tableHeader {

View File

@@ -236,60 +236,53 @@ export default function Dashboard({ onExchangeSelect }: DashboardProps = {}) {
}
return (
<>
{/* Scrollable content */}
<div className={styles.content}>
{/* Exchanges table */}
<div className={styles.tableSection}>
<div className={styles.tableHeader}>
<span className={styles.tableTitle}>
{textFilter ? (
<>
<Search size={14} style={{ marginRight: 4, verticalAlign: -2 }} />
Search: &ldquo;{textFilter}&rdquo;
<button
className={styles.clearSearch}
onClick={() => setSearchParams({})}
title="Clear search"
>
<X size={12} />
</button>
</>
) : 'Recent Exchanges'}
</span>
<div className={styles.tableRight}>
<span className={styles.tableMeta}>
{rows.length.toLocaleString()} of {(searchResult?.total ?? 0).toLocaleString()} exchanges
</span>
{!textFilter && <Badge label="LIVE" color="success" />}
</div>
</div>
<div className={styles.tableScroll}>
<DataTable
columns={columns}
data={rows}
onRowClick={handleRowClick}
selectedId={selectedId}
sortable
flush
onSortChange={handleSortChange}
rowAccent={handleRowAccent}
expandedContent={(row: Row) =>
row.errorMessage ? (
<div className={styles.inlineError}>
<span className={styles.inlineErrorIcon}><AlertTriangle size={14} /></span>
<div>
<div className={styles.inlineErrorText}>{row.errorMessage}</div>
<div className={styles.inlineErrorHint}>Click to view full stack trace</div>
</div>
</div>
) : null
}
/>
</div>
<div className={styles.content}>
<div className={styles.tableHeader}>
<span className={styles.tableTitle}>
{textFilter ? (
<>
<Search size={14} style={{ marginRight: 4, verticalAlign: -2 }} />
Search: &ldquo;{textFilter}&rdquo;
<button
className={styles.clearSearch}
onClick={() => setSearchParams({})}
title="Clear search"
>
<X size={12} />
</button>
</>
) : 'Recent Exchanges'}
</span>
<div className={styles.tableRight}>
<span className={styles.tableMeta}>
{rows.length.toLocaleString()} of {(searchResult?.total ?? 0).toLocaleString()} exchanges
</span>
{!textFilter && <Badge label="LIVE" color="success" />}
</div>
</div>
</>
<DataTable
columns={columns}
data={rows}
onRowClick={handleRowClick}
selectedId={selectedId}
sortable
flush
fillHeight
onSortChange={handleSortChange}
rowAccent={handleRowAccent}
expandedContent={(row: Row) =>
row.errorMessage ? (
<div className={styles.inlineError}>
<span className={styles.inlineErrorIcon}><AlertTriangle size={14} /></span>
<div>
<div className={styles.inlineErrorText}>{row.errorMessage}</div>
<div className={styles.inlineErrorHint}>Click to view full stack trace</div>
</div>
</div>
) : null
}
/>
</div>
)
}

View File

@@ -5,7 +5,9 @@
}
.leftPanel {
overflow: auto;
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
flex-shrink: 0;
}