This commit is contained in:
CanbiZ 2025-02-07 17:16:53 +01:00
parent f8c308679e
commit 020f179b82
2 changed files with 116 additions and 80 deletions

View File

@ -5,6 +5,7 @@ import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css"; import "react-datepicker/dist/react-datepicker.css";
import ApplicationChart from "../../components/ApplicationChart"; import ApplicationChart from "../../components/ApplicationChart";
import FilterComponent from "../../components/FilterComponent"; import FilterComponent from "../../components/FilterComponent";
import { AlertCircle } from "lucide-react"; // Modernes Icon für Fehler
interface DataModel { interface DataModel {
id: number; id: number;
@ -45,6 +46,25 @@ const DataFetcher: React.FC = () => {
setItemsPerPage((prev) => prev + 25); setItemsPerPage((prev) => prev + 25);
}; };
const handleShowError = (event: React.MouseEvent, errorMessage: string) => {
setErrorPopup({
open: true,
message: errorMessage,
x: event.clientX,
y: event.clientY,
});
};
const handleCloseError = () => {
setErrorPopup({ open: false, message: "", x: 0, y: 0 });
};
const [errorPopup, setErrorPopup] = useState<{ open: boolean; message: string; x: number; y: number }>({
open: false,
message: "",
x: 0,
y: 0,
});
const formatDate = (dateString: string): string => { const formatDate = (dateString: string): string => {
const date = new Date(dateString); const date = new Date(dateString);
const year = date.getFullYear(); const year = date.getFullYear();
@ -60,7 +80,7 @@ const DataFetcher: React.FC = () => {
failed: filteredData.filter(item => item.status === "failed").length, failed: filteredData.filter(item => item.status === "failed").length,
unknown: filteredData.filter(item => !["installing", "done", "failed"].includes(item.status)).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) {
@ -252,46 +272,42 @@ const DataFetcher: React.FC = () => {
</div> </div>
<br></br> <br></br>
{/* 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"> <p className="text-lg font">
Status Legend: 🔄 installing {filteredCounts.installing} | completed {filteredCounts.done} | failed {filteredCounts.failed} | unknown {filteredCounts.unknown} 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 */} <div className="max-w-screen-2xl mx-auto overflow-x-auto">
<table className="table-fixed border-collapse w-full"> <table className="table-auto w-full border-collapse border border-gray-600">
<thead> <thead>
<tr> <tr className="bg-gray-800 text-white">
{columns.map(({ key, type, label }) => ( {columns.map(({ key, type, label }) => (
<th key={key}> <th key={key} className="px-4 py-3 border border-gray-600 text-left whitespace-nowrap">
<div className="flex items-center space-x-1 flex-row-reverse"> <div className="flex items-center justify-start">
{sortColumn === key && renderSortIcon(key)} {/* Filter-Icon links */}
<span
className="font-semibold cursor-pointer"
onClick={() => toggleSort(key)}
>
{label}
</span>
{key !== "created_at" && ( {key !== "created_at" && (
<FilterComponent <div className="mr-2">
column={key} <FilterComponent
type={type} column={key}
activeFilters={filters[key] || []} type={columns.find(col => col.key === key)?.type || "text"}
onApplyFilter={applyFilters} activeFilters={filters[key] || []}
onRemoveFilter={removeFilter} onApplyFilter={applyFilters}
allData={data} onRemoveFilter={removeFilter}
/> allData={data}
/>
</div>
)} )}
<span className="font-semibold cursor-pointer" onClick={() => toggleSort(key)}>
{label} {sortColumn === key && renderSortIcon(key)}
</span>
</div> </div>
</th> </th>
))} ))}
</tr> </tr>
</thead>
{/* Filters Row - Displays below headers */} {/* Aktive Filter-Zeile */}
<thead> <tr className="bg-gray-700 text-white border-t border-gray-600">
<tr>
{columns.map(({ key }) => ( {columns.map(({ key }) => (
<th key={key} className="px-4 py-3 border-b text-left"> <th key={key} className="px-4 py-3 border-b text-left">
{filters[key] && filters[key].length > 0 ? ( {filters[key] && filters[key].length > 0 ? (
@ -308,80 +324,99 @@ const DataFetcher: React.FC = () => {
))} ))}
</div> </div>
) : ( ) : (
<span className="px-10 py-2 text-right text-gray-500"></span> <span className="text-gray-500"></span>
)} )}
</th> </th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody>
<tbody className="bg-gray-900 text-white">
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.slice(0, itemsPerPage).map((item, index) => ( filteredData.slice(0, itemsPerPage).map((item, index) => (
<tr key={index}> <tr key={index} className="border border-gray-700 hover:bg-gray-800 transition">
<td className="px-4 py-2 border-b text-center"> {/* Status mit Icon */}
<td className="px-4 py-2 text-center border border-gray-700">
<div className="relative group"> <div className="relative group">
{item.status === "done" ? ( {item.status === "done" ? "✔️" :
<span className="group-hover:tooltip"></span> item.status === "failed" ? "❌" :
) : item.status === "failed" ? ( item.status === "installing" ? "🔄" : item.status}
<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> </div>
</td> </td>
<td className="px-4 py-2 border-b text-center"> {/* Type mit Icon */}
<td className="px-4 py-2 text-center border border-gray-700">
<div className="relative group"> <div className="relative group">
{item.type === "lxc" ? ( {item.type === "lxc" ? "📦" :
<span className="group-hover:tooltip">📦</span> item.type === "vm" ? "🖥️" : item.type}
) : 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> </div>
</td> </td>
{/* Dynamische Spaltenbreiten */}
<td className="px-4 py-3 border border-gray-700 max-w-[18%] truncate">{item.nsapp}</td>
<td className="px-4 py-3 border border-gray-700 max-w-[14%] truncate">{item.os_type}</td>
{/* Kleinere Spalten für HDD, Cores, RAM */}
<td className="px-4 py-3 border border-gray-700 text-center w-[6%]">{item.disk_size}</td>
<td className="px-4 py-3 border border-gray-700 text-center w-[6%]">{item.core_count}</td>
<td className="px-4 py-3 border border-gray-700 text-center w-[6%]">{item.ram_size}</td>
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.nsapp}</td> {/* Weitere Spalten mit Overflow-Fix */}
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.os_type}</td> <td className="px-4 py-3 border border-gray-700 max-w-[12%] truncate">{item.method}</td>
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.disk_size}</td> <td className="px-4 py-3 border border-gray-700 max-w-[10%] truncate">{item.pve_version}</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> {/* Fehler mit Tooltip & Icon */}
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.method}</td> <td className="px-4 py-3 border border-gray-700 text-center relative">
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.pve_version}</td> {item.error && item.error !== "none" ? (
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{item.error}</td> <button onClick={(e) => handleShowError(e, item.error)} className="text-yellow-500 hover:text-yellow-300">
<td className="px-4 py-3 border-b overflow-hidden text-ellipsis">{formatDate(item.created_at)}</td> <AlertCircle size={20} />
</button>
) : (
<span className="text-gray-500"></span>
)}
</td>
{/* Created At mit Formatierung */}
<td className="px-4 py-3 border border-gray-700 whitespace-nowrap">{formatDate(item.created_at)}</td>
</tr> </tr>
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan={columns.length} className="px-4 py-2 text-center text-gray-500"> <td colSpan={columns.length} className="px-4 py-3 text-center text-gray-500 border border-gray-700">
No results found No results found
</td> </td>
</tr> </tr>
)} )}
</tbody> </tbody>
</table> </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>
{/* Fehler-Popup jetzt direkt neben dem Icon */}
{errorPopup.open && (
<div
className="absolute bg-gray-900 text-white p-4 rounded shadow-lg z-50"
style={{ top: errorPopup.y, left: errorPopup.x + 30 }} // Popup erscheint direkt rechts vom Icon
>
<p className="text-sm">{errorPopup.message}</p>
<button
onClick={handleCloseError}
className="mt-2 px-2 py-1 bg-red-500 text-white rounded hover:bg-red-700"
>
Close
</button>
</div>
)}
{/* Load More Button */}
{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>
); );
}; }
export default DataFetcher; export default DataFetcher;

View File

@ -34,19 +34,19 @@ const FilterComponent: React.FC<FilterProps> = ({ column, type, activeFilters, o
setFilters((prevFilters) => { setFilters((prevFilters) => {
const updatedFilters = [...prevFilters]; const updatedFilters = [...prevFilters];
updatedFilters[index][key] = newValue; updatedFilters[index][key] = newValue;
if (key === "value" && type === "text") { if (key === "value" && type === "text") {
handleAutocomplete(newValue as string); handleAutocomplete(newValue as string);
} }
return updatedFilters; return updatedFilters;
}); });
if (key === "value") { if (key === "value") {
setTimeout(() => setShowSuggestions(false), 100); // Vorschläge ausblenden, sobald Wert gesetzt wird setTimeout(() => setShowSuggestions(false), 100); // Vorschläge ausblenden, sobald Wert gesetzt wird
} }
}; };
const handleAutocomplete = (input: string) => { const handleAutocomplete = (input: string) => {
let filteredSuggestions: string[] = []; let filteredSuggestions: string[] = [];
@ -119,12 +119,13 @@ const FilterComponent: React.FC<FilterProps> = ({ column, type, activeFilters, o
<div className="relative flex items-center"> <div className="relative flex items-center">
<input <input
type={type === "number" ? "number" : "text"} type={type === "number" ? "number" : "text"}
value={filter.value} value={filters[index].value}
onChange={(e) => updateFilter(index, "value", e.target.value)} onChange={(e) => updateFilter(index, "value", e.target.value)}
className="w-full mt-2 p-1 border rounded" className="w-full mt-2 p-1 border rounded"
onFocus={() => handleAutocomplete("")} onFocus={() => handleAutocomplete("")} // Zeige Vorschläge an
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)} // Delay hiding to allow clicking onBlur={() => setTimeout(() => setShowSuggestions(false), 200)} // Verhindert sofortiges Schließen
/> />
{type === "text" && ( {type === "text" && (
<button <button
onClick={() => handleAutocomplete("")} onClick={() => handleAutocomplete("")}