redesign: modern dark dashboard with gradients and animations
- New color system: deep #0a0a0f bg with indigo/violet/emerald accents - Sidebar redesign: gradient DH badge, improved active state, cleaner typography - Card system: .card class with colored top-border accents, hover glow - WeatherWidget: large temperature display, gradient bg, better 7-day grid - CalendarWidget: gradient today highlight, violet accents, improved events panel - TasksWidget: priority dots, hover states, max-height scroll - System page: circular SVG gauges with glow, accent cards, better bars - Bookmarks page: hover lift effect, colored category headers - Claude widgets: gradient badge headers, accent borders - DashboardHeader: live clock, gradient greeting text - ServicesGrid: pulsing online indicator (animate-ping), card lift on hover - globals.css: Inter font, custom scrollbar, card/glass-card, gradient-text helper - tailwind.config.ts: dash colors, gradient BG images, glow/float animations
This commit is contained in:
@@ -20,54 +20,44 @@ interface Metrics {
|
||||
network: { rxBytes: number | null; txBytes: number | null };
|
||||
}
|
||||
|
||||
function GaugeChart({ value, label, color }: { value: number; label: string; color: string }) {
|
||||
const r = 36;
|
||||
const circ = 2 * Math.PI * r;
|
||||
const half = circ / 2;
|
||||
const offset = half - (value / 100) * half;
|
||||
|
||||
function CircularGauge({ value, size = 96, color, label }: { value: number; size?: number; color: string; label: string }) {
|
||||
const radius = (size - 14) / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference - (value / 100) * circumference;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="relative w-24 h-14 overflow-hidden">
|
||||
<svg width="96" height="56" className="absolute bottom-0">
|
||||
<path
|
||||
d={`M 8 52 A ${r} ${r} 0 0 1 88 52`}
|
||||
fill="none"
|
||||
stroke="rgb(30,41,59)"
|
||||
strokeWidth="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d={`M 8 52 A ${r} ${r} 0 0 1 88 52`}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="10"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={`${half}`}
|
||||
strokeDashoffset={`${offset}`}
|
||||
style={{ transition: "stroke-dashoffset 0.6s ease" }}
|
||||
<div className="relative" style={{ width: size, height: size }}>
|
||||
<svg width={size} height={size} className="-rotate-90">
|
||||
<circle cx={size/2} cy={size/2} r={radius} fill="none" stroke="rgba(255,255,255,0.05)" strokeWidth="10" />
|
||||
<circle
|
||||
cx={size/2} cy={size/2} r={radius}
|
||||
fill="none" stroke={color}
|
||||
strokeWidth="10" strokeLinecap="round"
|
||||
strokeDasharray={circumference} strokeDashoffset={offset}
|
||||
style={{ transition: "stroke-dashoffset 0.8s ease", filter: `drop-shadow(0 0 6px ${color}60)` }}
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute bottom-0 w-full text-center pb-1">
|
||||
<span className="text-xl font-bold text-white">{value}%</span>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-lg font-bold text-white">{value}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400">{label}</span>
|
||||
<span className="text-xs text-slate-400 font-medium">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UsageBar({ label, value, color, detail }: { label: string; value: number; color: string; detail?: string }) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-slate-400">{label}</span>
|
||||
<span className="text-slate-300">{detail || `${value}%`}</span>
|
||||
<span className="text-slate-400 font-medium">{label}</span>
|
||||
<span className="text-white font-semibold">{detail || `${value}%`}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div className="h-1.5 rounded-full overflow-hidden" style={{ background: "rgba(255,255,255,0.05)" }}>
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-700"
|
||||
style={{ width: `${value}%`, backgroundColor: color }}
|
||||
style={{ width: `${value}%`, background: color, boxShadow: `0 0 8px ${color}80` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,18 +102,19 @@ export default function SystemPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-6 max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">System Monitor</h1>
|
||||
<p className="text-slate-400 text-sm">
|
||||
<h1 className="text-3xl font-bold text-white">System Monitor</h1>
|
||||
<p className="text-slate-500 text-sm mt-1">
|
||||
{lastUpdated ? `Обновлено: ${lastUpdated.toLocaleTimeString("ru-RU")}` : "Загрузка..."}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={fetchMetrics}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 text-sm transition-colors disabled:opacity-50"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-xl border border-white/10 text-slate-300 text-sm transition-all hover:border-white/20 hover:bg-white/5 disabled:opacity-50"
|
||||
style={{ background: "rgba(255,255,255,0.03)" }}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} />
|
||||
Обновить
|
||||
@@ -131,16 +122,17 @@ export default function SystemPage() {
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 bg-slate-800/50 p-1 rounded-lg w-fit">
|
||||
<div className="flex gap-1 p-1 rounded-xl w-fit border border-white/5" style={{ background: "rgba(255,255,255,0.03)" }}>
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
|
||||
className={`px-5 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
activeTab === tab
|
||||
? "bg-indigo-600 text-white shadow"
|
||||
: "text-slate-400 hover:text-slate-200"
|
||||
? "text-white"
|
||||
: "text-slate-500 hover:text-slate-300"
|
||||
}`}
|
||||
style={activeTab === tab ? { background: "linear-gradient(135deg, #6366f1, #8b5cf6)" } : {}}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
@@ -148,30 +140,30 @@ export default function SystemPage() {
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="glass-card p-8 text-center">
|
||||
<div className="card p-8 text-center">
|
||||
<div className="text-red-400 text-sm mb-2">⚠️ Не удалось получить метрики</div>
|
||||
<div className="text-slate-500 text-xs">Убедитесь что node_exporter запущен на хосте</div>
|
||||
<button onClick={fetchMetrics} className="mt-4 text-indigo-400 text-sm hover:underline">
|
||||
<div className="text-slate-600 text-xs">Убедитесь что node_exporter запущен на хосте</div>
|
||||
<button onClick={fetchMetrics} className="mt-4 text-indigo-400 text-sm hover:text-indigo-300 transition-colors">
|
||||
Попробовать снова
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Gauges row */}
|
||||
<div className="glass-card p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Activity className="w-4 h-4 text-indigo-400" />
|
||||
<span className="text-sm font-medium text-slate-300">Загрузка</span>
|
||||
{/* Circular gauges */}
|
||||
<div className="card card-accent-violet p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Activity className="w-4 h-4 text-violet-400" />
|
||||
<span className="text-sm font-semibold text-white">Загрузка системы</span>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="flex gap-8 justify-center animate-pulse">
|
||||
{[1,2,3].map(i => <div key={i} className="w-24 h-20 bg-slate-700/50 rounded" />)}
|
||||
<div className="flex gap-12 justify-center animate-pulse">
|
||||
{[1,2,3].map(i => <div key={i} className="w-24 h-28 bg-white/5 rounded-full" />)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-8 justify-around">
|
||||
<GaugeChart value={metrics?.cpu ?? 0} label="CPU" color="#6366f1" />
|
||||
<GaugeChart value={metrics?.ram?.percent ?? 0} label="RAM" color="#8b5cf6" />
|
||||
<GaugeChart value={metrics?.disk?.percent ?? 0} label="Disk" color="#10b981" />
|
||||
<div className="flex flex-wrap gap-10 justify-around">
|
||||
<CircularGauge value={metrics?.cpu ?? 0} label="CPU" color="#6366f1" />
|
||||
<CircularGauge value={metrics?.ram?.percent ?? 0} label="RAM" color="#8b5cf6" />
|
||||
<CircularGauge value={metrics?.disk?.percent ?? 0} label="Disk" color="#10b981" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -179,90 +171,51 @@ export default function SystemPage() {
|
||||
{/* Stats grid */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[
|
||||
{
|
||||
icon: Cpu,
|
||||
label: "CPU",
|
||||
value: loading ? "..." : `${metrics?.cpu ?? "—"}%`,
|
||||
sub: loading ? "" : `Load: ${metrics?.load1?.toFixed(2) ?? "—"} · ${metrics?.cpuCount ?? "?"} cores`,
|
||||
color: "text-indigo-400",
|
||||
},
|
||||
{
|
||||
icon: MemoryStick,
|
||||
label: "RAM",
|
||||
value: loading ? "..." : `${metrics?.ram?.used ?? "—"} GB`,
|
||||
sub: loading ? "" : `из ${metrics?.ram?.total ?? "?"} GB`,
|
||||
color: "text-violet-400",
|
||||
},
|
||||
{
|
||||
icon: HardDrive,
|
||||
label: "Disk",
|
||||
value: loading ? "..." : `${metrics?.disk?.used ?? "—"} GB`,
|
||||
sub: loading ? "" : `из ${metrics?.disk?.total ?? "?"} GB`,
|
||||
color: "text-emerald-400",
|
||||
},
|
||||
{
|
||||
icon: Clock,
|
||||
label: "Uptime",
|
||||
value: loading ? "..." : (metrics?.uptime ?? "—"),
|
||||
sub: "Время работы",
|
||||
color: "text-amber-400",
|
||||
},
|
||||
{ icon: Cpu, label: "CPU", value: loading ? "..." : `${metrics?.cpu ?? "—"}%`, sub: loading ? "" : `Load: ${metrics?.load1?.toFixed(2) ?? "—"} · ${metrics?.cpuCount ?? "?"} cores`, color: "text-indigo-400", accent: "card-accent-blue" },
|
||||
{ icon: MemoryStick, label: "RAM", value: loading ? "..." : `${metrics?.ram?.used ?? "—"} GB`, sub: loading ? "" : `из ${metrics?.ram?.total ?? "?"} GB`, color: "text-violet-400", accent: "card-accent-violet" },
|
||||
{ icon: HardDrive, label: "Disk", value: loading ? "..." : `${metrics?.disk?.used ?? "—"} GB`, sub: loading ? "" : `из ${metrics?.disk?.total ?? "?"} GB`, color: "text-emerald-400", accent: "card-accent-emerald" },
|
||||
{ icon: Clock, label: "Uptime", value: loading ? "..." : (metrics?.uptime ?? "—"), sub: "Время работы", color: "text-amber-400", accent: "card-accent-amber" },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="glass-card p-5">
|
||||
<div key={item.label} className={`card ${item.accent} p-5`}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<item.icon className={`w-4 h-4 ${item.color}`} />
|
||||
<span className="text-sm text-slate-400">{item.label}</span>
|
||||
<span className="text-xs font-semibold text-slate-500 uppercase tracking-wider">{item.label}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-white">{item.value}</div>
|
||||
{item.sub && <div className="text-xs text-slate-500 mt-1">{item.sub}</div>}
|
||||
{item.sub && <div className="text-xs text-slate-600 mt-1">{item.sub}</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Usage bars */}
|
||||
{!loading && metrics && (
|
||||
<div className="glass-card p-5 space-y-4">
|
||||
<h3 className="text-sm font-medium text-slate-300">Использование ресурсов</h3>
|
||||
<UsageBar
|
||||
label="CPU"
|
||||
value={metrics.cpu ?? 0}
|
||||
color="#6366f1"
|
||||
detail={`${metrics.cpu ?? "—"}%`}
|
||||
/>
|
||||
<div className="card p-5 space-y-5">
|
||||
<h3 className="text-sm font-semibold text-white">Использование ресурсов</h3>
|
||||
<UsageBar label="CPU" value={metrics.cpu ?? 0} color="#6366f1" detail={`${metrics.cpu ?? "—"}%`} />
|
||||
{metrics.ram && (
|
||||
<UsageBar
|
||||
label="RAM"
|
||||
value={metrics.ram.percent}
|
||||
color="#8b5cf6"
|
||||
detail={`${metrics.ram.used} / ${metrics.ram.total} GB`}
|
||||
/>
|
||||
<UsageBar label="RAM" value={metrics.ram.percent} color="#8b5cf6" detail={`${metrics.ram.used} / ${metrics.ram.total} GB`} />
|
||||
)}
|
||||
{metrics.disk && (
|
||||
<UsageBar
|
||||
label="Disk"
|
||||
value={metrics.disk.percent}
|
||||
color="#10b981"
|
||||
detail={`${metrics.disk.used} / ${metrics.disk.total} GB`}
|
||||
/>
|
||||
<UsageBar label="Disk" value={metrics.disk.percent} color="#10b981" detail={`${metrics.disk.used} / ${metrics.disk.total} GB`} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Network */}
|
||||
{!loading && metrics?.network && (
|
||||
<div className="glass-card p-5">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="card card-accent-cyan p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Wifi className="w-4 h-4 text-cyan-400" />
|
||||
<span className="text-sm font-medium text-slate-300">Сеть (всего)</span>
|
||||
<span className="text-sm font-semibold text-white">Сеть (всего)</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-1">↓ Получено</div>
|
||||
<div className="text-lg font-semibold text-white">{formatBytes(metrics.network.rxBytes)}</div>
|
||||
<div className="rounded-xl p-3 border border-white/5" style={{ background: "rgba(6,182,212,0.05)" }}>
|
||||
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">↓ Получено</div>
|
||||
<div className="text-xl font-bold text-white">{formatBytes(metrics.network.rxBytes)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-1">↑ Отправлено</div>
|
||||
<div className="text-lg font-semibold text-white">{formatBytes(metrics.network.txBytes)}</div>
|
||||
<div className="rounded-xl p-3 border border-white/5" style={{ background: "rgba(6,182,212,0.05)" }}>
|
||||
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">↑ Отправлено</div>
|
||||
<div className="text-xl font-bold text-white">{formatBytes(metrics.network.txBytes)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user