refactor: UI consistency — shared CSS, design system colors, no inline styles
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:
6
ui/src/pages/Admin/AdminLayout.module.css
Normal file
6
ui/src/pages/Admin/AdminLayout.module.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
padding: 20px 24px 40px;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 & Taps</SectionHeader>
|
||||
<span className={styles.sectionSummary}>
|
||||
{tracedCount} traced · {tapCount} taps · 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
44
ui/src/pages/Admin/DatabaseAdminPage.module.css
Normal file
44
ui/src/pages/Admin/DatabaseAdminPage.module.css
Normal 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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user