feat: Add complete dashboard for customers
🎨 Dashboard UI (Next.js 14) - Modern dark theme with purple/green accents - Responsive layout with sidebar navigation - Real-time metrics cards with alerts - Interactive charts (CPU, Memory) with Recharts - Hosts table with status indicators - Alerts list with AI suggestions 🤖 AI Copilot Chat - Slide-in panel with chat interface - Quick action buttons - Command suggestions with execute option - Real-time loading indicators 📊 Components - MetricCard with trend indicators - HostsTable with colored metrics - AlertsList with severity levels - AIInsights panel with predictions - CpuChart and MemoryChart 🛠️ Tech Stack - Next.js 14 with App Router - TypeScript - Tailwind CSS - Recharts for visualization - Zustand for state management
This commit is contained in:
6
dashboard/next.config.js
Normal file
6
dashboard/next.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
33
dashboard/package.json
Normal file
33
dashboard/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "ophion-dashboard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev -p 3000",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start -p 3000",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "14.1.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"next-auth": "^4.24.0",
|
||||||
|
"@tanstack/react-query": "^5.17.0",
|
||||||
|
"recharts": "^2.10.0",
|
||||||
|
"lucide-react": "^0.312.0",
|
||||||
|
"clsx": "^2.1.0",
|
||||||
|
"tailwind-merge": "^2.2.0",
|
||||||
|
"date-fns": "^3.2.0",
|
||||||
|
"zustand": "^4.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.3.0",
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"autoprefixer": "^10.4.0",
|
||||||
|
"postcss": "^8.4.0",
|
||||||
|
"tailwindcss": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
dashboard/postcss.config.js
Normal file
6
dashboard/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
63
dashboard/src/app/globals.css
Normal file
63
dashboard/src/app/globals.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: 2 6 23;
|
||||||
|
--foreground: 248 250 252;
|
||||||
|
--card: 15 23 42;
|
||||||
|
--card-foreground: 248 250 252;
|
||||||
|
--primary: 34 197 94;
|
||||||
|
--primary-foreground: 255 255 255;
|
||||||
|
--secondary: 139 92 246;
|
||||||
|
--muted: 51 65 85;
|
||||||
|
--muted-foreground: 148 163 184;
|
||||||
|
--accent: 34 197 94;
|
||||||
|
--destructive: 239 68 68;
|
||||||
|
--border: 51 65 85;
|
||||||
|
--ring: 34 197 94;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: rgb(var(--background));
|
||||||
|
color: rgb(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgb(15, 23, 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(51, 65, 85);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgb(71, 85, 105);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4); }
|
||||||
|
50% { box-shadow: 0 0 0 8px rgba(34, 197, 94, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-glow {
|
||||||
|
animation: pulse-glow 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card hover effects */
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
24
dashboard/src/app/layout.tsx
Normal file
24
dashboard/src/app/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'OPHION Dashboard',
|
||||||
|
description: 'Observability Platform with AI',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="pt-BR" className="dark">
|
||||||
|
<body className={`${inter.className} bg-slate-950 text-white`}>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
134
dashboard/src/app/page.tsx
Normal file
134
dashboard/src/app/page.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import Sidebar from '@/components/layout/Sidebar'
|
||||||
|
import Header from '@/components/layout/Header'
|
||||||
|
import MetricCard from '@/components/ui/MetricCard'
|
||||||
|
import HostsTable from '@/components/ui/HostsTable'
|
||||||
|
import AlertsList from '@/components/ui/AlertsList'
|
||||||
|
import CpuChart from '@/components/charts/CpuChart'
|
||||||
|
import MemoryChart from '@/components/charts/MemoryChart'
|
||||||
|
import AIInsights from '@/components/ui/AIInsights'
|
||||||
|
import Copilot from '@/components/ui/Copilot'
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const [showCopilot, setShowCopilot] = useState(false)
|
||||||
|
const [metrics, setMetrics] = useState({
|
||||||
|
totalHosts: 0,
|
||||||
|
healthyHosts: 0,
|
||||||
|
activeAlerts: 0,
|
||||||
|
cpuAvg: 0,
|
||||||
|
memoryAvg: 0,
|
||||||
|
diskAvg: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulated data - replace with real API calls
|
||||||
|
useEffect(() => {
|
||||||
|
setMetrics({
|
||||||
|
totalHosts: 12,
|
||||||
|
healthyHosts: 11,
|
||||||
|
activeAlerts: 3,
|
||||||
|
cpuAvg: 42.5,
|
||||||
|
memoryAvg: 68.3,
|
||||||
|
diskAvg: 54.2,
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen overflow-hidden">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<Sidebar />
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<Header onCopilotClick={() => setShowCopilot(true)} />
|
||||||
|
|
||||||
|
{/* Dashboard Content */}
|
||||||
|
<main className="flex-1 overflow-y-auto p-6 bg-slate-950">
|
||||||
|
{/* Top Metrics */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
|
<MetricCard
|
||||||
|
title="Total Hosts"
|
||||||
|
value={metrics.totalHosts}
|
||||||
|
subtitle={`${metrics.healthyHosts} healthy`}
|
||||||
|
icon="🖥️"
|
||||||
|
trend="stable"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="CPU Average"
|
||||||
|
value={`${metrics.cpuAvg}%`}
|
||||||
|
subtitle="across all hosts"
|
||||||
|
icon="⚡"
|
||||||
|
trend={metrics.cpuAvg > 70 ? 'up' : 'stable'}
|
||||||
|
alert={metrics.cpuAvg > 80}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Memory Average"
|
||||||
|
value={`${metrics.memoryAvg}%`}
|
||||||
|
subtitle="across all hosts"
|
||||||
|
icon="💾"
|
||||||
|
trend={metrics.memoryAvg > 70 ? 'up' : 'stable'}
|
||||||
|
alert={metrics.memoryAvg > 85}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Active Alerts"
|
||||||
|
value={metrics.activeAlerts}
|
||||||
|
subtitle="2 critical, 1 warning"
|
||||||
|
icon="🚨"
|
||||||
|
trend={metrics.activeAlerts > 0 ? 'up' : 'down'}
|
||||||
|
alert={metrics.activeAlerts > 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Charts Row */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div className="bg-slate-900 rounded-xl p-6 border border-slate-800">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">CPU Usage (24h)</h3>
|
||||||
|
<CpuChart />
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-900 rounded-xl p-6 border border-slate-800">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Memory Usage (24h)</h3>
|
||||||
|
<MemoryChart />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AI Insights */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<AIInsights />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Section */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Hosts Table */}
|
||||||
|
<div className="bg-slate-900 rounded-xl p-6 border border-slate-800">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">Hosts</h3>
|
||||||
|
<button className="text-sm text-green-400 hover:text-green-300">
|
||||||
|
View all →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<HostsTable />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Alerts */}
|
||||||
|
<div className="bg-slate-900 rounded-xl p-6 border border-slate-800">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">Recent Alerts</h3>
|
||||||
|
<button className="text-sm text-green-400 hover:text-green-300">
|
||||||
|
View all →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<AlertsList />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AI Copilot Sidebar */}
|
||||||
|
{showCopilot && (
|
||||||
|
<Copilot onClose={() => setShowCopilot(false)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
64
dashboard/src/components/charts/CpuChart.tsx
Normal file
64
dashboard/src/components/charts/CpuChart.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Area, AreaChart } from 'recharts'
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{ time: '00:00', value: 35 },
|
||||||
|
{ time: '02:00', value: 28 },
|
||||||
|
{ time: '04:00', value: 22 },
|
||||||
|
{ time: '06:00', value: 25 },
|
||||||
|
{ time: '08:00', value: 45 },
|
||||||
|
{ time: '10:00', value: 62 },
|
||||||
|
{ time: '12:00', value: 58 },
|
||||||
|
{ time: '14:00', value: 72 },
|
||||||
|
{ time: '16:00', value: 68 },
|
||||||
|
{ time: '18:00', value: 55 },
|
||||||
|
{ time: '20:00', value: 48 },
|
||||||
|
{ time: '22:00', value: 42 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function CpuChart() {
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
<AreaChart data={data}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="cpuGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.3} />
|
||||||
|
<stop offset="95%" stopColor="#22c55e" stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
stroke="#64748b"
|
||||||
|
fontSize={12}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
stroke="#64748b"
|
||||||
|
fontSize={12}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={(value) => `${value}%`}
|
||||||
|
domain={[0, 100]}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#1e293b',
|
||||||
|
border: '1px solid #334155',
|
||||||
|
borderRadius: '8px',
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: '#94a3b8' }}
|
||||||
|
formatter={(value: number) => [`${value}%`, 'CPU']}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="value"
|
||||||
|
stroke="#22c55e"
|
||||||
|
strokeWidth={2}
|
||||||
|
fill="url(#cpuGradient)"
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
64
dashboard/src/components/charts/MemoryChart.tsx
Normal file
64
dashboard/src/components/charts/MemoryChart.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{ time: '00:00', value: 58 },
|
||||||
|
{ time: '02:00', value: 55 },
|
||||||
|
{ time: '04:00', value: 52 },
|
||||||
|
{ time: '06:00', value: 54 },
|
||||||
|
{ time: '08:00', value: 62 },
|
||||||
|
{ time: '10:00', value: 68 },
|
||||||
|
{ time: '12:00', value: 72 },
|
||||||
|
{ time: '14:00', value: 78 },
|
||||||
|
{ time: '16:00', value: 75 },
|
||||||
|
{ time: '18:00', value: 70 },
|
||||||
|
{ time: '20:00', value: 65 },
|
||||||
|
{ time: '22:00', value: 60 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function MemoryChart() {
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
<AreaChart data={data}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="memoryGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.3} />
|
||||||
|
<stop offset="95%" stopColor="#8b5cf6" stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
stroke="#64748b"
|
||||||
|
fontSize={12}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
stroke="#64748b"
|
||||||
|
fontSize={12}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={(value) => `${value}%`}
|
||||||
|
domain={[0, 100]}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#1e293b',
|
||||||
|
border: '1px solid #334155',
|
||||||
|
borderRadius: '8px',
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: '#94a3b8' }}
|
||||||
|
formatter={(value: number) => [`${value}%`, 'Memory']}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="value"
|
||||||
|
stroke="#8b5cf6"
|
||||||
|
strokeWidth={2}
|
||||||
|
fill="url(#memoryGradient)"
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
68
dashboard/src/components/layout/Header.tsx
Normal file
68
dashboard/src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
onCopilotClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Header({ onCopilotClick }: HeaderProps) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="h-16 bg-slate-900 border-b border-slate-800 flex items-center justify-between px-6">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="flex-1 max-w-xl">
|
||||||
|
<div className="relative">
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500">
|
||||||
|
🔍
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search hosts, metrics, logs..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg focus:outline-none focus:border-green-500 text-sm"
|
||||||
|
/>
|
||||||
|
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500 bg-slate-700 px-2 py-0.5 rounded">
|
||||||
|
⌘K
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side */}
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{/* Time Range */}
|
||||||
|
<select className="bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-green-500">
|
||||||
|
<option>Last 1 hour</option>
|
||||||
|
<option>Last 6 hours</option>
|
||||||
|
<option>Last 24 hours</option>
|
||||||
|
<option>Last 7 days</option>
|
||||||
|
<option>Last 30 days</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Refresh */}
|
||||||
|
<button className="p-2 hover:bg-slate-800 rounded-lg transition" title="Refresh">
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Notifications */}
|
||||||
|
<button className="relative p-2 hover:bg-slate-800 rounded-lg transition" title="Notifications">
|
||||||
|
🔔
|
||||||
|
<span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full text-xs flex items-center justify-center">
|
||||||
|
3
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* AI Copilot Button */}
|
||||||
|
<button
|
||||||
|
onClick={onCopilotClick}
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 bg-gradient-to-r from-purple-600 to-purple-700 hover:from-purple-500 hover:to-purple-600 rounded-lg transition font-medium"
|
||||||
|
>
|
||||||
|
<span>🤖</span>
|
||||||
|
<span>AI Copilot</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
96
dashboard/src/components/layout/Sidebar.tsx
Normal file
96
dashboard/src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ name: 'Overview', icon: '📊', href: '/' },
|
||||||
|
{ name: 'Hosts', icon: '🖥️', href: '/hosts' },
|
||||||
|
{ name: 'Containers', icon: '🐳', href: '/containers' },
|
||||||
|
{ name: 'Metrics', icon: '📈', href: '/metrics' },
|
||||||
|
{ name: 'Logs', icon: '📝', href: '/logs' },
|
||||||
|
{ name: 'Traces', icon: '🔍', href: '/traces' },
|
||||||
|
{ name: 'Alerts', icon: '🚨', href: '/alerts' },
|
||||||
|
{ name: 'Dashboards', icon: '📋', href: '/dashboards' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const bottomItems = [
|
||||||
|
{ name: 'AI Insights', icon: '🤖', href: '/ai' },
|
||||||
|
{ name: 'Settings', icon: '⚙️', href: '/settings' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Sidebar() {
|
||||||
|
const pathname = usePathname()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="w-64 bg-slate-900 border-r border-slate-800 flex flex-col">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="p-6 border-b border-slate-800">
|
||||||
|
<Link href="/" className="flex items-center space-x-3">
|
||||||
|
<span className="text-3xl">🐍</span>
|
||||||
|
<div>
|
||||||
|
<span className="text-xl font-bold bg-gradient-to-r from-green-400 to-emerald-500 bg-clip-text text-transparent">
|
||||||
|
OPHION
|
||||||
|
</span>
|
||||||
|
<span className="ml-2 text-xs bg-purple-600 px-2 py-0.5 rounded-full">AI</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Navigation */}
|
||||||
|
<nav className="flex-1 p-4 space-y-1">
|
||||||
|
{menuItems.map((item) => {
|
||||||
|
const isActive = pathname === item.href
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-all ${
|
||||||
|
isActive
|
||||||
|
? 'bg-green-600/20 text-green-400 border-l-2 border-green-400'
|
||||||
|
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-xl">{item.icon}</span>
|
||||||
|
<span className="font-medium">{item.name}</span>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Bottom Navigation */}
|
||||||
|
<div className="p-4 border-t border-slate-800 space-y-1">
|
||||||
|
{bottomItems.map((item) => {
|
||||||
|
const isActive = pathname === item.href
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-all ${
|
||||||
|
isActive
|
||||||
|
? 'bg-purple-600/20 text-purple-400'
|
||||||
|
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-xl">{item.icon}</span>
|
||||||
|
<span className="font-medium">{item.name}</span>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User */}
|
||||||
|
<div className="p-4 border-t border-slate-800">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-green-500 to-emerald-600 flex items-center justify-center font-bold">
|
||||||
|
A
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Admin</p>
|
||||||
|
<p className="text-xs text-slate-500">admin@empresa.com</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
78
dashboard/src/components/ui/AIInsights.tsx
Normal file
78
dashboard/src/components/ui/AIInsights.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
const insights = [
|
||||||
|
{
|
||||||
|
type: 'anomaly',
|
||||||
|
icon: '🔍',
|
||||||
|
title: 'Anomalia Detectada',
|
||||||
|
description: 'O servidor cache-01 está com consumo 40% acima do padrão para este horário.',
|
||||||
|
severity: 'high',
|
||||||
|
action: 'Investigar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'prediction',
|
||||||
|
icon: '🔮',
|
||||||
|
title: 'Previsão de Capacidade',
|
||||||
|
description: 'O disco do servidor db-01 vai atingir 90% em aproximadamente 12 dias.',
|
||||||
|
severity: 'medium',
|
||||||
|
action: 'Planejar expansão',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'optimization',
|
||||||
|
icon: '💡',
|
||||||
|
title: 'Oportunidade de Economia',
|
||||||
|
description: 'O servidor dev-02 está subutilizado (CPU média: 5%). Considere reduzir recursos.',
|
||||||
|
severity: 'low',
|
||||||
|
action: 'Ver detalhes',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function AIInsights() {
|
||||||
|
const getSeverityColor = (severity: string) => {
|
||||||
|
switch (severity) {
|
||||||
|
case 'high':
|
||||||
|
return 'border-red-500/50 bg-red-950/20'
|
||||||
|
case 'medium':
|
||||||
|
return 'border-yellow-500/50 bg-yellow-950/20'
|
||||||
|
default:
|
||||||
|
return 'border-green-500/50 bg-green-950/20'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-r from-purple-900/30 to-slate-900 rounded-xl p-6 border border-purple-500/30">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<span className="text-2xl">🤖</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">AI Insights</h3>
|
||||||
|
<p className="text-sm text-slate-400">Análises geradas automaticamente</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="text-sm text-purple-400 hover:text-purple-300">
|
||||||
|
Ver todos →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{insights.map((insight, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`p-4 rounded-lg border ${getSeverityColor(insight.severity)} hover:bg-slate-800/50 transition cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<span className="text-2xl">{insight.icon}</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium">{insight.title}</p>
|
||||||
|
<p className="text-sm text-slate-400 mt-1">{insight.description}</p>
|
||||||
|
<button className="mt-3 text-sm text-purple-400 hover:text-purple-300">
|
||||||
|
{insight.action} →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
85
dashboard/src/components/ui/AlertsList.tsx
Normal file
85
dashboard/src/components/ui/AlertsList.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
const alerts = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
severity: 'critical',
|
||||||
|
title: 'High CPU Usage',
|
||||||
|
host: 'cache-01',
|
||||||
|
message: 'CPU at 92% for 10 minutes',
|
||||||
|
time: '2 min ago',
|
||||||
|
aiSuggestion: 'Consider restarting Redis or scaling horizontally',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
severity: 'critical',
|
||||||
|
title: 'Memory Critical',
|
||||||
|
host: 'cache-01',
|
||||||
|
message: 'Memory at 94%',
|
||||||
|
time: '5 min ago',
|
||||||
|
aiSuggestion: 'Memory leak detected. Recommended: restart service',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
severity: 'warning',
|
||||||
|
title: 'High Memory Usage',
|
||||||
|
host: 'api-01',
|
||||||
|
message: 'Memory at 85%',
|
||||||
|
time: '15 min ago',
|
||||||
|
aiSuggestion: 'Monitor for next hour before action',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function AlertsList() {
|
||||||
|
const getSeverityStyles = (severity: string) => {
|
||||||
|
switch (severity) {
|
||||||
|
case 'critical':
|
||||||
|
return 'border-l-red-500 bg-red-950/30'
|
||||||
|
case 'warning':
|
||||||
|
return 'border-l-yellow-500 bg-yellow-950/30'
|
||||||
|
default:
|
||||||
|
return 'border-l-blue-500 bg-blue-950/30'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSeverityIcon = (severity: string) => {
|
||||||
|
switch (severity) {
|
||||||
|
case 'critical':
|
||||||
|
return '🔴'
|
||||||
|
case 'warning':
|
||||||
|
return '🟡'
|
||||||
|
default:
|
||||||
|
return '🔵'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{alerts.map((alert) => (
|
||||||
|
<div
|
||||||
|
key={alert.id}
|
||||||
|
className={`p-4 rounded-lg border-l-4 ${getSeverityStyles(alert.severity)} cursor-pointer hover:bg-slate-800/50 transition`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<span className="text-xl">{getSeverityIcon(alert.severity)}</span>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{alert.title}</p>
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
{alert.host} • {alert.message}
|
||||||
|
</p>
|
||||||
|
{alert.aiSuggestion && (
|
||||||
|
<div className="mt-2 flex items-start space-x-2 text-sm text-purple-400">
|
||||||
|
<span>🤖</span>
|
||||||
|
<span>{alert.aiSuggestion}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-slate-500">{alert.time}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
251
dashboard/src/components/ui/Copilot.tsx
Normal file
251
dashboard/src/components/ui/Copilot.tsx
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect } from 'react'
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
role: 'user' | 'assistant'
|
||||||
|
content: string
|
||||||
|
actions?: Array<{
|
||||||
|
type: string
|
||||||
|
description: string
|
||||||
|
command?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CopilotProps {
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Copilot({ onClose }: CopilotProps) {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'Olá! Sou o OPHION Copilot. Posso ajudar com análise de métricas, troubleshooting, ou qualquer dúvida sobre sua infraestrutura. Como posso ajudar?',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
const [input, setInput] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
}, [messages])
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!input.trim() || isLoading) return
|
||||||
|
|
||||||
|
const userMessage = input.trim()
|
||||||
|
setInput('')
|
||||||
|
setMessages((prev) => [...prev, { role: 'user', content: userMessage }])
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
// Simulate AI response - replace with real API call
|
||||||
|
setTimeout(() => {
|
||||||
|
const response = generateMockResponse(userMessage)
|
||||||
|
setMessages((prev) => [...prev, response])
|
||||||
|
setIsLoading(false)
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateMockResponse = (query: string): Message => {
|
||||||
|
const lowerQuery = query.toLowerCase()
|
||||||
|
|
||||||
|
if (lowerQuery.includes('cpu') || lowerQuery.includes('lento')) {
|
||||||
|
return {
|
||||||
|
role: 'assistant',
|
||||||
|
content: `Analisei as métricas do servidor cache-01 que está com CPU alta (92%).
|
||||||
|
|
||||||
|
**Causa provável:** O processo Redis está consumindo mais CPU que o normal, provavelmente devido a um aumento de requisições ou operações de BGSAVE.
|
||||||
|
|
||||||
|
**Métricas relacionadas:**
|
||||||
|
- Conexões ativas: 1,247 (média: 800)
|
||||||
|
- Operações/s: 45,000 (média: 30,000)
|
||||||
|
- Memory: 94% (próximo do limite)
|
||||||
|
|
||||||
|
**Recomendações:**`,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'command',
|
||||||
|
description: 'Reiniciar Redis gracefully',
|
||||||
|
command: 'redis-cli SHUTDOWN NOSAVE && systemctl start redis',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'scale',
|
||||||
|
description: 'Adicionar réplica Redis',
|
||||||
|
command: 'kubectl scale deployment redis --replicas=2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lowerQuery.includes('disco') || lowerQuery.includes('storage')) {
|
||||||
|
return {
|
||||||
|
role: 'assistant',
|
||||||
|
content: `Analisei o uso de disco dos seus servidores:
|
||||||
|
|
||||||
|
**Previsões de capacidade:**
|
||||||
|
- **db-01:** 68% usado, vai atingir 90% em ~12 dias
|
||||||
|
- **web-01:** 34% usado, estável
|
||||||
|
- **api-01:** 52% usado, crescendo 2%/dia
|
||||||
|
|
||||||
|
**Maiores consumidores em db-01:**
|
||||||
|
- /var/lib/postgresql: 45GB
|
||||||
|
- /var/log: 12GB
|
||||||
|
- /tmp: 3GB
|
||||||
|
|
||||||
|
**Sugestão:** Rotacionar logs antigos e configurar retenção.`,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'command',
|
||||||
|
description: 'Limpar logs antigos',
|
||||||
|
command: 'find /var/log -type f -mtime +7 -delete',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: 'assistant',
|
||||||
|
content: `Entendi sua pergunta sobre "${query}".
|
||||||
|
|
||||||
|
Posso ajudar com:
|
||||||
|
- 📊 Análise de métricas (CPU, memória, disco, rede)
|
||||||
|
- 🔍 Investigação de problemas
|
||||||
|
- 📝 Análise de logs
|
||||||
|
- 🚨 Explicação de alertas
|
||||||
|
- 💡 Sugestões de otimização
|
||||||
|
|
||||||
|
O que você gostaria de saber especificamente?`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const quickActions = [
|
||||||
|
'Por que o cache-01 está lento?',
|
||||||
|
'Analise os alertas ativos',
|
||||||
|
'Previsão de disco',
|
||||||
|
'Resumo das últimas 24h',
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-96 bg-slate-900 border-l border-slate-800 flex flex-col h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-4 border-b border-slate-800 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<span className="text-2xl">🤖</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">OPHION Copilot</h3>
|
||||||
|
<p className="text-xs text-green-400 flex items-center">
|
||||||
|
<span className="w-2 h-2 bg-green-400 rounded-full mr-1 animate-pulse"></span>
|
||||||
|
Online
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-slate-800 rounded-lg transition"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
|
{messages.map((message, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-[85%] rounded-lg p-3 ${
|
||||||
|
message.role === 'user'
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-slate-800 text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
|
||||||
|
|
||||||
|
{message.actions && message.actions.length > 0 && (
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
{message.actions.map((action, actionIndex) => (
|
||||||
|
<div
|
||||||
|
key={actionIndex}
|
||||||
|
className="bg-slate-900/50 rounded p-2 border border-slate-700"
|
||||||
|
>
|
||||||
|
<p className="text-xs text-slate-400">{action.description}</p>
|
||||||
|
{action.command && (
|
||||||
|
<code className="text-xs text-green-400 bg-slate-950 px-2 py-1 rounded mt-1 block">
|
||||||
|
{action.command}
|
||||||
|
</code>
|
||||||
|
)}
|
||||||
|
<button className="text-xs text-purple-400 mt-2 hover:text-purple-300">
|
||||||
|
Executar →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="bg-slate-800 rounded-lg p-3">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<div className="w-2 h-2 bg-purple-400 rounded-full animate-bounce"></div>
|
||||||
|
<div className="w-2 h-2 bg-purple-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
||||||
|
<div className="w-2 h-2 bg-purple-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
{messages.length === 1 && (
|
||||||
|
<div className="px-4 pb-2">
|
||||||
|
<p className="text-xs text-slate-500 mb-2">Perguntas frequentes:</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{quickActions.map((action, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setInput(action)}
|
||||||
|
className="text-xs bg-slate-800 hover:bg-slate-700 px-3 py-1.5 rounded-full transition"
|
||||||
|
>
|
||||||
|
{action}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<form onSubmit={handleSubmit} className="p-4 border-t border-slate-800">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
placeholder="Pergunte qualquer coisa..."
|
||||||
|
className="flex-1 bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-sm focus:outline-none focus:border-purple-500"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !input.trim()}
|
||||||
|
className="px-4 py-2 bg-purple-600 hover:bg-purple-500 disabled:bg-slate-700 disabled:cursor-not-allowed rounded-lg transition"
|
||||||
|
>
|
||||||
|
➤
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
76
dashboard/src/components/ui/HostsTable.tsx
Normal file
76
dashboard/src/components/ui/HostsTable.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
const hosts = [
|
||||||
|
{ name: 'web-01', status: 'healthy', cpu: 45, memory: 62, disk: 34 },
|
||||||
|
{ name: 'web-02', status: 'healthy', cpu: 38, memory: 58, disk: 41 },
|
||||||
|
{ name: 'api-01', status: 'warning', cpu: 78, memory: 85, disk: 52 },
|
||||||
|
{ name: 'db-01', status: 'healthy', cpu: 22, memory: 71, disk: 68 },
|
||||||
|
{ name: 'cache-01', status: 'critical', cpu: 92, memory: 94, disk: 45 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function HostsTable() {
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'healthy':
|
||||||
|
return 'bg-green-500'
|
||||||
|
case 'warning':
|
||||||
|
return 'bg-yellow-500'
|
||||||
|
case 'critical':
|
||||||
|
return 'bg-red-500'
|
||||||
|
default:
|
||||||
|
return 'bg-slate-500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMetricColor = (value: number) => {
|
||||||
|
if (value >= 90) return 'text-red-400'
|
||||||
|
if (value >= 70) return 'text-yellow-400'
|
||||||
|
return 'text-green-400'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-left text-slate-400 text-sm border-b border-slate-800">
|
||||||
|
<th className="pb-3">Host</th>
|
||||||
|
<th className="pb-3">Status</th>
|
||||||
|
<th className="pb-3">CPU</th>
|
||||||
|
<th className="pb-3">Memory</th>
|
||||||
|
<th className="pb-3">Disk</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{hosts.map((host) => (
|
||||||
|
<tr
|
||||||
|
key={host.name}
|
||||||
|
className="border-b border-slate-800/50 hover:bg-slate-800/30 transition cursor-pointer"
|
||||||
|
>
|
||||||
|
<td className="py-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span>🖥️</span>
|
||||||
|
<span className="font-medium">{host.name}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className={`w-2 h-2 rounded-full ${getStatusColor(host.status)}`}></span>
|
||||||
|
<span className="capitalize text-sm">{host.status}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={`py-3 ${getMetricColor(host.cpu)}`}>
|
||||||
|
{host.cpu}%
|
||||||
|
</td>
|
||||||
|
<td className={`py-3 ${getMetricColor(host.memory)}`}>
|
||||||
|
{host.memory}%
|
||||||
|
</td>
|
||||||
|
<td className={`py-3 ${getMetricColor(host.disk)}`}>
|
||||||
|
{host.disk}%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
57
dashboard/src/components/ui/MetricCard.tsx
Normal file
57
dashboard/src/components/ui/MetricCard.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
interface MetricCardProps {
|
||||||
|
title: string
|
||||||
|
value: string | number
|
||||||
|
subtitle?: string
|
||||||
|
icon: string
|
||||||
|
trend?: 'up' | 'down' | 'stable'
|
||||||
|
alert?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MetricCard({ title, value, subtitle, icon, trend, alert }: MetricCardProps) {
|
||||||
|
const getTrendIcon = () => {
|
||||||
|
switch (trend) {
|
||||||
|
case 'up':
|
||||||
|
return <span className="text-red-400">↑</span>
|
||||||
|
case 'down':
|
||||||
|
return <span className="text-green-400">↓</span>
|
||||||
|
default:
|
||||||
|
return <span className="text-slate-400">→</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`bg-slate-900 rounded-xl p-6 border transition-all card-hover ${
|
||||||
|
alert ? 'border-red-500/50 bg-red-950/20' : 'border-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-400 text-sm mb-1">{title}</p>
|
||||||
|
<p className="text-3xl font-bold">{value}</p>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="text-slate-500 text-sm mt-1 flex items-center space-x-1">
|
||||||
|
{getTrendIcon()}
|
||||||
|
<span>{subtitle}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`text-3xl p-3 rounded-lg ${
|
||||||
|
alert ? 'bg-red-500/20' : 'bg-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{alert && (
|
||||||
|
<div className="mt-4 flex items-center space-x-2 text-red-400 text-sm">
|
||||||
|
<span className="w-2 h-2 bg-red-400 rounded-full animate-pulse"></span>
|
||||||
|
<span>Needs attention</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
13
dashboard/tailwind.config.js
Normal file
13
dashboard/tailwind.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: 'class',
|
||||||
|
content: [
|
||||||
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
20
dashboard/tsconfig.json
Normal file
20
dashboard/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": { "@/*": ["./src/*"] }
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user