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
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--amber);
|
||||
color: #fff;
|
||||
color: var(--bg-surface);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
@@ -308,7 +308,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 80px;
|
||||
background: var(--bg-surface-raised);
|
||||
background: var(--bg-raised);
|
||||
border: 1px dashed var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 12px;
|
||||
@@ -323,96 +323,6 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Log viewer */
|
||||
.logCard {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 420px;
|
||||
}
|
||||
|
||||
.logHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.logToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.logSearchWrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.logSearchInput {
|
||||
width: 100%;
|
||||
padding: 5px 28px 5px 10px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-body);
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-family: var(--font-body);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.logSearchInput:focus {
|
||||
border-color: var(--amber);
|
||||
}
|
||||
|
||||
.logSearchInput::placeholder {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.logSearchClear {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.logClearFilters {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logClearFilters:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.logEmpty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: var(--text-faint);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Event card (timeline panel) */
|
||||
.eventCard {
|
||||
background: var(--bg-surface);
|
||||
@@ -433,26 +343,3 @@
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sortBtn,
|
||||
.refreshBtn {
|
||||
background: none;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
padding: 2px 6px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sortBtn:hover,
|
||||
.refreshBtn:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--amber);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@cameleer/design-system';
|
||||
import type { Column, FeedEvent, LogEntry, ButtonGroupItem } from '@cameleer/design-system';
|
||||
import styles from './AgentHealth.module.css';
|
||||
import logStyles from '../../styles/log-panel.module.css';
|
||||
import { useAgents, useAgentEvents } from '../../api/queries/agents';
|
||||
import { useApplicationLogs } from '../../api/queries/logs';
|
||||
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
||||
@@ -508,24 +509,24 @@ export default function AgentHealth() {
|
||||
|
||||
{/* Log + Timeline side by side */}
|
||||
<div className={styles.bottomRow}>
|
||||
<div className={styles.logCard}>
|
||||
<div className={styles.logHeader}>
|
||||
<div className={logStyles.logCard}>
|
||||
<div className={logStyles.logHeader}>
|
||||
<SectionHeader>Application Log</SectionHeader>
|
||||
<div className={styles.headerActions}>
|
||||
<div className={logStyles.headerActions}>
|
||||
<span className={styles.sectionMeta}>{logEntries.length} entries</span>
|
||||
<button className={styles.sortBtn} onClick={() => setLogSortAsc((v) => !v)} title={logSortAsc ? 'Oldest first' : 'Newest first'}>
|
||||
<button className={logStyles.sortBtn} onClick={() => setLogSortAsc((v) => !v)} title={logSortAsc ? 'Oldest first' : 'Newest first'}>
|
||||
{logSortAsc ? '\u2191' : '\u2193'}
|
||||
</button>
|
||||
<button className={styles.refreshBtn} onClick={() => setLogRefreshTo(new Date().toISOString())} title="Refresh">
|
||||
<button className={logStyles.refreshBtn} onClick={() => setLogRefreshTo(new Date().toISOString())} title="Refresh">
|
||||
<RefreshCw size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.logToolbar}>
|
||||
<div className={styles.logSearchWrap}>
|
||||
<div className={logStyles.logToolbar}>
|
||||
<div className={logStyles.logSearchWrap}>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.logSearchInput}
|
||||
className={logStyles.logSearchInput}
|
||||
placeholder="Search logs\u2026"
|
||||
value={logSearch}
|
||||
onChange={(e) => setLogSearch(e.target.value)}
|
||||
@@ -534,7 +535,7 @@ export default function AgentHealth() {
|
||||
{logSearch && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.logSearchClear}
|
||||
className={logStyles.logSearchClear}
|
||||
onClick={() => setLogSearch('')}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
@@ -544,7 +545,7 @@ export default function AgentHealth() {
|
||||
</div>
|
||||
<ButtonGroup items={LOG_LEVEL_ITEMS} value={logLevels} onChange={setLogLevels} />
|
||||
{logLevels.size > 0 && (
|
||||
<button className={styles.logClearFilters} onClick={() => setLogLevels(new Set())}>
|
||||
<button className={logStyles.logClearFilters} onClick={() => setLogLevels(new Set())}>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
@@ -552,7 +553,7 @@ export default function AgentHealth() {
|
||||
{filteredLogs.length > 0 ? (
|
||||
<LogViewer entries={filteredLogs} maxHeight={360} />
|
||||
) : (
|
||||
<div className={styles.logEmpty}>
|
||||
<div className={logStyles.logEmpty}>
|
||||
{logSearch || logLevels.size > 0 ? 'No matching log entries' : 'No log entries available'}
|
||||
</div>
|
||||
)}
|
||||
@@ -561,12 +562,12 @@ export default function AgentHealth() {
|
||||
<div className={styles.eventCard}>
|
||||
<div className={styles.eventCardHeader}>
|
||||
<span className={styles.sectionTitle}>Timeline</span>
|
||||
<div className={styles.headerActions}>
|
||||
<div className={logStyles.headerActions}>
|
||||
<span className={styles.sectionMeta}>{feedEvents.length} events</span>
|
||||
<button className={styles.sortBtn} onClick={() => setEventSortAsc((v) => !v)} title={eventSortAsc ? 'Oldest first' : 'Newest first'}>
|
||||
<button className={logStyles.sortBtn} onClick={() => setEventSortAsc((v) => !v)} title={eventSortAsc ? 'Oldest first' : 'Newest first'}>
|
||||
{eventSortAsc ? '\u2191' : '\u2193'}
|
||||
</button>
|
||||
<button className={styles.refreshBtn} onClick={() => setEventRefreshTo(new Date().toISOString())} title="Refresh">
|
||||
<button className={logStyles.refreshBtn} onClick={() => setEventRefreshTo(new Date().toISOString())} title="Refresh">
|
||||
<RefreshCw size={14} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -574,7 +575,7 @@ export default function AgentHealth() {
|
||||
{feedEvents.length > 0 ? (
|
||||
<EventFeed events={feedEvents} maxItems={100} />
|
||||
) : (
|
||||
<div className={styles.logEmpty}>No events in the selected time range.</div>
|
||||
<div className={logStyles.logEmpty}>No events in the selected time range.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -90,15 +90,6 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chartCard {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chartHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -125,121 +116,6 @@
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* Log viewer */
|
||||
.logCard {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 420px;
|
||||
}
|
||||
|
||||
.logHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sortBtn,
|
||||
.refreshBtn {
|
||||
background: none;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
padding: 2px 6px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sortBtn:hover,
|
||||
.refreshBtn:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--amber);
|
||||
}
|
||||
|
||||
.logToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.logSearchWrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.logSearchInput {
|
||||
width: 100%;
|
||||
padding: 5px 28px 5px 10px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-body);
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-family: var(--font-body);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.logSearchInput:focus {
|
||||
border-color: var(--amber);
|
||||
}
|
||||
|
||||
.logSearchInput::placeholder {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.logSearchClear {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.logClearFilters {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logClearFilters:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Empty state (shared) */
|
||||
.logEmpty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: var(--text-faint);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Timeline card */
|
||||
.timelineCard {
|
||||
background: var(--bg-surface);
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from '@cameleer/design-system';
|
||||
import type { FeedEvent, LogEntry, ButtonGroupItem } from '@cameleer/design-system';
|
||||
import styles from './AgentInstance.module.css';
|
||||
import logStyles from '../../styles/log-panel.module.css';
|
||||
import chartCardStyles from '../../styles/chart-card.module.css';
|
||||
import { useAgents, useAgentEvents } from '../../api/queries/agents';
|
||||
import { useApplicationLogs } from '../../api/queries/logs';
|
||||
import { useStatsTimeseries } from '../../api/queries/executions';
|
||||
@@ -290,7 +292,7 @@ export default function AgentInstance() {
|
||||
|
||||
{/* Charts grid — 3x2 */}
|
||||
<div className={styles.chartsGrid}>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<span className={styles.chartTitle}>CPU Usage</span>
|
||||
<span className={styles.chartMeta}>
|
||||
@@ -309,7 +311,7 @@ export default function AgentInstance() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.chartCard}>
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<span className={styles.chartTitle}>Memory (Heap)</span>
|
||||
<span className={styles.chartMeta}>
|
||||
@@ -325,7 +327,7 @@ export default function AgentInstance() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.chartCard}>
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<span className={styles.chartTitle}>Throughput</span>
|
||||
<span className={styles.chartMeta}>
|
||||
@@ -339,7 +341,7 @@ export default function AgentInstance() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.chartCard}>
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<span className={styles.chartTitle}>Error Rate</span>
|
||||
<span className={styles.chartMeta}>
|
||||
@@ -353,7 +355,7 @@ export default function AgentInstance() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.chartCard}>
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<span className={styles.chartTitle}>Thread Count</span>
|
||||
<span className={styles.chartMeta}>
|
||||
@@ -369,7 +371,7 @@ export default function AgentInstance() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.chartCard}>
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<span className={styles.chartTitle}>GC Pauses</span>
|
||||
<span className={styles.chartMeta} />
|
||||
@@ -384,24 +386,24 @@ export default function AgentInstance() {
|
||||
|
||||
{/* Log + Timeline side by side */}
|
||||
<div className={styles.bottomRow}>
|
||||
<div className={styles.logCard}>
|
||||
<div className={styles.logHeader}>
|
||||
<div className={logStyles.logCard}>
|
||||
<div className={logStyles.logHeader}>
|
||||
<SectionHeader>Application Log</SectionHeader>
|
||||
<div className={styles.headerActions}>
|
||||
<div className={logStyles.headerActions}>
|
||||
<span className={styles.chartMeta}>{logEntries.length} entries</span>
|
||||
<button className={styles.sortBtn} onClick={() => setLogSortAsc((v) => !v)} title={logSortAsc ? 'Oldest first' : 'Newest first'}>
|
||||
<button className={logStyles.sortBtn} onClick={() => setLogSortAsc((v) => !v)} title={logSortAsc ? 'Oldest first' : 'Newest first'}>
|
||||
{logSortAsc ? '\u2191' : '\u2193'}
|
||||
</button>
|
||||
<button className={styles.refreshBtn} onClick={() => setLogRefreshTo(new Date().toISOString())} title="Refresh">
|
||||
<button className={logStyles.refreshBtn} onClick={() => setLogRefreshTo(new Date().toISOString())} title="Refresh">
|
||||
<RefreshCw size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.logToolbar}>
|
||||
<div className={styles.logSearchWrap}>
|
||||
<div className={logStyles.logToolbar}>
|
||||
<div className={logStyles.logSearchWrap}>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.logSearchInput}
|
||||
className={logStyles.logSearchInput}
|
||||
placeholder="Search logs\u2026"
|
||||
value={logSearch}
|
||||
onChange={(e) => setLogSearch(e.target.value)}
|
||||
@@ -410,7 +412,7 @@ export default function AgentInstance() {
|
||||
{logSearch && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.logSearchClear}
|
||||
className={logStyles.logSearchClear}
|
||||
onClick={() => setLogSearch('')}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
@@ -420,7 +422,7 @@ export default function AgentInstance() {
|
||||
</div>
|
||||
<ButtonGroup items={LOG_LEVEL_ITEMS} value={logLevels} onChange={setLogLevels} />
|
||||
{logLevels.size > 0 && (
|
||||
<button className={styles.logClearFilters} onClick={() => setLogLevels(new Set())}>
|
||||
<button className={logStyles.logClearFilters} onClick={() => setLogLevels(new Set())}>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
@@ -428,7 +430,7 @@ export default function AgentInstance() {
|
||||
{filteredLogs.length > 0 ? (
|
||||
<LogViewer entries={filteredLogs} maxHeight={360} />
|
||||
) : (
|
||||
<div className={styles.logEmpty}>
|
||||
<div className={logStyles.logEmpty}>
|
||||
{logSearch || logLevels.size > 0 ? 'No matching log entries' : 'No log entries available'}
|
||||
</div>
|
||||
)}
|
||||
@@ -437,12 +439,12 @@ export default function AgentInstance() {
|
||||
<div className={styles.timelineCard}>
|
||||
<div className={styles.timelineHeader}>
|
||||
<span className={styles.chartTitle}>Timeline</span>
|
||||
<div className={styles.headerActions}>
|
||||
<div className={logStyles.headerActions}>
|
||||
<span className={styles.chartMeta}>{feedEvents.length} events</span>
|
||||
<button className={styles.sortBtn} onClick={() => setEventSortAsc((v) => !v)} title={eventSortAsc ? 'Oldest first' : 'Newest first'}>
|
||||
<button className={logStyles.sortBtn} onClick={() => setEventSortAsc((v) => !v)} title={eventSortAsc ? 'Oldest first' : 'Newest first'}>
|
||||
{eventSortAsc ? '\u2191' : '\u2193'}
|
||||
</button>
|
||||
<button className={styles.refreshBtn} onClick={() => setEventRefreshTo(new Date().toISOString())} title="Refresh">
|
||||
<button className={logStyles.refreshBtn} onClick={() => setEventRefreshTo(new Date().toISOString())} title="Refresh">
|
||||
<RefreshCw size={14} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -450,7 +452,7 @@ export default function AgentInstance() {
|
||||
{feedEvents.length > 0 ? (
|
||||
<EventFeed events={feedEvents} maxItems={50} />
|
||||
) : (
|
||||
<div className={styles.logEmpty}>No events in the selected time range.</div>
|
||||
<div className={logStyles.logEmpty}>No events in the selected time range.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
.stepIndicator {
|
||||
font-size: 12px;
|
||||
color: var(--accent, #6c7aff);
|
||||
color: var(--amber);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -143,8 +143,8 @@
|
||||
}
|
||||
|
||||
.subTabActive {
|
||||
color: var(--accent, #6c7aff);
|
||||
border-bottom-color: var(--accent, #6c7aff);
|
||||
color: var(--amber);
|
||||
border-bottom-color: var(--amber);
|
||||
}
|
||||
|
||||
/* Table */
|
||||
@@ -230,7 +230,7 @@
|
||||
|
||||
.editBannerActive {
|
||||
border-color: var(--warning);
|
||||
background: rgba(251, 191, 36, 0.06);
|
||||
background: color-mix(in srgb, var(--amber) 6%, transparent);
|
||||
}
|
||||
|
||||
.editBannerText {
|
||||
@@ -432,3 +432,44 @@
|
||||
.removeBtn:hover {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* Visually hidden file inputs */
|
||||
.visuallyHidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Fixed-width inputs */
|
||||
.inputXs {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.inputSm {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.inputMd {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.inputLg {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.inputXl {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
/* Table cell flex layout */
|
||||
.cellFlex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
<span className={styles.configLabel}>Application JAR</span>
|
||||
<div className={styles.fileRow}>
|
||||
<input ref={fileInputRef} type="file" accept=".jar"
|
||||
style={{ position: 'absolute', width: 1, height: 1, margin: -1, padding: 0, overflow: 'hidden', clip: 'rect(0,0,0,0)', border: 0 }}
|
||||
className={styles.visuallyHidden}
|
||||
onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
|
||||
<Button size="sm" variant="secondary" type="button" onClick={() => fileInputRef.current?.click()} disabled={busy}>
|
||||
{file ? 'Change file' : 'Select JAR'}
|
||||
@@ -355,8 +355,8 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
|
||||
<span className={styles.configLabel}>Max Payload Size</span>
|
||||
<div className={styles.configInline}>
|
||||
<Input disabled={busy} value={payloadSize} onChange={(e) => setPayloadSize(e.target.value)} style={{ width: 70 }} />
|
||||
<Select disabled={busy} value={payloadUnit} onChange={(e) => setPayloadUnit(e.target.value)} style={{ width: 90 }}
|
||||
<Input disabled={busy} value={payloadSize} onChange={(e) => setPayloadSize(e.target.value)} className={styles.inputMd} />
|
||||
<Select disabled={busy} value={payloadUnit} onChange={(e) => setPayloadUnit(e.target.value)} className={styles.inputXl}
|
||||
options={[{ value: 'bytes', label: 'bytes' }, { value: 'KB', label: 'KB' }, { value: 'MB', label: 'MB' }]} />
|
||||
</div>
|
||||
|
||||
@@ -373,12 +373,12 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
<Toggle checked={metricsEnabled} onChange={() => !busy && setMetricsEnabled(!metricsEnabled)} disabled={busy} />
|
||||
<span className={metricsEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{metricsEnabled ? 'Enabled' : 'Disabled'}</span>
|
||||
<span className={styles.cellMeta} style={{ marginLeft: 8 }}>Interval</span>
|
||||
<Input disabled={busy} value={metricsInterval} onChange={(e) => setMetricsInterval(e.target.value)} style={{ width: 50 }} />
|
||||
<Input disabled={busy} value={metricsInterval} onChange={(e) => setMetricsInterval(e.target.value)} className={styles.inputXs} />
|
||||
<span className={styles.cellMeta}>s</span>
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>Sampling Rate</span>
|
||||
<Input disabled={busy} value={samplingRate} onChange={(e) => setSamplingRate(e.target.value)} style={{ width: 80 }} />
|
||||
<Input disabled={busy} value={samplingRate} onChange={(e) => setSamplingRate(e.target.value)} className={styles.inputLg} />
|
||||
|
||||
<span className={styles.configLabel}>Compress Success</span>
|
||||
<div className={styles.configInline}>
|
||||
@@ -406,25 +406,25 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
<div className={styles.configGrid}>
|
||||
<span className={styles.configLabel}>Memory Limit</span>
|
||||
<div className={styles.configInline}>
|
||||
<Input disabled={busy} value={memoryLimit} onChange={(e) => setMemoryLimit(e.target.value)} style={{ width: 80 }} />
|
||||
<Input disabled={busy} value={memoryLimit} onChange={(e) => setMemoryLimit(e.target.value)} className={styles.inputLg} />
|
||||
<span className={styles.cellMeta}>MB</span>
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>Memory Reserve</span>
|
||||
<div>
|
||||
<div className={styles.configInline}>
|
||||
<Input disabled={!isProd || busy} value={memoryReserve} onChange={(e) => setMemoryReserve(e.target.value)} placeholder="---" style={{ width: 80 }} />
|
||||
<Input disabled={!isProd || busy} value={memoryReserve} onChange={(e) => setMemoryReserve(e.target.value)} placeholder="---" className={styles.inputLg} />
|
||||
<span className={styles.cellMeta}>MB</span>
|
||||
</div>
|
||||
{!isProd && <span className={styles.configHint}>Available in production environments only</span>}
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>CPU Request</span>
|
||||
<Input disabled={busy} value={cpuRequest} onChange={(e) => setCpuRequest(e.target.value)} style={{ width: 80 }} />
|
||||
<Input disabled={busy} value={cpuRequest} onChange={(e) => setCpuRequest(e.target.value)} className={styles.inputLg} />
|
||||
|
||||
<span className={styles.configLabel}>CPU Limit</span>
|
||||
<div className={styles.configInline}>
|
||||
<Input disabled={busy} value={cpuLimit} onChange={(e) => setCpuLimit(e.target.value)} placeholder="e.g. 1000" style={{ width: 80 }} />
|
||||
<Input disabled={busy} value={cpuLimit} onChange={(e) => setCpuLimit(e.target.value)} placeholder="e.g. 1000" className={styles.inputLg} />
|
||||
<span className={styles.cellMeta}>millicores</span>
|
||||
</div>
|
||||
|
||||
@@ -443,10 +443,10 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>App Port</span>
|
||||
<Input disabled={busy} value={appPort} onChange={(e) => setAppPort(e.target.value)} style={{ width: 80 }} />
|
||||
<Input disabled={busy} value={appPort} onChange={(e) => setAppPort(e.target.value)} className={styles.inputLg} />
|
||||
|
||||
<span className={styles.configLabel}>Replicas</span>
|
||||
<Input disabled={busy} value={replicas} onChange={(e) => setReplicas(e.target.value)} style={{ width: 60 }} type="number" />
|
||||
<Input disabled={busy} value={replicas} onChange={(e) => setReplicas(e.target.value)} className={styles.inputSm} type="number" />
|
||||
|
||||
<span className={styles.configLabel}>Deploy Strategy</span>
|
||||
<Select disabled={busy} value={deployStrategy} onChange={(e) => setDeployStrategy(e.target.value)}
|
||||
@@ -551,7 +551,7 @@ function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: s
|
||||
</div>
|
||||
<div className={styles.detailActions}>
|
||||
<input ref={fileInputRef} type="file" accept=".jar"
|
||||
style={{ position: 'absolute', width: 1, height: 1, margin: -1, padding: 0, overflow: 'hidden', clip: 'rect(0,0,0,0)', border: 0 }}
|
||||
className={styles.visuallyHidden}
|
||||
onChange={handleUpload} />
|
||||
<Button size="sm" variant="primary" type="button" onClick={() => fileInputRef.current?.click()} loading={uploadJar.isPending}>Upload JAR</Button>
|
||||
<Button size="sm" variant="danger" onClick={() => setDeleteConfirm(true)}>Delete App</Button>
|
||||
@@ -636,7 +636,7 @@ function OverviewSubTab({ app, deployments, versions, environments, envMap, sele
|
||||
<Badge label={dEnv?.displayName ?? '?'} color={dEnv?.production ? 'error' : 'auto'} />
|
||||
</td>
|
||||
<td><Badge label={version ? `v${version.version}` : '?'} color="auto" /></td>
|
||||
<td style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<td className={styles.cellFlex}>
|
||||
<StatusDot variant={DEPLOY_STATUS_DOT[d.status] ?? 'dead'} />
|
||||
<Badge label={d.status} color={STATUS_COLORS[d.status] ?? 'auto'} />
|
||||
</td>
|
||||
@@ -962,8 +962,8 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
||||
|
||||
<span className={styles.configLabel}>Max Payload Size</span>
|
||||
<div className={styles.configInline}>
|
||||
<Input disabled={!editing} value={payloadSize} onChange={(e) => setPayloadSize(e.target.value)} style={{ width: 70 }} />
|
||||
<Select disabled={!editing} value={payloadUnit} onChange={(e) => setPayloadUnit(e.target.value)} style={{ width: 90 }}
|
||||
<Input disabled={!editing} value={payloadSize} onChange={(e) => setPayloadSize(e.target.value)} className={styles.inputMd} />
|
||||
<Select disabled={!editing} value={payloadUnit} onChange={(e) => setPayloadUnit(e.target.value)} className={styles.inputXl}
|
||||
options={[{ value: 'bytes', label: 'bytes' }, { value: 'KB', label: 'KB' }, { value: 'MB', label: 'MB' }]} />
|
||||
</div>
|
||||
|
||||
@@ -980,12 +980,12 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
||||
<Toggle checked={metricsEnabled} onChange={() => editing && setMetricsEnabled(!metricsEnabled)} disabled={!editing} />
|
||||
<span className={metricsEnabled ? styles.toggleEnabled : styles.toggleDisabled}>{metricsEnabled ? 'Enabled' : 'Disabled'}</span>
|
||||
<span className={styles.cellMeta} style={{ marginLeft: 8 }}>Interval</span>
|
||||
<Input disabled={!editing} value={metricsInterval} onChange={(e) => setMetricsInterval(e.target.value)} style={{ width: 50 }} />
|
||||
<Input disabled={!editing} value={metricsInterval} onChange={(e) => setMetricsInterval(e.target.value)} className={styles.inputXs} />
|
||||
<span className={styles.cellMeta}>s</span>
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>Sampling Rate</span>
|
||||
<Input disabled={!editing} value={samplingRate} onChange={(e) => setSamplingRate(e.target.value)} style={{ width: 80 }} />
|
||||
<Input disabled={!editing} value={samplingRate} onChange={(e) => setSamplingRate(e.target.value)} className={styles.inputLg} />
|
||||
|
||||
<span className={styles.configLabel}>Compress Success</span>
|
||||
<div className={styles.configInline}>
|
||||
@@ -1032,25 +1032,25 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
||||
<div className={styles.configGrid}>
|
||||
<span className={styles.configLabel}>Memory Limit</span>
|
||||
<div className={styles.configInline}>
|
||||
<Input disabled={!editing} value={memoryLimit} onChange={(e) => setMemoryLimit(e.target.value)} style={{ width: 80 }} />
|
||||
<Input disabled={!editing} value={memoryLimit} onChange={(e) => setMemoryLimit(e.target.value)} className={styles.inputLg} />
|
||||
<span className={styles.cellMeta}>MB</span>
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>Memory Reserve</span>
|
||||
<div>
|
||||
<div className={styles.configInline}>
|
||||
<Input disabled value={memoryReserve} onChange={(e) => setMemoryReserve(e.target.value)} placeholder="---" style={{ width: 80 }} />
|
||||
<Input disabled value={memoryReserve} onChange={(e) => setMemoryReserve(e.target.value)} placeholder="---" className={styles.inputLg} />
|
||||
<span className={styles.cellMeta}>MB</span>
|
||||
</div>
|
||||
{!isProd && <span className={styles.configHint}>Available in production environments only</span>}
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>CPU Request</span>
|
||||
<Input disabled={!editing} value={cpuRequest} onChange={(e) => setCpuRequest(e.target.value)} style={{ width: 80 }} />
|
||||
<Input disabled={!editing} value={cpuRequest} onChange={(e) => setCpuRequest(e.target.value)} className={styles.inputLg} />
|
||||
|
||||
<span className={styles.configLabel}>CPU Limit</span>
|
||||
<div className={styles.configInline}>
|
||||
<Input disabled={!editing} value={cpuLimit} onChange={(e) => setCpuLimit(e.target.value)} placeholder="e.g. 1000" style={{ width: 80 }} />
|
||||
<Input disabled={!editing} value={cpuLimit} onChange={(e) => setCpuLimit(e.target.value)} placeholder="e.g. 1000" className={styles.inputLg} />
|
||||
<span className={styles.cellMeta}>millicores</span>
|
||||
</div>
|
||||
|
||||
@@ -1069,10 +1069,10 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
||||
</div>
|
||||
|
||||
<span className={styles.configLabel}>App Port</span>
|
||||
<Input disabled={!editing} value={appPort} onChange={(e) => setAppPort(e.target.value)} style={{ width: 80 }} />
|
||||
<Input disabled={!editing} value={appPort} onChange={(e) => setAppPort(e.target.value)} className={styles.inputLg} />
|
||||
|
||||
<span className={styles.configLabel}>Replicas</span>
|
||||
<Input disabled={!editing} value={replicas} onChange={(e) => setReplicas(e.target.value)} style={{ width: 60 }} type="number" />
|
||||
<Input disabled={!editing} value={replicas} onChange={(e) => setReplicas(e.target.value)} className={styles.inputSm} type="number" />
|
||||
|
||||
<span className={styles.configLabel}>Deploy Strategy</span>
|
||||
<Select disabled={!editing} value={deployStrategy} onChange={(e) => setDeployStrategy(e.target.value)}
|
||||
|
||||
@@ -34,16 +34,16 @@
|
||||
height: 18px;
|
||||
margin-left: 4px;
|
||||
border: none;
|
||||
background: var(--bg-hover, #F5F0EA);
|
||||
color: var(--text-secondary, #5C5347);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.clearSearch:hover {
|
||||
background: var(--border, #E4DFD8);
|
||||
color: var(--text-primary, #1A1612);
|
||||
background: var(--border);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tableRight {
|
||||
@@ -109,8 +109,8 @@
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #5db866;
|
||||
box-shadow: 0 0 4px rgba(93, 184, 102, 0.4);
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 4px color-mix(in srgb, var(--success) 40%, transparent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ function buildBaseColumns(): Column<Row>[] {
|
||||
<span className={styles.statusCell}>
|
||||
<StatusDot variant={statusToVariant(row.status)} />
|
||||
<MonoText size="xs">{statusLabel(row.status)}</MonoText>
|
||||
{row.hasTraceData && <Footprints size={11} color="#3D7C47" style={{ marginLeft: 2, flexShrink: 0 }} />}
|
||||
{row.hasTraceData && <Footprints size={11} color="var(--success)" style={{ marginLeft: 2, flexShrink: 0 }} />}
|
||||
{row.isReplay && <RotateCcw size={11} color="var(--amber)" style={{ marginLeft: 2, flexShrink: 0 }} />}
|
||||
</span>
|
||||
),
|
||||
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
type HealthStatus,
|
||||
} from './dashboard-utils';
|
||||
import styles from './DashboardTab.module.css';
|
||||
import tableStyles from '../../styles/table-section.module.css';
|
||||
import refreshStyles from '../../styles/refresh-indicator.module.css';
|
||||
import rateStyles from '../../styles/rate-colors.module.css';
|
||||
|
||||
// ── Row type for application health table ───────────────────────────────────
|
||||
|
||||
@@ -75,7 +78,7 @@ const APP_COLUMNS: Column<AppRow>[] = [
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const pct = row.successRate;
|
||||
const cls = pct >= 99 ? styles.rateGood : pct >= 97 ? styles.rateWarn : styles.rateBad;
|
||||
const cls = pct >= 99 ? rateStyles.rateGood : pct >= 97 ? rateStyles.rateWarn : rateStyles.rateBad;
|
||||
return <MonoText size="sm" className={cls}>{pct.toFixed(1)}%</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -84,7 +87,7 @@ const APP_COLUMNS: Column<AppRow>[] = [
|
||||
header: 'P99',
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const cls = row.p99DurationMs > 300 ? styles.rateBad : row.p99DurationMs > 200 ? styles.rateWarn : styles.rateGood;
|
||||
const cls = row.p99DurationMs > 300 ? rateStyles.rateBad : row.p99DurationMs > 200 ? rateStyles.rateWarn : rateStyles.rateGood;
|
||||
return <MonoText size="sm" className={cls}>{Math.round(row.p99DurationMs)}ms</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -93,7 +96,7 @@ const APP_COLUMNS: Column<AppRow>[] = [
|
||||
header: 'SLA %',
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const cls = row.slaCompliance >= 99 ? styles.rateGood : row.slaCompliance >= 95 ? styles.rateWarn : styles.rateBad;
|
||||
const cls = row.slaCompliance >= 99 ? rateStyles.rateGood : row.slaCompliance >= 95 ? rateStyles.rateWarn : rateStyles.rateBad;
|
||||
return <MonoText size="sm" className={cls}>{formatSlaCompliance(row.slaCompliance)}</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -102,7 +105,7 @@ const APP_COLUMNS: Column<AppRow>[] = [
|
||||
header: 'Errors',
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const cls = row.errorCount > 10 ? styles.rateBad : row.errorCount > 0 ? styles.rateWarn : styles.rateGood;
|
||||
const cls = row.errorCount > 10 ? rateStyles.rateBad : row.errorCount > 0 ? rateStyles.rateWarn : rateStyles.rateGood;
|
||||
return <MonoText size="sm" className={cls}>{row.errorCount.toLocaleString()}</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -399,20 +402,20 @@ export default function DashboardL1() {
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.refreshIndicator}>
|
||||
<span className={styles.refreshDot} />
|
||||
<span className={styles.refreshText}>Auto-refresh: 30s</span>
|
||||
<div className={refreshStyles.refreshIndicator}>
|
||||
<span className={refreshStyles.refreshDot} />
|
||||
<span className={refreshStyles.refreshText}>Auto-refresh: 30s</span>
|
||||
</div>
|
||||
|
||||
{/* KPI header cards */}
|
||||
<KpiStrip items={kpiItems} />
|
||||
|
||||
{/* Application Health table */}
|
||||
<div className={styles.tableSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span className={styles.tableTitle}>Application Health</span>
|
||||
<div className={styles.tableRight}>
|
||||
<span className={styles.tableMeta}>{appRows.length} applications</span>
|
||||
<div className={tableStyles.tableSection}>
|
||||
<div className={tableStyles.tableHeader}>
|
||||
<span className={tableStyles.tableTitle}>Application Health</span>
|
||||
<div className={tableStyles.tableRight}>
|
||||
<span className={tableStyles.tableMeta}>{appRows.length} applications</span>
|
||||
<Badge label="ALL" color="auto" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,9 @@ import {
|
||||
formatRelativeTime,
|
||||
} from './dashboard-utils';
|
||||
import styles from './DashboardTab.module.css';
|
||||
import tableStyles from '../../styles/table-section.module.css';
|
||||
import refreshStyles from '../../styles/refresh-indicator.module.css';
|
||||
import rateStyles from '../../styles/rate-colors.module.css';
|
||||
|
||||
// ── Route table row type ────────────────────────────────────────────────────
|
||||
|
||||
@@ -72,7 +75,7 @@ const ROUTE_COLUMNS: Column<RouteRow>[] = [
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const pct = row.successRate * 100;
|
||||
const cls = pct >= 99 ? styles.rateGood : pct >= 97 ? styles.rateWarn : styles.rateBad;
|
||||
const cls = pct >= 99 ? rateStyles.rateGood : pct >= 97 ? rateStyles.rateWarn : rateStyles.rateBad;
|
||||
return <MonoText size="sm" className={cls}>{pct.toFixed(1)}%</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -89,7 +92,7 @@ const ROUTE_COLUMNS: Column<RouteRow>[] = [
|
||||
header: 'P99(ms)',
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const cls = row.p99DurationMs > 300 ? styles.rateBad : row.p99DurationMs > 200 ? styles.rateWarn : styles.rateGood;
|
||||
const cls = row.p99DurationMs > 300 ? rateStyles.rateBad : row.p99DurationMs > 200 ? rateStyles.rateWarn : rateStyles.rateGood;
|
||||
return <MonoText size="sm" className={cls}>{Math.round(row.p99DurationMs)}</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -98,7 +101,7 @@ const ROUTE_COLUMNS: Column<RouteRow>[] = [
|
||||
header: 'SLA%',
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const cls = row.slaCompliance >= 99 ? styles.rateGood : row.slaCompliance >= 95 ? styles.rateWarn : styles.rateBad;
|
||||
const cls = row.slaCompliance >= 99 ? rateStyles.rateGood : row.slaCompliance >= 95 ? rateStyles.rateWarn : rateStyles.rateBad;
|
||||
return <MonoText size="sm" className={cls}>{formatSlaCompliance(row.slaCompliance)}</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -146,9 +149,9 @@ const ERROR_COLUMNS: Column<ErrorRow>[] = [
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const arrow = trendArrow(row.trend);
|
||||
const cls = row.trend === 'accelerating' ? styles.rateBad
|
||||
: row.trend === 'decelerating' ? styles.rateGood
|
||||
: styles.rateNeutral;
|
||||
const cls = row.trend === 'accelerating' ? rateStyles.rateBad
|
||||
: row.trend === 'decelerating' ? rateStyles.rateGood
|
||||
: rateStyles.rateNeutral;
|
||||
return <MonoText size="sm" className={cls}>{row.velocity.toFixed(1)}/min {arrow}</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -364,20 +367,20 @@ export default function DashboardL2() {
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.refreshIndicator}>
|
||||
<span className={styles.refreshDot} />
|
||||
<span className={styles.refreshText}>Auto-refresh: 30s</span>
|
||||
<div className={refreshStyles.refreshIndicator}>
|
||||
<span className={refreshStyles.refreshDot} />
|
||||
<span className={refreshStyles.refreshText}>Auto-refresh: 30s</span>
|
||||
</div>
|
||||
|
||||
{/* KPI Strip */}
|
||||
<KpiStrip items={kpiItems} />
|
||||
|
||||
{/* Route Performance Table */}
|
||||
<div className={styles.tableSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span className={styles.tableTitle}>Route Performance</span>
|
||||
<div className={styles.tableRight}>
|
||||
<span className={styles.tableMeta}>{routeRows.length} routes</span>
|
||||
<div className={tableStyles.tableSection}>
|
||||
<div className={tableStyles.tableHeader}>
|
||||
<span className={tableStyles.tableTitle}>Route Performance</span>
|
||||
<div className={tableStyles.tableRight}>
|
||||
<span className={tableStyles.tableMeta}>{routeRows.length} routes</span>
|
||||
<Badge label="AUTO" color="success" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -416,9 +419,9 @@ export default function DashboardL2() {
|
||||
{/* Top 5 Errors — hidden when empty */}
|
||||
{errorRows.length > 0 && (
|
||||
<div className={styles.errorsSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span className={styles.tableTitle}>Top Errors</span>
|
||||
<span className={styles.tableMeta}>{errorRows.length} error types</span>
|
||||
<div className={tableStyles.tableHeader}>
|
||||
<span className={tableStyles.tableTitle}>Top Errors</span>
|
||||
<span className={tableStyles.tableMeta}>{errorRows.length} error types</span>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={ERROR_COLUMNS}
|
||||
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
trendIndicator,
|
||||
} from './dashboard-utils';
|
||||
import styles from './DashboardTab.module.css';
|
||||
import tableStyles from '../../styles/table-section.module.css';
|
||||
import refreshStyles from '../../styles/refresh-indicator.module.css';
|
||||
import rateStyles from '../../styles/rate-colors.module.css';
|
||||
|
||||
// ── Row types ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -80,10 +83,10 @@ const PROCESSOR_COLUMNS: Column<ProcessorRow>[] = [
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const cls = row.p99DurationMs > 300
|
||||
? styles.rateBad
|
||||
? rateStyles.rateBad
|
||||
: row.p99DurationMs > 200
|
||||
? styles.rateWarn
|
||||
: styles.rateGood;
|
||||
? rateStyles.rateWarn
|
||||
: rateStyles.rateGood;
|
||||
return <MonoText size="sm" className={cls}>{Math.round(row.p99DurationMs)}</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -93,7 +96,7 @@ const PROCESSOR_COLUMNS: Column<ProcessorRow>[] = [
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const pct = row.errorRate * 100;
|
||||
const cls = pct > 5 ? styles.rateBad : pct > 1 ? styles.rateWarn : styles.rateGood;
|
||||
const cls = pct > 5 ? rateStyles.rateBad : pct > 1 ? rateStyles.rateWarn : rateStyles.rateGood;
|
||||
return <MonoText size="sm" className={cls}>{pct.toFixed(2)}%</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -347,9 +350,9 @@ export default function DashboardL3() {
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.refreshIndicator}>
|
||||
<span className={styles.refreshDot} />
|
||||
<span className={styles.refreshText}>Auto-refresh: 30s</span>
|
||||
<div className={refreshStyles.refreshIndicator}>
|
||||
<span className={refreshStyles.refreshDot} />
|
||||
<span className={refreshStyles.refreshText}>Auto-refresh: 30s</span>
|
||||
</div>
|
||||
|
||||
{/* KPI Strip */}
|
||||
@@ -398,11 +401,11 @@ export default function DashboardL3() {
|
||||
)}
|
||||
|
||||
{/* Processor Metrics Table */}
|
||||
<div className={styles.tableSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span className={styles.tableTitle}>Processor Metrics</span>
|
||||
<div className={tableStyles.tableSection}>
|
||||
<div className={tableStyles.tableHeader}>
|
||||
<span className={tableStyles.tableTitle}>Processor Metrics</span>
|
||||
<div>
|
||||
<span className={styles.tableMeta}>
|
||||
<span className={tableStyles.tableMeta}>
|
||||
{processorRows.length} processor{processorRows.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
@@ -417,8 +420,8 @@ export default function DashboardL3() {
|
||||
{/* Top 5 Errors — hidden if empty */}
|
||||
{errorRows.length > 0 && (
|
||||
<div className={styles.errorsSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span className={styles.tableTitle}>Top 5 Errors</span>
|
||||
<div className={tableStyles.tableHeader}>
|
||||
<span className={tableStyles.tableTitle}>Top 5 Errors</span>
|
||||
<Badge label={`${errorRows.length}`} color="error" />
|
||||
</div>
|
||||
<DataTable
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { useParams } from 'react-router';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Spinner } from '@cameleer/design-system';
|
||||
import { PageLoader } from '../../components/PageLoader';
|
||||
|
||||
const DashboardL1 = lazy(() => import('./DashboardL1'));
|
||||
const DashboardL2 = lazy(() => import('./DashboardL2'));
|
||||
const DashboardL3 = lazy(() => import('./DashboardL3'));
|
||||
|
||||
const Fallback = <div style={{ display: 'flex', justifyContent: 'center', padding: '4rem' }}><Spinner size="lg" /></div>;
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { appId, routeId } = useParams<{ appId?: string; routeId?: string }>();
|
||||
|
||||
if (routeId && appId) {
|
||||
return <Suspense fallback={Fallback}><DashboardL3 /></Suspense>;
|
||||
return <Suspense fallback={<PageLoader />}><DashboardL3 /></Suspense>;
|
||||
}
|
||||
if (appId) {
|
||||
return <Suspense fallback={Fallback}><DashboardL2 /></Suspense>;
|
||||
return <Suspense fallback={<PageLoader />}><DashboardL2 /></Suspense>;
|
||||
}
|
||||
return <Suspense fallback={Fallback}><DashboardL1 /></Suspense>;
|
||||
return <Suspense fallback={<PageLoader />}><DashboardL1 /></Suspense>;
|
||||
}
|
||||
|
||||
@@ -8,61 +8,6 @@
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.refreshIndicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.refreshDot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 4px rgba(61, 124, 71, 0.5);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.refreshText {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.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);
|
||||
}
|
||||
|
||||
.tableMeta {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Charts */
|
||||
.chartGrid {
|
||||
@@ -92,12 +37,6 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Rate coloring */
|
||||
.rateGood { color: var(--success); }
|
||||
.rateWarn { color: var(--warning); }
|
||||
.rateBad { color: var(--error); }
|
||||
.rateNeutral { color: var(--text-secondary); }
|
||||
|
||||
/* Diagram container */
|
||||
.diagramSection {
|
||||
background: var(--bg-surface);
|
||||
@@ -108,13 +47,6 @@
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
/* Table right side (meta + badge) */
|
||||
.tableRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Chart fill */
|
||||
.chart {
|
||||
width: 100%;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-raised, var(--surface));
|
||||
background: var(--bg-raised);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -136,23 +136,6 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Rate color classes */
|
||||
.rateGood {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.rateWarn {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.rateBad {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.rateNeutral {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Route name in table */
|
||||
.routeNameCell {
|
||||
font-size: 12px;
|
||||
@@ -161,42 +144,10 @@
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Table section (reused for processor table) */
|
||||
.tableSection {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.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);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Chart grid */
|
||||
.chartGrid {
|
||||
display: grid;
|
||||
@@ -204,15 +155,6 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chartCard {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chartTitle {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
@@ -370,35 +312,6 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.typeSelector {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.typeOption {
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-body);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.typeOption:hover {
|
||||
border-color: var(--border);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.typeOptionActive {
|
||||
background: var(--amber-bg);
|
||||
color: var(--amber-deep);
|
||||
border-color: var(--amber);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tapModalFooter {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -408,56 +321,3 @@
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
/* Test expression */
|
||||
.testSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.testTabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.testTabBtn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.testTabBtnActive {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.testBody {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.testResult {
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.testSuccess {
|
||||
background: var(--success-bg);
|
||||
border: 1px solid var(--success-border);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.testError {
|
||||
background: var(--error-bg);
|
||||
border: 1px solid var(--error-border);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ import type { ExecutionSummary } from '../../api/types';
|
||||
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
|
||||
import { buildFlowSegments } from '../../utils/diagram-mapping';
|
||||
import styles from './RouteDetail.module.css';
|
||||
import tableStyles from '../../styles/table-section.module.css';
|
||||
import rateStyles from '../../styles/rate-colors.module.css';
|
||||
import chartCardStyles from '../../styles/chart-card.module.css';
|
||||
import tapModalStyles from '../../components/TapConfigModal.module.css';
|
||||
|
||||
// ── Row types ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -714,11 +718,11 @@ export default function RouteDetail() {
|
||||
</div>
|
||||
|
||||
{/* Processor Performance table (full width) */}
|
||||
<div className={styles.tableSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span className={styles.tableTitle}>Processor Performance</span>
|
||||
<div className={styles.tableRight}>
|
||||
<span className={styles.tableMeta}>{processorRows.length} processors</span>
|
||||
<div className={`${tableStyles.tableSection} ${styles.tableSection}`}>
|
||||
<div className={tableStyles.tableHeader}>
|
||||
<span className={tableStyles.tableTitle}>Processor Performance</span>
|
||||
<div className={tableStyles.tableRight}>
|
||||
<span className={tableStyles.tableMeta}>{processorRows.length} processors</span>
|
||||
<Badge label="AUTO" color="success" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -732,8 +736,8 @@ export default function RouteDetail() {
|
||||
{/* Route Flow section */}
|
||||
{diagramFlows.length > 0 && (
|
||||
<div className={styles.routeFlowSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span className={styles.tableTitle}>Route Flow</span>
|
||||
<div className={tableStyles.tableHeader}>
|
||||
<span className={tableStyles.tableTitle}>Route Flow</span>
|
||||
</div>
|
||||
<RouteFlow flows={diagramFlows} />
|
||||
</div>
|
||||
@@ -745,7 +749,7 @@ export default function RouteDetail() {
|
||||
|
||||
{activeTab === 'performance' && (
|
||||
<div className={styles.chartGrid} style={{ marginTop: 16 }}>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartTitle}>Throughput</div>
|
||||
<AreaChart
|
||||
series={[{
|
||||
@@ -755,7 +759,7 @@ export default function RouteDetail() {
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartTitle}>Latency</div>
|
||||
<LineChart
|
||||
series={[{
|
||||
@@ -766,7 +770,7 @@ export default function RouteDetail() {
|
||||
threshold={{ value: 300, label: 'SLA 300ms' }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartTitle}>Errors</div>
|
||||
<BarChart
|
||||
series={[{
|
||||
@@ -776,7 +780,7 @@ export default function RouteDetail() {
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartTitle}>Success Rate</div>
|
||||
<AreaChart
|
||||
series={[{
|
||||
@@ -890,13 +894,13 @@ export default function RouteDetail() {
|
||||
</FormField>
|
||||
|
||||
<FormField label="Type">
|
||||
<div className={styles.typeSelector}>
|
||||
<div className={tapModalStyles.typeSelector}>
|
||||
{typeChoices.map(tc => (
|
||||
<button
|
||||
key={tc.value}
|
||||
type="button"
|
||||
title={tc.tooltip}
|
||||
className={`${styles.typeOption} ${tapType === tc.value ? styles.typeOptionActive : ''}`}
|
||||
className={`${tapModalStyles.typeOption} ${tapType === tc.value ? tapModalStyles.typeOptionActive : ''}`}
|
||||
onClick={() => setTapType(tc.value)}
|
||||
>
|
||||
{tc.label}
|
||||
@@ -913,18 +917,18 @@ export default function RouteDetail() {
|
||||
|
||||
{/* Test Expression */}
|
||||
<Collapsible title="Test Expression" defaultOpen>
|
||||
<div className={styles.testSection}>
|
||||
<div className={styles.testTabs}>
|
||||
<div className={tapModalStyles.testSection}>
|
||||
<div className={tapModalStyles.testTabs}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.testTabBtn} ${testTab === 'recent' ? styles.testTabBtnActive : ''}`}
|
||||
className={`${tapModalStyles.testTabBtn} ${testTab === 'recent' ? tapModalStyles.testTabBtnActive : ''}`}
|
||||
onClick={() => setTestTab('recent')}
|
||||
>
|
||||
Recent Exchange
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.testTabBtn} ${testTab === 'custom' ? styles.testTabBtnActive : ''}`}
|
||||
className={`${tapModalStyles.testTabBtn} ${testTab === 'custom' ? tapModalStyles.testTabBtnActive : ''}`}
|
||||
onClick={() => setTestTab('custom')}
|
||||
>
|
||||
Custom Payload
|
||||
@@ -932,7 +936,7 @@ export default function RouteDetail() {
|
||||
</div>
|
||||
|
||||
{testTab === 'recent' && (
|
||||
<div className={styles.testBody}>
|
||||
<div className={tapModalStyles.testBody}>
|
||||
<Select
|
||||
options={recentExchangeOptions.length > 0 ? recentExchangeOptions : [{ value: '', label: 'No recent exchanges' }]}
|
||||
value={testExchangeId}
|
||||
@@ -942,7 +946,7 @@ export default function RouteDetail() {
|
||||
)}
|
||||
|
||||
{testTab === 'custom' && (
|
||||
<div className={styles.testBody}>
|
||||
<div className={tapModalStyles.testBody}>
|
||||
<Textarea
|
||||
className={styles.monoTextarea}
|
||||
value={testPayload}
|
||||
@@ -953,7 +957,7 @@ export default function RouteDetail() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.testBody}>
|
||||
<div className={tapModalStyles.testBody}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -966,7 +970,7 @@ export default function RouteDetail() {
|
||||
</div>
|
||||
|
||||
{testResult && (
|
||||
<div className={`${styles.testResult} ${testResult.error ? styles.testError : styles.testSuccess}`}>
|
||||
<div className={`${tapModalStyles.testResult} ${testResult.error ? tapModalStyles.testError : tapModalStyles.testSuccess}`}>
|
||||
{testResult.error ?? testResult.result ?? 'No result'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,68 +5,6 @@
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.refreshIndicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.refreshDot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 4px rgba(61, 124, 71, 0.5);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.refreshText {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Route performance table */
|
||||
.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);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Route name in table */
|
||||
.routeNameCell {
|
||||
font-size: 12px;
|
||||
@@ -81,23 +19,6 @@
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Rate color classes */
|
||||
.rateGood {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.rateWarn {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.rateBad {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.rateNeutral {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 2x2 chart grid */
|
||||
.chartGrid {
|
||||
display: grid;
|
||||
|
||||
@@ -17,6 +17,9 @@ import { useRouteMetrics } from '../../api/queries/catalog';
|
||||
import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions';
|
||||
import type { RouteMetrics } from '../../api/types';
|
||||
import styles from './RoutesMetrics.module.css';
|
||||
import tableStyles from '../../styles/table-section.module.css';
|
||||
import refreshStyles from '../../styles/refresh-indicator.module.css';
|
||||
import rateStyles from '../../styles/rate-colors.module.css';
|
||||
|
||||
interface RouteRow {
|
||||
id: string;
|
||||
@@ -64,7 +67,7 @@ const ROUTE_COLUMNS: Column<RouteRow>[] = [
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const pct = row.successRate * 100;
|
||||
const cls = pct >= 99 ? styles.rateGood : pct >= 97 ? styles.rateWarn : styles.rateBad;
|
||||
const cls = pct >= 99 ? rateStyles.rateGood : pct >= 97 ? rateStyles.rateWarn : rateStyles.rateBad;
|
||||
return <MonoText size="sm" className={cls}>{pct.toFixed(1)}%</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -81,7 +84,7 @@ const ROUTE_COLUMNS: Column<RouteRow>[] = [
|
||||
header: 'p99 Duration',
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const cls = row.p99DurationMs > 300 ? styles.rateBad : row.p99DurationMs > 200 ? styles.rateWarn : styles.rateGood;
|
||||
const cls = row.p99DurationMs > 300 ? rateStyles.rateBad : row.p99DurationMs > 200 ? rateStyles.rateWarn : rateStyles.rateGood;
|
||||
return <MonoText size="sm" className={cls}>{Math.round(row.p99DurationMs)}ms</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -91,7 +94,7 @@ const ROUTE_COLUMNS: Column<RouteRow>[] = [
|
||||
sortable: true,
|
||||
render: (_, row) => {
|
||||
const pct = row.errorRate * 100;
|
||||
const cls = pct > 5 ? styles.rateBad : pct > 1 ? styles.rateWarn : styles.rateGood;
|
||||
const cls = pct > 5 ? rateStyles.rateBad : pct > 1 ? rateStyles.rateWarn : rateStyles.rateGood;
|
||||
return <MonoText size="sm" className={cls}>{pct.toFixed(1)}%</MonoText>;
|
||||
},
|
||||
},
|
||||
@@ -281,20 +284,20 @@ export default function RoutesMetrics() {
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.refreshIndicator}>
|
||||
<span className={styles.refreshDot} />
|
||||
<span className={styles.refreshText}>Auto-refresh: 30s</span>
|
||||
<div className={refreshStyles.refreshIndicator}>
|
||||
<span className={refreshStyles.refreshDot} />
|
||||
<span className={refreshStyles.refreshText}>Auto-refresh: 30s</span>
|
||||
</div>
|
||||
|
||||
{/* KPI header cards */}
|
||||
<KpiStrip items={kpiItems} />
|
||||
|
||||
{/* Per-route performance table */}
|
||||
<div className={styles.tableSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span className={styles.tableTitle}>Per-Route Performance</span>
|
||||
<div className={styles.tableRight}>
|
||||
<span className={styles.tableMeta}>{rows.length} routes</span>
|
||||
<div className={tableStyles.tableSection}>
|
||||
<div className={tableStyles.tableHeader}>
|
||||
<span className={tableStyles.tableTitle}>Per-Route Performance</span>
|
||||
<div className={tableStyles.tableRight}>
|
||||
<span className={tableStyles.tableMeta}>{rows.length} routes</span>
|
||||
<Badge label="AUTO" color="success" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { useParams } from 'react-router';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Spinner } from '@cameleer/design-system';
|
||||
import { PageLoader } from '../../components/PageLoader';
|
||||
|
||||
const AgentHealth = lazy(() => import('../AgentHealth/AgentHealth'));
|
||||
const AgentInstance = lazy(() => import('../AgentInstance/AgentInstance'));
|
||||
|
||||
const Fallback = <div style={{ display: 'flex', justifyContent: 'center', padding: '4rem' }}><Spinner size="lg" /></div>;
|
||||
|
||||
export default function RuntimePage() {
|
||||
const { instanceId } = useParams<{ appId?: string; instanceId?: string }>();
|
||||
|
||||
if (instanceId) {
|
||||
return <Suspense fallback={Fallback}><AgentInstance /></Suspense>;
|
||||
return <Suspense fallback={<PageLoader />}><AgentInstance /></Suspense>;
|
||||
}
|
||||
return <Suspense fallback={Fallback}><AgentHealth /></Suspense>;
|
||||
return <Suspense fallback={<PageLoader />}><AgentHealth /></Suspense>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user