Add Docker WebUI MVP
This commit is contained in:
10
src/App.tsx
10
src/App.tsx
@@ -3,7 +3,10 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Index from "./pages/Index";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import Login from "./pages/Login";
|
||||
import ContainerDetails from "./pages/ContainerDetails";
|
||||
import Settings from "./pages/Settings";
|
||||
import NotFound from "./pages/NotFound";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
@@ -15,7 +18,10 @@ const App = () => (
|
||||
<Sonner />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/" element={<Login />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/container/:id" element={<ContainerDetails />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
|
||||
105
src/index.css
105
src/index.css
@@ -8,89 +8,77 @@ All colors MUST be HSL.
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--background: 210 20% 98%;
|
||||
--foreground: 215 25% 15%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--card-foreground: 215 25% 15%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--popover-foreground: 215 25% 15%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--primary: 210 100% 50%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 210 15% 92%;
|
||||
--secondary-foreground: 215 25% 15%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--muted: 210 15% 95%;
|
||||
--muted-foreground: 215 15% 45%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--accent: 188 95% 50%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive: 0 85% 60%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--success: 142 76% 45%;
|
||||
--success-foreground: 0 0% 100%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
--warning: 38 92% 50%;
|
||||
--warning-foreground: 0 0% 100%;
|
||||
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--border: 215 20% 88%;
|
||||
--input: 215 20% 88%;
|
||||
--ring: 210 100% 50%;
|
||||
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--sidebar-border: 220 13% 91%;
|
||||
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--background: 222 47% 11%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card: 217 33% 17%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover: 217 33% 17%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--primary: 210 100% 60%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary: 217 33% 22%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--muted: 217 33% 22%;
|
||||
--muted-foreground: 215 20% 65%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--accent: 188 95% 50%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive: 0 85% 60%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
--success: 142 76% 45%;
|
||||
--success-foreground: 0 0% 100%;
|
||||
|
||||
--warning: 38 92% 50%;
|
||||
--warning-foreground: 0 0% 100%;
|
||||
|
||||
--border: 217 33% 25%;
|
||||
--input: 217 33% 25%;
|
||||
--ring: 210 100% 60%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +88,11 @@ All colors MUST be HSL.
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply bg-background text-foreground font-sans antialiased;
|
||||
}
|
||||
|
||||
code, pre, .font-mono {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
276
src/pages/ContainerDetails.tsx
Normal file
276
src/pages/ContainerDetails.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ArrowLeft, Play, Square, RotateCw, Activity, Database, Network, HardDrive } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
const ContainerDetails = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const logLines = [
|
||||
{ time: "2025-10-20T10:22:31Z", level: "INFO", message: "Server started on port 8080" },
|
||||
{ time: "2025-10-20T10:22:35Z", level: "INFO", message: "Connected to database" },
|
||||
{ time: "2025-10-20T10:22:42Z", level: "WARN", message: "High memory usage detected: 87%" },
|
||||
{ time: "2025-10-20T10:23:01Z", level: "INFO", message: "Request processed: GET /api/health" },
|
||||
{ time: "2025-10-20T10:23:15Z", level: "ERROR", message: "Connection timeout to external service" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background dark">
|
||||
{/* Header */}
|
||||
<header className="border-b border-border bg-card sticky top-0 z-10">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" size="sm" onClick={() => navigate("/dashboard")}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">redis-prod</h1>
|
||||
<Badge variant="default" className="bg-success hover:bg-success">
|
||||
Running
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground font-mono mt-1">abc123def456</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Square className="w-4 h-4 mr-2" />
|
||||
Stop
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<RotateCw className="w-4 h-4 mr-2" />
|
||||
Restart
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<Tabs defaultValue="overview" className="space-y-6">
|
||||
<TabsList className="bg-card border border-border">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="metrics">Metrics</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
{/* Container Info */}
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4">Container Information</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">Image</p>
|
||||
<p className="font-mono text-foreground">redis:7.2-alpine</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">Status</p>
|
||||
<p className="text-foreground">Running (3d 14h)</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">IP Address</p>
|
||||
<p className="font-mono text-foreground">172.18.0.5</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">Ports</p>
|
||||
<p className="font-mono text-foreground">6379:6379/tcp</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">Network</p>
|
||||
<p className="font-mono text-foreground">bridge</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">Restart Policy</p>
|
||||
<p className="text-foreground">unless-stopped</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Mounts */}
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4">Volume Mounts</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-secondary/50">
|
||||
<div>
|
||||
<p className="font-mono text-sm text-foreground">/var/lib/docker/volumes/redis_data</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">→ /data</p>
|
||||
</div>
|
||||
<Badge variant="outline">Read/Write</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="metrics" className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="w-5 h-5 text-primary" />
|
||||
<h3 className="font-semibold text-card-foreground">CPU</h3>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-primary">2.5%</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Real-time usage</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="w-5 h-5 text-accent" />
|
||||
<h3 className="font-semibold text-card-foreground">Memory</h3>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-accent">128 MB</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">of 512 MB limit</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Network className="w-5 h-5 text-success" />
|
||||
<h3 className="font-semibold text-card-foreground">Network</h3>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-success">1.2 MB/s</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">↓ 0.8 MB/s ↑ 0.4 MB/s</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="w-5 h-5 text-warning" />
|
||||
<h3 className="font-semibold text-card-foreground">Block I/O</h3>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-warning">45 KB/s</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Read + Write</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4">Resource Usage Over Time</h3>
|
||||
<div className="h-64 flex items-center justify-center text-muted-foreground">
|
||||
Live metrics chart (real-time WebSocket updates)
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="logs" className="space-y-4">
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-foreground">Live Log Stream</h3>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Filter logs (regex)..."
|
||||
className="w-64 bg-secondary border-border"
|
||||
/>
|
||||
<Button variant="outline" size="sm">
|
||||
Pause
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-background rounded-lg p-4 font-mono text-sm space-y-1 max-h-[600px] overflow-y-auto">
|
||||
{logLines.map((log, i) => (
|
||||
<div key={i} className="flex gap-4 hover:bg-secondary/50 px-2 py-1 rounded">
|
||||
<span className="text-muted-foreground">{log.time}</span>
|
||||
<span
|
||||
className={
|
||||
log.level === "ERROR"
|
||||
? "text-destructive font-bold"
|
||||
: log.level === "WARN"
|
||||
? "text-warning font-bold"
|
||||
: "text-success"
|
||||
}
|
||||
>
|
||||
[{log.level}]
|
||||
</span>
|
||||
<span className="text-foreground">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-4">
|
||||
Logs are streamed in real-time and not stored. Filtering happens client-side.
|
||||
</p>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings" className="space-y-6">
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-6">Alert Configuration</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Enable Alerts</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Send Discord notifications for this container
|
||||
</p>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Alert on Stop</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Notify when container stops or crashes
|
||||
</p>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-foreground">Error Pattern (Regex)</label>
|
||||
<Input
|
||||
defaultValue="(?i)(error|err|exception|traceback|crit(ical)?)"
|
||||
className="font-mono bg-secondary border-border"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Logs matching this pattern will trigger an alert
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-foreground">Debounce Interval (seconds)</label>
|
||||
<Input
|
||||
type="number"
|
||||
defaultValue="30"
|
||||
className="bg-secondary border-border"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Minimum time between alerts to prevent spam
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-foreground">Custom Webhook URL (Optional)</label>
|
||||
<Input
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
className="font-mono bg-secondary border-border"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Leave empty to use global webhook
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button className="w-full bg-primary hover:bg-primary/90">
|
||||
Save Settings
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContainerDetails;
|
||||
184
src/pages/Dashboard.tsx
Normal file
184
src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Activity, Server, HardDrive, Network, Play, Square, RotateCw, AlertTriangle } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
const Dashboard = () => {
|
||||
const containers = [
|
||||
{ id: "abc123", name: "redis-prod", status: "running", cpu: "2.5%", memory: "128 MB", uptime: "3d 14h" },
|
||||
{ id: "def456", name: "postgres-main", status: "running", cpu: "5.2%", memory: "512 MB", uptime: "7d 2h" },
|
||||
{ id: "ghi789", name: "nginx-proxy", status: "running", cpu: "0.8%", memory: "64 MB", uptime: "12d 8h" },
|
||||
{ id: "jkl012", name: "api-backend", status: "stopped", cpu: "0%", memory: "0 MB", uptime: "-" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background dark">
|
||||
{/* Header */}
|
||||
<header className="border-b border-border bg-card">
|
||||
<div className="container mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<Server className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">Docker WebUI</h1>
|
||||
<p className="text-sm text-muted-foreground">docker-node-01</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => window.location.href = '/settings'}>
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
{/* System Metrics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="w-5 h-5 text-primary" />
|
||||
<h3 className="font-semibold text-card-foreground">CPU</h3>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-primary">24%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
||||
<div className="h-full bg-primary rounded-full" style={{ width: "24%" }} />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">4 cores @ 3.2 GHz</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="w-5 h-5 text-accent" />
|
||||
<h3 className="font-semibold text-card-foreground">Memory</h3>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-accent">8.2 GB</span>
|
||||
</div>
|
||||
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
||||
<div className="h-full bg-accent rounded-full" style={{ width: "51%" }} />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">51% of 16 GB</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="w-5 h-5 text-warning" />
|
||||
<h3 className="font-semibold text-card-foreground">Disk</h3>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-warning">124 GB</span>
|
||||
</div>
|
||||
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
||||
<div className="h-full bg-warning rounded-full" style={{ width: "62%" }} />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">62% of 200 GB</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Network className="w-5 h-5 text-success" />
|
||||
<h3 className="font-semibold text-card-foreground">Network</h3>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-success">2.4 MB/s</span>
|
||||
</div>
|
||||
<div className="flex gap-2 text-xs text-muted-foreground">
|
||||
<span>↓ 1.8 MB/s</span>
|
||||
<span>•</span>
|
||||
<span>↑ 0.6 MB/s</span>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Container Stats */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-2xl font-bold text-foreground">Containers</h2>
|
||||
<div className="flex gap-4 text-sm">
|
||||
<span className="text-success flex items-center gap-1">
|
||||
<div className="w-2 h-2 rounded-full bg-success" />
|
||||
3 Running
|
||||
</span>
|
||||
<span className="text-destructive flex items-center gap-1">
|
||||
<div className="w-2 h-2 rounded-full bg-destructive" />
|
||||
1 Stopped
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Container List */}
|
||||
<Card className="bg-card border-border">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">Status</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">Name</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">ID</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">CPU</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">Memory</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">Uptime</th>
|
||||
<th className="px-6 py-4 text-right text-sm font-semibold text-muted-foreground">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{containers.map((container) => (
|
||||
<tr key={container.id} className="border-b border-border hover:bg-secondary/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<Badge variant={container.status === "running" ? "default" : "destructive"} className={container.status === "running" ? "bg-success hover:bg-success" : ""}>
|
||||
{container.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-medium text-foreground">{container.name}</td>
|
||||
<td className="px-6 py-4 font-mono text-sm text-muted-foreground">{container.id}</td>
|
||||
<td className="px-6 py-4 text-sm text-foreground">{container.cpu}</td>
|
||||
<td className="px-6 py-4 text-sm text-foreground">{container.memory}</td>
|
||||
<td className="px-6 py-4 text-sm text-muted-foreground">{container.uptime}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{container.status === "running" ? (
|
||||
<>
|
||||
<Button size="sm" variant="outline" className="h-8 w-8 p-0">
|
||||
<Square className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="h-8 w-8 p-0">
|
||||
<RotateCw className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button size="sm" variant="outline" className="h-8 w-8 p-0">
|
||||
<Play className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Active Alerts */}
|
||||
<Card className="mt-8 p-6 bg-card border-border">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<AlertTriangle className="w-5 h-5 text-warning" />
|
||||
<h3 className="text-lg font-semibold text-foreground">Recent Alerts</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-destructive/10 border border-destructive/20">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">api-backend stopped</p>
|
||||
<p className="text-sm text-muted-foreground">2 minutes ago</p>
|
||||
</div>
|
||||
<Badge variant="destructive">Container Stopped</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
80
src/pages/Login.tsx
Normal file
80
src/pages/Login.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Server, Lock, User } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Login = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogin = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// Mock login - in real app would authenticate
|
||||
navigate("/dashboard");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background dark flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md p-8 bg-card border-border">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="w-16 h-16 rounded-xl bg-primary/10 flex items-center justify-center mb-4">
|
||||
<Server className="w-10 h-10 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-foreground mb-2">Docker WebUI</h1>
|
||||
<p className="text-muted-foreground text-center">
|
||||
Lightweight container monitoring and control
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="username" className="text-sm font-medium text-foreground">
|
||||
Username
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="admin"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="pl-10 bg-secondary border-border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="password" className="text-sm font-medium text-foreground">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="pl-10 bg-secondary border-border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full bg-primary hover:bg-primary/90">
|
||||
Sign In
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<p className="mt-6 text-xs text-center text-muted-foreground">
|
||||
First time? You'll be prompted to change your password after login.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
178
src/pages/Settings.tsx
Normal file
178
src/pages/Settings.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ArrowLeft, Bell, Clock, Globe } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
const Settings = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background dark">
|
||||
{/* Header */}
|
||||
<header className="border-b border-border bg-card sticky top-0 z-10">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" size="sm" onClick={() => navigate("/dashboard")}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold text-foreground">Settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-6 py-8 max-w-4xl space-y-6">
|
||||
{/* Discord Webhooks */}
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Bell className="w-5 h-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold text-foreground">Discord Webhooks</h3>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-foreground">Global Webhook URL</label>
|
||||
<Input
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
className="font-mono bg-secondary border-border"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Default webhook for all containers without a custom webhook
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full">
|
||||
Test Webhook
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Metrics & Monitoring */}
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Clock className="w-5 h-5 text-accent" />
|
||||
<h3 className="text-lg font-semibold text-foreground">Metrics & Monitoring</h3>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-foreground">Scrape Interval (ms)</label>
|
||||
<Input
|
||||
type="number"
|
||||
defaultValue="2000"
|
||||
className="bg-secondary border-border"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
How often to collect metrics from Docker (lower = more resource usage)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-foreground">Retention Points</label>
|
||||
<Input
|
||||
type="number"
|
||||
defaultValue="120"
|
||||
className="bg-secondary border-border"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Number of data points to keep in memory (for charts)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-foreground">Default Error Pattern</label>
|
||||
<Input
|
||||
defaultValue="(?i)(error|err|exception|traceback|crit(ical)?)"
|
||||
className="font-mono bg-secondary border-border"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Default regex pattern for detecting errors in logs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-foreground">Default Debounce (seconds)</label>
|
||||
<Input
|
||||
type="number"
|
||||
defaultValue="30"
|
||||
className="bg-secondary border-border"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Default time between alerts to prevent spam
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* General Settings */}
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Globe className="w-5 h-5 text-success" />
|
||||
<h3 className="text-lg font-semibold text-foreground">General</h3>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-foreground">Timezone</label>
|
||||
<Input
|
||||
defaultValue="Europe/Copenhagen"
|
||||
className="bg-secondary border-border"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Used for timestamps in logs and alerts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Dark Mode</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Use dark theme (recommended for monitoring dashboards)
|
||||
</p>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Admin */}
|
||||
<Card className="p-6 bg-card border-border">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-6">Admin</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-foreground">Change Password</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Current password"
|
||||
className="bg-secondary border-border"
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
className="bg-secondary border-border"
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
className="bg-secondary border-border"
|
||||
/>
|
||||
</div>
|
||||
<Button className="w-full bg-primary hover:bg-primary/90">
|
||||
Update Password
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Save All */}
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline" className="flex-1" onClick={() => navigate("/dashboard")}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="flex-1 bg-primary hover:bg-primary/90">
|
||||
Save All Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
Reference in New Issue
Block a user