mirror of
https://github.com/community-scripts/ProxmoxVE
synced 2025-02-04 23:10:16 +00:00
[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
This commit is contained in:
parent
8bc50f4d71
commit
139f84a934
53
frontend/package-lock.json
generated
53
frontend/package-lock.json
generated
@ -38,6 +38,7 @@
|
|||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
"prettier-plugin-organize-imports": "^4.1.0",
|
||||||
"react": "19.0.0-rc-02c0e824-20241028",
|
"react": "19.0.0-rc-02c0e824-20241028",
|
||||||
"react-code-blocks": "^0.1.6",
|
"react-code-blocks": "^0.1.6",
|
||||||
|
"react-datepicker": "^7.6.0",
|
||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "8.10.1",
|
||||||
"react-dom": "19.0.0-rc-02c0e824-20241028",
|
"react-dom": "19.0.0-rc-02c0e824-20241028",
|
||||||
"react-icons": "^5.1.0",
|
"react-icons": "^5.1.0",
|
||||||
@ -1083,9 +1084,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/utils": {
|
"node_modules/@floating-ui/utils": {
|
||||||
"version": "0.2.8",
|
"version": "0.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||||
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
|
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
@ -8017,6 +8018,46 @@
|
|||||||
"react": ">=16"
|
"react": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-datepicker": {
|
||||||
|
"version": "7.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.6.0.tgz",
|
||||||
|
"integrity": "sha512-9cQH6Z/qa4LrGhzdc3XoHbhrxNcMi9MKjZmYgF/1MNNaJwvdSjv3Xd+jjvrEEbKEf71ZgCA3n7fQbdwd70qCRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react": "^0.27.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^3.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-datepicker/node_modules/@floating-ui/react": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-CLHnes3ixIFFKVQDdICjel8muhFLOBdQH7fgtHNPY8UbCNqbeKZ262G7K66lGQOUQWWnYocf7ZbUsLJgGfsLHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.1.2",
|
||||||
|
"@floating-ui/utils": "^0.2.9",
|
||||||
|
"tabbable": "^6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17.0.0",
|
||||||
|
"react-dom": ">=17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-datepicker/node_modules/date-fns": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-day-picker": {
|
"node_modules/react-day-picker": {
|
||||||
"version": "8.10.1",
|
"version": "8.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
|
||||||
@ -9055,6 +9096,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tabbable": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "2.5.4",
|
"version": "2.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
|
||||||
|
@ -49,6 +49,7 @@
|
|||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
"prettier-plugin-organize-imports": "^4.1.0",
|
||||||
"react": "19.0.0-rc-02c0e824-20241028",
|
"react": "19.0.0-rc-02c0e824-20241028",
|
||||||
"react-code-blocks": "^0.1.6",
|
"react-code-blocks": "^0.1.6",
|
||||||
|
"react-datepicker": "^7.6.0",
|
||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "8.10.1",
|
||||||
"react-dom": "19.0.0-rc-02c0e824-20241028",
|
"react-dom": "19.0.0-rc-02c0e824-20241028",
|
||||||
"react-icons": "^5.1.0",
|
"react-icons": "^5.1.0",
|
||||||
|
236
frontend/src/app/data/page.tsx
Normal file
236
frontend/src/app/data/page.tsx
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import DatePicker from 'react-datepicker';
|
||||||
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
|
import { string } from "zod";
|
||||||
|
|
||||||
|
|
||||||
|
interface DataModel {
|
||||||
|
id: number;
|
||||||
|
ct_type: number;
|
||||||
|
disk_size: number;
|
||||||
|
core_count: number;
|
||||||
|
ram_size: number;
|
||||||
|
verbose: string;
|
||||||
|
os_type: string;
|
||||||
|
os_version: string;
|
||||||
|
hn: string;
|
||||||
|
disableip6: string;
|
||||||
|
ssh: string;
|
||||||
|
tags: string;
|
||||||
|
nsapp: string;
|
||||||
|
created_at: string;
|
||||||
|
method: string;
|
||||||
|
pve_version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const DataFetcher: React.FC = () => {
|
||||||
|
const [data, setData] = useState<DataModel[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
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(5);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("http://api.htl-braunau.at/data/json");
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch data: ${response.statusText}");
|
||||||
|
const result: DataModel[] = await response.json();
|
||||||
|
setData(result);
|
||||||
|
} catch (err) {
|
||||||
|
setError((err as Error).message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const requestSort = (key: keyof DataModel | null) => {
|
||||||
|
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 {
|
||||||
|
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();
|
||||||
|
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`;
|
||||||
|
};
|
||||||
|
|
||||||
|
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>;
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 mt-20">
|
||||||
|
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
|
||||||
|
<div className="mb-4 flex space-x-4">
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
<div className="mb-4 flex justify-between items-center">
|
||||||
|
<p className="text-lg font-bold">{filteredData.length} results found</p>
|
||||||
|
<select value={itemsPerPage} onChange={handleItemsPerPageChange} className="p-2 border">
|
||||||
|
<option value={5}>5</option>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={20}>20</option>
|
||||||
|
<option value={50}>50</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<div className="overflow-y-auto lg:overflow-y-visible">
|
||||||
|
<table className="min-w-full table-auto border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_version')}>OS Version</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('disk_size')}>Disk Size</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('core_count')}>Core Count</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ram_size')}>RAM Size</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('hn')}>Hostname</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ssh')}>SSH</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('verbose')}>Verb</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('tags')}>Tags</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('method')}>Method</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('pve_version')}>PVE Version</th>
|
||||||
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{paginatedData.map((item, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td className="px-4 py-2 border-b">{item.nsapp}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{item.os_type}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{item.os_version}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{item.disk_size}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{item.core_count}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{item.ram_size}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{item.hn}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{item.ssh}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{item.verbose}</td>
|
||||||
|
<td className="px-4 py-2 border-b">{item.tags.replace(/;/g, ' ')}</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">{formatDate(item.created_at)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-between items-center">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="p-2 border"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<span>Page {currentPage}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => (prev * itemsPerPage < sortedData.length ? prev + 1 : prev))}
|
||||||
|
disabled={currentPage * itemsPerPage >= sortedData.length}
|
||||||
|
className="p-2 border"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default DataFetcher;
|
Loading…
Reference in New Issue
Block a user