+ {/* KPI row */}
+
+ }
+ />
+ }
+ />
+ }
+ />
+ : }
+ iconTone={health.tone}
+ />
+
- {/* Cluster Status */}
-
-
-
- Cluster Status
-
-
-
-
-
- {healthStatus.label}
-
-
-
- {clusterHealth?.connectedNodes || 0}/{clusterHealth?.knownNodes || 0} nodes connected
-
-
-
+ {/* Cluster row */}
+
+ }
+ />
+ }
+ />
+ }
+ />
+
-
-
- Storage Nodes
-
-
-
-
- {clusterHealth?.storageNodesUp || 0}/{clusterHealth?.storageNodes || 0}
-
-
- Healthy storage nodes
-
-
-
-
-
-
- Partitions
-
-
-
-
- {clusterHealth?.partitionsAllOk || 0}/{clusterHealth?.partitions || 0}
-
-
- Healthy partitions
-
-
-
-
-
- {/* Charts Row 1 */}
-
- {/* Bucket Usage Chart */}
-
-
- Storage Usage by Bucket
- Distribution of storage across buckets
-
-
+ {/* Charts */}
+
+
{metrics?.usageByBucket && metrics.usageByBucket.length > 0 ? (
) : (
- No data available
+ No data available
)}
-
-
+
- {/* Bucket Details Table */}
-
-
- Storage Usage by Bucket (Table)
- Detailed breakdown of storage across all buckets
-
-
-
- {metrics?.usageByBucket && metrics.usageByBucket.length > 0 ? (
- metrics.usageByBucket.map((bucket) => (
-
-
-
{bucket.bucketName}
-
-
- {bucket.objectCount.toLocaleString()} objects
-
- {formatBytes(bucket.size)}
-
- {bucket.percentage.toFixed(1)}%
-
-
-
-
+
+ {metrics?.usageByBucket && metrics.usageByBucket.length > 0 ? (
+
+ {metrics.usageByBucket.map((bucket) => (
+
+
+
{bucket.bucketName}
+
+ {bucket.objectCount.toLocaleString()} objects
+ {formatBytes(bucket.size)}
+ {bucket.percentage.toFixed(1)}%
- ))
- ) : (
-
No buckets available
- )}
-
-
-
-
-
- {/* Recent Buckets */}
-
-
- Recent Buckets
- Your most recently created buckets
-
-
-
- {buckets.slice(0, 5).map((bucket) => (
-
-
-
-
{bucket.name}
-
+ ))}
+
+ ) : (
+
No buckets available
+ )}
+
+
+
+ {/* Recent buckets */}
+
+ {buckets.length === 0 ? (
+ }
+ title="No buckets yet"
+ description="Create your first bucket from the Buckets page to start storing objects."
+ tone="neutral"
+ />
+ ) : (
+
+ {buckets.slice(0, 5).map((bucket) => (
+ -
+ } tone="primary" size="md" />
+
+
{bucket.name}
+
Created {new Date(bucket.creationDate).toLocaleDateString()}
-
-
-
{bucket.objectCount?.toLocaleString()} objects
-
- {bucket.size ? formatBytes(bucket.size) : '---'}
-
-
-
- ))}
-
-
-
-
+
+
{bucket.objectCount?.toLocaleString() ?? '—'} objects
+
+ {bucket.size ? formatBytes(bucket.size) : '—'}
+
+
+
+ ))}
+
+ )}
+
+
+ )}
);
}
+
+function StatCard({
+ label,
+ value,
+ sub,
+ icon,
+ iconTone = 'primary',
+ valueTone = 'neutral',
+}: {
+ label: string;
+ value: string;
+ sub?: string;
+ icon: React.ReactNode;
+ iconTone?: StatTone;
+ valueTone?: StatTone;
+}) {
+ const valueColor =
+ valueTone === 'primary'
+ ? 'text-[var(--primary)]'
+ : valueTone === 'destructive'
+ ? 'text-[var(--destructive)]'
+ : 'text-[var(--foreground)]';
+ return (
+
+
+
+ {label}
+
+
+
+
+ {value}
+
+ {sub &&
{sub}
}
+
+ );
+}
+
+function Card({
+ title,
+ description,
+ children,
+}: {
+ title: string;
+ description?: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {title}
+ {description && {description}
}
+
+ {children}
+
+ );
+}
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
index 92408a9..c4bbd50 100644
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -7,6 +7,10 @@ export default {
],
theme: {
extend: {
+ fontFamily: {
+ sans: ['Geist Sans', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'system-ui', 'sans-serif'],
+ mono: ['Geist Mono', 'ui-monospace', '"SF Mono"', 'Menlo', 'Consolas', 'monospace'],
+ },
colors: {
border: 'var(--border)',
input: 'var(--input)',
@@ -16,6 +20,8 @@ export default {
primary: {
DEFAULT: 'var(--primary)',
foreground: 'var(--primary-foreground)',
+ soft: 'var(--accent-primary-soft)',
+ softBorder: 'var(--accent-primary-border)',
},
secondary: {
DEFAULT: 'var(--secondary)',
@@ -24,6 +30,11 @@ export default {
destructive: {
DEFAULT: 'var(--destructive)',
foreground: 'var(--destructive-foreground)',
+ soft: 'var(--danger-soft)',
+ softBorder: 'var(--danger-border)',
+ },
+ success: {
+ soft: 'var(--success-soft)',
},
muted: {
DEFAULT: 'var(--muted)',
@@ -41,6 +52,7 @@ export default {
DEFAULT: 'var(--card)',
foreground: 'var(--card-foreground)',
},
+ sunken: 'var(--surface-sunken)',
},
borderRadius: {
lg: 'var(--radius)',