diff --git a/snaptrack-frontend/app/components/Sidebar.tsx b/snaptrack-frontend/app/components/Sidebar.tsx index 1a48a43..5fd12fc 100644 --- a/snaptrack-frontend/app/components/Sidebar.tsx +++ b/snaptrack-frontend/app/components/Sidebar.tsx @@ -9,7 +9,7 @@ import { const navItems = [ { icon: , label: 'Dashboard', path: '/' }, - { icon: , label: 'Service', path: '/main/Service' }, + { icon: , label: 'Service', path: '/main/services' }, { icon: , label: 'Backups', path: '/main/backups' }, { icon: , label: 'Firewall', path: '/main/Firewall' }, { icon: , label: 'Logs', path: '/main/logs' }, diff --git a/snaptrack-frontend/app/context/SocketContext.tsx b/snaptrack-frontend/app/context/SocketContext.tsx index f131e30..5327f91 100644 --- a/snaptrack-frontend/app/context/SocketContext.tsx +++ b/snaptrack-frontend/app/context/SocketContext.tsx @@ -1,12 +1,22 @@ "use client"; import React, { createContext, useContext, useEffect, useState } from "react"; +import { toast, ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +interface ServiceInfo { + name: string; + status: string; + uptime: string; + memory: string; + version: string; +} interface Metrics { diskTotal: number; uptimeSeconds: number; - netOutBytes: any; - netInBytes: any; + netOutBytes: number; + netInBytes: number; ramUsedBytes: number; ramTotalBytes: number; diskTotalBytes: number; @@ -16,14 +26,26 @@ interface Metrics { diskPercent: number; } +interface LogMessage { + type: string; + service: string; + log: string; +} + interface SocketContextType { socket: WebSocket | null; metrics: Metrics | null; + services: ServiceInfo[] | null; + logs: { [service: string]: string[] }; + sendAction: (type: "start" | "stop" | "restart" | "logs", service: string) => void; } const SocketContext = createContext({ socket: null, metrics: null, + services: null, + logs: {}, + sendAction: () => {}, }); export const useSocket = () => useContext(SocketContext); @@ -31,42 +53,75 @@ export const useSocket = () => useContext(SocketContext); export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [socket, setSocket] = useState(null); const [metrics, setMetrics] = useState(null); + const [services, setServices] = useState(null); + const [logs, setLogs] = useState<{ [service: string]: string[] }>({}); + + const sendAction = (type: "start" | "stop" | "restart" | "logs", service: string) => { + if (socket && socket.readyState === WebSocket.OPEN) { + const action = { type, service }; + socket.send(JSON.stringify(action)); + console.log(`Sent action: ${JSON.stringify(action)}`); + } else { + console.error("WebSocket is not open"); + toast.error("WebSocket connection not established"); + } + }; useEffect(() => { const socket = new WebSocket("ws://localhost:8000/ws"); socket.onopen = () => { - console.log("✅ Connected"); + console.log("✅ WebSocket connected"); + toast.success("Connected to server"); }; - socket.onmessage = (event) => { + socket.onmessage = (event: MessageEvent) => { try { - const data: Metrics = JSON.parse(event.data); - console.log(data) - setMetrics(data); + const data = JSON.parse(event.data); + console.log("Received:", data); + + if (data.type === "services") { + setServices(data.services); + } else if (data.type === "metrics") { + setMetrics(data.stats); + } else if (data.type === "log") { + setLogs((prev) => ({ + ...prev, + [data.service]: data.log.split("\n").filter((line: string) => line.trim()), + })); + } else if (data.type === "action_response") { + toast[data.success ? "success" : "error"](data.message, { + position: "top-right", + autoClose: 3000, + }); + } } catch (e) { - console.error("Failed to parse message", e); + console.error("Failed to parse message:", e); + toast.error("Error processing server message"); } }; socket.onclose = () => { - console.log("❌ Disconnected"); + console.log("❌ WebSocket disconnected"); + toast.error("Disconnected from server"); }; - socket.onerror = (err) => { - console.error("❗ Error:", err); + socket.onerror = (err: Event) => { + console.error("❗ WebSocket error:", err); + toast.error("WebSocket error occurred"); }; setSocket(socket); return () => { socket.close(); + console.log("WebSocket cleanup"); }; }, []); return ( - + {children} ); -}; +}; \ No newline at end of file diff --git a/snaptrack-frontend/app/main/Firewall/page.tsx b/snaptrack-frontend/app/main/Firewall/page.tsx new file mode 100644 index 0000000..7b92a05 --- /dev/null +++ b/snaptrack-frontend/app/main/Firewall/page.tsx @@ -0,0 +1,10 @@ +const FirewallPage = () => { + return ( +
+

Firewall Settings

+

Configure your firewall settings here.

+ {/* Add your firewall configuration components here */} +
+ ); +} +export default FirewallPage; \ No newline at end of file diff --git a/snaptrack-frontend/app/main/backups/page.tsx b/snaptrack-frontend/app/main/backups/page.tsx index d9bd7ab..22dcd0e 100644 --- a/snaptrack-frontend/app/main/backups/page.tsx +++ b/snaptrack-frontend/app/main/backups/page.tsx @@ -14,7 +14,6 @@ type BackupWithLogs = Backup & { }[]; }; -// Ensure Backup type status property includes all possible values type BackupType = { id: string; app: string; @@ -54,7 +53,6 @@ const Home = () => { useEffect(() => { const fetchBackups = async () => { if (!token) { - addToast('No authentication token found', 'error'); return; } try { diff --git a/snaptrack-frontend/app/main/deployments/page.tsx b/snaptrack-frontend/app/main/deployments/page.tsx deleted file mode 100644 index 48bd996..0000000 --- a/snaptrack-frontend/app/main/deployments/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -const app = () => { - return( -
page
- ) -} - -export default app \ No newline at end of file diff --git a/snaptrack-frontend/app/main/services/page.tsx b/snaptrack-frontend/app/main/services/page.tsx new file mode 100644 index 0000000..ba06ef4 --- /dev/null +++ b/snaptrack-frontend/app/main/services/page.tsx @@ -0,0 +1,201 @@ +"use client"; + +import React, { useState } from "react"; +import { toast, ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import { motion, AnimatePresence } from "framer-motion"; +import { FaPlay, FaStop, FaSync, FaFileAlt, FaTimes } from "react-icons/fa"; +import { useSocket } from "@/app/context/SocketContext"; + +interface ServiceInfo { + name: string; + status: string; + uptime: string; + memory: string; + version: string; +} + +const ServiceRow: React.FC<{ service: ServiceInfo }> = ({ service }) => { + const { sendAction, logs } = useSocket(); + const [showLogs, setShowLogs] = useState(false); + + const handleAction = (action: string) => { + sendAction(action, service.name); + toast.success(`${action.charAt(0).toUpperCase() + action.slice(1)} initiated for ${service.name}`, { + position: "top-right", + autoClose: 2000, + }); + }; + + return ( + <> + + {service.name} + + + {service.status} + + + {service.version} + {service.uptime} + {service.memory} + + handleAction("start")} + className="p-2 bg-green-500 text-white rounded-full hover:bg-green-600 transition-colors" + title="Start Service" + > + + + handleAction("stop")} + className="p-2 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors" + title="Stop Service" + > + + + handleAction("restart")} + className="p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 transition-colors" + title="Restart Service" + > + + + { + handleAction("logs"); + setShowLogs(true); + }} + className="p-2 bg-sky-500 text-white rounded-full hover:bg-sky-600 transition-colors" + title="View Logs" + > + + + + + + + {showLogs && logs[service.name] && ( + + +
+

Logs for {service.name}

+ setShowLogs(false)} + className="text-sky-600 hover:text-sky-800" + > + + +
+
+ {logs[service.name].map((log: string, index: number) => ( + + {log} + + ))} +
+
+
+ )} +
+ + ); +}; + +const ServicesPage: React.FC = () => { + const { services } = useSocket(); + + return ( +
+ + System Services Dashboard + +
+ {services ? ( + services.length > 0 ? ( +
+ + + + + + + + + + + + + {services.map((service: ServiceInfo) => ( + + ))} + +
ServiceStatusVersionUptimeMemoryActions
+
+ ) : ( + + No services found. + + ) + ) : ( + + Loading services... + + )} +
+ +
+ ); +}; + +export default ServicesPage; \ No newline at end of file diff --git a/snaptrack-frontend/package-lock.json b/snaptrack-frontend/package-lock.json index 4b16dcb..57f91a0 100644 --- a/snaptrack-frontend/package-lock.json +++ b/snaptrack-frontend/package-lock.json @@ -14,6 +14,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-icons": "^5.5.0", + "react-toastify": "^11.0.5", "react-tooltip": "^5.28.1", "socket.io-client": "^2.4.0", "uuid": "^11.1.0" @@ -23,6 +24,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-toastify": "^4.0.2", "@types/socket.io-client": "^1.4.36", "eslint": "^9", "eslint-config-next": "15.1.8", @@ -691,6 +693,27 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-toastify": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/react-toastify/-/react-toastify-4.0.2.tgz", + "integrity": "sha512-pHjCstnN0ZgopIWQ9UiWsD9n+HsXs1PnMQC4hIZuSzpDO0lRjigpTuqsUtnBkMbLIg+mGFSAsBjL49SspzoLKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@types/react-transition-group": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/socket.io-client": { "version": "1.4.36", "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", @@ -4114,6 +4137,19 @@ "dev": true, "license": "MIT" }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/react-tooltip": { "version": "5.28.1", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.1.tgz", diff --git a/snaptrack-frontend/package.json b/snaptrack-frontend/package.json index d12ae1a..a8dbbbd 100644 --- a/snaptrack-frontend/package.json +++ b/snaptrack-frontend/package.json @@ -15,6 +15,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-icons": "^5.5.0", + "react-toastify": "^11.0.5", "react-tooltip": "^5.28.1", "socket.io-client": "^2.4.0", "uuid": "^11.1.0" @@ -24,6 +25,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-toastify": "^4.0.2", "@types/socket.io-client": "^1.4.36", "eslint": "^9", "eslint-config-next": "15.1.8",