add api frontend
This commit is contained in:
parent
4b646a40ee
commit
3e9b3e6fe9
193
frontend/src/app/data/new.tsx
Normal file
193
frontend/src/app/data/new.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import FilterComponent from "../../components/FilterComponent";
|
||||
|
||||
interface DataModel {
|
||||
status: string;
|
||||
type: string;
|
||||
nsapp: string;
|
||||
os_type: string;
|
||||
disk_size: number;
|
||||
core_count: number;
|
||||
ram_size: number;
|
||||
method: string;
|
||||
pve_version: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const DataFetcher: React.FC = () => {
|
||||
const [data, setData] = useState<DataModel[]>([]);
|
||||
const [filteredData, setFilteredData] = useState<DataModel[]>([]);
|
||||
const [filters, setFilters] = useState<Record<string, any>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const response = await fetch("https://api.htl-braunau.at/data/json");
|
||||
const result: DataModel[] = await response.json();
|
||||
setData(result);
|
||||
setFilteredData(result);
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const applyFilters = async (column: string, operator: string, value: any) => {
|
||||
setFilters((prev) => {
|
||||
const updatedFilters = { ...prev };
|
||||
if (!updatedFilters[column]) updatedFilters[column] = [];
|
||||
|
||||
// Prevent duplicate filters
|
||||
const alreadyExists = updatedFilters[column].some((filter: { operator: string; value: any }) =>
|
||||
filter.operator === operator && filter.value === value
|
||||
);
|
||||
|
||||
if (!alreadyExists) {
|
||||
updatedFilters[column].push({ operator, value });
|
||||
}
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
};
|
||||
|
||||
const removeFilter = (column: string, index: number) => {
|
||||
setFilters((prev) => {
|
||||
const updatedFilters = { ...prev };
|
||||
updatedFilters[column] = updatedFilters[column].filter((_, i) => i !== index);
|
||||
|
||||
// If no filters remain, remove the column entry
|
||||
if (updatedFilters[column].length === 0) delete updatedFilters[column];
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let filtered = [...data];
|
||||
|
||||
Object.keys(filters).forEach((key) => {
|
||||
if (!filters[key] || filters[key].length === 0) return;
|
||||
|
||||
filtered = filtered.filter((item) => {
|
||||
const itemValue = item[key as keyof DataModel];
|
||||
|
||||
return filters[key].some(({ operator, value }: { operator: string; value: any }) => {
|
||||
if (typeof itemValue === "number") {
|
||||
value = parseFloat(value);
|
||||
if (operator === "greater") return itemValue > value;
|
||||
if (operator === "greater or equal") return itemValue >= value;
|
||||
if (operator === "less") return itemValue < value;
|
||||
if (operator === "less or equal") return itemValue <= value;
|
||||
}
|
||||
|
||||
if (typeof itemValue === "string") {
|
||||
if (operator === "equals") return itemValue.toLowerCase() === value.toLowerCase();
|
||||
if (operator === "not equals") return itemValue.toLowerCase() !== value.toLowerCase();
|
||||
if (operator === "contains") return itemValue.toLowerCase().includes(value.toLowerCase());
|
||||
if (operator === "does not contain") return !itemValue.toLowerCase().includes(value.toLowerCase());
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
setFilteredData(filtered);
|
||||
}, [filters, data]);
|
||||
|
||||
const columns = [
|
||||
{ key: "status", type: "text" },
|
||||
{ key: "type", type: "text" },
|
||||
{ key: "nsapp", type: "text" },
|
||||
{ key: "os_type", type: "text" },
|
||||
{ key: "disk_size", type: "number" },
|
||||
{ key: "core_count", type: "number" },
|
||||
{ key: "ram_size", type: "number" },
|
||||
{ key: "method", type: "text" },
|
||||
{ key: "pve_version", type: "text" },
|
||||
{ key: "created_at", type: "text" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6 mt-20">
|
||||
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
|
||||
|
||||
<table className="min-w-full table-auto border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(({ key, type }) => (
|
||||
<th key={key} className="px-4 py-2 border-b text-left">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-semibold">{key}</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.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.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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataFetcher;
|
@ -25,16 +25,12 @@ import { Chart as ChartJS, ArcElement, Tooltip as ChartTooltip, Legend } from "c
|
||||
import ChartDataLabels from "chartjs-plugin-datalabels";
|
||||
import { BarChart3, PieChart } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { Pie, Bar } from "react-chartjs-2";
|
||||
import { Pie } from "react-chartjs-2";
|
||||
|
||||
ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
|
||||
|
||||
interface SummaryData {
|
||||
nsapp_count: Record<string, number>;
|
||||
}
|
||||
|
||||
interface ApplicationChartProps {
|
||||
data: SummaryData | null;
|
||||
data: { nsapp: string }[];
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
@ -61,9 +57,13 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
|
||||
const [chartStartIndex, setChartStartIndex] = useState(0);
|
||||
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
|
||||
|
||||
if (!data) return null;
|
||||
// Calculate application counts
|
||||
const appCounts = data.reduce((acc, item) => {
|
||||
acc[item.nsapp] = (acc[item.nsapp] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
const sortedApps = Object.entries(data.nsapp_count)
|
||||
const sortedApps = Object.entries(appCounts)
|
||||
.sort(([, a], [, b]) => b - a);
|
||||
|
||||
const chartApps = sortedApps.slice(
|
||||
|
185
frontend/src/components/FilterComponent.tsx
Normal file
185
frontend/src/components/FilterComponent.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
|
||||
interface FilterProps {
|
||||
column: string;
|
||||
type: "text" | "number";
|
||||
activeFilters: { operator: string; value: any }[];
|
||||
onApplyFilter: (column: string, operator: string, value: any) => Promise<void>;
|
||||
onRemoveFilter: (column: string, index: number) => void;
|
||||
allData: any[];
|
||||
}
|
||||
|
||||
const FilterComponent: React.FC<FilterProps> = ({ column, type, activeFilters, onApplyFilter, onRemoveFilter, allData }) => {
|
||||
const [filters, setFilters] = useState<{ operator: string; value: string | number }[]>([
|
||||
{ operator: "equals", value: "" }
|
||||
]);
|
||||
const [showFilter, setShowFilter] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
const [showSuggestions, setShowSuggestions] = useState<boolean>(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const operators = {
|
||||
text: ["equals", "not equals", "contains", "does not contain", "is empty"],
|
||||
number: ["equals", "not equals", "greater", "greater or equal", "less", "less or equal"]
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFilters(activeFilters.length > 0 ? activeFilters : [{ operator: "equals", value: "" }]);
|
||||
}, [activeFilters]);
|
||||
|
||||
const updateFilter = (index: number, key: "operator" | "value", newValue: string | number) => {
|
||||
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[] = [];
|
||||
|
||||
const uniqueValues = [...new Set(allData.map((item) => item[column]?.toString()))];
|
||||
|
||||
if (!input) {
|
||||
filteredSuggestions = uniqueValues;
|
||||
} else {
|
||||
filteredSuggestions = uniqueValues.filter((value) =>
|
||||
value && value.toLowerCase().includes(input.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
setSuggestions(filteredSuggestions.slice(0, 5));
|
||||
setShowSuggestions(true);
|
||||
};
|
||||
|
||||
|
||||
const applyFilters = async () => {
|
||||
setLoading(true);
|
||||
for (const filter of filters) {
|
||||
await onApplyFilter(column, filter.operator, filter.value);
|
||||
}
|
||||
setLoading(false);
|
||||
setShowFilter(false);
|
||||
setSuggestions([]); // Close suggestions after applying filter
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setFilters([{ operator: "equals", value: "" }]);
|
||||
setShowFilter(false);
|
||||
setSuggestions([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative inline-block text-left">
|
||||
<button
|
||||
onClick={() => setShowFilter(!showFilter)}
|
||||
className="ml-2 p-1 rounded bg-gray-800 hover:bg-gray-600 transition text-white"
|
||||
>
|
||||
🔽
|
||||
</button>
|
||||
|
||||
{showFilter && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="absolute left-0 mt-2 bg-white dark:bg-gray-900 text-black dark:text-white border border-gray-300 dark:border-gray-700 shadow-lg rounded-lg w-56 p-4 z-50"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="text-sm font-medium">Filter by {column}</label>
|
||||
<button onClick={resetFilters} className="text-red-500 hover:text-red-700 transition">
|
||||
✖
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{filters.map((filter, index) => (
|
||||
<div key={index} className="mb-2 p-2 border rounded relative">
|
||||
<select
|
||||
value={filter.operator}
|
||||
onChange={(e) => updateFilter(index, "operator", e.target.value)}
|
||||
className="w-full p-1 border rounded bg-gray-100 dark:bg-gray-800 text-black dark:text-white"
|
||||
>
|
||||
{operators[type].map((op) => (
|
||||
<option key={op} value={op}>
|
||||
{op}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
type={type === "number" ? "number" : "text"}
|
||||
value={filters[index].value}
|
||||
onChange={(e) => updateFilter(index, "value", e.target.value)}
|
||||
className="w-full mt-2 p-1 border rounded"
|
||||
onFocus={() => handleAutocomplete("")} // Zeige Vorschläge an
|
||||
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)} // Verhindert sofortiges Schließen
|
||||
/>
|
||||
|
||||
{type === "text" && (
|
||||
<button
|
||||
onClick={() => handleAutocomplete("")}
|
||||
className="ml-2 bg-gray-300 dark:bg-gray-600 px-2 py-1 rounded text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
🔽
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showSuggestions && suggestions.length > 0 && (
|
||||
<ul className="absolute top-full left-0 w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 mt-1 rounded shadow-lg z-50">
|
||||
{suggestions.map((suggestion, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault(); // Verhindert, dass das Input-Feld sofort das Blur-Event auslöst
|
||||
updateFilter(index, "value", suggestion);
|
||||
setSuggestions([]); // Vorschläge ausblenden
|
||||
setShowSuggestions(false);
|
||||
}}
|
||||
onClick={() => setFilters([{ operator: filters[index].operator, value: suggestion }])} // Setzt den Wert im Input zurück
|
||||
>
|
||||
{suggestion}
|
||||
</li>
|
||||
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button onClick={() => setFilters([...filters, { operator: "equals", value: "" }])}
|
||||
className="w-full bg-gray-500 hover:bg-gray-600 text-white p-1 rounded"
|
||||
>
|
||||
+ Add Another Filter
|
||||
</button>
|
||||
|
||||
<button
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
{loading ? "Applying..." : "Apply"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterComponent;
|
@ -37,6 +37,10 @@ export default function CodeCopyButton({
|
||||
);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// toast.success(`copied ${type} to clipboard`, {
|
||||
// icon: <ClipboardCheck className="h-4 w-4" />,
|
||||
// });
|
||||
};
|
||||
|
||||
return (
|
||||
@ -45,17 +49,17 @@ export default function CodeCopyButton({
|
||||
<div className="overflow-x-auto whitespace-pre-wrap text-nowrap break-all pr-4 text-sm">
|
||||
{!isMobile && children ? children : "Copy install command"}
|
||||
</div>
|
||||
<button
|
||||
<div
|
||||
className={cn(" right-0 cursor-pointer bg-muted px-3 py-4")}
|
||||
onClick={() => handleCopy("install command", children)}
|
||||
className={cn("bg-muted px-3 py-4")}
|
||||
title="Copy"
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ClipboardIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<span className="sr-only">Copy</span>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user