add api frontend

This commit is contained in:
CanbiZ 2025-03-12 13:49:51 +01:00
parent 4b646a40ee
commit 3e9b3e6fe9
4 changed files with 394 additions and 12 deletions

View 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;

View File

@ -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(

View 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;

View File

@ -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>
);