Reorder Overview layout: charts hero, remove redundant health card
- System Trends promoted to full-width hero row (most important info first) - Uptime and load average moved into Trends card header - Removed separate System Health card (redundant with VitalsBar + Trends) - Services + Deployments in row 2, Quick Links + WhyRating in row 3 - Taller charts (h-20), tighter gaps, cleaner visual hierarchy Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,37 +3,10 @@
|
||||
import { usePortal } from '@/lib/PortalContext';
|
||||
import { Icon } from './Icons';
|
||||
import { SystemTrends } from './SystemTrends';
|
||||
import { getVitalsBg, getVitalsTrack, formatUptime } from '@/lib/stats';
|
||||
import { formatUptime } from '@/lib/stats';
|
||||
import { STATUS_COLORS, STATUS_LABELS, formatRelativeTime, formatDuration } from '@/lib/deployments';
|
||||
import type { DeploymentStatus } from '@/lib/deployments';
|
||||
|
||||
function StatBar({ label, percent, used, total, unit }: {
|
||||
label: string;
|
||||
percent: number;
|
||||
used: string;
|
||||
total: string;
|
||||
unit: string;
|
||||
}) {
|
||||
const bg = getVitalsBg(percent);
|
||||
const track = getVitalsTrack(percent);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-600 dark:text-stone-400">{label}</span>
|
||||
<span className="text-slate-900 dark:text-stone-100 tabular-nums font-medium">{Math.round(percent)}%</span>
|
||||
</div>
|
||||
<div className={`w-full h-2 rounded-full ${track}`}>
|
||||
<div
|
||||
className={`h-full rounded-full ${bg} transition-all duration-500`}
|
||||
style={{ width: `${Math.min(percent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 dark:text-stone-600 tabular-nums">{used} / {total} {unit}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const quickLinks = [
|
||||
{ name: 'Coolify', url: 'http://192.168.1.3:8000', icon: 'coolify', desc: 'Service manager' },
|
||||
{ name: 'Dozzle', url: 'http://192.168.1.3:9999', icon: 'scroll-text', desc: 'Container logs' },
|
||||
@@ -52,7 +25,6 @@ const whyratingApps = [
|
||||
export function OverviewTab() {
|
||||
const {
|
||||
systemStats,
|
||||
statsLoading,
|
||||
services,
|
||||
healthStatus,
|
||||
deployments,
|
||||
@@ -68,69 +40,17 @@ export function OverviewTab() {
|
||||
const isDiscovered = discoveredServices.length > 0;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 max-w-6xl">
|
||||
{/* System Health */}
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-4 flex items-center gap-2">
|
||||
<Icon name="activity" size={16} className="text-slate-400 dark:text-stone-500" />
|
||||
System Health
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5 max-w-6xl">
|
||||
|
||||
{statsLoading && !systemStats ? (
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="space-y-1">
|
||||
<div className="h-4 w-24 rounded bg-slate-200 dark:bg-stone-800 animate-pulse" />
|
||||
<div className="h-2 w-full rounded-full bg-slate-200 dark:bg-stone-800 animate-pulse" />
|
||||
<div className="h-3 w-20 rounded bg-slate-200 dark:bg-stone-800 animate-pulse" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : systemStats ? (
|
||||
<div className="space-y-4">
|
||||
<StatBar
|
||||
label="CPU"
|
||||
percent={systemStats.cpu_percent}
|
||||
used={`${systemStats.cpu_percent}%`}
|
||||
total="100%"
|
||||
unit=""
|
||||
/>
|
||||
<StatBar
|
||||
label="RAM"
|
||||
percent={systemStats.ram_percent}
|
||||
used={`${(systemStats.ram_used_mb / 1024).toFixed(1)}G`}
|
||||
total={`${(systemStats.ram_total_mb / 1024).toFixed(1)}G`}
|
||||
unit=""
|
||||
/>
|
||||
<StatBar
|
||||
label="Disk"
|
||||
percent={systemStats.disk_percent}
|
||||
used={`${systemStats.disk_used_gb}G`}
|
||||
total={`${systemStats.disk_total_gb}G`}
|
||||
unit=""
|
||||
/>
|
||||
{systemStats.swap_percent > 50 && (
|
||||
<StatBar
|
||||
label="Swap"
|
||||
percent={systemStats.swap_percent}
|
||||
used={`${(systemStats.swap_used_mb / 1024).toFixed(1)}G`}
|
||||
total={`${(systemStats.swap_total_mb / 1024).toFixed(1)}G`}
|
||||
unit=""
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-4 pt-2 text-xs text-slate-500 dark:text-stone-500 border-t border-slate-100 dark:border-stone-800">
|
||||
<span>Uptime: {formatUptime(systemStats.uptime_seconds)}</span>
|
||||
<span>Load: {systemStats.load_avg.join(' / ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-400 dark:text-stone-600">Stats unavailable</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Row 1: System Trends (full-width hero) with live stats in header */}
|
||||
<SystemTrends
|
||||
uptimeLabel={systemStats ? formatUptime(systemStats.uptime_seconds) : undefined}
|
||||
loadAvg={systemStats?.load_avg}
|
||||
/>
|
||||
|
||||
{/* Service Summary */}
|
||||
{/* Row 2 left: Services */}
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-4 flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
|
||||
<Icon name="server" size={16} className="text-slate-400 dark:text-stone-500" />
|
||||
Services
|
||||
{isDiscovered && (
|
||||
@@ -140,12 +60,12 @@ export function OverviewTab() {
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<div className="flex items-baseline gap-3 mb-4">
|
||||
<div className="flex items-baseline gap-3 mb-3">
|
||||
<span className="text-3xl font-bold text-slate-900 dark:text-stone-100 tabular-nums">{runningCount}</span>
|
||||
<span className="text-sm text-slate-500 dark:text-stone-500">of {totalCount} running</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mb-4">
|
||||
<div className="flex gap-3 mb-3">
|
||||
<div className="flex items-center gap-1.5 text-sm">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
||||
<span className="text-slate-600 dark:text-stone-400">{runningCount} running</span>
|
||||
@@ -164,7 +84,6 @@ export function OverviewTab() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mini service status dots */}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{services.map(s => {
|
||||
const status = healthStatus[s.name];
|
||||
@@ -180,12 +99,9 @@ export function OverviewTab() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Trends (6h from Prometheus) */}
|
||||
<SystemTrends />
|
||||
|
||||
{/* Recent Deployments */}
|
||||
{/* Row 2 right: Recent Deployments */}
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-4 flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
|
||||
<Icon name="rocket" size={16} className="text-slate-400 dark:text-stone-500" />
|
||||
Recent Deployments
|
||||
</h2>
|
||||
@@ -227,9 +143,9 @@ export function OverviewTab() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
{/* Row 3 left: Quick Links */}
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-4 flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
|
||||
<Icon name="external-link" size={16} className="text-slate-400 dark:text-stone-500" />
|
||||
Quick Links
|
||||
</h2>
|
||||
@@ -253,14 +169,14 @@ export function OverviewTab() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* WhyRating Project */}
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5 lg:col-span-2">
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-4 flex items-center gap-2">
|
||||
{/* Row 3 right: WhyRating Project */}
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
|
||||
<Icon name="whyrating" size={16} />
|
||||
WhyRating Project
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div className="space-y-2">
|
||||
{whyratingApps.map(app => {
|
||||
const svc = services.find(s => s.url === app.url);
|
||||
const status = svc ? healthStatus[svc.name] : undefined;
|
||||
|
||||
@@ -30,7 +30,7 @@ function useMetrics() {
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
const interval = setInterval(refresh, 60000); // refresh every 60s
|
||||
const interval = setInterval(refresh, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, [refresh]);
|
||||
|
||||
@@ -58,13 +58,13 @@ function SparkChart({ label, series, color, fillColor, formatValue, domain, unit
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-baseline justify-between mb-1">
|
||||
<div className="flex items-baseline justify-between mb-1.5">
|
||||
<span className="text-xs font-medium text-slate-500 dark:text-stone-500">{label}</span>
|
||||
<span className="text-xs font-semibold tabular-nums" style={{ color }}>
|
||||
<span className="text-sm font-semibold tabular-nums" style={{ color }}>
|
||||
{formatValue(lastVal)}{unit || ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-16">
|
||||
<div className="h-20">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData} margin={{ top: 2, right: 0, bottom: 0, left: 0 }}>
|
||||
<defs>
|
||||
@@ -116,28 +116,43 @@ function SparkChart({ label, series, color, fillColor, formatValue, domain, unit
|
||||
function ShimmerChart() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-baseline justify-between mb-1">
|
||||
<div className="flex items-baseline justify-between mb-1.5">
|
||||
<div className="h-3 w-10 rounded bg-slate-200 dark:bg-stone-800 animate-pulse" />
|
||||
<div className="h-3 w-12 rounded bg-slate-200 dark:bg-stone-800 animate-pulse" />
|
||||
</div>
|
||||
<div className="h-16 rounded bg-slate-100 dark:bg-stone-800/50 animate-pulse" />
|
||||
<div className="h-20 rounded bg-slate-100 dark:bg-stone-800/50 animate-pulse" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SystemTrends() {
|
||||
interface SystemTrendsProps {
|
||||
uptimeLabel?: string;
|
||||
loadAvg?: [number, number, number];
|
||||
}
|
||||
|
||||
export function SystemTrends({ uptimeLabel, loadAvg }: SystemTrendsProps) {
|
||||
const { data, loading, error } = useMetrics();
|
||||
|
||||
if (error && !data) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5 lg:col-span-2">
|
||||
{/* Header with title, live stats, and Grafana link */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 flex items-center gap-2">
|
||||
<Icon name="activity" size={16} className="text-slate-400 dark:text-stone-500" />
|
||||
System Trends
|
||||
<span className="text-[10px] font-normal text-slate-400 dark:text-stone-600">6h</span>
|
||||
</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 flex items-center gap-2">
|
||||
<Icon name="activity" size={16} className="text-slate-400 dark:text-stone-500" />
|
||||
System Trends
|
||||
<span className="text-[10px] font-normal text-slate-400 dark:text-stone-600">6h</span>
|
||||
</h2>
|
||||
{(uptimeLabel || loadAvg) && (
|
||||
<div className="hidden sm:flex items-center gap-3 text-[11px] text-slate-400 dark:text-stone-600">
|
||||
<span className="w-px h-3 bg-slate-200 dark:bg-stone-700" />
|
||||
{uptimeLabel && <span>Up {uptimeLabel}</span>}
|
||||
{loadAvg && <span>Load {loadAvg.map(v => v.toFixed(1)).join(' ')}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href="http://192.168.1.3:3333"
|
||||
target="_blank"
|
||||
@@ -149,14 +164,15 @@ export function SystemTrends() {
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
{loading && !data ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
<ShimmerChart />
|
||||
<ShimmerChart />
|
||||
<ShimmerChart />
|
||||
</div>
|
||||
) : data ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
<SparkChart
|
||||
label="CPU"
|
||||
series={data.cpu}
|
||||
|
||||
Reference in New Issue
Block a user