1
0
mirror of https://github.com/community-scripts/ProxmoxVE.git synced 2025-03-08 13:19:05 +00:00

Add basic pagination (#2715)

This commit is contained in:
Michel Roegl-Brunner 2025-02-27 16:56:56 +02:00 committed by GitHub
parent a5039cff58
commit c01abd559b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 162 deletions

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { JSX, useEffect, useState } from "react";
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
import ApplicationChart from "../../components/ApplicationChart"; import ApplicationChart from "../../components/ApplicationChart";
@ -21,28 +21,45 @@ interface DataModel {
status: string; status: string;
error: string; error: string;
type: string; type: string;
[key: string]: any;
} }
interface SummaryData {
total_entries: number;
status_count: Record<string, number>;
nsapp_count: Record<string, number>;
}
const DataFetcher: React.FC = () => { const DataFetcher: React.FC = () => {
const [data, setData] = useState<DataModel[]>([]); const [data, setData] = useState<DataModel[]>([]);
const [summary, setSummary] = useState<SummaryData | null>(null);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [startDate, setStartDate] = useState<Date | null>(null);
const [endDate, setEndDate] = useState<Date | null>(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 [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(25);
const [showErrorRow, setShowErrorRow] = useState<number | null>(null); const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'ascending' | 'descending' } | null>(null);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchSummary = async () => {
try { try {
const response = await fetch("https://api.htl-braunau.at/data/json"); const response = await fetch("https://api.htl-braunau.at/data/summary");
if (!response.ok) throw new Error("Failed to fetch data: ${response.statusText}"); 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(); const result: DataModel[] = await response.json();
setData(result); setData(result);
} catch (err) { } catch (err) {
@ -52,52 +69,34 @@ const DataFetcher: React.FC = () => {
} }
}; };
fetchData(); fetchPaginatedData();
}, []); }, [currentPage, itemsPerPage]);
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;
});
const sortedData = React.useMemo(() => { const sortedData = React.useMemo(() => {
let sortableData = [...filteredData]; if (!sortConfig) return data;
if (sortConfig.key !== null) { const sorted = [...data].sort((a, b) => {
sortableData.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) {
if (sortConfig.key !== null && a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? -1 : 1;
return sortConfig.direction === 'ascending' ? -1 : 1; }
} if (a[sortConfig.key] > b[sortConfig.key]) {
if (sortConfig.key !== null && a[sortConfig.key] > b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? 1 : -1;
return sortConfig.direction === 'ascending' ? 1 : -1; }
} return 0;
return 0; });
}); return sorted;
} }, [data, sortConfig]);
return sortableData;
}, [filteredData, sortConfig]);
const requestSort = (key: keyof DataModel | null) => { if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
const requestSort = (key: string) => {
let direction: 'ascending' | 'descending' = 'ascending'; let direction: 'ascending' | 'descending' = 'ascending';
if (sortConfig.key === key && sortConfig.direction === 'ascending') { if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') {
direction = 'descending';
} else if (sortConfig.key === key && sortConfig.direction === 'descending') {
direction = 'ascending';
} else {
direction = 'descending'; direction = 'descending';
} }
setSortConfig({ key, direction }); setSortConfig({ key, direction });
}; };
interface SortConfig {
key: keyof DataModel | null;
direction: 'ascending' | 'descending';
}
const formatDate = (dateString: string): string => { const formatDate = (dateString: string): string => {
const date = new Date(dateString); const date = new Date(dateString);
const year = date.getFullYear(); const year = date.getFullYear();
@ -109,86 +108,15 @@ const DataFetcher: React.FC = () => {
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`; return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
}; };
const handleItemsPerPageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setItemsPerPage(Number(event.target.value));
setCurrentPage(1);
};
const paginatedData = sortedData.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
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 ( return (
<div className="p-6 mt-20"> <div className="p-6 mt-20">
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1> <h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
<div className="mb-4 flex space-x-4"> <ApplicationChart data={summary} />
<div> <p className="text-lg font-bold mt-4"> </p>
<input
type="text"
placeholder="Search..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="p-2 border"
/>
<label className="text-sm text-gray-600 mt-1 block">Search by keyword</label>
</div>
<div>
<DatePicker
selected={startDate}
onChange={date => setStartDate(date)}
selectsStart
startDate={startDate}
endDate={endDate}
placeholderText="Start date"
className="p-2 border"
/>
<label className="text-sm text-gray-600 mt-1 block">Set a start date</label>
</div>
<div>
<DatePicker
selected={endDate}
onChange={date => setEndDate(date)}
selectsEnd
startDate={startDate}
endDate={endDate}
placeholderText="End date"
className="p-2 border"
/>
<label className="text-sm text-gray-600 mt-1 block">Set a end date</label>
</div>
</div>
<ApplicationChart data={filteredData} />
<div className="mb-4 flex justify-between items-center"> <div className="mb-4 flex justify-between items-center">
<p className="text-lg font-bold">{filteredData.length} results found</p> <p className="text-lg font-bold">{summary?.total_entries} results found</p>
<p className="text-lg font">Status Legend: 🔄 installing {installingCounts} | completetd {doneCounts} | failed {failedCounts} | unknown {unknownCounts}</p> <p className="text-lg font">Status Legend: 🔄 installing {summary?.status_count["installing"] ?? 0} | completed {summary?.status_count["done"] ?? 0} | failed {summary?.status_count["failed"] ?? 0} | unknown</p>
<select value={itemsPerPage} onChange={handleItemsPerPageChange} className="p-2 border"> </div>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={200}>200</option>
</select>
</div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<div className="overflow-y-auto lg:overflow-y-visible"> <div className="overflow-y-auto lg:overflow-y-visible">
<table className="min-w-full table-auto border-collapse"> <table className="min-w-full table-auto border-collapse">
@ -209,7 +137,7 @@ const DataFetcher: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{paginatedData.map((item, index) => ( {sortedData.map((item, index) => (
<tr key={index}> <tr key={index}>
<td className="px-4 py-2 border-b"> <td className="px-4 py-2 border-b">
{item.status === "done" ? ( {item.status === "done" ? (
@ -237,20 +165,7 @@ const DataFetcher: React.FC = () => {
<td className="px-4 py-2 border-b">{item.ram_size}</td> <td className="px-4 py-2 border-b">{item.ram_size}</td>
<td className="px-4 py-2 border-b">{item.method}</td> <td className="px-4 py-2 border-b">{item.method}</td>
<td className="px-4 py-2 border-b">{item.pve_version}</td> <td className="px-4 py-2 border-b">{item.pve_version}</td>
<td className="px-4 py-2 border-b"> <td className="px-4 py-2 border-b">{item.error}</td>
{item.error && item.error !== "none" ? (
showErrorRow === index ? (
<>
{item.error}
<button onClick={() => setShowErrorRow(null)}>{item.error}</button>
</>
) : (
<button onClick={() => setShowErrorRow(index)}>Click to show error</button>
)
) : (
"none"
)}
</td>
<td className="px-4 py-2 border-b">{formatDate(item.created_at)}</td> <td className="px-4 py-2 border-b">{formatDate(item.created_at)}</td>
</tr> </tr>
))} ))}
@ -259,26 +174,25 @@ const DataFetcher: React.FC = () => {
</div> </div>
</div> </div>
<div className="mt-4 flex justify-between items-center"> <div className="mt-4 flex justify-between items-center">
<button <button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border">Previous</button>
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="p-2 border"
>
Previous
</button>
<span>Page {currentPage}</span> <span>Page {currentPage}</span>
<button <button onClick={() => setCurrentPage(prev => prev + 1)} className="p-2 border">Next</button>
onClick={() => setCurrentPage(prev => (prev * itemsPerPage < sortedData.length ? prev + 1 : prev))} <select
disabled={currentPage * itemsPerPage >= sortedData.length} value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="p-2 border" className="p-2 border"
> >
Next <option value={10}>10</option>
</button> <option value={20}>20</option>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={250}>250</option>
<option value={500}>500</option>
<option value={5000}>5000</option>
</select>
</div> </div>
</div> </div>
); );
}; };
export default DataFetcher; export default DataFetcher;

View File

@ -25,12 +25,16 @@ import { Chart as ChartJS, ArcElement, Tooltip as ChartTooltip, Legend } from "c
import ChartDataLabels from "chartjs-plugin-datalabels"; import ChartDataLabels from "chartjs-plugin-datalabels";
import { BarChart3, PieChart } from "lucide-react"; import { BarChart3, PieChart } from "lucide-react";
import React, { useState } from "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); ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
interface SummaryData {
nsapp_count: Record<string, number>;
}
interface ApplicationChartProps { interface ApplicationChartProps {
data: { nsapp: string }[]; data: SummaryData | null;
} }
const ITEMS_PER_PAGE = 20; const ITEMS_PER_PAGE = 20;
@ -57,13 +61,9 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
const [chartStartIndex, setChartStartIndex] = useState(0); const [chartStartIndex, setChartStartIndex] = useState(0);
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE); const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
// Calculate application counts if (!data) return null;
const appCounts = data.reduce((acc, item) => {
acc[item.nsapp] = (acc[item.nsapp] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const sortedApps = Object.entries(appCounts) const sortedApps = Object.entries(data.nsapp_count)
.sort(([, a], [, b]) => b - a); .sort(([, a], [, b]) => b - a);
const chartApps = sortedApps.slice( const chartApps = sortedApps.slice(