diff --git a/frontend/src/app/data/page.tsx b/frontend/src/app/data/page.tsx index b0694e3cd..04943c991 100644 --- a/frontend/src/app/data/page.tsx +++ b/frontend/src/app/data/page.tsx @@ -1,9 +1,67 @@ "use client"; -import React, { useEffect, useState } from "react"; -import "react-datepicker/dist/react-datepicker.css"; +import { + ArrowUpDown, + Box, + CheckCircle2, + ChevronLeft, + ChevronRight, + List, + Loader2, + Trophy, + XCircle, +} from "lucide-react"; +import { + Bar, + BarChart, + CartesianGrid, + Cell, + LabelList, + XAxis, +} from "recharts"; +import React, { useEffect, useMemo, useState } from "react"; -import ApplicationChart from "../../components/application-chart"; +import type { ChartConfig } from "@/components/ui/chart"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; +import { formattedBadge } from "@/components/command-menu"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; type DataModel = { id: number; @@ -30,42 +88,76 @@ type SummaryData = { nsapp_count: Record; }; -const DataFetcher: React.FC = () => { +// Chart colors optimized for both light and dark modes +// Medium-toned colors that are visible and not too flashy in both themes +const CHART_COLORS = [ + "#5B8DEF", // blue - medium tone + "#4ECDC4", // teal - medium tone + "#FF8C42", // orange - medium tone + "#A78BFA", // purple - medium tone + "#F472B6", // pink - medium tone + "#38BDF8", // cyan - medium tone + "#4ADE80", // green - medium tone + "#FBBF24", // yellow - medium tone + "#818CF8", // indigo - medium tone + "#FB7185", // rose - medium tone + "#2DD4BF", // turquoise - medium tone + "#C084FC", // violet - medium tone + "#60A5FA", // sky blue - medium tone + "#84CC16", // lime - medium tone + "#F59E0B", // amber - medium tone + "#A855F7", // purple - medium tone + "#10B981", // emerald - medium tone + "#EAB308", // gold - medium tone + "#3B82F6", // royal blue - medium tone + "#EF4444", // red - medium tone +]; + +const chartConfigApps = { + count: { + label: "Installations", + color: "hsl(var(--chart-1))", + }, +} satisfies ChartConfig; + +export default function DataPage() { const [data, setData] = useState([]); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(25); - const [sortConfig, setSortConfig] = useState<{ key: string; direction: "ascending" | "descending" } | null>(null); + const [sortConfig, setSortConfig] = useState<{ + key: string; + direction: "ascending" | "descending"; + } | null>(null); + const nf = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }); useEffect(() => { - const fetchSummary = async () => { - try { - const response = await fetch("https://api.htl-braunau.at/data/summary"); - if (!response.ok) - throw new Error(`Failed to fetch summary: ${response.statusText}`); - const result: SummaryData = await response.json(); - setSummary(result); - } - catch (err) { - setError((err as Error).message); - } - }; - - fetchSummary(); - }, []); - - useEffect(() => { - const fetchPaginatedData = async () => { + const fetchData = async () => { setLoading(true); try { - const response = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? "" : itemsPerPage}`); - if (!response.ok) - throw new Error(`Failed to fetch data: ${response.statusText}`); - const result: DataModel[] = await response.json(); - setData(result); + const [summaryRes, dataRes] = await Promise.all([ + fetch("https://api.htl-braunau.at/data/summary"), + fetch( + `https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? "" : itemsPerPage + }`, + ), + ]); + + if (!summaryRes.ok) { + throw new Error(`Failed to fetch summary: ${summaryRes.statusText}`); + } + if (!dataRes.ok) { + throw new Error(`Failed to fetch data: ${dataRes.statusText}`); + } + + const summaryData: SummaryData = await summaryRes.json(); + const pageData: DataModel[] = await dataRes.json(); + + setSummary(summaryData); + setData(pageData); } catch (err) { setError((err as Error).message); @@ -75,13 +167,13 @@ const DataFetcher: React.FC = () => { } }; - fetchPaginatedData(); + fetchData(); }, [currentPage, itemsPerPage]); - const sortedData = React.useMemo(() => { + const sortedData = useMemo(() => { if (!sortConfig) return data; - const sorted = [...data].sort((a, b) => { + return [...data].sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === "ascending" ? -1 : 1; } @@ -90,23 +182,15 @@ const DataFetcher: React.FC = () => { } return 0; }); - return sorted; }, [data, sortConfig]); - if (loading) - return

Loading...

; - if (error) { - return ( -

- Error: - {error} -

- ); - } - const requestSort = (key: string) => { let direction: "ascending" | "descending" = "ascending"; - if (sortConfig && sortConfig.key === key && sortConfig.direction === "ascending") { + if ( + sortConfig + && sortConfig.key === key + && sortConfig.direction === "ascending" + ) { direction = "descending"; } setSortConfig({ key, direction }); @@ -114,135 +198,447 @@ const DataFetcher: React.FC = () => { const formatDate = (dateString: string): string => { const date = new Date(dateString); - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - const hours = String(date.getHours()).padStart(2, "0"); - const minutes = String(date.getMinutes()).padStart(2, "0"); - const timezoneOffset = dateString.slice(-6); - return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`; + return new Intl.DateTimeFormat("en-US", { + dateStyle: "medium", + timeStyle: "short", + }).format(date); }; - return ( -
-

Created LXCs

- -

-
-

- {nf.format( - summary?.total_entries ?? 0, - )} - {" "} - results found -

-

- Status Legend: 🔄 installing - {" "} - {nf.format(summary?.status_count.installing ?? 0)} - {" "} - | ✔️ completed - {" "} - {nf.format(summary?.status_count.done ?? 0)} - {" "} - | ❌ failed - {" "} - {nf.format(summary?.status_count.failed ?? 0)} - {" "} - | ❓ unknown + const getTypeBadge = (type: string) => { + if (type === "lxc") + return formattedBadge("ct"); + if (type === "vm") + return formattedBadge("vm"); + return null; + }; + + // Stats calculations + const successCount = summary?.status_count.done ?? 0; + const failureCount = summary?.status_count.failed ?? 0; + const totalCount = summary?.total_entries ?? 0; + const successRate = totalCount > 0 ? (successCount / totalCount) * 100 : 0; + + const allApps = useMemo(() => { + if (!summary?.nsapp_count) + return []; + return Object.entries(summary.nsapp_count).sort(([, a], [, b]) => b - a); + }, [summary]); + + const topApps = useMemo(() => { + return allApps.slice(0, 15); + }, [allApps]); + + const mostPopularApp = topApps[0]; + + // Chart Data + const appsChartData = topApps.map(([name, count], index) => ({ + app: name, + count, + fill: CHART_COLORS[index % CHART_COLORS.length], + })); + + if (error) { + return ( +

+

+ Error loading data: + {error}

-
-
- - - - - - - - - - - - - - - - - - - {sortedData.map((item, index) => ( - -
requestSort("status")}>Status requestSort("type")}>Type requestSort("nsapp")}>Application requestSort("os_type")}>OS requestSort("os_version")}>OS Version requestSort("disk_size")}>Disk Size requestSort("core_count")}>Core Count requestSort("ram_size")}>RAM Size requestSort("method")}>Method requestSort("pve_version")}>PVE Version requestSort("error")}>Error Message requestSort("created_at")}>Created At
- {item.status === "done" + ); + } + + return ( +
+
+
+ {/* Header */} +
+

Analytics

+

+ Overview of container installations and system statistics. +

+
+ + {/* Widgets */} +
+ + + Total Created + + + +
{nf.format(totalCount)}
+

+ Total LXC/VM entries found +

+
+
+ + + + Success Rate + + + +
+ {successRate.toFixed(1)} + % +
+

+ {nf.format(successCount)} + {" "} + successful installations +

+
+
+ + + + Failures + + + +
{nf.format(failureCount)}
+

+ Installations encountered errors +

+
+
+ + + + Most Popular + + + +
+ {mostPopularApp ? mostPopularApp[0] : "N/A"} +
+

+ {mostPopularApp ? nf.format(mostPopularApp[1]) : 0} + {" "} + installations +

+
+
+
+ + {/* Graphs */} + + +
+ Top Applications + + The most frequently installed applications. + +
+ + + + + + + Application Statistics + + Installation counts for all + {" "} + {allApps.length} + {" "} + applications. + + + +
+ {allApps.map(([name, count], index) => ( +
+
+ + {index + 1} + . + + {name} +
+ {nf.format(count)} +
+ ))} +
+
+
+
+
+ +
+ {loading + ? ( +
+ +
+ ) + : ( + + + + (value.length > 8 ? `${value.slice(0, 8)}...` : value)} + /> + } + /> + + {appsChartData.map((entry, index) => ( + + ))} + + + + + )} +
+
+
+ + {/* Data Table */} + + +
+ Installation Log + + Detailed records of all container creation attempts. + +
+
+ +
+
+ +
+ + + + requestSort("status")} + > + Status + {sortConfig?.key === "status" && ( + + )} + + requestSort("type")} + > + Type + {sortConfig?.key === "type" && ( + + )} + + requestSort("nsapp")} + > + Application + {sortConfig?.key === "nsapp" && ( + + )} + + requestSort("os_type")} + > + OS + {sortConfig?.key === "os_type" && ( + + )} + + requestSort("disk_size")} + > + Disk Size + {sortConfig?.key === "disk_size" && ( + + )} + + requestSort("core_count")} + > + Core Count + {sortConfig?.key === "core_count" && ( + + )} + + requestSort("ram_size")} + > + RAM Size + {sortConfig?.key === "ram_size" && ( + + )} + + requestSort("created_at")} + > + Created At + {sortConfig?.key === "created_at" && ( + + )} + + + + + {loading ? ( - "✔️" + + +
+ + {" "} + Loading data... +
+
+
) - : item.status === "failed" + : sortedData.length > 0 ? ( - "❌" - ) - : item.status === "installing" - ? ( - "🔄" - ) - : ( - item.status - )} - -
- - - - - - - - - - - - ))} - -
- {item.type === "lxc" - ? ( - "📦" - ) - : item.type === "vm" - ? ( - "🖥️" + sortedData.map((item, idx) => ( + + + {item.status === "done" + ? ( + + Success + + ) + : item.status === "failed" + ? ( + + Failed + + ) + : item.status === "installing" + ? ( + + Installing + + ) + : ( + + {item.status} + + )} + + + {getTypeBadge(item.type) || ( + + {item.type} + + )} + + + {item.nsapp} + + + {item.os_type} + {" "} + {item.os_version} + + + {item.disk_size} + MB + + + {item.core_count} + + + {item.ram_size} + MB + + + {formatDate(item.created_at)} + + + )) ) : ( - item.type + + + No results found. + + )} - {item.nsapp}{item.os_type}{item.os_version}{item.disk_size}{item.core_count}{item.ram_size}{item.method}{item.pve_version}{item.error}{formatDate(item.created_at)}
+ +
+
+ +
+ +
+ Page + {" "} + {currentPage} +
+ +
+ +
-
- - - Page - {currentPage} - - - -
); -}; - -export default DataFetcher; +}