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

View File

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

View File

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

View File

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

View File

@@ -5,25 +5,8 @@
flex: 1; flex: 1;
min-height: 0; min-height: 0;
min-width: 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; overflow: hidden;
} background: var(--bg-body);
.tableScroll {
flex: 1;
min-height: 0;
overflow-y: auto;
} }
.tableHeader { .tableHeader {

View File

@@ -236,11 +236,7 @@ export default function Dashboard({ onExchangeSelect }: DashboardProps = {}) {
} }
return ( return (
<>
{/* Scrollable content */}
<div className={styles.content}> <div className={styles.content}>
{/* Exchanges table */}
<div className={styles.tableSection}>
<div className={styles.tableHeader}> <div className={styles.tableHeader}>
<span className={styles.tableTitle}> <span className={styles.tableTitle}>
{textFilter ? ( {textFilter ? (
@@ -265,7 +261,6 @@ export default function Dashboard({ onExchangeSelect }: DashboardProps = {}) {
</div> </div>
</div> </div>
<div className={styles.tableScroll}>
<DataTable <DataTable
columns={columns} columns={columns}
data={rows} data={rows}
@@ -273,6 +268,7 @@ export default function Dashboard({ onExchangeSelect }: DashboardProps = {}) {
selectedId={selectedId} selectedId={selectedId}
sortable sortable
flush flush
fillHeight
onSortChange={handleSortChange} onSortChange={handleSortChange}
rowAccent={handleRowAccent} rowAccent={handleRowAccent}
expandedContent={(row: Row) => expandedContent={(row: Row) =>
@@ -288,8 +284,5 @@ export default function Dashboard({ onExchangeSelect }: DashboardProps = {}) {
} }
/> />
</div> </div>
</div>
</div>
</>
) )
} }

View File

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