mirror of
https://github.com/community-scripts/ProxmoxVE
synced 2025-02-11 02:09:17 +00:00
fix
This commit is contained in:
parent
3cc7a8ceea
commit
f8c308679e
@ -33,8 +33,8 @@ const DataFetcher: React.FC = () => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
||||
const [sortColumn, setSortColumn] = useState<string | null>("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 <span className="text-gray-400">▲▼</span>;
|
||||
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(() => {
|
||||
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 <p>Loading...</p>;
|
||||
if (error) return <p>Error: {error}</p>;
|
||||
@ -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 (
|
||||
<div className="p-6 mt-20">
|
||||
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
|
||||
<div className="p-6 mt-20 max-w-full">
|
||||
|
||||
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs - {filteredData.length} Installations</h1>
|
||||
<ApplicationChart data={filteredData} />
|
||||
<br></br>
|
||||
|
||||
{/* 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" />
|
||||
<DatePicker selected={startDate} onChange={setStartDate} placeholderText="Start date" className="p-2 border" />
|
||||
<DatePicker selected={endDate} onChange={setEndDate} placeholderText="End date" className="p-2 border" />
|
||||
</div>
|
||||
|
||||
|
||||
<ApplicationChart data={filteredData} />
|
||||
<br></br>
|
||||
{/* Status Legend */}
|
||||
<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">
|
||||
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>
|
||||
<br></br>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<table className="min-w-full table-auto border-collapse">
|
||||
<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>
|
||||
))
|
||||
) : (
|
||||
<div className="max-w-screen-2xl mx-auto overflow-visible">
|
||||
{/* Table */}
|
||||
<table className="table-fixed border-collapse w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-4 py-2 text-center text-gray-500">
|
||||
No results found
|
||||
</td>
|
||||
{columns.map(({ key, type, label }) => (
|
||||
<th key={key}>
|
||||
<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>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
</thead>
|
||||
{/* Filters Row - Displays below headers */}
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(({ key }) => (
|
||||
<th key={key} className="px-4 py-3 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="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>
|
||||
);
|
||||
};
|
||||
|
@ -34,15 +34,19 @@ const FilterComponent: React.FC<FilterProps> = ({ 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<FilterProps> = ({ 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}
|
||||
</li>
|
||||
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
@ -163,8 +169,8 @@ const FilterComponent: React.FC<FilterProps> = ({ 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"}
|
||||
|
Loading…
Reference in New Issue
Block a user