diff --git a/frontend/src/app/data/page.tsx b/frontend/src/app/data/page.tsx index 9a142787..4ff80984 100644 --- a/frontend/src/app/data/page.tsx +++ b/frontend/src/app/data/page.tsx @@ -33,8 +33,8 @@ const DataFetcher: React.FC = () => { const [searchQuery, setSearchQuery] = useState(""); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); - const [sortColumn, setSortColumn] = useState(null); - const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + const [sortColumn, setSortColumn] = useState("created_at"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); const [installingCounts, setInstallingCounts] = useState(0); const [failedCounts, setFailedCounts] = useState(0); const [doneCounts, setDoneCounts] = useState(0); @@ -54,6 +54,13 @@ const DataFetcher: React.FC = () => { const minutes = String(date.getMinutes()).padStart(2, '0'); return `${day}.${month}.${year} ${hours}:${minutes}`; }; + const filteredCounts = { + installing: filteredData.filter(item => item.status === "installing").length, + done: filteredData.filter(item => item.status === "done").length, + failed: filteredData.filter(item => item.status === "failed").length, + unknown: filteredData.filter(item => !["installing", "done", "failed"].includes(item.status)).length, + }; + const toggleSort = (column: string) => { if (sortColumn === column) { @@ -67,27 +74,11 @@ const DataFetcher: React.FC = () => { const renderSortIcon = (column: string) => { if (sortColumn !== column) return ▲▼; return sortDirection === "asc" ? ( - + ) : ( - + ); }; - const sortData = (data: DataModel[]) => { - if (!sortColumn) return data; - - return [...data].sort((a, b) => { - const valA = a[sortColumn as keyof DataModel]; - const valB = b[sortColumn as keyof DataModel]; - - if (typeof valA === "number" && typeof valB === "number") { - return sortDirection === "asc" ? valA - valB : valB - valA; - } - if (typeof valA === "string" && typeof valB === "string") { - return sortDirection === "asc" ? valA.localeCompare(valB) : valB.localeCompare(valA); - } - return 0; - }); - }; useEffect(() => { const fetchData = async () => { @@ -201,6 +192,12 @@ const DataFetcher: React.FC = () => { }); }); }); + const filteredCounts = { + installing: filteredData.filter(item => item.status === "installing").length, + done: filteredData.filter(item => item.status === "done").length, + failed: filteredData.filter(item => item.status === "failed").length, + unknown: filteredData.filter(item => !["installing", "done", "failed"].includes(item.status)).length, + }; // Sortieren if (sortColumn) { @@ -218,9 +215,10 @@ const DataFetcher: React.FC = () => { }); } - // Nur die Anzahl der angezeigten Einträge limitieren - setFilteredData(sortedAndFilteredData.slice(0, itemsPerPage)); - }, [filters, data, searchQuery, startDate, endDate, sortColumn, sortDirection, itemsPerPage]); + // Daten sofort aktualisieren + setFilteredData(sortedAndFilteredData); + }, [filters, data, searchQuery, startDate, endDate, sortColumn, sortDirection]); + if (loading) return

Loading...

; if (error) return

Error: {error}

; @@ -228,121 +226,160 @@ const DataFetcher: React.FC = () => { const columns = [ { key: "status", type: "text", label: "Status" }, { key: "type", type: "text", label: "Type" }, - { key: "nsapp", type: "text", label: "Application" }, + { key: "nsapp", type: "text", label: "App" }, { key: "os_type", type: "text", label: "OS" }, - { key: "disk_size", type: "number", label: "HDD Size" }, + { key: "disk_size", type: "number", label: "HDD" }, { key: "core_count", type: "number", label: "Cores" }, { key: "ram_size", type: "number", label: "RAM" }, { key: "method", type: "text", label: "Method" }, - { key: "pve_version", type: "text", label: "PVE Version" }, + { key: "pve_version", type: "text", label: "Version" }, { key: "error", type: "text", label: "Error" }, - { key: "created_at", type: "text", label: "Created At" } + { key: "created_at", type: "text", label: "Created" } ]; return ( -
-

Created LXCs

+
+ +

Created LXCs - {filteredData.length} Installations

+ +

{/* Search & Date Filters */} -
+
setSearchQuery(e.target.value)} className="p-2 border" />
- -

{/* Status Legend */}
-

{filteredData.length} results found

- Status Legend: 🔄 installing {installingCounts} | ✔️ completed {doneCounts} | ❌ failed {failedCounts} | ❓ unknown {unknownCounts} + Status Legend: 🔄 installing {filteredCounts.installing} | ✔️ completed {filteredCounts.done} | ❌ failed {filteredCounts.failed} | ❓ unknown {filteredCounts.unknown}

+

- - {/* Table */} - - - - {columns.map(({ key, type, label }) => ( - - - ))} - - - {/* Filters Row - Displays below headers */} - - - {columns.map(({ key }) => ( - - ))} - - - - {filteredData.length > 0 ? ( - filteredData.slice(0, itemsPerPage).map((item, index) => ( - - - - - - - - - - - - - - )) - ) : ( +
+ {/* Table */} +
toggleSort(key)} className="cursor-pointer px-4 py-2 border-b text-left"> -
- {sortColumn === key && renderSortIcon(key)} - {label} - -
-
- {filters[key] && filters[key].length > 0 ? ( -
- {filters[key].map((filter: { operator: string; value: any }, index: number) => ( -
- - {filter.operator} "{filter.value}" - - -
- ))} -
- ) : ( - - )} -
{item.status}{item.type}{item.nsapp}{item.os_type}{item.disk_size}{item.core_count}{item.ram_size}{item.method}{item.pve_version}{item.error}{formatDate(item.created_at)}
+ - + {columns.map(({ key, type, label }) => ( + + ))} - )} - -
- No results found - +
+ {sortColumn === key && renderSortIcon(key)} + toggleSort(key)} + > + {label} + + {key !== "created_at" && ( + + )} +
+
- {itemsPerPage < filteredData.length && ( -
- -
- )} + + {/* Filters Row - Displays below headers */} + + + {columns.map(({ key }) => ( + + {filters[key] && filters[key].length > 0 ? ( +
+ {filters[key].map((filter: { operator: string; value: any }, index: number) => ( +
+ + {filter.operator} "{filter.value}" + + +
+ ))} +
+ ) : ( + + )} + + ))} + + + + {filteredData.length > 0 ? ( + filteredData.slice(0, itemsPerPage).map((item, index) => ( + + +
+ {item.status === "done" ? ( + ✔️ + ) : item.status === "failed" ? ( + + ) : item.status === "installing" ? ( + 🔄 + ) : ( + {item.status} + )} + + {item.status} + +
+ + +
+ {item.type === "lxc" ? ( + 📦 + ) : item.type === "vm" ? ( + 🖥️ + ) : ( + {item.type} + )} + + {item.type} + +
+ + + + + {item.nsapp} + {item.os_type} + {item.disk_size} + {item.core_count} + {item.ram_size} + {item.method} + {item.pve_version} + {item.error} + {formatDate(item.created_at)} + + )) + ) : ( + + + No results found + + + )} + + + {itemsPerPage < filteredData.length && ( +
+ +
+ )} +
); }; diff --git a/frontend/src/components/FilterComponent.tsx b/frontend/src/components/FilterComponent.tsx index 2b807d5e..60325659 100644 --- a/frontend/src/components/FilterComponent.tsx +++ b/frontend/src/components/FilterComponent.tsx @@ -34,15 +34,19 @@ const FilterComponent: React.FC = ({ column, type, activeFilters, o setFilters((prevFilters) => { const updatedFilters = [...prevFilters]; updatedFilters[index][key] = newValue; - + if (key === "value" && type === "text") { handleAutocomplete(newValue as string); } - + return updatedFilters; }); + + if (key === "value") { + setTimeout(() => setShowSuggestions(false), 100); // Vorschläge ausblenden, sobald Wert gesetzt wird + } }; - + const handleAutocomplete = (input: string) => { let filteredSuggestions: string[] = []; @@ -138,14 +142,16 @@ const FilterComponent: React.FC = ({ column, type, activeFilters, o key={i} className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer" onMouseDown={(e) => { - e.preventDefault(); // Damit das Blur-Event das Schließen nicht überschreibt + e.preventDefault(); // Verhindert, dass das Input-Feld sofort das Blur-Event auslöst updateFilter(index, "value", suggestion); - setSuggestions([]); + setSuggestions([]); // Vorschläge ausblenden setShowSuggestions(false); }} + onClick={() => setFilters([{ operator: filters[index].operator, value: suggestion }])} // Setzt den Wert im Input zurück > {suggestion} + ))} )} @@ -163,8 +169,8 @@ const FilterComponent: React.FC = ({ column, type, activeFilters, o onClick={applyFilters} disabled={loading} className={`w-full p-2 rounded-md font-semibold mt-3 transition ${loading - ? "bg-blue-300 text-gray-700 cursor-not-allowed" - : "bg-blue-500 hover:bg-blue-600 text-white" + ? "bg-blue-300 text-gray-700 cursor-not-allowed" + : "bg-blue-500 hover:bg-blue-600 text-white" }`} > {loading ? "Applying..." : "Apply"}