diff --git a/frontend/src/app/data/new.tsx b/frontend/src/app/data/new.tsx new file mode 100644 index 0000000..738e5fe --- /dev/null +++ b/frontend/src/app/data/new.tsx @@ -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([]); + const [filteredData, setFilteredData] = useState([]); + const [filters, setFilters] = useState>({}); + + 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 ( +
+

Created LXCs

+ + + + + {columns.map(({ key, type }) => ( + + ))} + + + + {/* Filters Row - Displays below headers */} + + + {columns.map(({ key }) => ( + + ))} + + + + + + {filteredData.length > 0 ? ( + filteredData.map((item, index) => ( + + + + + + + + + + + + + )) + ) : ( + + + + )} + +
+
+ {key} + +
+
+ {filters[key] && filters[key].length > 0 ? ( +
+ {filters[key].map((filter: { operator: string; value: any }, index: number) => ( +
+ + {filter.operator} "{filter.value}" + + +
+ ))} +
+ ) : ( + + )} +
{item.status}{item.type}{item.nsapp}{item.os_type}{item.disk_size}{item.core_count}{item.ram_size}{item.method}{item.pve_version}{item.created_at}
+ No results found +
+
+ ); +}; + +export default DataFetcher; diff --git a/frontend/src/components/ApplicationChart.tsx b/frontend/src/components/ApplicationChart.tsx index f70fa70..e62c109 100644 --- a/frontend/src/components/ApplicationChart.tsx +++ b/frontend/src/components/ApplicationChart.tsx @@ -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; -} - 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); - const sortedApps = Object.entries(data.nsapp_count) + const sortedApps = Object.entries(appCounts) .sort(([, a], [, b]) => b - a); const chartApps = sortedApps.slice( diff --git a/frontend/src/components/FilterComponent.tsx b/frontend/src/components/FilterComponent.tsx new file mode 100644 index 0000000..fe6a83b --- /dev/null +++ b/frontend/src/components/FilterComponent.tsx @@ -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; + onRemoveFilter: (column: string, index: number) => void; + allData: any[]; +} + +const FilterComponent: React.FC = ({ column, type, activeFilters, onApplyFilter, onRemoveFilter, allData }) => { + const [filters, setFilters] = useState<{ operator: string; value: string | number }[]>([ + { operator: "equals", value: "" } + ]); + const [showFilter, setShowFilter] = useState(false); + const [loading, setLoading] = useState(false); + const [suggestions, setSuggestions] = useState([]); + const [showSuggestions, setShowSuggestions] = useState(false); + const dropdownRef = useRef(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 ( +
+ + + {showFilter && ( +
+
+ + +
+ + {filters.map((filter, index) => ( +
+ + +
+ 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" && ( + + )} +
+ + {showSuggestions && suggestions.length > 0 && ( +
    + {suggestions.map((suggestion, i) => ( +
  • { + 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} +
  • + + ))} +
+ )} + +
+ ))} + + + + +
+ )} +
+ ); +}; + +export default FilterComponent; diff --git a/frontend/src/components/ui/code-copy-button.tsx b/frontend/src/components/ui/code-copy-button.tsx index edcf0d0..6c79ce7 100644 --- a/frontend/src/components/ui/code-copy-button.tsx +++ b/frontend/src/components/ui/code-copy-button.tsx @@ -37,6 +37,10 @@ export default function CodeCopyButton({ ); }, 500); } + + // toast.success(`copied ${type} to clipboard`, { + // icon: , + // }); }; return ( @@ -45,17 +49,17 @@ export default function CodeCopyButton({
{!isMobile && children ? children : "Copy install command"}
- + Copy + );