diff --git a/src/App.tsx b/src/App.tsx index 6ffc4e0..656c631 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,12 +2,13 @@ import { Toaster } from "@/components/ui/toaster"; 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 { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; 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 { ProtectedRoute } from "./components/ProtectedRoute"; const queryClient = new QueryClient(); @@ -18,10 +19,32 @@ const App = () => ( - } /> - } /> - } /> - } /> + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..40677ff --- /dev/null +++ b/src/components/ProtectedRoute.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; +import { Navigate } from 'react-router-dom'; +import { supabase } from '@/integrations/supabase/client'; + +export function ProtectedRoute({ children }: { children: React.ReactNode }) { + const [loading, setLoading] = useState(true); + const [authenticated, setAuthenticated] = useState(false); + + useEffect(() => { + const checkAuth = async () => { + const { data: { session } } = await supabase.auth.getSession(); + setAuthenticated(!!session); + setLoading(false); + }; + + checkAuth(); + + const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { + setAuthenticated(!!session); + }); + + return () => subscription.unsubscribe(); + }, []); + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + if (!authenticated) { + return ; + } + + return <>{children}; +} diff --git a/src/hooks/useDockerContainers.ts b/src/hooks/useDockerContainers.ts new file mode 100644 index 0000000..f65e791 --- /dev/null +++ b/src/hooks/useDockerContainers.ts @@ -0,0 +1,96 @@ +import { useState, useEffect } from 'react'; +import { supabase } from '@/integrations/supabase/client'; +import { useToast } from '@/components/ui/use-toast'; + +export interface DockerContainer { + Id: string; + Names: string[]; + Image: string; + State: string; + Status: string; + Created: number; + Ports?: any[]; +} + +export function useDockerContainers() { + const [containers, setContainers] = useState([]); + const [loading, setLoading] = useState(true); + const { toast } = useToast(); + + const fetchContainers = async () => { + try { + const { data: { session } } = await supabase.auth.getSession(); + if (!session) return; + + const response = await fetch( + `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/docker-api/containers`, + { + headers: { + Authorization: `Bearer ${session.access_token}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (!response.ok) throw new Error('Failed to fetch containers'); + + const data = await response.json(); + setContainers(data); + } catch (error: any) { + console.error('Error fetching containers:', error); + toast({ + title: 'Error', + description: 'Failed to load containers', + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }; + + const controlContainer = async (containerId: string, action: 'start' | 'stop' | 'restart') => { + try { + const { data: { session } } = await supabase.auth.getSession(); + if (!session) return; + + const response = await fetch( + `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/docker-api/containers/${containerId}/${action}`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${session.access_token}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (!response.ok) throw new Error(`Failed to ${action} container`); + + toast({ + title: 'Success', + description: `Container ${action}ed successfully`, + }); + + // Refresh container list + await fetchContainers(); + } catch (error: any) { + console.error(`Error ${action}ing container:`, error); + toast({ + title: 'Error', + description: error.message, + variant: 'destructive', + }); + } + }; + + useEffect(() => { + fetchContainers(); + + // Poll for updates every 5 seconds as backup to WebSocket + const interval = setInterval(fetchContainers, 5000); + + return () => clearInterval(interval); + }, []); + + return { containers, loading, controlContainer, refetch: fetchContainers }; +} diff --git a/src/hooks/useDockerMetrics.ts b/src/hooks/useDockerMetrics.ts new file mode 100644 index 0000000..aa75f56 --- /dev/null +++ b/src/hooks/useDockerMetrics.ts @@ -0,0 +1,109 @@ +import { useEffect, useState } from 'react'; +import { supabase } from '@/integrations/supabase/client'; + +interface ContainerMetrics { + id: string; + name: string; + cpu: string; + memory: { + used: number; + limit: number; + percent: string; + }; + network: { + rx: number; + tx: number; + }; + blockIO: { + read: number; + write: number; + }; +} + +interface HostMetrics { + cpu: { usage: string }; + memory: { + total: number; + free: number; + available: number; + used: number; + usage: string; + }; + uptime: number; +} + +interface MetricsData { + type: string; + timestamp: string; + host: HostMetrics; + containers: ContainerMetrics[]; +} + +export function useDockerMetrics() { + const [metrics, setMetrics] = useState(null); + const [connected, setConnected] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let ws: WebSocket | null = null; + + const connect = async () => { + try { + const { data: { session } } = await supabase.auth.getSession(); + if (!session) { + setError('Not authenticated'); + return; + } + + const wsUrl = `${import.meta.env.VITE_SUPABASE_URL.replace('https://', 'wss://')}/functions/v1/docker-metrics`; + ws = new WebSocket(wsUrl, ['Bearer', session.access_token]); + + ws.onopen = () => { + console.log('WebSocket connected'); + setConnected(true); + setError(null); + }; + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + setMetrics(data); + } catch (e) { + console.error('Failed to parse metrics:', e); + } + }; + + ws.onerror = (e) => { + console.error('WebSocket error:', e); + setError('Connection error'); + setConnected(false); + }; + + ws.onclose = () => { + console.log('WebSocket closed'); + setConnected(false); + // Attempt to reconnect after 5 seconds + setTimeout(() => { + if (!ws || ws.readyState === WebSocket.CLOSED) { + connect(); + } + }, 5000); + }; + + } catch (e) { + console.error('Failed to connect:', e); + setError('Failed to connect'); + } + }; + + connect(); + + return () => { + if (ws) { + ws.close(); + } + }; + }, []); + + return { metrics, connected, error }; +} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index bb59108..bc49437 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,15 +1,37 @@ 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 { Activity, Server, HardDrive, Network, Play, Square, RotateCw, AlertTriangle, Wifi, WifiOff } from "lucide-react"; import { Badge } from "@/components/ui/badge"; +import { useDockerMetrics } from "@/hooks/useDockerMetrics"; +import { useDockerContainers } from "@/hooks/useDockerContainers"; +import { useNavigate } from "react-router-dom"; +import { supabase } from "@/integrations/supabase/client"; 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: "-" }, - ]; + const { metrics, connected } = useDockerMetrics(); + const { containers, loading, controlContainer } = useDockerContainers(); + const navigate = useNavigate(); + + const handleLogout = async () => { + await supabase.auth.signOut(); + navigate('/login'); + }; + + const formatBytes = (bytes: number) => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; + }; + + const getContainerMetrics = (containerId: string) => { + if (!metrics) return null; + return metrics.containers.find(c => c.id.startsWith(containerId.substring(0, 12))); + }; + + const runningCount = containers.filter(c => c.State === 'running').length; + const stoppedCount = containers.filter(c => c.State !== 'running').length; return (
@@ -22,12 +44,24 @@ const Dashboard = () => {

Docker WebUI

-

docker-node-01

+
+

docker-node-01

+ {connected ? ( + + ) : ( + + )} +
- +
+ + +
@@ -40,12 +74,17 @@ const Dashboard = () => {

CPU

- 24% + + {metrics?.host?.cpu?.usage || '0'}% +
-
+
-

4 cores @ 3.2 GHz

+

Host CPU usage

@@ -54,41 +93,49 @@ const Dashboard = () => {

Memory

- 8.2 GB + + {metrics?.host?.memory ? formatBytes(metrics.host.memory.used * 1024) : '0 MB'} +
-
+
-

51% of 16 GB

+

+ {metrics?.host?.memory?.usage || '0'}% of {metrics?.host?.memory ? formatBytes(metrics.host.memory.total * 1024) : '0 GB'} +

-

Disk

+

Containers

- 124 GB + {containers.length}
-
-
+
+ {runningCount} running + + {stoppedCount} stopped
-

62% of 200 GB

-

Network

+

Uptime

- 2.4 MB/s -
-
- ↓ 1.8 MB/s - - ↑ 0.6 MB/s + + {metrics?.host?.uptime ? Math.floor(metrics.host.uptime / 86400) + 'd' : '0d'} +
+

+ {metrics?.host?.uptime ? new Date(Date.now() - metrics.host.uptime * 1000).toLocaleString() : 'Loading...'} +

@@ -97,12 +144,12 @@ const Dashboard = () => {

Containers

-
- 3 Running +
+ {runningCount} Running
- 1 Stopped + {stoppedCount} Stopped
@@ -110,72 +157,129 @@ const Dashboard = () => { {/* Container List */}
- - - - - - - - - - - - - - {containers.map((container) => ( - - - - - - - - + {loading ? ( +
Loading containers...
+ ) : containers.length === 0 ? ( +
+ No containers found. Create one using Docker CLI and it will appear here automatically. +
+ ) : ( +
StatusNameIDCPUMemoryUptimeActions
- - {container.status} - - {container.name}{container.id}{container.cpu}{container.memory}{container.uptime} -
- {container.status === "running" ? ( - <> - - - - ) : ( - - )} -
-
+ + + + + + + + + - ))} - -
StatusNameImageCPUMemoryNetworkActions
+ + + {containers.map((container) => { + const containerMetrics = getContainerMetrics(container.Id); + const isRunning = container.State === 'running'; + + return ( + + + + {container.State} + + + +
+

{container.Names[0].replace(/^\//, '')}

+

{container.Id.substring(0, 12)}

+
+ + {container.Image} + + {containerMetrics?.cpu || '0'}% + + + {containerMetrics?.memory ? formatBytes(containerMetrics.memory.used) : '0 MB'} + + + {containerMetrics?.network ? ( +
+
↓ {formatBytes(containerMetrics.network.rx)}
+
↑ {formatBytes(containerMetrics.network.tx)}
+
+ ) : ( + '-' + )} + + +
+ {isRunning ? ( + <> + + + + ) : ( + + )} + +
+ + + ); + })} + + + )}
- {/* Active Alerts */} - -
- -

Recent Alerts

-
-
-
+ {/* Connection Status */} + {!connected && ( + +
+
-

api-backend stopped

-

2 minutes ago

+

Metrics Disconnected

+

+ Real-time metrics are temporarily unavailable. Attempting to reconnect... +

- Container Stopped
-
- + + )}
);