generated from Schmax/deno-react-template
Hacked some shit together with Github Copilot
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import Index from "./pages/Index.tsx";
|
||||
import Dinosaur from "./pages/Dinosaur.tsx";
|
||||
import HistoryPage from "./pages/HistoryPage.tsx";
|
||||
import ServerPage from "./pages/ServerPage.tsx";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/:selectedDinosaur" element={<Dinosaur />} />
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/history" element={<HistoryPage />} />
|
||||
<Route path="/servers" element={<ServerPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
44
client/src/components/History.tsx
Normal file
44
client/src/components/History.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import SnapshotCard from "./SnapshotCard.tsx";
|
||||
|
||||
interface Snapshot {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
progress: number;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
}
|
||||
|
||||
interface HistoryProps {
|
||||
currentA: Snapshot[];
|
||||
historyA: Snapshot[];
|
||||
currentB: Snapshot[];
|
||||
historyB: Snapshot[];
|
||||
}
|
||||
|
||||
export default function History({ currentA, historyA, currentB, historyB }: HistoryProps) {
|
||||
// Combine current and history snapshots for both servers
|
||||
const allSnapshotsA = [...currentA, ...historyA];
|
||||
const allSnapshotsB = [...currentB, ...historyB];
|
||||
|
||||
// Pair snapshots by ID
|
||||
const pairedHistory = allSnapshotsA.map((snapshotA) => {
|
||||
const snapshotB = allSnapshotsB.find((snap) => snap.id === snapshotA.id);
|
||||
return { snapshotA, snapshotB };
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full mt-6 p-6 border rounded-lg shadow-md bg-gray-50 dark:bg-gray-800">
|
||||
<h3 className="text-xl font-bold mb-4 text-gray-800 dark:text-gray-200">
|
||||
Snapshot History
|
||||
</h3>
|
||||
<div className="space-y-6">
|
||||
{pairedHistory.map(({ snapshotA, snapshotB }) => (
|
||||
<div key={snapshotA.id} className="flex flex-row gap-4">
|
||||
<SnapshotCard {...snapshotA} />
|
||||
{snapshotB && <SnapshotCard {...snapshotB} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
client/src/components/ServerCard.tsx
Normal file
33
client/src/components/ServerCard.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import SnapshotCard from "./SnapshotCard.tsx";
|
||||
|
||||
interface Snapshot {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
progress: number;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
}
|
||||
|
||||
interface ServerCardProps {
|
||||
serverName: string;
|
||||
snapshots: Snapshot[];
|
||||
}
|
||||
|
||||
export default function ServerCard({ serverName, snapshots }: ServerCardProps) {
|
||||
return (
|
||||
<div className="w-full p-6 border rounded-lg shadow-md bg-gray-50 dark:bg-gray-800">
|
||||
<h2 className="text-2xl font-bold mb-6 text-gray-800 dark:text-gray-200">{serverName}</h2>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{snapshots.map((snapshot) => (
|
||||
<SnapshotCard
|
||||
key={snapshot.id}
|
||||
id={snapshot.id}
|
||||
timestamp={snapshot.timestamp}
|
||||
progress={snapshot.progress}
|
||||
status={snapshot.status}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
client/src/components/SnapshotCard.tsx
Normal file
36
client/src/components/SnapshotCard.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
|
||||
interface Snapshot {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
progress: number;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
}
|
||||
|
||||
export default function SnapshotCard({ id, timestamp, progress, status }: Snapshot) {
|
||||
const statusColors = {
|
||||
completed: "bg-green-100 dark:bg-green-800",
|
||||
"in-progress": "bg-blue-100 dark:bg-blue-800",
|
||||
error: "bg-red-100 dark:bg-red-800",
|
||||
pending: "bg-gray-100 dark:bg-gray-800",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`p-4 rounded-lg shadow-sm ${statusColors[status]}`}>
|
||||
<div className="text-lg font-semibold text-gray-700 dark:text-gray-200">{id}</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{new Date(timestamp).toLocaleString()}
|
||||
</div>
|
||||
{status === "in-progress" && (
|
||||
<div className="mt-2">
|
||||
<div className="w-full bg-gray-300 dark:bg-gray-700 rounded-full h-2.5">
|
||||
<div
|
||||
className="bg-blue-500 dark:bg-blue-400 h-2.5 rounded-full"
|
||||
style={{ width: `${progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { Dino } from "../types";
|
||||
|
||||
export default function Dinosaur() {
|
||||
const { selectedDinosaur } = useParams();
|
||||
const [dinosaur, setDino] = useState<Dino>({ name: "", description: "" });
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const resp = await fetch(`/api/dinosaurs/${selectedDinosaur}`);
|
||||
const dino = await resp.json() as Dino;
|
||||
setDino(dino);
|
||||
})();
|
||||
}, [selectedDinosaur]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{dinosaur.name}</h1>
|
||||
<p>{dinosaur.description}</p>
|
||||
<Link to="/">🠠 Back to all dinosaurs</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
70
client/src/pages/HistoryPage.tsx
Normal file
70
client/src/pages/HistoryPage.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import History from "../components/History.tsx";
|
||||
|
||||
interface Snapshot {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
progress: number;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
}
|
||||
|
||||
interface ServerData {
|
||||
name: string;
|
||||
current: Snapshot[];
|
||||
history: Snapshot[];
|
||||
}
|
||||
|
||||
export default function HistoryPage() {
|
||||
const [serverA, setServerA] = useState<ServerData | null>(null);
|
||||
const [serverB, setServerB] = useState<ServerData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const response = await fetch("/api/snapshots");
|
||||
const data = await response.json();
|
||||
setServerA(data.serverA);
|
||||
setServerB(data.serverB);
|
||||
} catch (error) {
|
||||
console.error("Error fetching snapshot data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!serverA || !serverB) {
|
||||
return <div>Error loading server data.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh flex flex-col bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200">
|
||||
{/* Header */}
|
||||
<header className="bg-gray-200 dark:bg-gray-800 p-4 shadow-md">
|
||||
<h1 className="text-3xl font-bold text-center">Snapshot History</h1>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 p-6">
|
||||
<History
|
||||
currentA={serverA.current}
|
||||
historyA={serverA.history}
|
||||
currentB={serverB.current}
|
||||
historyB={serverB.history}
|
||||
/>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-200 dark:bg-gray-800 p-4 text-center">
|
||||
<p>© 2023 ZFS Monitoring. All rights reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +1,124 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Dino } from "../types.ts";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface Snapshot {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
progress: number;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
}
|
||||
|
||||
interface ServerData {
|
||||
name: string;
|
||||
current: Snapshot[];
|
||||
history: Snapshot[];
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
const [dinosaurs, setDinosaurs] = useState<Dino[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const [serverA, setServerA] = useState<ServerData | null>(null);
|
||||
const [serverB, setServerB] = useState<ServerData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await fetch(`/api/dinosaurs/`);
|
||||
const allDinosaurs = await response.json() as Dino[];
|
||||
setDinosaurs(allDinosaurs);
|
||||
})();
|
||||
async function fetchData() {
|
||||
try {
|
||||
const response = await fetch("/api/snapshots");
|
||||
const data = await response.json();
|
||||
setServerA(data.serverA);
|
||||
setServerB(data.serverB);
|
||||
} catch (error) {
|
||||
console.error("Error fetching snapshot data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!serverA || !serverB) {
|
||||
return <div>Error loading server data.</div>;
|
||||
}
|
||||
|
||||
const latestSnapshot = (server: ServerData) =>
|
||||
server.current.length > 0 ? server.current[0] : null;
|
||||
|
||||
const recentHistory = (server: ServerData) =>
|
||||
server.history.slice(0, 3);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Welcome to the Dinosaur app</h1>
|
||||
<p>Click on a dinosaur below to learn more.</p>
|
||||
{dinosaurs.map((dinosaur: Dino) => {
|
||||
return (
|
||||
<Link
|
||||
to={`/${dinosaur.name.toLowerCase()}`}
|
||||
key={dinosaur.name}
|
||||
className="dinosaur"
|
||||
>
|
||||
{dinosaur.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</main>
|
||||
<div className="min-h-dvh flex flex-col bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200">
|
||||
{/* Header */}
|
||||
<header className="bg-gray-200 dark:bg-gray-800 p-4 shadow-md">
|
||||
<h1 className="text-3xl font-bold text-center">ZFS Snapshot Monitoring Dashboard</h1>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Server Section */}
|
||||
<div className="p-6 border rounded-lg shadow-md bg-blue-500 text-white">
|
||||
<h2 className="text-2xl font-bold mb-4">Server Overview</h2>
|
||||
<div className="space-y-4">
|
||||
{[serverA, serverB].map((server, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-4 border rounded-lg bg-blue-600 hover:bg-blue-700 transition"
|
||||
onClick={() => navigate("/servers")}
|
||||
>
|
||||
<h3 className="text-xl font-bold">{server.name}</h3>
|
||||
{latestSnapshot(server) ? (
|
||||
<p className="mt-2">
|
||||
Latest Snapshot:{" "}
|
||||
<span className="font-mono">
|
||||
{latestSnapshot(server)?.timestamp}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-2">No snapshots available.</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* History Section */}
|
||||
<div className="p-6 border rounded-lg shadow-md bg-green-500 text-white">
|
||||
<h2 className="text-2xl font-bold mb-4">Recent History</h2>
|
||||
<div className="space-y-4">
|
||||
{[serverA, serverB].map((server, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-4 border rounded-lg bg-green-600 hover:bg-green-700 transition"
|
||||
onClick={() => navigate("/history")}
|
||||
>
|
||||
<h3 className="text-xl font-bold">{server.name}</h3>
|
||||
<ul className="mt-2 space-y-1">
|
||||
{recentHistory(server).map((snapshot) => (
|
||||
<li
|
||||
key={snapshot.id}
|
||||
className="font-mono text-sm"
|
||||
>
|
||||
{snapshot.timestamp} - {snapshot.status}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-200 dark:bg-gray-800 p-4 text-center">
|
||||
<p>© 2023 ZFS Monitoring. All rights reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
68
client/src/pages/ServerPage.tsx
Normal file
68
client/src/pages/ServerPage.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import ServerCard from "../components/ServerCard.tsx";
|
||||
|
||||
interface Snapshot {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
progress: number;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
}
|
||||
|
||||
interface ServerData {
|
||||
name: string;
|
||||
current: Snapshot[];
|
||||
history: Snapshot[];
|
||||
}
|
||||
|
||||
export default function ServerPage() {
|
||||
const [serverA, setServerA] = useState<ServerData | null>(null);
|
||||
const [serverB, setServerB] = useState<ServerData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const response = await fetch("/api/snapshots");
|
||||
const data = await response.json();
|
||||
setServerA(data.serverA);
|
||||
setServerB(data.serverB);
|
||||
} catch (error) {
|
||||
console.error("Error fetching snapshot data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!serverA || !serverB) {
|
||||
return <div>Error loading server data.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh flex flex-col bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200">
|
||||
{/* Header */}
|
||||
<header className="bg-gray-200 dark:bg-gray-800 p-4 shadow-md">
|
||||
<h1 className="text-3xl font-bold text-center">Server Snapshots</h1>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<ServerCard serverName={serverA.name} snapshots={serverA.current} />
|
||||
<ServerCard serverName={serverB.name} snapshots={serverB.current} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-200 dark:bg-gray-800 p-4 text-center">
|
||||
<p>© 2023 ZFS Monitoring. All rights reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user