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

@@ -0,0 +1,57 @@
name: SonarQube
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
sonarqube:
runs-on: ubuntu-latest
container:
image: gitea.siegeln.net/cameleer/cameleer-build:1
credentials:
username: cameleer
password: ${{ secrets.REGISTRY_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Gitea Maven Registry
run: |
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << 'SETTINGS'
<settings>
<servers>
<server>
<id>gitea</id>
<username>cameleer</username>
<password>${env.REGISTRY_TOKEN}</password>
</server>
</servers>
</settings>
SETTINGS
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-maven-
- name: Build and Test with coverage
run: mvn clean verify -DskipITs -U --batch-mode
- name: SonarQube Analysis
run: |
mvn sonar:sonar --batch-mode \
-Dsonar.host.url="$SONAR_HOST_URL" \
-Dsonar.token="$SONAR_TOKEN" \
-Dsonar.projectKey=cameleer3-server \
-Dsonar.projectName="Cameleer3 Server"
env:
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

8
ui/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "ui",
"version": "0.0.0",
"dependencies": {
"@cameleer/design-system": "^0.1.18",
"@cameleer/design-system": "^0.1.19",
"@tanstack/react-query": "^5.90.21",
"lucide-react": "^1.7.0",
"openapi-fetch": "^0.17.0",
@@ -277,9 +277,9 @@
}
},
"node_modules/@cameleer/design-system": {
"version": "0.1.18",
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.18/design-system-0.1.18.tgz",
"integrity": "sha512-uvGr4PFw6Eya+h9DSD0wBnzjIXhZpcndR2dDJX2tMvQqgy+32WTTTQ8BZZWZjOKLSv63UpBN/fwVSXtkA4dnqA==",
"version": "0.1.19",
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.19/design-system-0.1.19.tgz",
"integrity": "sha512-YpYJysWycqRiTMco3Fco8AIatJz/IU7EecTmUQLUrkcBUGfHOWzCTMYm47jRvNAjQrANoanYnXPKXRHg91NS2w==",
"dependencies": {
"lucide-react": "^1.7.0",
"react": "^19.0.0",

View File

@@ -14,7 +14,7 @@
"generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts"
},
"dependencies": {
"@cameleer/design-system": "^0.1.18",
"@cameleer/design-system": "^0.1.19",
"@tanstack/react-query": "^5.90.21",
"lucide-react": "^1.7.0",
"openapi-fetch": "^0.17.0",

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;
}