fix: improve Infrastructure page readability
Use Card and KpiStrip design system components, add database icons to section headers, right-align numeric columns, replace text toggles with chevron icons, and constrain max width to prevent ultra-wide stretching. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
255
ui/src/pages/vendor/InfrastructurePage.tsx
vendored
255
ui/src/pages/vendor/InfrastructurePage.tsx
vendored
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Spinner } from '@cameleer/design-system';
|
import { Card, KpiStrip, Spinner } from '@cameleer/design-system';
|
||||||
|
import { Database, ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
useInfraPostgres,
|
useInfraPostgres,
|
||||||
useInfraClickHouse,
|
useInfraClickHouse,
|
||||||
@@ -29,30 +30,9 @@ function formatNumber(n: number): string {
|
|||||||
return n.toLocaleString();
|
return n.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
const kpiStyle: React.CSSProperties = {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 2,
|
|
||||||
minWidth: 120,
|
|
||||||
};
|
|
||||||
|
|
||||||
const kpiLabelStyle: React.CSSProperties = {
|
|
||||||
fontSize: 11,
|
|
||||||
color: 'var(--text-muted)',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.05em',
|
|
||||||
};
|
|
||||||
|
|
||||||
const kpiValueStyle: React.CSSProperties = {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: 'var(--amber)',
|
|
||||||
fontVariantNumeric: 'tabular-nums',
|
|
||||||
};
|
|
||||||
|
|
||||||
const thStyle: React.CSSProperties = {
|
const thStyle: React.CSSProperties = {
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
padding: '6px 12px',
|
padding: '8px 16px',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: 'var(--text-muted)',
|
color: 'var(--text-muted)',
|
||||||
@@ -62,14 +42,14 @@ const thStyle: React.CSSProperties = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tdStyle: React.CSSProperties = {
|
const tdStyle: React.CSSProperties = {
|
||||||
padding: '8px 12px',
|
padding: '10px 16px',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
borderBottom: '1px solid var(--border)',
|
borderBottom: '1px solid var(--border)',
|
||||||
fontVariantNumeric: 'tabular-nums',
|
fontVariantNumeric: 'tabular-nums',
|
||||||
};
|
};
|
||||||
|
|
||||||
const monoStyle: React.CSSProperties = {
|
const monoStyle: React.CSSProperties = {
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'var(--font-mono, monospace)',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,32 +78,30 @@ function PgDetailRow({ slug }: { slug: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<tr>
|
||||||
<tr>
|
<td colSpan={5} style={{ padding: 0 }}>
|
||||||
<td colSpan={5} style={{ padding: 0 }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse', background: 'var(--surface-2)' }}>
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', background: 'var(--surface-2)' }}>
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th style={{ ...thStyle, paddingLeft: 48 }}>Table</th>
|
||||||
<th style={{ ...thStyle, paddingLeft: 48 }}>Table</th>
|
<th style={{ ...thStyle, textAlign: 'right' }}>Rows</th>
|
||||||
<th style={thStyle}>Rows</th>
|
<th style={{ ...thStyle, textAlign: 'right' }}>Data Size</th>
|
||||||
<th style={thStyle}>Data Size</th>
|
<th style={{ ...thStyle, textAlign: 'right' }}>Index Size</th>
|
||||||
<th style={thStyle}>Index Size</th>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((t) => (
|
||||||
|
<tr key={t.tableName}>
|
||||||
|
<td style={{ ...tdStyle, ...monoStyle, paddingLeft: 48 }}>{t.tableName}</td>
|
||||||
|
<td style={{ ...tdStyle, textAlign: 'right' }}>{formatNumber(t.rowCount)}</td>
|
||||||
|
<td style={{ ...tdStyle, textAlign: 'right' }}>{formatBytes(t.dataSizeBytes)}</td>
|
||||||
|
<td style={{ ...tdStyle, textAlign: 'right' }}>{formatBytes(t.indexSizeBytes)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
))}
|
||||||
<tbody>
|
</tbody>
|
||||||
{data.map((t) => (
|
</table>
|
||||||
<tr key={t.tableName}>
|
</td>
|
||||||
<td style={{ ...tdStyle, ...monoStyle, paddingLeft: 48 }}>{t.tableName}</td>
|
</tr>
|
||||||
<td style={tdStyle}>{formatNumber(t.rowCount)}</td>
|
|
||||||
<td style={tdStyle}>{formatBytes(t.dataSizeBytes)}</td>
|
|
||||||
<td style={tdStyle}>{formatBytes(t.indexSizeBytes)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,14 +136,14 @@ function ChDetailRow({ tenantId }: { tenantId: string }) {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{ ...thStyle, paddingLeft: 48 }}>Table</th>
|
<th style={{ ...thStyle, paddingLeft: 48 }}>Table</th>
|
||||||
<th style={thStyle}>Rows</th>
|
<th style={{ ...thStyle, textAlign: 'right' }}>Rows</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.map((t) => (
|
{data.map((t) => (
|
||||||
<tr key={t.tableName}>
|
<tr key={t.tableName}>
|
||||||
<td style={{ ...tdStyle, ...monoStyle, paddingLeft: 48 }}>{t.tableName}</td>
|
<td style={{ ...tdStyle, ...monoStyle, paddingLeft: 48 }}>{t.tableName}</td>
|
||||||
<td style={tdStyle}>{formatNumber(t.rowCount)}</td>
|
<td style={{ ...tdStyle, textAlign: 'right' }}>{formatNumber(t.rowCount)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -179,6 +157,9 @@ function ChDetailRow({ tenantId }: { tenantId: string }) {
|
|||||||
|
|
||||||
function PgTenantRow({ tenant }: { tenant: TenantPgStats }) {
|
function PgTenantRow({ tenant }: { tenant: TenantPgStats }) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const chevron = expanded
|
||||||
|
? <ChevronDown size={14} style={{ color: 'var(--text-muted)' }} />
|
||||||
|
: <ChevronRight size={14} style={{ color: 'var(--text-muted)' }} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -186,13 +167,12 @@ function PgTenantRow({ tenant }: { tenant: TenantPgStats }) {
|
|||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => setExpanded((e) => !e)}
|
onClick={() => setExpanded((e) => !e)}
|
||||||
>
|
>
|
||||||
<td style={{ ...tdStyle, ...monoStyle }}>{tenant.slug}</td>
|
<td style={{ ...tdStyle, ...monoStyle, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<td style={tdStyle}>{formatBytes(tenant.schemaSizeBytes)}</td>
|
{chevron} {tenant.slug}
|
||||||
<td style={tdStyle}>{formatNumber(tenant.tableCount)}</td>
|
|
||||||
<td style={tdStyle}>{formatNumber(tenant.totalRows)}</td>
|
|
||||||
<td style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: 11 }}>
|
|
||||||
{expanded ? '▲ hide' : '▼ detail'}
|
|
||||||
</td>
|
</td>
|
||||||
|
<td style={{ ...tdStyle, textAlign: 'right' }}>{formatBytes(tenant.schemaSizeBytes)}</td>
|
||||||
|
<td style={{ ...tdStyle, textAlign: 'right' }}>{formatNumber(tenant.tableCount)}</td>
|
||||||
|
<td style={{ ...tdStyle, textAlign: 'right' }}>{formatNumber(tenant.totalRows)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{expanded && <PgDetailRow slug={tenant.slug} />}
|
{expanded && <PgDetailRow slug={tenant.slug} />}
|
||||||
</>
|
</>
|
||||||
@@ -207,6 +187,9 @@ function ChTenantRow({ tenant }: { tenant: TenantChStats }) {
|
|||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map(([tbl, rows]) => `${tbl}: ${formatNumber(rows)}`)
|
.map(([tbl, rows]) => `${tbl}: ${formatNumber(rows)}`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
const chevron = expanded
|
||||||
|
? <ChevronDown size={14} style={{ color: 'var(--text-muted)' }} />
|
||||||
|
: <ChevronRight size={14} style={{ color: 'var(--text-muted)' }} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -214,11 +197,13 @@ function ChTenantRow({ tenant }: { tenant: TenantChStats }) {
|
|||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => setExpanded((e) => !e)}
|
onClick={() => setExpanded((e) => !e)}
|
||||||
>
|
>
|
||||||
<td style={{ ...tdStyle, ...monoStyle }}>{tenant.tenantId}</td>
|
<td style={{ ...tdStyle, ...monoStyle, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<td style={tdStyle}>{formatNumber(tenant.totalRows)}</td>
|
{chevron} {tenant.tenantId}
|
||||||
|
</td>
|
||||||
|
<td style={{ ...tdStyle, textAlign: 'right' }}>{formatNumber(tenant.totalRows)}</td>
|
||||||
<td style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: 12 }}>
|
<td style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: 12 }}>
|
||||||
{tablePreview || '—'}
|
{tablePreview || '\u2014'}
|
||||||
{Object.keys(tenant.rowsByTable).length > 3 ? ' …' : ''}
|
{Object.keys(tenant.rowsByTable).length > 3 ? ' \u2026' : ''}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{expanded && <ChDetailRow tenantId={tenant.tenantId} />}
|
{expanded && <ChDetailRow tenantId={tenant.tenantId} />}
|
||||||
@@ -226,88 +211,58 @@ function ChTenantRow({ tenant }: { tenant: TenantChStats }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Section Header ---
|
||||||
|
|
||||||
|
function SectionHeader({ title, loading, error }: { title: string; loading?: boolean; error?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '16px 20px', borderBottom: '1px solid var(--border)' }}>
|
||||||
|
<Database size={18} style={{ color: 'var(--amber)' }} />
|
||||||
|
<h2 style={{ margin: 0, fontSize: '1rem', fontWeight: 600 }}>{title}</h2>
|
||||||
|
{loading && <Spinner size="sm" />}
|
||||||
|
{error && <span style={{ fontSize: 13, color: 'var(--error, #ef4444)' }}>Failed to load</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Main Page ---
|
// --- Main Page ---
|
||||||
|
|
||||||
export function InfrastructurePage() {
|
export function InfrastructurePage() {
|
||||||
const pg = useInfraPostgres();
|
const pg = useInfraPostgres();
|
||||||
const ch = useInfraClickHouse();
|
const ch = useInfraClickHouse();
|
||||||
|
|
||||||
const cardStyle: React.CSSProperties = {
|
|
||||||
background: 'var(--surface-1)',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 8,
|
|
||||||
overflow: 'hidden',
|
|
||||||
};
|
|
||||||
|
|
||||||
const cardHeaderStyle: React.CSSProperties = {
|
|
||||||
padding: '16px 20px',
|
|
||||||
borderBottom: '1px solid var(--border)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
};
|
|
||||||
|
|
||||||
const cardTitleStyle: React.CSSProperties = {
|
|
||||||
margin: 0,
|
|
||||||
fontSize: '1rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
};
|
|
||||||
|
|
||||||
const kpiRowStyle: React.CSSProperties = {
|
|
||||||
padding: '16px 20px',
|
|
||||||
display: 'flex',
|
|
||||||
gap: 32,
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
borderBottom: '1px solid var(--border)',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 24 }}>
|
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 24, maxWidth: 960 }}>
|
||||||
<h1 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 600 }}>Infrastructure</h1>
|
<h1 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 600 }}>Infrastructure</h1>
|
||||||
|
|
||||||
{/* PostgreSQL Card */}
|
{/* PostgreSQL Card */}
|
||||||
<div style={cardStyle}>
|
<Card>
|
||||||
<div style={cardHeaderStyle}>
|
<SectionHeader title="PostgreSQL" loading={pg.isLoading} error={pg.isError} />
|
||||||
<h2 style={cardTitleStyle}>PostgreSQL</h2>
|
|
||||||
{pg.isLoading && <Spinner size="sm" />}
|
|
||||||
{pg.isError && (
|
|
||||||
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>Failed to load</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{pg.data && (
|
{pg.data && (
|
||||||
<>
|
<>
|
||||||
<div style={kpiRowStyle}>
|
<div style={{ padding: '4px 20px' }}>
|
||||||
<div style={kpiStyle}>
|
<KpiStrip
|
||||||
<span style={kpiLabelStyle}>Version</span>
|
items={[
|
||||||
<span style={{ ...kpiValueStyle, fontSize: 14, fontFamily: 'monospace' }}>
|
{ label: 'Version', value: pg.data.overview.version.split(' ').slice(0, 2).join(' ') },
|
||||||
{pg.data.overview.version.split(' ').slice(0, 2).join(' ')}
|
{ label: 'Database Size', value: formatBytes(pg.data.overview.databaseSizeBytes) },
|
||||||
</span>
|
{ label: 'Active Connections', value: String(pg.data.overview.activeConnections) },
|
||||||
</div>
|
]}
|
||||||
<div style={kpiStyle}>
|
/>
|
||||||
<span style={kpiLabelStyle}>Database Size</span>
|
|
||||||
<span style={kpiValueStyle}>{formatBytes(pg.data.overview.databaseSizeBytes)}</span>
|
|
||||||
</div>
|
|
||||||
<div style={kpiStyle}>
|
|
||||||
<span style={kpiLabelStyle}>Active Connections</span>
|
|
||||||
<span style={kpiValueStyle}>{formatNumber(pg.data.overview.activeConnections)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={thStyle}>Tenant Slug</th>
|
<th style={thStyle}>Tenant</th>
|
||||||
<th style={thStyle}>Schema Size</th>
|
<th style={{ ...thStyle, textAlign: 'right' }}>Schema Size</th>
|
||||||
<th style={thStyle}>Tables</th>
|
<th style={{ ...thStyle, textAlign: 'right' }}>Tables</th>
|
||||||
<th style={thStyle}>Total Rows</th>
|
<th style={{ ...thStyle, textAlign: 'right' }}>Rows</th>
|
||||||
<th style={thStyle}></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{pg.data.tenants.length === 0 ? (
|
{pg.data.tenants.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} style={{ ...tdStyle, color: 'var(--text-muted)', textAlign: 'center' }}>
|
<td colSpan={4} style={{ ...tdStyle, color: 'var(--text-muted)', textAlign: 'center' }}>
|
||||||
No tenant schemas found
|
No tenant schemas found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -320,55 +275,33 @@ export function InfrastructurePage() {
|
|||||||
</table>
|
</table>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
{/* ClickHouse Card */}
|
{/* ClickHouse Card */}
|
||||||
<div style={cardStyle}>
|
<Card>
|
||||||
<div style={cardHeaderStyle}>
|
<SectionHeader title="ClickHouse" loading={ch.isLoading} error={ch.isError} />
|
||||||
<h2 style={cardTitleStyle}>ClickHouse</h2>
|
|
||||||
{ch.isLoading && <Spinner size="sm" />}
|
|
||||||
{ch.isError && (
|
|
||||||
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>Failed to load</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ch.data && (
|
{ch.data && (
|
||||||
<>
|
<>
|
||||||
<div style={kpiRowStyle}>
|
<div style={{ padding: '4px 20px' }}>
|
||||||
<div style={kpiStyle}>
|
<KpiStrip
|
||||||
<span style={kpiLabelStyle}>Version</span>
|
items={[
|
||||||
<span style={{ ...kpiValueStyle, fontSize: 14, fontFamily: 'monospace' }}>
|
{ label: 'Version', value: ch.data.overview.version },
|
||||||
{ch.data.overview.version}
|
{ label: 'Uptime', value: formatUptime(ch.data.overview.uptimeSeconds) },
|
||||||
</span>
|
{ label: 'Disk Usage', value: formatBytes(ch.data.overview.totalDiskBytes) },
|
||||||
</div>
|
{ label: 'Total Rows', value: formatNumber(ch.data.overview.totalRows) },
|
||||||
<div style={kpiStyle}>
|
{ label: 'Compression', value: `${ch.data.overview.compressionRatio.toFixed(1)}x` },
|
||||||
<span style={kpiLabelStyle}>Uptime</span>
|
{ label: 'Active Merges', value: String(ch.data.overview.activeMerges) },
|
||||||
<span style={kpiValueStyle}>{formatUptime(ch.data.overview.uptimeSeconds)}</span>
|
]}
|
||||||
</div>
|
/>
|
||||||
<div style={kpiStyle}>
|
|
||||||
<span style={kpiLabelStyle}>Disk Usage</span>
|
|
||||||
<span style={kpiValueStyle}>{formatBytes(ch.data.overview.totalDiskBytes)}</span>
|
|
||||||
</div>
|
|
||||||
<div style={kpiStyle}>
|
|
||||||
<span style={kpiLabelStyle}>Total Rows</span>
|
|
||||||
<span style={kpiValueStyle}>{formatNumber(ch.data.overview.totalRows)}</span>
|
|
||||||
</div>
|
|
||||||
<div style={kpiStyle}>
|
|
||||||
<span style={kpiLabelStyle}>Compression</span>
|
|
||||||
<span style={kpiValueStyle}>{ch.data.overview.compressionRatio.toFixed(1)}x</span>
|
|
||||||
</div>
|
|
||||||
<div style={kpiStyle}>
|
|
||||||
<span style={kpiLabelStyle}>Active Merges</span>
|
|
||||||
<span style={kpiValueStyle}>{formatNumber(ch.data.overview.activeMerges)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={thStyle}>Tenant ID</th>
|
<th style={thStyle}>Tenant</th>
|
||||||
<th style={thStyle}>Total Rows</th>
|
<th style={{ ...thStyle, textAlign: 'right' }}>Total Rows</th>
|
||||||
<th style={thStyle}>By Table (preview)</th>
|
<th style={thStyle}>By Table</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -387,7 +320,7 @@ export function InfrastructurePage() {
|
|||||||
</table>
|
</table>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user