refactor: UI consistency — shared CSS, design system colors, no inline styles
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m22s
CI / docker (push) Successful in 1m9s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s

Phase 1: Extract 6 shared CSS modules (table-section, log-panel,
rate-colors, refresh-indicator, chart-card, section-card) eliminating
~135 duplicate class definitions across 11 files.

Phase 2: Replace all hardcoded hex colors in CSS modules with design
system variables. Strip ~55 hex fallbacks from var() patterns. Fix 4
undefined variable names (--accent, --bg-base, --surface, --bg-surface-raised).

Phase 3: Replace ~45 hardcoded hex values in ProcessDiagram SVG
components with var() CSS custom properties. Fix Dashboard.tsx color prop.

Phase 4: Create CSS modules for AdminLayout, DatabaseAdminPage,
OidcCallback (previously 100% inline). Extract shared PageLoader
component (replaces 3 copy-pasted spinner patterns). Move AppsTab
static inline styles to CSS classes. Extract LayoutShell StarredList styles.

58 files changed, net -219 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-09 14:55:54 +02:00
parent bfed8174ca
commit ff62a34d89
58 changed files with 770 additions and 989 deletions

View File

@@ -0,0 +1,6 @@
.content {
flex: 1;
overflow: auto;
min-height: 0;
padding: 20px 24px 40px;
}

View File

