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