Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion snaptrack-frontend/app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {

const navItems = [
{ icon: <FaTachometerAlt />, label: 'Dashboard', path: '/' },
{ icon: <FaServer />, label: 'Service', path: '/main/Service' },
{ icon: <FaServer />, label: 'Service', path: '/main/services' },
{ icon: <FaDatabase />, label: 'Backups', path: '/main/backups' },
{ icon: <FaChartLine />, label: 'Firewall', path: '/main/Firewall' },
{ icon: <FaFileAlt />, label: 'Logs', path: '/main/logs' },
Expand Down
81 changes: 68 additions & 13 deletions snaptrack-frontend/app/context/SocketContext.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,57 +26,102 @@ 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<SocketContextType>({
socket: null,
metrics: null,
services: null,
logs: {},
sendAction: () => {},
});

export const useSocket = () => useContext(SocketContext);

export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [socket, setSocket] = useState<WebSocket | null>(null);
const [metrics, setMetrics] = useState<Metrics | null>(null);
const [services, setServices] = useState<ServiceInfo[] | null>(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 (
<SocketContext.Provider value={{ socket, metrics }}>
<SocketContext.Provider value={{ socket, metrics, services, logs, sendAction }}>
{children}
</SocketContext.Provider>
);
};
};
10 changes: 10 additions & 0 deletions snaptrack-frontend/app/main/Firewall/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const FirewallPage = () => {
return (
<div>
<h1>Firewall Settings</h1>
<p>Configure your firewall settings here.</p>
{/* Add your firewall configuration components here */}
</div>
);
}
export default FirewallPage;
2 changes: 0 additions & 2 deletions snaptrack-frontend/app/main/backups/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ type BackupWithLogs = Backup & {
}[];
};

// Ensure Backup type status property includes all possible values
type BackupType = {
id: string;
app: string;
Expand Down Expand Up @@ -54,7 +53,6 @@ const Home = () => {
useEffect(() => {
const fetchBackups = async () => {
if (!token) {
addToast('No authentication token found', 'error');
return;
}
try {
Expand Down
7 changes: 0 additions & 7 deletions snaptrack-frontend/app/main/deployments/page.tsx

This file was deleted.

201 changes: 201 additions & 0 deletions snaptrack-frontend/app/main/services/page.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(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 (
<>
<motion.tr
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="border-b border-sky-100 bg-white hover:bg-sky-50 transition-colors"
>
<td className="py-4 px-6 text-sky-800 font-semibold">{service.name}</td>
<td className="py-4 px-6">
<span
className={`inline-block px-3 py-1 rounded-full text-sm font-medium ${
service.status === "active"
? "bg-green-100 text-green-600"
: "bg-red-100 text-red-600"
}`}
>
{service.status}
</span>
</td>
<td className="py-4 px-6 text-sky-700">{service.version}</td>
<td className="py-4 px-6 text-sky-700">{service.uptime}</td>
<td className="py-4 px-6 text-sky-700">{service.memory}</td>
<td className="py-4 px-6 flex space-x-3">
<motion.button
whileHover={{ scale: 1.1, rotate: 5 }}
whileTap={{ scale: 0.9 }}
onClick={() => handleAction("start")}
className="p-2 bg-green-500 text-white rounded-full hover:bg-green-600 transition-colors"
title="Start Service"
>
<FaPlay size={16} />
</motion.button>
<motion.button
whileHover={{ scale: 1.1, rotate: 5 }}
whileTap={{ scale: 0.9 }}
onClick={() => handleAction("stop")}
className="p-2 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors"
title="Stop Service"
>
<FaStop size={16} />
</motion.button>
<motion.button
whileHover={{ scale: 1.1, rotate: 5 }}
whileTap={{ scale: 0.9 }}
onClick={() => handleAction("restart")}
className="p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 transition-colors"
title="Restart Service"
>
<FaSync size={16} />
</motion.button>
<motion.button
whileHover={{ scale: 1.1, rotate: 5 }}
whileTap={{ scale: 0.9 }}
onClick={() => {
handleAction("logs");
setShowLogs(true);
}}
className="p-2 bg-sky-500 text-white rounded-full hover:bg-sky-600 transition-colors"
title="View Logs"
>
<FaFileAlt size={16} />
</motion.button>
</td>
</motion.tr>

<AnimatePresence>
{showLogs && logs[service.name] && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
>
<motion.div
initial={{ scale: 0.8, y: 50 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.8, y: 50 }}
className="bg-white rounded-2xl p-6 w-full max-w-2xl max-h-[80vh] flex flex-col shadow-2xl"
>
<div className="flex justify-between items-center mb-4">
<h4 className="text-xl font-bold text-sky-800">Logs for {service.name}</h4>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => setShowLogs(false)}
className="text-sky-600 hover:text-sky-800"
>
<FaTimes size={20} />
</motion.button>
</div>
<div className="bg-sky-50 p-4 rounded-lg max-h-96 overflow-y-auto text-sm text-sky-700">
{logs[service.name].map((log: string, index: number) => (
<motion.p
key={index}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
className="py-1 border-b border-sky-100 last:border-b-0"
>
{log}
</motion.p>
))}
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</>
);
};

const ServicesPage: React.FC = () => {
const { services } = useSocket();

return (
<div >
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-4xl font-extrabold text-sky-800 mb-8 text-start"
>
System Services Dashboard
</motion.h1>
<div className="container mx-auto">
{services ? (
services.length > 0 ? (
<div className="overflow-x-auto bg-white rounded-2xl shadow-xl">
<table className="w-full table-auto">
<thead>
<tr className="bg-sky-600 text-white">
<th className="py-3 px-6 text-left text-sm font-semibold">Service</th>
<th className="py-3 px-6 text-left text-sm font-semibold">Status</th>
<th className="py-3 px-6 text-left text-sm font-semibold">Version</th>
<th className="py-3 px-6 text-left text-sm font-semibold">Uptime</th>
<th className="py-3 px-6 text-left text-sm font-semibold">Memory</th>
<th className="py-3 px-6 text-left text-sm font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{services.map((service: ServiceInfo) => (
<ServiceRow key={service.name} service={service} />
))}
</tbody>
</table>
</div>
) : (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center text-sky-600 text-lg"
>
No services found.
</motion.p>
)
) : (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center text-sky-600 text-lg"
>
Loading services...
</motion.p>
)}
</div>
<ToastContainer position="top-right" autoClose={2000} theme="colored" />
</div>
);
};

export default ServicesPage;
Loading
Loading