This commit is contained in:
CanbiZ 2025-02-07 16:43:44 +01:00
parent 3cc7a8ceea
commit f8c308679e
2 changed files with 164 additions and 121 deletions

View File

@ -33,8 +33,8 @@ const DataFetcher: React.FC = () => {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [startDate, setStartDate] = useState<Date | null>(null); const [startDate, setStartDate] = useState<Date | null>(null);
const [endDate, setEndDate] = useState<Date | null>(null); const [endDate, setEndDate] = useState<Date | null>(null);
const [sortColumn, setSortColumn] = useState<string | null>(null); const [sortColumn, setSortColumn] = useState<string | null>("created_at");
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
const [installingCounts, setInstallingCounts] = useState(0); const [installingCounts, setInstallingCounts] = useState(0);
const [failedCounts, setFailedCounts] = useState(0); const [failedCounts, setFailedCounts] = useState(0);
const [doneCounts, setDoneCounts] = useState(0); const [doneCounts, setDoneCounts] = useState(0);
@ -54,6 +54,13 @@ const DataFetcher: React.FC = () => {
const minutes = String(date.getMinutes()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}.${month}.${year} ${hours}:${minutes}`; 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) => { const toggleSort = (column: string) => {
if (sortColumn === column) { if (sortColumn === column) {
@ -67,27 +74,11 @@ const DataFetcher: React.FC = () => {
const renderSortIcon = (column: string) => { const renderSortIcon = (column: string) => {
if (sortColumn !== column) return <span className="text-gray-400"></span>; if (sortColumn !== column) return <span className="text-gray-400"></span>;
return sortDirection === "asc" ? ( return sortDirection === "asc" ? (
<span className="text-red-500"></span> <span className="text-gray-400"></span>
) : ( ) : (
<span className="text-red-500"></span> <span className="text-gray-400"></span>
); );
}; };
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(() => { useEffect(() => {
const fetchData = async () => { 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 // Sortieren
if (sortColumn) { if (sortColumn) {
@ -218,9 +215,10 @@ const DataFetcher: React.FC = () => {
}); });
} }
// Nur die Anzahl der angezeigten Einträge limitieren // Daten sofort aktualisieren
setFilteredData(sortedAndFilteredData.slice(0, itemsPerPage)); setFilteredData(sortedAndFilteredData);
}, [filters, data, searchQuery, startDate, endDate, sortColumn, sortDirection, itemsPerPage]); }, [filters, data, searchQuery, startDate, endDate, sortColumn, sortDirection]);
if (loading) return <p>Loading...</p>; if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>; if (error) return <p>Error: {error}</p>;
@ -228,121 +226,160 @@ const DataFetcher: React.FC = () => {
const columns = [ const columns = [
{ key: "status", type: "text", label: "Status" }, { key: "status", type: "text", label: "Status" },
{ key: "type", type: "text", label: "Type" }, { 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: "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: "core_count", type: "number", label: "Cores" },
{ key: "ram_size", type: "number", label: "RAM" }, { key: "ram_size", type: "number", label: "RAM" },
{ key: "method", type: "text", label: "Method" }, { 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: "error", type: "text", label: "Error" },
{ key: "created_at", type: "text", label: "Created At" } { key: "created_at", type: "text", label: "Created" }
]; ];
return ( return (
<div className="p-6 mt-20"> <div className="p-6 mt-20 max-w-full">
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs - {filteredData.length} Installations</h1>
<ApplicationChart data={filteredData} />
<br></br>
{/* Search & Date Filters */} {/* Search & Date Filters */}
<div className="mb-4 flex space-x-4"> <div className="mb-4 flex space-x-4 w-full">
<input type="text" placeholder="Search..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="p-2 border" /> <input type="text" placeholder="Search..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="p-2 border" />
<DatePicker selected={startDate} onChange={setStartDate} placeholderText="Start date" className="p-2 border" /> <DatePicker selected={startDate} onChange={setStartDate} placeholderText="Start date" className="p-2 border" />
<DatePicker selected={endDate} onChange={setEndDate} placeholderText="End date" className="p-2 border" /> <DatePicker selected={endDate} onChange={setEndDate} placeholderText="End date" className="p-2 border" />
</div> </div>
<ApplicationChart data={filteredData} />
<br></br> <br></br>
{/* Status Legend */} {/* Status Legend */}
<div className="mb-4 flex justify-between items-center"> <div className="mb-4 flex justify-between items-center">
<p className="text-lg font-bold">{filteredData.length} results found</p>
<p className="text-lg font"> <p className="text-lg font">
Status Legend: 🔄 installing {installingCounts} | completed {doneCounts} | failed {failedCounts} | unknown {unknownCounts} Status Legend: 🔄 installing {filteredCounts.installing} | completed {filteredCounts.done} | failed {filteredCounts.failed} | unknown {filteredCounts.unknown}
</p> </p>
<br></br>
</div> </div>
<div className="max-w-screen-2xl mx-auto overflow-visible">
{/* Table */} {/* Table */}
<table className="min-w-full table-auto border-collapse"> <table className="table-fixed border-collapse w-full">
<thead> <thead>
<tr>
{columns.map(({ key, type, label }) => (
<th key={key} onClick={() => toggleSort(key)} className="cursor-pointer px-4 py-2 border-b text-left">
<div className="flex items-center space-x-1 flex-row-reverse">
{sortColumn === key && renderSortIcon(key)}
<span className="font-semibold">{label}</span>
<FilterComponent
column={key}
type={type}
activeFilters={filters[key] || []}
onApplyFilter={applyFilters}
onRemoveFilter={removeFilter}
allData={data}
/>
</div>
</th>
))}
</tr>
</thead>
{/* Filters Row - Displays below headers */}
<thead>
<tr>
{columns.map(({ key }) => (
<th key={key} className="px-4 py-2 border-b text-left">
{filters[key] && filters[key].length > 0 ? (
<div className="flex flex-wrap gap-1">
{filters[key].map((filter: { operator: string; value: any }, index: number) => (
<div key={`${key}-${filter.value}-${index}`} className="bg-gray-800 text-white px-2 py-1 rounded flex items-center">
<span className="text-sm italic">
{filter.operator} <b>"{filter.value}"</b>
</span>
<button className="text-red-500 ml-2" onClick={() => removeFilter(key, index)}>
</button>
</div>
))}
</div>
) : (
<span className="text-gray-500"></span>
)}
</th>
))}
</tr>
</thead>
<tbody>
{filteredData.length > 0 ? (
filteredData.slice(0, itemsPerPage).map((item, index) => (
<tr key={index}>
<td className="px-4 py-2 border-b">{item.status}</td>
<td className="px-4 py-2 border-b">{item.type}</td>
<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.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.method}</td>
<td className="px-4 py-2 border-b">{item.pve_version}</td>
<td className="px-4 py-2 border-b">{item.error}</td>
<td className="px-4 py-2 border-b">{formatDate(item.created_at)}</td>
</tr>
))
) : (
<tr> <tr>
<td colSpan={columns.length} className="px-4 py-2 text-center text-gray-500"> {columns.map(({ key, type, label }) => (
No results found <th key={key}>
</td> <div className="flex items-center space-x-1 flex-row-reverse">
{sortColumn === key && renderSortIcon(key)}
<span
className="font-semibold cursor-pointer"
onClick={() => toggleSort(key)}
>
{label}
</span>
{key !== "created_at" && (
<FilterComponent
column={key}
type={type}
activeFilters={filters[key] || []}
onApplyFilter={applyFilters}
onRemoveFilter={removeFilter}
allData={data}
/>
)}
</div>
</th>
))}
</tr> </tr>
)} </thead>
</tbody> {/* Filters Row - Displays below headers */}
</table> <thead>
{itemsPerPage < filteredData.length && ( <tr>
<div className="text-center mt-4"> {columns.map(({ key }) => (
<button onClick={handleLoadMore} className="px-4 py-2 bg-blue-500 text-white rounded"> <th key={key} className="px-4 py-3 border-b text-left">
Load more... {filters[key] && filters[key].length > 0 ? (
</button> <div className="flex flex-wrap gap-1">
</div> {filters[key].map((filter: { operator: string; value: any }, index: number) => (
)} <div key={`${key}-${filter.value}-${index}`} className="bg-gray-800 text-white px-2 py-1 rounded flex items-center">
<span className="text-sm italic">
{filter.operator} <b>"{filter.value}"</b>
</span>
<button className="text-red-500 ml-2" onClick={() => removeFilter(key, index)}>
</button>
</div>
))}
</div>
) : (
<span className="px-10 py-2 text-right text-gray-500"></span>
)}
</th>
))}
</tr>
</thead>
<tbody>
{filteredData.length > 0 ? (
filteredData.slice(0, itemsPerPage).map((item, index) => (
<tr key={index}>
<td className="px-4 py-2 border-b text-center">
<div className="relative group">
{item.status === "done" ? (
<span className="group-hover:tooltip"></span>
) : item.status === "failed" ? (
<span className="group-hover:tooltip"></span>
) : item.status === "installing" ? (
<span className="group-hover:tooltip">🔄</span>
) : (
<span>{item.status}</span>
)}
<span className="absolute hidden group-hover:block bg-gray-900 text-white text-xs px-2 py-1 rounded">
{item.status}
</span>
</div>
</td>
<td className="px-4 py-2 border-b text-center">
<div className="relative group">
{item.type === "lxc" ? (
<span className="group-hover:tooltip">📦</span>
) : item.type === "vm" ? (
<span className="group-hover:tooltip">🖥</span>
) : (
<span>{item.type}</span>
)}
<span className="absolute hidden group-hover:block bg-gray-900 text-white text-xs px-2 py-1 rounded -top-6 left-1/2 transform -translate-x-1/2">
{item.type}
</span>
</div>
</td>
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.nsapp}</td>
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.os_type}</td>
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.disk_size}</td>
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.core_count}</td>
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.ram_size}</td>
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.method}</td>
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.pve_version}</td>
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.error}</td>
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{formatDate(item.created_at)}</td>
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="px-4 py-2 text-center text-gray-500">
No results found
</td>
</tr>
)}
</tbody>
</table>
{itemsPerPage < filteredData.length && (
<div className="text-center mt-4">
<button onClick={handleLoadMore} className="px-4 py-2 bg-blue-500 text-white rounded">
Load more...
</button>
</div>
)}
</div>
</div> </div>
); );
}; };

View File

@ -41,6 +41,10 @@ const FilterComponent: React.FC<FilterProps> = ({ column, type, activeFilters, o
return updatedFilters; return updatedFilters;
}); });
if (key === "value") {
setTimeout(() => setShowSuggestions(false), 100); // Vorschläge ausblenden, sobald Wert gesetzt wird
}
}; };
const handleAutocomplete = (input: string) => { const handleAutocomplete = (input: string) => {
@ -138,14 +142,16 @@ const FilterComponent: React.FC<FilterProps> = ({ column, type, activeFilters, o
key={i} key={i}
className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer" className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer"
onMouseDown={(e) => { 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); updateFilter(index, "value", suggestion);
setSuggestions([]); setSuggestions([]); // Vorschläge ausblenden
setShowSuggestions(false); setShowSuggestions(false);
}} }}
onClick={() => setFilters([{ operator: filters[index].operator, value: suggestion }])} // Setzt den Wert im Input zurück
> >
{suggestion} {suggestion}
</li> </li>
))} ))}
</ul> </ul>
)} )}
@ -163,8 +169,8 @@ const FilterComponent: React.FC<FilterProps> = ({ column, type, activeFilters, o
onClick={applyFilters} onClick={applyFilters}
disabled={loading} disabled={loading}
className={`w-full p-2 rounded-md font-semibold mt-3 transition ${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-300 text-gray-700 cursor-not-allowed"
: "bg-blue-500 hover:bg-blue-600 text-white" : "bg-blue-500 hover:bg-blue-600 text-white"
}`} }`}
> >
{loading ? "Applying..." : "Apply"} {loading ? "Applying..." : "Apply"}