@@ -1,8 +1,9 @@
import { Outlet } from 'react-router';
import styles from './AdminLayout.module.css';
export default function AdminLayout() {
return (
<div style={{ flex: 1, overflow: 'auto', minHeight: 0, padding: '20px 24px 40px' }}>
<div className={styles.content}>
<Outlet />
</div>
);

View File

@@ -103,18 +103,6 @@
color: var(--text-muted);
}
.section {
margin-bottom: 16px;
display: flex;
flex-direction: column;
gap: 12px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
padding: 16px 20px;
}
.sectionSummary {
font-size: 12px;
color: var(--text-muted);

View File

@@ -11,6 +11,7 @@ import { useCatalog } from '../../api/queries/catalog';
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
import { applyTracedProcessorUpdate, applyRouteRecordingUpdate } from '../../utils/config-draft-utils';
import styles from './AppConfigDetailPage.module.css';
import sectionStyles from '../../styles/section-card.module.css';
type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto';
@@ -325,7 +326,7 @@ export default function AppConfigDetailPage() {
</div>
{/* ── Settings ──────────────────────────────────────────────────── */}
<div className={styles.section}>
<div className={sectionStyles.section}>
<SectionHeader>Settings</SectionHeader>
<div className={styles.settingsGrid}>
<div className={styles.field}>
@@ -424,7 +425,7 @@ export default function AppConfigDetailPage() {
</div>
{/* ── Traces & Taps ─────────────────────────────────────────────── */}
<div className={styles.section}>
<div className={sectionStyles.section}>
<SectionHeader>Traces &amp; Taps</SectionHeader>
<span className={styles.sectionSummary}>
{tracedCount} traced &middot; {tapCount} taps &middot; manage taps on route pages
@@ -440,7 +441,7 @@ export default function AppConfigDetailPage() {
</div>
{/* ── Route Recording ───────────────────────────────────────────── */}
<div className={styles.section}>
<div className={sectionStyles.section}>
<SectionHeader>Route Recording</SectionHeader>
<span className={styles.sectionSummary}>
{recordingCount} of {routeRecordingRows.length} routes recording

View File

@@ -13,39 +13,6 @@
width: 160px;
}
.tableSection {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
overflow: hidden;
}
.tableHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border-subtle);
}
.tableTitle {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.tableRight {
display: flex;
align-items: center;
gap: 10px;
}
.tableMeta {
font-size: 12px;
color: var(--text-muted);
}
.target {
display: inline-block;
max-width: 220px;

View File

@@ -5,6 +5,7 @@ import {
import type { Column } from '@cameleer/design-system';
import { useAuditLog, type AuditEvent } from '../../api/queries/admin/audit';
import styles from './AuditLogPage.module.css';
import tableStyles from '../../styles/table-section.module.css';
const CATEGORIES = [
{ value: '', label: 'All categories' },
@@ -117,11 +118,11 @@ export default function AuditLogPage() {
/>
</div>
<div className={styles.tableSection}>
<div className={styles.tableHeader}>
<span className={styles.tableTitle}>Audit Log</span>
<div className={styles.tableRight}>
<span className={styles.tableMeta}>
<div className={tableStyles.tableSection}>
<div className={tableStyles.tableHeader}>
<span className={tableStyles.tableTitle}>Audit Log</span>
<div className={tableStyles.tableRight}>
<span className={tableStyles.tableMeta}>
{totalCount} events
</span>
<Badge label="AUTO" color="success" />

View File

@@ -34,32 +34,7 @@
}
.tableSection {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
margin-bottom: 16px;
overflow: hidden;
}
.tableHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border-subtle);
}
.tableTitle {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.tableMeta {
font-size: 12px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.queryText {

View File

@@ -2,6 +2,7 @@ import { StatCard, DataTable, ProgressBar } from '@cameleer/design-system';
import type { Column } from '@cameleer/design-system';
import { useClickHouseStatus, useClickHouseTables, useClickHousePerformance, useClickHouseQueries, useIndexerPipeline } from '../../api/queries/admin/clickhouse';
import styles from './ClickHouseAdminPage.module.css';
import tableStyles from '../../styles/table-section.module.css';
export default function ClickHouseAdminPage() {
const { data: status, isError: statusError } = useClickHouseStatus();
@@ -65,10 +66,10 @@ export default function ClickHouseAdminPage() {
)}
{/* Tables */}
<div className={styles.tableSection}>
<div className={styles.tableHeader}>
<span className={styles.tableTitle}>Tables ({(tables || []).length})</span>
{totalSizeLabel && <span className={styles.tableMeta}>{totalSizeLabel} total</span>}
<div className={`${tableStyles.tableSection} ${styles.tableSection}`}>
<div className={tableStyles.tableHeader}>
<span className={tableStyles.tableTitle}>Tables ({(tables || []).length})</span>
{totalSizeLabel && <span className={tableStyles.tableMeta}>{totalSizeLabel} total</span>}
</div>
<DataTable
columns={tableColumns}
@@ -80,9 +81,9 @@ export default function ClickHouseAdminPage() {
</div>
{/* Active Queries */}
<div className={styles.tableSection}>
<div className={styles.tableHeader}>
<span className={styles.tableTitle}>Active Queries ({(queries || []).length})</span>
<div className={`${tableStyles.tableSection} ${styles.tableSection}`}>
<div className={tableStyles.tableHeader}>
<span className={tableStyles.tableTitle}>Active Queries ({(queries || []).length})</span>
</div>
<DataTable
columns={queryColumns}

View File

@@ -0,0 +1,44 @@
.pageTitle {
font-size: 13px;
font-weight: 600;
margin-bottom: 1rem;
}
.statStrip {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.cardBody {
padding: 1rem;
}
.sectionTitle {
font-size: 13px;
font-weight: 600;
margin-bottom: 0.5rem;
}
.poolStats {
display: flex;
gap: 2rem;
margin-top: 0.5rem;
font-size: 0.875rem;
}
.section {
margin-top: 1.5rem;
}
.sectionHeading {
font-size: 13px;
font-weight: 600;
margin-bottom: 0.75rem;
}
.querySnippet {
font-size: 0.75rem;
font-family: var(--font-mono);
}

View File

@@ -1,6 +1,7 @@
import { StatCard, Card, DataTable, Badge, Button, ProgressBar, Spinner } from '@cameleer/design-system';
import type { Column } from '@cameleer/design-system';
import { useDatabaseStatus, useConnectionPool, useDatabaseTables, useActiveQueries, useKillQuery } from '../../api/queries/admin/database';
import styles from './DatabaseAdminPage.module.css';
export default function DatabaseAdminPage() {
const { data: status, isError: statusError } = useDatabaseStatus();
@@ -23,7 +24,7 @@ export default function DatabaseAdminPage() {
{ key: 'pid', header: 'PID' },
{ key: 'durationSeconds', header: 'Duration', render: (v) => `${v}s` },
{ key: 'state', header: 'State', render: (v) => <Badge label={String(v)} /> },
{ key: 'query', header: 'Query', render: (v) => <span style={{ fontSize: '0.75rem', fontFamily: 'var(--font-mono)' }}>{String(v).slice(0, 80)}</span> },
{ key: 'query', header: 'Query', render: (v) => <span className={styles.querySnippet}>{String(v).slice(0, 80)}</span> },
{
key: 'pid', header: '', width: '80px',
render: (v) => <Button variant="danger" size="sm" onClick={() => killQuery.mutate(v as number)}>Kill</Button>,
@@ -32,19 +33,19 @@ export default function DatabaseAdminPage() {
return (
<div>
<h2 style={{ marginBottom: '1rem' }}>Database Administration</h2>
<div className={styles.pageTitle}>Database Administration</div>
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
<div className={styles.statStrip}>
<StatCard label="Status" value={unreachable ? 'Disconnected' : status ? 'Connected' : '\u2014'} accent={unreachable ? 'error' : status ? 'success' : undefined} />
<StatCard label="Version" value={status?.version ?? '—'} />
</div>
{pool && (
<Card>
<div style={{ padding: '1rem' }}>
<h3 style={{ marginBottom: '0.5rem' }}>Connection Pool</h3>
<div className={styles.cardBody}>
<div className={styles.sectionTitle}>Connection Pool</div>
<ProgressBar value={poolPct} />
<div style={{ display: 'flex', gap: '2rem', marginTop: '0.5rem', fontSize: '0.875rem' }}>
<div className={styles.poolStats}>
<span>Active: {pool.activeConnections}</span>
<span>Idle: {pool.idleConnections}</span>
<span>Max: {pool.maximumPoolSize}</span>
@@ -53,13 +54,13 @@ export default function DatabaseAdminPage() {
</Card>
)}
<div style={{ marginTop: '1.5rem' }}>
<h3 style={{ marginBottom: '0.75rem' }}>Tables</h3>
<div className={styles.section}>
<div className={styles.sectionHeading}>Tables</div>
<DataTable columns={tableColumns} data={(tables || []).map((t: any) => ({ ...t, id: t.tableName }))} sortable pageSize={20} />
</div>
<div style={{ marginTop: '1.5rem' }}>
<h3 style={{ marginBottom: '0.75rem' }}>Active Queries</h3>
<div className={styles.section}>
<div className={styles.sectionHeading}>Active Queries</div>
<DataTable columns={queryColumns} data={(queries || []).map((q: any) => ({ ...q, id: String(q.pid) }))} />
</div>
</div>

View File

@@ -10,18 +10,6 @@
margin-bottom: 20px;
}
.section {
margin-bottom: 16px;
display: flex;
flex-direction: column;
gap: 12px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
padding: 16px 20px;
}
.toggleRow {
display: flex;
align-items: center;

View File

@@ -5,6 +5,7 @@ import {
import { useToast } from '@cameleer/design-system';
import { adminFetch } from '../../api/queries/admin/admin-api';
import styles from './OidcConfigPage.module.css';
import sectionStyles from '../../styles/section-card.module.css';
interface OidcFormData {
enabled: boolean;
@@ -137,7 +138,7 @@ export default function OidcConfigPage() {
{error && <div style={{ marginBottom: 16 }}><Alert variant="error">{error}</Alert></div>}
<section className={styles.section}>
<section className={sectionStyles.section}>
<SectionHeader>Behavior</SectionHeader>
<div className={styles.toggleRow}>
<Toggle
@@ -156,7 +157,7 @@ export default function OidcConfigPage() {
</div>
</section>
<section className={styles.section}>
<section className={sectionStyles.section}>
<SectionHeader>Provider Settings</SectionHeader>
<FormField label="Issuer URI" htmlFor="issuer">
<Input
@@ -200,7 +201,7 @@ export default function OidcConfigPage() {
</FormField>
</section>
<section className={styles.section}>
<section className={sectionStyles.section}>
<SectionHeader>Claim Mapping</SectionHeader>
<FormField label="Roles Claim" htmlFor="roles-claim" hint="JSON path to roles in the access token or ID token">
<Input
@@ -225,7 +226,7 @@ export default function OidcConfigPage() {
</FormField>
</section>
<section className={styles.section}>
<section className={sectionStyles.section}>
<SectionHeader>Default Roles</SectionHeader>
<div className={styles.tagList}>
{(form.defaultRoles || []).map((role) => (
@@ -249,7 +250,7 @@ export default function OidcConfigPage() {
</div>
</section>
<section className={styles.section}>
<section className={sectionStyles.section}>
<SectionHeader>Danger Zone</SectionHeader>
<Button size="sm" variant="danger" onClick={() => setDeleteOpen(true)}>
Delete OIDC Configuration