mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-09-11 18:04:38 +00:00
feat: enhance DataFetcher with better UI components and add reactive data fetching intervals (#1901) (#1902)
This commit is contained in:
parent
3adc22d837
commit
c8829beddd
3 changed files with 509 additions and 295 deletions
|
@ -1,132 +1,193 @@
|
|||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Chart as ChartJS, ArcElement, Tooltip as ChartTooltip, Legend } from "chart.js";
|
||||
import ChartDataLabels from "chartjs-plugin-datalabels";
|
||||
import { BarChart3, PieChart } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { Pie } from "react-chartjs-2";
|
||||
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
|
||||
import ChartDataLabels from "chartjs-plugin-datalabels";
|
||||
import Modal from "@/components/Modal";
|
||||
|
||||
ChartJS.register(ArcElement, Tooltip, Legend, ChartDataLabels);
|
||||
ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
|
||||
|
||||
interface ApplicationChartProps {
|
||||
data: { nsapp: string }[];
|
||||
}
|
||||
|
||||
const ApplicationChart: React.FC<ApplicationChartProps> = ({ data }) => {
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
const CHART_COLORS = [
|
||||
"#ff6384",
|
||||
"#36a2eb",
|
||||
"#ffce56",
|
||||
"#4bc0c0",
|
||||
"#9966ff",
|
||||
"#ff9f40",
|
||||
"#4dc9f6",
|
||||
"#f67019",
|
||||
"#537bc4",
|
||||
"#acc236",
|
||||
"#166a8f",
|
||||
"#00a950",
|
||||
"#58595b",
|
||||
"#8549ba",
|
||||
];
|
||||
|
||||
export default function ApplicationChart({ data }: ApplicationChartProps) {
|
||||
const [isChartOpen, setIsChartOpen] = useState(false);
|
||||
const [isTableOpen, setIsTableOpen] = useState(false);
|
||||
const [chartStartIndex, setChartStartIndex] = useState(0);
|
||||
const [tableLimit, setTableLimit] = useState(20);
|
||||
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
|
||||
|
||||
const appCounts: Record<string, number> = {};
|
||||
data.forEach((item) => {
|
||||
appCounts[item.nsapp] = (appCounts[item.nsapp] || 0) + 1;
|
||||
});
|
||||
// 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(appCounts).sort(([, a], [, b]) => b - a);
|
||||
const chartApps = sortedApps.slice(chartStartIndex, chartStartIndex + 20);
|
||||
const sortedApps = Object.entries(appCounts)
|
||||
.sort(([, a], [, b]) => b - a);
|
||||
|
||||
const chartApps = sortedApps.slice(
|
||||
chartStartIndex,
|
||||
chartStartIndex + ITEMS_PER_PAGE
|
||||
);
|
||||
|
||||
const chartData = {
|
||||
labels: chartApps.map(([name]) => name),
|
||||
datasets: [
|
||||
{
|
||||
label: "Applications",
|
||||
data: chartApps.map(([, count]) => count),
|
||||
backgroundColor: [
|
||||
"#ff6384",
|
||||
"#36a2eb",
|
||||
"#ffce56",
|
||||
"#4bc0c0",
|
||||
"#9966ff",
|
||||
"#ff9f40",
|
||||
],
|
||||
backgroundColor: CHART_COLORS,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const chartOptions = {
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
datalabels: {
|
||||
color: "white",
|
||||
font: { weight: "bold" as const },
|
||||
formatter: (value: number, context: any) => {
|
||||
const label = context.chart.data.labels?.[context.dataIndex];
|
||||
return `${label}\n(${value})`;
|
||||
},
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6 text-center">
|
||||
<button
|
||||
onClick={() => setIsChartOpen(true)}
|
||||
className="m-2 p-2 bg-blue-500 text-white rounded"
|
||||
>
|
||||
📊 Open Chart
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsTableOpen(true)}
|
||||
className="m-2 p-2 bg-green-500 text-white rounded"
|
||||
>
|
||||
📋 Open Table
|
||||
</button>
|
||||
<div className="mt-6 flex justify-center gap-4">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setIsChartOpen(true)}
|
||||
>
|
||||
<PieChart className="h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Open Chart View</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Modal isOpen={isChartOpen} onClose={() => setIsChartOpen(false)}>
|
||||
<h2 className="text-xl font-bold text-black dark:text-white mb-4">Top Applications (Chart)</h2>
|
||||
<div className="w-3/4 mx-auto">
|
||||
<Pie
|
||||
data={chartData}
|
||||
options={{
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
datalabels: {
|
||||
color: "white",
|
||||
font: { weight: "bold" },
|
||||
formatter: (value, context) =>
|
||||
context.chart.data.labels?.[context.dataIndex] || "",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center space-x-4 mt-4">
|
||||
<button
|
||||
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - 20))}
|
||||
disabled={chartStartIndex === 0}
|
||||
className="p-2 border rounded bg-blue-500 text-white"
|
||||
>
|
||||
◀ Last 20
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setChartStartIndex(chartStartIndex + 20)}
|
||||
disabled={chartStartIndex + 20 >= sortedApps.length}
|
||||
className="p-2 border rounded bg-blue-500 text-white"
|
||||
>
|
||||
Next 20 ▶
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setIsTableOpen(true)}
|
||||
>
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Open Table View</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Modal isOpen={isTableOpen} onClose={() => setIsTableOpen(false)}>
|
||||
<h2 className="text-xl font-bold text-black dark:text-white mb-4">Application Count Table</h2>
|
||||
<table className="w-full border-collapse border border-gray-600 dark:border-gray-500">
|
||||
<thead>
|
||||
<tr className="bg-gray-800 text-white">
|
||||
<th className="p-2 border">Application</th>
|
||||
<th className="p-2 border">Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedApps.slice(0, tableLimit).map(([name, count]) => (
|
||||
<tr key={name} className="hover:bg-gray-200 dark:hover:bg-gray-700 text-black dark:text-white">
|
||||
<td className="p-2 border">{name}</td>
|
||||
<td className="p-2 border">{count}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Dialog open={isChartOpen} onOpenChange={setIsChartOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Applications Distribution</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="h-[60vh] w-full">
|
||||
<Pie data={chartData} options={chartOptions} />
|
||||
</div>
|
||||
<div className="flex justify-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))}
|
||||
disabled={chartStartIndex === 0}
|
||||
>
|
||||
Previous {ITEMS_PER_PAGE}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)}
|
||||
disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length}
|
||||
>
|
||||
Next {ITEMS_PER_PAGE}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{tableLimit < sortedApps.length && (
|
||||
<div className="text-center mt-4">
|
||||
<button
|
||||
onClick={() => setTableLimit(tableLimit + 20)}
|
||||
className="p-2 bg-green-500 text-white rounded"
|
||||
<Dialog open={isTableOpen} onOpenChange={setIsTableOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Applications Count</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[60vh] overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Application</TableHead>
|
||||
<TableHead className="text-right">Count</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedApps.slice(0, tableLimit).map(([name, count]) => (
|
||||
<TableRow key={name}>
|
||||
<TableCell>{name}</TableCell>
|
||||
<TableCell className="text-right">{count}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{tableLimit < sortedApps.length && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => setTableLimit(prev => prev + ITEMS_PER_PAGE)}
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</Button>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationChart;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue