mirror of
				https://github.com/community-scripts/ProxmoxVE.git
				synced 2025-11-04 02:12:49 +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:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						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",
 | 
			
		||||
        "react": "19.0.0-rc-02c0e824-20241028",
 | 
			
		||||
        "react-code-blocks": "^0.1.6",
 | 
			
		||||
        "react-datepicker": "^7.6.0",
 | 
			
		||||
        "react-day-picker": "8.10.1",
 | 
			
		||||
        "react-dom": "19.0.0-rc-02c0e824-20241028",
 | 
			
		||||
        "react-icons": "^5.1.0",
 | 
			
		||||
@@ -1083,9 +1084,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@floating-ui/utils": {
 | 
			
		||||
      "version": "0.2.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
 | 
			
		||||
      "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
 | 
			
		||||
      "version": "0.2.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
 | 
			
		||||
      "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@humanfs/core": {
 | 
			
		||||
@@ -8017,6 +8018,46 @@
 | 
			
		||||
        "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": {
 | 
			
		||||
      "version": "8.10.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
 | 
			
		||||
@@ -9055,6 +9096,12 @@
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "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": {
 | 
			
		||||
      "version": "2.5.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,7 @@
 | 
			
		||||
    "prettier-plugin-organize-imports": "^4.1.0",
 | 
			
		||||
    "react": "19.0.0-rc-02c0e824-20241028",
 | 
			
		||||
    "react-code-blocks": "^0.1.6",
 | 
			
		||||
    "react-datepicker": "^7.6.0",
 | 
			
		||||
    "react-day-picker": "8.10.1",
 | 
			
		||||
    "react-dom": "19.0.0-rc-02c0e824-20241028",
 | 
			
		||||
    "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;
 | 
			
		||||
		Reference in New Issue
	
	Block a user