Skip to content

Commit 139f84a

Browse files
[Frontend] Add /data to show API results (#1841)
* [Frontend] Add /data to show API results * [Frontend] Add /data to show API results * update page.tsx * update page.tsx * update page.tsx * update page.tsx
1 parent 8bc50f4 commit 139f84a

File tree

3 files changed

+287
-3
lines changed

3 files changed

+287
-3
lines changed

frontend/package-lock.json

Lines changed: 50 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"prettier-plugin-organize-imports": "^4.1.0",
5050
"react": "19.0.0-rc-02c0e824-20241028",
5151
"react-code-blocks": "^0.1.6",
52+
"react-datepicker": "^7.6.0",
5253
"react-day-picker": "8.10.1",
5354
"react-dom": "19.0.0-rc-02c0e824-20241028",
5455
"react-icons": "^5.1.0",

frontend/src/app/data/page.tsx

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
"use client";
2+
3+
import React, { useEffect, useState } from "react";
4+
import DatePicker from 'react-datepicker';
5+
import 'react-datepicker/dist/react-datepicker.css';
6+
import { string } from "zod";
7+
8+
9+
interface DataModel {
10+
id: number;
11+
ct_type: number;
12+
disk_size: number;
13+
core_count: number;
14+
ram_size: number;
15+
verbose: string;
16+
os_type: string;
17+
os_version: string;
18+
hn: string;
19+
disableip6: string;
20+
ssh: string;
21+
tags: string;
22+
nsapp: string;
23+
created_at: string;
24+
method: string;
25+
pve_version: string;
26+
}
27+
28+
29+
const DataFetcher: React.FC = () => {
30+
const [data, setData] = useState<DataModel[]>([]);
31+
const [loading, setLoading] = useState<boolean>(true);
32+
const [error, setError] = useState<string | null>(null);
33+
const [searchQuery, setSearchQuery] = useState('');
34+
const [startDate, setStartDate] = useState<Date | null>(null);
35+
const [endDate, setEndDate] = useState<Date | null>(null);
36+
const [sortConfig, setSortConfig] = useState<{ key: keyof DataModel | null, direction: 'ascending' | 'descending' }>({ key: 'id', direction: 'descending' });
37+
const [itemsPerPage, setItemsPerPage] = useState(5);
38+
const [currentPage, setCurrentPage] = useState(1);
39+
40+
useEffect(() => {
41+
const fetchData = async () => {
42+
try {
43+
const response = await fetch("http://api.htl-braunau.at/data/json");
44+
if (!response.ok) throw new Error("Failed to fetch data: ${response.statusText}");
45+
const result: DataModel[] = await response.json();
46+
setData(result);
47+
} catch (err) {
48+
setError((err as Error).message);
49+
} finally {
50+
setLoading(false);
51+
}
52+
};
53+
54+
fetchData();
55+
}, []);
56+
57+
58+
const filteredData = data.filter(item => {
59+
const matchesSearchQuery = Object.values(item).some(value =>
60+
value.toString().toLowerCase().includes(searchQuery.toLowerCase())
61+
);
62+
const itemDate = new Date(item.created_at);
63+
const matchesDateRange = (!startDate || itemDate >= startDate) && (!endDate || itemDate <= endDate);
64+
return matchesSearchQuery && matchesDateRange;
65+
});
66+
67+
const sortedData = React.useMemo(() => {
68+
let sortableData = [...filteredData];
69+
if (sortConfig.key !== null) {
70+
sortableData.sort((a, b) => {
71+
if (sortConfig.key !== null && a[sortConfig.key] < b[sortConfig.key]) {
72+
return sortConfig.direction === 'ascending' ? -1 : 1;
73+
}
74+
if (sortConfig.key !== null && a[sortConfig.key] > b[sortConfig.key]) {
75+
return sortConfig.direction === 'ascending' ? 1 : -1;
76+
}
77+
return 0;
78+
});
79+
}
80+
return sortableData;
81+
}, [filteredData, sortConfig]);
82+
83+
const requestSort = (key: keyof DataModel | null) => {
84+
let direction: 'ascending' | 'descending' = 'ascending';
85+
if (sortConfig.key === key && sortConfig.direction === 'ascending') {
86+
direction = 'descending';
87+
} else if (sortConfig.key === key && sortConfig.direction === 'descending') {
88+
direction = 'ascending';
89+
} else {
90+
direction = 'descending';
91+
}
92+
setSortConfig({ key, direction });
93+
};
94+
95+
interface SortConfig {
96+
key: keyof DataModel | null;
97+
direction: 'ascending' | 'descending';
98+
}
99+
100+
const formatDate = (dateString: string): string => {
101+
const date = new Date(dateString);
102+
const year = date.getFullYear();
103+
const month = date.getMonth() + 1;
104+
const day = date.getDate();
105+
const hours = String(date.getHours()).padStart(2, '0');
106+
const minutes = String(date.getMinutes()).padStart(2, '0');
107+
const timezoneOffset = dateString.slice(-6);
108+
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
109+
};
110+
111+
const handleItemsPerPageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
112+
setItemsPerPage(Number(event.target.value));
113+
setCurrentPage(1);
114+
};
115+
116+
const paginatedData = sortedData.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
117+
118+
if (loading) return <p>Loading...</p>;
119+
if (error) return <p>Error: {error}</p>;
120+
121+
122+
return (
123+
<div className="p-6 mt-20">
124+
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
125+
<div className="mb-4 flex space-x-4">
126+
<div>
127+
<input
128+
type="text"
129+
placeholder="Search..."
130+
value={searchQuery}
131+
onChange={e => setSearchQuery(e.target.value)}
132+
className="p-2 border"
133+
/>
134+
<label className="text-sm text-gray-600 mt-1 block">Search by keyword</label>
135+
</div>
136+
<div>
137+
<DatePicker
138+
selected={startDate}
139+
onChange={date => setStartDate(date)}
140+
selectsStart
141+
startDate={startDate}
142+
endDate={endDate}
143+
placeholderText="Start date"
144+
className="p-2 border"
145+
/>
146+
<label className="text-sm text-gray-600 mt-1 block">Set a start date</label>
147+
</div>
148+
149+
<div>
150+
<DatePicker
151+
selected={endDate}
152+
onChange={date => setEndDate(date)}
153+
selectsEnd
154+
startDate={startDate}
155+
endDate={endDate}
156+
placeholderText="End date"
157+
className="p-2 border"
158+
/>
159+
<label className="text-sm text-gray-600 mt-1 block">Set a end date</label>
160+
</div>
161+
</div>
162+
<div className="mb-4 flex justify-between items-center">
163+
<p className="text-lg font-bold">{filteredData.length} results found</p>
164+
<select value={itemsPerPage} onChange={handleItemsPerPageChange} className="p-2 border">
165+
<option value={5}>5</option>
166+
<option value={10}>10</option>
167+
<option value={20}>20</option>
168+
<option value={50}>50</option>
169+
</select>
170+
</div>
171+
<div className="overflow-x-auto">
172+
<div className="overflow-y-auto lg:overflow-y-visible">
173+
<table className="min-w-full table-auto border-collapse">
174+
<thead>
175+
<tr>
176+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</th>
177+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</th>
178+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_version')}>OS Version</th>
179+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('disk_size')}>Disk Size</th>
180+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('core_count')}>Core Count</th>
181+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ram_size')}>RAM Size</th>
182+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('hn')}>Hostname</th>
183+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ssh')}>SSH</th>
184+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('verbose')}>Verb</th>
185+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('tags')}>Tags</th>
186+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('method')}>Method</th>
187+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('pve_version')}>PVE Version</th>
188+
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</th>
189+
</tr>
190+
</thead>
191+
<tbody>
192+
{paginatedData.map((item, index) => (
193+
<tr key={index}>
194+
<td className="px-4 py-2 border-b">{item.nsapp}</td>
195+
<td className="px-4 py-2 border-b">{item.os_type}</td>
196+
<td className="px-4 py-2 border-b">{item.os_version}</td>
197+
<td className="px-4 py-2 border-b">{item.disk_size}</td>
198+
<td className="px-4 py-2 border-b">{item.core_count}</td>
199+
<td className="px-4 py-2 border-b">{item.ram_size}</td>
200+
<td className="px-4 py-2 border-b">{item.hn}</td>
201+
<td className="px-4 py-2 border-b">{item.ssh}</td>
202+
<td className="px-4 py-2 border-b">{item.verbose}</td>
203+
<td className="px-4 py-2 border-b">{item.tags.replace(/;/g, ' ')}</td>
204+
<td className="px-4 py-2 border-b">{item.method}</td>
205+
<td className="px-4 py-2 border-b">{item.pve_version}</td>
206+
<td className="px-4 py-2 border-b">{formatDate(item.created_at)}</td>
207+
</tr>
208+
))}
209+
</tbody>
210+
</table>
211+
</div>
212+
</div>
213+
<div className="mt-4 flex justify-between items-center">
214+
<button
215+
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
216+
disabled={currentPage === 1}
217+
className="p-2 border"
218+
>
219+
Previous
220+
</button>
221+
<span>Page {currentPage}</span>
222+
<button
223+
onClick={() => setCurrentPage(prev => (prev * itemsPerPage < sortedData.length ? prev + 1 : prev))}
224+
disabled={currentPage * itemsPerPage >= sortedData.length}
225+
className="p-2 border"
226+
>
227+
Next
228+
</button>
229+
</div>
230+
</div>
231+
);
232+
};
233+
234+
235+
236+
export default DataFetcher;

0 commit comments

Comments
 (0)