Log and Database
This commit is contained in:
+31
-5
@@ -1,15 +1,41 @@
|
||||
import React from "react";
|
||||
import { NavLink, Route, Routes } from "react-router-dom";
|
||||
import Stacks from "./Stacks.jsx";
|
||||
import Logs from "./Logs.jsx";
|
||||
|
||||
const navLinkBase =
|
||||
"px-4 py-2 rounded-md font-medium transition-colors duration-150";
|
||||
|
||||
const getNavClass = ({ isActive }) =>
|
||||
`${navLinkBase} ${isActive ? "bg-purple-600 text-white" : "text-gray-300 hover:bg-gray-700"}`;
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<header className="p-6 bg-gray-800 shadow-md">
|
||||
<h1 className="text-2xl font-bold text-white">StackPulse</h1>
|
||||
<p className="text-gray-400 mt-1">Verwalte deine Docker Stacks</p>
|
||||
<header className="bg-gray-800 shadow-md">
|
||||
<div className="max-w-6xl mx-auto px-6 py-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">StackPulse</h1>
|
||||
<p className="text-gray-400 mt-1">Verwalte deine Docker Stacks</p>
|
||||
</div>
|
||||
<nav className="flex gap-2">
|
||||
<NavLink to="/" end className={getNavClass}>
|
||||
Stacks
|
||||
</NavLink>
|
||||
<NavLink to="/logs" className={getNavClass}>
|
||||
Logs
|
||||
</NavLink>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="p-6">
|
||||
<Stacks />
|
||||
<main className="max-w-6xl mx-auto px-6 py-6">
|
||||
<Routes>
|
||||
<Route path="/" element={<Stacks />} />
|
||||
<Route path="/logs" element={<Logs />} />
|
||||
<Route path="*" element={<Stacks />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
const STATUS_COLORS = {
|
||||
success: "text-green-400",
|
||||
warning: "text-yellow-400",
|
||||
error: "text-red-400"
|
||||
};
|
||||
|
||||
const formatTimestamp = (value) => {
|
||||
if (!value) return "-";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
});
|
||||
};
|
||||
|
||||
export default function Logs() {
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const fetchLogs = async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const response = await axios.get("/api/logs", {
|
||||
params: { limit: 200 }
|
||||
});
|
||||
setLogs(response.data);
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler beim Laden der Logs:", err);
|
||||
setError("Fehler beim Laden der Redeploy-Logs");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <p className="text-gray-400">Lade Logs...</p>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-900/20 border border-red-500/40 text-red-300 p-4 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">Redeploy-Logs</h2>
|
||||
<button
|
||||
onClick={fetchLogs}
|
||||
className="px-4 py-2 rounded-md font-medium transition bg-purple-500 hover:bg-purple-600"
|
||||
>
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto bg-gray-800/60 rounded-xl border border-gray-700">
|
||||
<table className="min-w-full divide-y divide-gray-700">
|
||||
<thead className="bg-gray-800">
|
||||
<tr className="text-left text-sm uppercase tracking-wide text-gray-400">
|
||||
<th className="px-4 py-3">Zeitpunkt</th>
|
||||
<th className="px-4 py-3">Stack</th>
|
||||
<th className="px-4 py-3">Status</th>
|
||||
<th className="px-4 py-3">Nachricht</th>
|
||||
<th className="px-4 py-3">Endpoint</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700 text-sm">
|
||||
{logs.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan="5" className="px-4 py-6 text-center text-gray-400">
|
||||
Keine Logs vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{logs.map((log) => {
|
||||
const statusClass = STATUS_COLORS[log.status] || "text-blue-300";
|
||||
return (
|
||||
<tr key={log.id} className="hover:bg-gray-700/40">
|
||||
<td className="px-4 py-3 whitespace-nowrap text-gray-300">
|
||||
{formatTimestamp(log.timestamp)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-200">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{log.stackName || "Unbekannt"}</span>
|
||||
<span className="text-xs text-gray-400">ID: {log.stackId}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className={`px-4 py-3 font-semibold ${statusClass}`}>
|
||||
{log.status}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-300">
|
||||
{log.message || "-"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-400">
|
||||
{log.endpoint ?? "-"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import App from "./App.jsx";
|
||||
import './index.css'; // Tailwind CSS
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user