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

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