Skip to content

Commit c01abd5

Browse files
Add basic pagination (#2715)
1 parent a5039cf commit c01abd5

File tree

2 files changed

+74
-160
lines changed

2 files changed

+74
-160
lines changed

frontend/src/app/data/page.tsx

Lines changed: 66 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React, { useEffect, useState } from "react";
3+
import React, { JSX, useEffect, useState } from "react";
44
import DatePicker from 'react-datepicker';
55
import 'react-datepicker/dist/react-datepicker.css';
66
import ApplicationChart from "../../components/ApplicationChart";
@@ -21,28 +21,45 @@ interface DataModel {
2121
status: string;
2222
error: string;
2323
type: string;
24+
[key: string]: any;
2425
}
2526

27+
interface SummaryData {
28+
total_entries: number;
29+
status_count: Record<string, number>;
30+
nsapp_count: Record<string, number>;
31+
}
2632

2733
const DataFetcher: React.FC = () => {
2834
const [data, setData] = useState<DataModel[]>([]);
35+
const [summary, setSummary] = useState<SummaryData | null>(null);
2936
const [loading, setLoading] = useState<boolean>(true);
3037
const [error, setError] = useState<string | null>(null);
31-
const [searchQuery, setSearchQuery] = useState('');
32-
const [startDate, setStartDate] = useState<Date | null>(null);
33-
const [endDate, setEndDate] = useState<Date | null>(null);
34-
const [sortConfig, setSortConfig] = useState<{ key: keyof DataModel | null, direction: 'ascending' | 'descending' }>({ key: 'id', direction: 'descending' });
35-
const [itemsPerPage, setItemsPerPage] = useState(25);
3638
const [currentPage, setCurrentPage] = useState(1);
39+
const [itemsPerPage, setItemsPerPage] = useState(25);
40+
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'ascending' | 'descending' } | null>(null);
3741

38-
const [showErrorRow, setShowErrorRow] = useState<number | null>(null);
42+
useEffect(() => {
43+
const fetchSummary = async () => {
44+
try {
45+
const response = await fetch("https://api.htl-braunau.at/data/summary");
46+
if (!response.ok) throw new Error(`Failed to fetch summary: ${response.statusText}`);
47+
const result: SummaryData = await response.json();
48+
setSummary(result);
49+
} catch (err) {
50+
setError((err as Error).message);
51+
}
52+
};
3953

54+
fetchSummary();
55+
}, []);
4056

4157
useEffect(() => {
42-
const fetchData = async () => {
58+
const fetchPaginatedData = async () => {
59+
setLoading(true);
4360
try {
44-
const response = await fetch("https://api.htl-braunau.at/data/json");
45-
if (!response.ok) throw new Error("Failed to fetch data: ${response.statusText}");
61+
const response = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? '' : itemsPerPage}`);
62+
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`);
4663
const result: DataModel[] = await response.json();
4764
setData(result);
4865
} catch (err) {
@@ -52,52 +69,34 @@ const DataFetcher: React.FC = () => {
5269
}
5370
};
5471

55-
fetchData();
56-
}, []);
57-
58-
59-
const filteredData = data.filter(item => {
60-
const matchesSearchQuery = Object.values(item).some(value =>
61-
value.toString().toLowerCase().includes(searchQuery.toLowerCase())
62-
);
63-
const itemDate = new Date(item.created_at);
64-
const matchesDateRange = (!startDate || itemDate >= startDate) && (!endDate || itemDate <= endDate);
65-
return matchesSearchQuery && matchesDateRange;
66-
});
72+
fetchPaginatedData();
73+
}, [currentPage, itemsPerPage]);
6774

6875
const sortedData = React.useMemo(() => {
69-
let sortableData = [...filteredData];
70-
if (sortConfig.key !== null) {
71-
sortableData.sort((a, b) => {
72-
if (sortConfig.key !== null && a[sortConfig.key] < b[sortConfig.key]) {
73-
return sortConfig.direction === 'ascending' ? -1 : 1;
74-
}
75-
if (sortConfig.key !== null && a[sortConfig.key] > b[sortConfig.key]) {
76-
return sortConfig.direction === 'ascending' ? 1 : -1;
77-
}
78-
return 0;
79-
});
80-
}
81-
return sortableData;
82-
}, [filteredData, sortConfig]);
76+
if (!sortConfig) return data;
77+
const sorted = [...data].sort((a, b) => {
78+
if (a[sortConfig.key] < b[sortConfig.key]) {
79+
return sortConfig.direction === 'ascending' ? -1 : 1;
80+
}
81+
if (a[sortConfig.key] > b[sortConfig.key]) {
82+
return sortConfig.direction === 'ascending' ? 1 : -1;
83+
}
84+
return 0;
85+
});
86+
return sorted;
87+
}, [data, sortConfig]);
8388

