From c01abd559b77bb96a31001687abde15f530f7c4a Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner <73236783+michelroegl-brunner@users.noreply.github.com> Date: Thu, 27 Feb 2025 16:56:56 +0200 Subject: [PATCH] Add basic pagination (#2715) --- frontend/src/app/data/page.tsx | 222 ++++++------------- frontend/src/components/ApplicationChart.tsx | 16 +- 2 files changed, 76 insertions(+), 162 deletions(-) diff --git a/frontend/src/app/data/page.tsx b/frontend/src/app/data/page.tsx index e78b4a675..b75246484 100644 --- a/frontend/src/app/data/page.tsx +++ b/frontend/src/app/data/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { JSX, useEffect, useState } from "react"; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import ApplicationChart from "../../components/ApplicationChart"; @@ -21,28 +21,45 @@ interface DataModel { status: string; error: string; type: string; + [key: string]: any; } +interface SummaryData { + total_entries: number; + status_count: Record; + nsapp_count: Record; +} const DataFetcher: React.FC = () => { const [data, setData] = useState([]); + const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(null); - const [sortConfig, setSortConfig] = useState<{ key: keyof DataModel | null, direction: 'ascending' | 'descending' }>({ key: 'id', direction: 'descending' }); - const [itemsPerPage, setItemsPerPage] = useState(25); const [currentPage, setCurrentPage] = useState(1); - - const [showErrorRow, setShowErrorRow] = useState(null); - + const [itemsPerPage, setItemsPerPage] = useState(25); + const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'ascending' | 'descending' } | null>(null); useEffect(() => { - const fetchData = async () => { + const fetchSummary = async () => { try { - const response = await fetch("https://api.htl-braunau.at/data/json"); - if (!response.ok) throw new Error("Failed to fetch data: ${response.statusText}"); + 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 () => { + 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); } catch (err) { @@ -52,52 +69,34 @@ const DataFetcher: React.FC = () => { } }; - fetchData(); - }, []); - - - const filteredData = data.filter(item => { - const matchesSearchQuery = Object.values(item).some(value => - value.toString().toLowerCase().includes(searchQuery.toLowerCase()) - ); - const itemDate = new Date(item.created_at); - const matchesDateRange = (!startDate || itemDate >= startDate) && (!endDate || itemDate <= endDate); - return matchesSearchQuery && matchesDateRange; - }); + fetchPaginatedData(); + }, [currentPage, itemsPerPage]); const sortedData = React.useMemo(() => { - let sortableData = [...filteredData]; - if (sortConfig.key !== null) { - sortableData.sort((a, b) => { - if (sortConfig.key !== null && a[sortConfig.key] < b[sortConfig.key]) { - return sortConfig.direction === 'ascending' ? -1 : 1; - } - if (sortConfig.key !== null && a[sortConfig.key] > b[sortConfig.key]) { - return sortConfig.direction === 'ascending' ? 1 : -1; - } - return 0; - }); - } - return sortableData; - }, [filteredData, sortConfig]); + if (!sortConfig) return data; + const sorted = [...data].sort((a, b) => { + if (a[sortConfig.key] < b[sortConfig.key]) { + return sortConfig.direction === 'ascending' ? -1 : 1; + } + if (a[sortConfig.key] > b[sortConfig.key]) { + return sortConfig.direction === 'ascending' ? 1 : -1; + } + return 0; + }); + return sorted; + }, [data, sortConfig]); - const requestSort = (key: keyof DataModel | null) => { + if (loading) return

Loading...

; + if (error) return

Error: {error}

; + + const requestSort = (key: string) => { let direction: 'ascending' | 'descending' = 'ascending'; - if (sortConfig.key === key && sortConfig.direction === 'ascending') { - direction = 'descending'; - } else if (sortConfig.key === key && sortConfig.direction === 'descending') { - direction = 'ascending'; - } else { + if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { direction = 'descending'; } setSortConfig({ key, direction }); }; - interface SortConfig { - key: keyof DataModel | null; - direction: 'ascending' | 'descending'; - } - const formatDate = (dateString: string): string => { const date = new Date(dateString); const year = date.getFullYear(); @@ -109,86 +108,15 @@ const DataFetcher: React.FC = () => { return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`; }; - const handleItemsPerPageChange = (event: React.ChangeEvent) => { - setItemsPerPage(Number(event.target.value)); - setCurrentPage(1); - }; - - const paginatedData = sortedData.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage); - - - if (loading) return

Loading...

; - if (error) return

Error: {error}

; - - var installingCounts: number = 0; - var failedCounts: number = 0; - var doneCounts: number = 0 - var unknownCounts: number = 0; - data.forEach((item) => { - if (item.status === "installing") { - installingCounts += 1; - } else if (item.status === "failed") { - failedCounts += 1; - } - else if (item.status === "done") { - doneCounts += 1; - } - else { - unknownCounts += 1; - } - }); - return (

Created LXCs

-
-
- setSearchQuery(e.target.value)} - className="p-2 border" - /> - -
-
- setStartDate(date)} - selectsStart - startDate={startDate} - endDate={endDate} - placeholderText="Start date" - className="p-2 border" - /> - -
- -
- setEndDate(date)} - selectsEnd - startDate={startDate} - endDate={endDate} - placeholderText="End date" - className="p-2 border" - /> - -
-
- + +

-

{filteredData.length} results found

-

Status Legend: 🔄 installing {installingCounts} | ✔️ completetd {doneCounts} | ❌ failed {failedCounts} | ❓ unknown {unknownCounts}

- -
+

{summary?.total_entries} results found

+

Status Legend: 🔄 installing {summary?.status_count["installing"] ?? 0} | ✔️ completed {summary?.status_count["done"] ?? 0} | ❌ failed {summary?.status_count["failed"] ?? 0} | ❓ unknown

+
@@ -209,7 +137,7 @@ const DataFetcher: React.FC = () => { - {paginatedData.map((item, index) => ( + {sortedData.map((item, index) => ( - + ))} @@ -259,26 +174,25 @@ const DataFetcher: React.FC = () => {
- + Page {currentPage} - +
); }; - - export default DataFetcher; diff --git a/frontend/src/components/ApplicationChart.tsx b/frontend/src/components/ApplicationChart.tsx index e62c10961..f70fa7098 100644 --- a/frontend/src/components/ApplicationChart.tsx +++ b/frontend/src/components/ApplicationChart.tsx @@ -25,12 +25,16 @@ import { Chart as ChartJS, ArcElement, Tooltip as ChartTooltip, Legend } from "c import ChartDataLabels from "chartjs-plugin-datalabels"; import { BarChart3, PieChart } from "lucide-react"; import React, { useState } from "react"; -import { Pie } from "react-chartjs-2"; +import { Pie, Bar } from "react-chartjs-2"; ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels); +interface SummaryData { + nsapp_count: Record; +} + interface ApplicationChartProps { - data: { nsapp: string }[]; + data: SummaryData | null; } const ITEMS_PER_PAGE = 20; @@ -57,13 +61,9 @@ export default function ApplicationChart({ data }: ApplicationChartProps) { const [chartStartIndex, setChartStartIndex] = useState(0); const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE); - // Calculate application counts - const appCounts = data.reduce((acc, item) => { - acc[item.nsapp] = (acc[item.nsapp] || 0) + 1; - return acc; - }, {} as Record); + if (!data) return null; - const sortedApps = Object.entries(appCounts) + const sortedApps = Object.entries(data.nsapp_count) .sort(([, a], [, b]) => b - a); const chartApps = sortedApps.slice(
{item.status === "done" ? ( @@ -237,20 +165,7 @@ const DataFetcher: React.FC = () => { {item.ram_size} {item.method} {item.pve_version} - {item.error && item.error !== "none" ? ( - showErrorRow === index ? ( - <> - {item.error} - - - ) : ( - - ) - ) : ( - "none" - )} - {item.error} {formatDate(item.created_at)}