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 { usePortal } from '@/lib/PortalContext';
|
||||||
import { Icon } from './Icons';
|
import { Icon } from './Icons';
|
||||||
import { SystemTrends } from './SystemTrends';
|
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 { STATUS_COLORS, STATUS_LABELS, formatRelativeTime, formatDuration } from '@/lib/deployments';
|
||||||
import type { DeploymentStatus } 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 = [
|
const quickLinks = [
|
||||||
{ name: 'Coolify', url: 'http://192.168.1.3:8000', icon: 'coolify', desc: 'Service manager' },
|
{ 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' },
|
{ name: 'Dozzle', url: 'http://192.168.1.3:9999', icon: 'scroll-text', desc: 'Container logs' },
|
||||||
@@ -52,7 +25,6 @@ const whyratingApps = [
|
|||||||
export function OverviewTab() {
|
export function OverviewTab() {
|
||||||
const {
|
const {
|
||||||
systemStats,
|
systemStats,
|
||||||
statsLoading,
|
|
||||||
services,
|
services,
|
||||||
healthStatus,
|
healthStatus,
|
||||||
deployments,
|
deployments,
|
||||||
@@ -68,69 +40,17 @@ export function OverviewTab() {
|
|||||||
const isDiscovered = discoveredServices.length > 0;
|
const isDiscovered = discoveredServices.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 max-w-6xl">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5 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>
|
|
||||||
|
|
||||||
{statsLoading && !systemStats ? (
|
{/* Row 1: System Trends (full-width hero) with live stats in header */}
|
||||||
<div className="space-y-4">
|
<SystemTrends
|
||||||
{[1, 2, 3].map(i => (
|
uptimeLabel={systemStats ? formatUptime(systemStats.uptime_seconds) : undefined}
|
||||||
<div key={i} className="space-y-1">
|
loadAvg={systemStats?.load_avg}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 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">
|
<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" />
|
<Icon name="server" size={16} className="text-slate-400 dark:text-stone-500" />
|
||||||
Services
|
Services
|
||||||
{isDiscovered && (
|
{isDiscovered && (
|
||||||
@@ -140,12 +60,12 @@ export function OverviewTab() {
|
|||||||
)}
|
)}
|
||||||
</h2>
|
</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-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>
|
<span className="text-sm text-slate-500 dark:text-stone-500">of {totalCount} running</span>
|
||||||
</div>
|
</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">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
||||||
<span className="text-slate-600 dark:text-stone-400">{runningCount} running</span>
|
<span className="text-slate-600 dark:text-stone-400">{runningCount} running</span>
|
||||||
@@ -164,7 +84,6 @@ export function OverviewTab() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mini service status dots */}
|
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{services.map(s => {
|
{services.map(s => {
|
||||||
const status = healthStatus[s.name];
|
const status = healthStatus[s.name];
|
||||||
@@ -180,12 +99,9 @@ export function OverviewTab() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* System Trends (6h from Prometheus) */}
|
{/* Row 2 right: Recent Deployments */}
|
||||||
<SystemTrends />
|
|
||||||
|
|
||||||
{/* 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">
|
<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" />
|
<Icon name="rocket" size={16} className="text-slate-400 dark:text-stone-500" />
|
||||||
Recent Deployments
|
Recent Deployments
|
||||||
</h2>
|
</h2>
|
||||||
@@ -227,9 +143,9 @@ export function OverviewTab() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<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" />
|
<Icon name="external-link" size={16} className="text-slate-400 dark:text-stone-500" />
|
||||||
Quick Links
|
Quick Links
|
||||||
</h2>
|
</h2>
|
||||||
@@ -253,14 +169,14 @@ export function OverviewTab() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* WhyRating Project */}
|
{/* 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 lg:col-span-2">
|
<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="whyrating" size={16} />
|
<Icon name="whyrating" size={16} />
|
||||||
WhyRating Project
|
WhyRating Project
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
<div className="space-y-2">
|
||||||
{whyratingApps.map(app => {
|
{whyratingApps.map(app => {
|
||||||
const svc = services.find(s => s.url === app.url);
|
const svc = services.find(s => s.url === app.url);
|
||||||
const status = svc ? healthStatus[svc.name] : undefined;
|
const status = svc ? healthStatus[svc.name] : undefined;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function useMetrics() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
const interval = setInterval(refresh, 60000); // refresh every 60s
|
const interval = setInterval(refresh, 60000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
@@ -58,13 +58,13 @@ function SparkChart({ label, series, color, fillColor, formatValue, domain, unit
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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-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 || ''}
|
{formatValue(lastVal)}{unit || ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-16">
|
<div className="h-20">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<AreaChart data={chartData} margin={{ top: 2, right: 0, bottom: 0, left: 0 }}>
|
<AreaChart data={chartData} margin={{ top: 2, right: 0, bottom: 0, left: 0 }}>
|
||||||
<defs>
|
<defs>
|
||||||
@@ -116,28 +116,43 @@ function SparkChart({ label, series, color, fillColor, formatValue, domain, unit
|
|||||||
function ShimmerChart() {
|
function ShimmerChart() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<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-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 className="h-3 w-12 rounded bg-slate-200 dark:bg-stone-800 animate-pulse" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SystemTrends() {
|
interface SystemTrendsProps {
|
||||||
|
uptimeLabel?: string;
|
||||||
|
loadAvg?: [number, number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SystemTrends({ uptimeLabel, loadAvg }: SystemTrendsProps) {
|
||||||
const { data, loading, error } = useMetrics();
|
const { data, loading, error } = useMetrics();
|
||||||
|
|
||||||
if (error && !data) return null;
|
if (error && !data) return null;
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<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">
|
<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" />
|
<Icon name="activity" size={16} className="text-slate-400 dark:text-stone-500" />
|
||||||
System Trends
|
System Trends
|
||||||
<span className="text-[10px] font-normal text-slate-400 dark:text-stone-600">6h</span>
|
<span className="text-[10px] font-normal text-slate-400 dark:text-stone-600">6h</span>
|
||||||
</h2>
|
</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
|
<a
|
||||||
href="http://192.168.1.3:3333"
|
href="http://192.168.1.3:3333"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -149,14 +164,15 @@ export function SystemTrends() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Charts */}
|
||||||
{loading && !data ? (
|
{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 />
|
<ShimmerChart />
|
||||||
<ShimmerChart />
|
<ShimmerChart />
|
||||||
</div>
|
</div>
|
||||||
) : data ? (
|
) : 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
|
<SparkChart
|
||||||
label="CPU"
|
label="CPU"
|
||||||
series={data.cpu}
|
series={data.cpu}
|
||||||
|
|||||||
Reference in New Issue
Block a user