84-
const requestSort = (key: keyof DataModel | null) => {
89+
if (loading) return <p>Loading...</p>;
90+
if (error) return <p>Error: {error}</p>;
91+
92+
const requestSort = (key: string) => {
8593
let direction: 'ascending' | 'descending' = 'ascending';
86-
if (sortConfig.key === key && sortConfig.direction === 'ascending') {
87-
direction = 'descending';
88-
} else if (sortConfig.key === key && sortConfig.direction === 'descending') {
89-
direction = 'ascending';
90-
} else {
94+
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') {
9195
direction = 'descending';
9296
}
9397
setSortConfig({ key, direction });
9498
};
9599

96-
interface SortConfig {
97-
key: keyof DataModel | null;
98-
direction: 'ascending' | 'descending';
99-
}
100-
101100
const formatDate = (dateString: string): string => {
102101
const date = new Date(dateString);
103102
const year = date.getFullYear();
@@ -109,86 +108,15 @@ const DataFetcher: React.FC = () => {
109108
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
110109
};
111110

112-
const handleItemsPerPageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
113-
setItemsPerPage(Number(event.target.value));
114-
setCurrentPage(1);
115-
};
116-
117-
const paginatedData = sortedData.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
118-
119-
120-
if (loading) return <p>Loading...</p>;
121-
if (error) return <p>Error: {error}</p>;
122-
123-
var installingCounts: number = 0;
124-
var failedCounts: number = 0;
125-
var doneCounts: number = 0
126-
var unknownCounts: number = 0;
127-
data.forEach((item) => {
128-
if (item.status === "installing") {
129-
installingCounts += 1;
130-
} else if (item.status === "failed") {
131-
failedCounts += 1;
132-
}
133-
else if (item.status === "done") {
134-
doneCounts += 1;
135-
}
136-
else {
137-
unknownCounts += 1;
138-
}
139-
});
140-
141111
return (
142112
<div className="p-6 mt-20">
143113
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
144-
<div className="mb-4 flex space-x-4">
145-
<div>
146-
<input
147-
type="text"
148-
placeholder="Search..."
149-
value={searchQuery}
150-
onChange={e => setSearchQuery(e.target.value)}
151-
className="p-2 border"
152-
/>
153-
<label className="text-sm text-gray-600 mt-1 block">Search by keyword</label>
154-
</div>
155-
<div>
156-
<DatePicker
157-
selected={startDate}
158-
onChange={date => setStartDate(date)}
159-
selectsStart
160-
startDate={startDate}
161-
endDate={endDate}
162-
placeholderText="Start date"
163-
className="p-2 border"
164-
/>
165-
<label className="text-sm text-gray-600 mt-1 block">Set a start date</label>
166-
</div>
167-
168-
<div>
169-
<DatePicker
170-
selected={endDate}
171-
onChange={date => setEndDate(date)}
172-
selectsEnd
173-
startDate={startDate}
174-
endDate={endDate}
175-
placeholderText="End date"
176-
className="p-2 border"
177-
/>
178-
<label className="text-sm text-gray-600 mt-1 block">Set a end date</label>
179-
</div>
180-
</div>
181-
<ApplicationChart data={filteredData} />
114+
<ApplicationChart data={summary} />
115+
<p className="text-lg font-bold mt-4"> </p>
182116
<div className="mb-4 flex justify-between items-center">
183-
<p className="text-lg font-bold">{filteredData.length} results found</p>
184-
<p className="text-lg font">Status Legend: 🔄 installing {installingCounts} | ✔️ completetd {doneCounts} | ❌ failed {failedCounts} | ❓ unknown {unknownCounts}</p>
185-
<select value={itemsPerPage} onChange={handleItemsPerPageChange} className="p-2 border">
186-
<option value={25}>25</option>
187-
<option value={50}>50</option>
188-
<option value={100}>100</option>
189-
<option value={200}>200</option>
190-
</select>
191-
</div>
117+
<p className="text-lg font-bold">{summary?.total_entries} results found</p>
118+
<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>
119+
</div>
192120
<div className="overflow-x-auto">
193121
<div className="overflow-y-auto lg:overflow-y-visible">
194122
<table className="min-w-full table-auto border-collapse">
@@ -209,7 +137,7 @@ const DataFetcher: React.FC = () => {
209137
</tr>
210138
</thead>
211139
<tbody>
212-
{paginatedData.map((item, index) => (
140+
{sortedData.map((item, index) => (
213141
<tr key={index}>
214142
<td className="px-4 py-2 border-b">
215143
{item.status === "done" ? (
@@ -237,20 +165,7 @@ const DataFetcher: React.FC = () => {
237165
<td className="px-4 py-2 border-b">{item.ram_size}</td>
238166
<td className="px-4 py-2 border-b">{item.method}</td>
239167
<td className="px-4 py-2 border-b">{item.pve_version}</td>
240-
<td className="px-4 py-2 border-b">
241-
{item.error && item.error !== "none" ? (
242-
showErrorRow === index ? (
243-
<>
244-
{item.error}
245-
<button onClick={() => setShowErrorRow(null)}>{item.error}</button>
246-
</>
247-
) : (
248-
<button onClick={() => setShowErrorRow(index)}>Click to show error</button>
249-
)
250-
) : (
251-
"none"
252-
)}
253-
</td>
168+
<td className="px-4 py-2 border-b">{item.error}</td>
254169
<td className="px-4 py-2 border-b">{formatDate(item.created_at)}</td>
255170
</tr>
256171
))}
@@ -259,26 +174,25 @@ const DataFetcher: React.FC = () => {
259174
</div>
260175
</div>
261176
<div className="mt-4 flex justify-between items-center">
262-
<button
263-
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
264-
disabled={currentPage === 1}
265-
className="p-2 border"
266-
>
267-
Previous
268-
</button>
177+
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border">Previous</button>
269178
<span>Page {currentPage}</span>
270-
<button
271-
onClick={() => setCurrentPage(prev => (prev * itemsPerPage < sortedData.length ? prev + 1 : prev))}
272-
disabled={currentPage * itemsPerPage >= sortedData.length}
179+
<button onClick={() => setCurrentPage(prev => prev + 1)} className="p-2 border">Next</button>
180+
<select
181+
value={itemsPerPage}
182+
onChange={(e) => setItemsPerPage(Number(e.target.value))}
273183
className="p-2 border"
274184
>
275-
Next
276-
</button>
185+
<option value={10}>10</option>
186+
<option value={20}>20</option>
187+
<option value={50}>50</option>
188+
<option value={100}>100</option>
189+
<option value={250}>250</option>
190+
<option value={500}>500</option>
191+
<option value={5000}>5000</option>
192+
</select>
277193
</div>
278194
</div>
279195
);
280196
};
281197

282-
283-
284198
export default DataFetcher;

frontend/src/components/ApplicationChart.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ import { Chart as ChartJS, ArcElement, Tooltip as ChartTooltip, Legend } from "c
2525
import ChartDataLabels from "chartjs-plugin-datalabels";
2626
import { BarChart3, PieChart } from "lucide-react";
2727
import React, { useState } from "react";
28-
import { Pie } from "react-chartjs-2";
28+
import { Pie, Bar } from "react-chartjs-2";
2929

3030
ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
3131

32+
interface SummaryData {
33+
nsapp_count: Record<string, number>;
34+
}
35+
3236
interface ApplicationChartProps {
33-
data: { nsapp: string }[];
37+
data: SummaryData | null;
3438
}
3539

3640
const ITEMS_PER_PAGE = 20;
@@ -57,13 +61,9 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
5761
const [chartStartIndex, setChartStartIndex] = useState(0);
5862
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
5963

60-
// Calculate application counts
61-
const appCounts = data.reduce((acc, item) => {
62-
acc[item.nsapp] = (acc[item.nsapp] || 0) + 1;
63-
return acc;
64-
}, {} as Record<string, number>);
64+
if (!data) return null;
6565

66-
const sortedApps = Object.entries(appCounts)
66+
const sortedApps = Object.entries(data.nsapp_count)
6767
.sort(([, a], [, b]) => b - a);
6868

6969
const chartApps = sortedApps.slice(

0 commit comments

Comments
 (0)