mirror of
https://github.com/community-scripts/ProxmoxVE
synced 2025-02-11 10:19:16 +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 [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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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"}
|
||||||
|
Loading…
Reference in New Issue
Block a user