Add Docker WebUI MVP
This commit is contained in:
10
index.html
10
index.html
@@ -3,12 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>docker-watch-desk</title>
|
<title>Docker WebUI - Container Monitoring & Control</title>
|
||||||
<meta name="description" content="Lovable Generated Project" />
|
<meta name="description" content="Lightweight self-hosted monitoring and control panel for Docker environments with live metrics, log streaming, and Discord alerts" />
|
||||||
<meta name="author" content="Lovable" />
|
<meta name="author" content="Docker WebUI" />
|
||||||
|
|
||||||
<meta property="og:title" content="docker-watch-desk" />
|
<meta property="og:title" content="Docker WebUI - Container Monitoring & Control" />
|
||||||
<meta property="og:description" content="Lovable Generated Project" />
|
<meta property="og:description" content="Lightweight self-hosted monitoring and control panel for Docker environments" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||||
|
|
||||||
|
|||||||
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 { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
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";
|
import NotFound from "./pages/NotFound";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -15,7 +18,10 @@ const App = () => (
|
|||||||
<Sonner />
|
<Sonner />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<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 */}
|
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
105
src/index.css
105
src/index.css
@@ -8,89 +8,77 @@ All colors MUST be HSL.
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 210 20% 98%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 215 25% 15%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 215 25% 15%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 215 25% 15%;
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 210 100% 50%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 210 15% 92%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 215 25% 15%;
|
||||||
|
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 210 15% 95%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 215 15% 45%;
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 188 95% 50%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 85% 60%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
--success: 142 76% 45%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--success-foreground: 0 0% 100%;
|
||||||
--ring: 222.2 84% 4.9%;
|
|
||||||
|
|
||||||
--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%;
|
--radius: 0.75rem;
|
||||||
|
|
||||||
--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%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 222 47% 11%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
--card: 222.2 84% 4.9%;
|
--card: 217 33% 17%;
|
||||||
--card-foreground: 210 40% 98%;
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--popover: 222.2 84% 4.9%;
|
--popover: 217 33% 17%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
--primary: 210 100% 60%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary: 217 33% 22%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--muted: 217 33% 22%;
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--muted-foreground: 215 20% 65%;
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--accent: 188 95% 50%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 85% 60%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
--success: 142 76% 45%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--success-foreground: 0 0% 100%;
|
||||||
--ring: 212.7 26.8% 83.9%;
|
|
||||||
--sidebar-background: 240 5.9% 10%;
|
--warning: 38 92% 50%;
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
--warning-foreground: 0 0% 100%;
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
--border: 217 33% 25%;
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
--input: 217 33% 25%;
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
--ring: 210 100% 60%;
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +88,11 @@ All colors MUST be HSL.
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
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;
|
||||||
@@ -31,6 +31,14 @@ export default {
|
|||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
},
|
},
|
||||||
|
success: {
|
||||||
|
DEFAULT: "hsl(var(--success))",
|
||||||
|
foreground: "hsl(var(--success-foreground))",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
DEFAULT: "hsl(var(--warning))",
|
||||||
|
foreground: "hsl(var(--warning-foreground))",
|
||||||
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: "hsl(var(--muted))",
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
@@ -47,16 +55,6 @@ export default {
|
|||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: "hsl(var(--card))",
|
||||||
foreground: "hsl(var(--card-foreground))",
|
foreground: "hsl(var(--card-foreground))",
|
||||||
},
|
},
|
||||||
sidebar: {
|
|
||||||
DEFAULT: "hsl(var(--sidebar-background))",
|
|
||||||
foreground: "hsl(var(--sidebar-foreground))",
|
|
||||||
primary: "hsl(var(--sidebar-primary))",
|
|
||||||
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
|
||||||
accent: "hsl(var(--sidebar-accent))",
|
|
||||||
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
|
||||||
border: "hsl(var(--sidebar-border))",
|
|
||||||
ring: "hsl(var(--sidebar-ring))",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: "var(--radius)",
|
lg: "var(--radius)",
|
||||||
|
|||||||
Reference in New Issue
Block a user