1
0
mirror of https://github.com/community-scripts/ProxmoxVE.git synced 2025-02-01 15:51:51 +00:00

feat: enhance DataFetcher with better UI components and add reactive data fetching intervals (#1901)

This commit is contained in:
Bram Suurd 2025-01-31 13:27:00 +01:00
parent 71b1288220
commit 8390298d52
3 changed files with 509 additions and 295 deletions

View File

@ -1,9 +1,33 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import ApplicationChart from "@/components/ApplicationChart";
import DatePicker from 'react-datepicker'; import { Button } from "@/components/ui/button";
import 'react-datepicker/dist/react-datepicker.css'; import { Calendar } from "@/components/ui/calendar";
import ApplicationChart from "../../components/ApplicationChart"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import React, { useCallback, useEffect, useState } from "react";
interface DataModel { interface DataModel {
id: number; id: number;
@ -40,23 +64,41 @@ const DataFetcher: React.FC = () => {
const [reloadInterval, setReloadInterval] = useState<NodeJS.Timeout | null>(null); const [reloadInterval, setReloadInterval] = useState<NodeJS.Timeout | null>(null);
useEffect(() => { const fetchData = useCallback(async () => {
const fetchData = async () => {
try { try {
const response = await fetch("https://api.htl-braunau.at/data/json"); const response = await fetch("https://api.htl-braunau.at/data/json");
if (!response.ok) throw new Error("Failed to fetch data: ${response.statusText}"); if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`);
const result: DataModel[] = await response.json(); const result: DataModel[] = await response.json();
setData(result); setData(result);
setLoading(false);
} catch (err) { } catch (err) {
setError((err as Error).message); setError((err as Error).message);
} finally {
setLoading(false); setLoading(false);
} }
};
fetchData();
}, []); }, []);
useEffect(() => {
fetchData();
const storedInterval = localStorage.getItem('reloadInterval');
if (storedInterval) {
setIntervalTime(Number(storedInterval));
}
}, [fetchData]);
useEffect(() => {
let intervalId: NodeJS.Timeout | null = null;
if (interval > 0) {
intervalId = setInterval(fetchData, Math.max(interval, 10) * 1000);
localStorage.setItem('reloadInterval', interval.toString());
} else {
localStorage.removeItem('reloadInterval');
}
return () => {
if (intervalId) clearInterval(intervalId);
};
}, [interval, fetchData]);
const filteredData = data.filter(item => { const filteredData = data.filter(item => {
const matchesSearchQuery = Object.values(item).some(value => const matchesSearchQuery = Object.values(item).some(value =>
@ -111,157 +153,146 @@ const DataFetcher: React.FC = () => {
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`; return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
}; };
const handleItemsPerPageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setItemsPerPage(Number(event.target.value));
setCurrentPage(1);
};
const paginatedData = sortedData.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage); const paginatedData = sortedData.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
useEffect(() => { const statusCounts = data.reduce((acc, item) => {
const storedInterval = localStorage.getItem('reloadInterval'); const status = item.status;
if (storedInterval) { acc[status] = (acc[status] || 0) + 1;
setIntervalTime(Number(storedInterval)); return acc;
} }, {} as Record<string, number>);
}, []);
if (loading) return <div className="flex justify-center items-center h-screen">Loading...</div>;
useEffect(() => { if (error) return <div className="flex justify-center items-center h-screen text-red-500">Error: {error}</div>;
if (interval <= 10) {
const newInterval = setInterval(() => {
window.location.reload();
}, 10000);
return () => clearInterval(newInterval);
} else {
const newInterval = setInterval(() => {
window.location.reload();
}, interval * 1000);
}
}, [interval]);
useEffect(() => {
if (interval > 0) {
localStorage.setItem('reloadInterval', interval.toString());
} else {
localStorage.removeItem('reloadInterval');
}
}, [interval]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
var installingCounts: number = 0;
var failedCounts: number = 0;
var doneCounts: number = 0
var unknownCounts: number = 0;
data.forEach((item) => {
if (item.status === "installing") {
installingCounts += 1;
} else if (item.status === "failed") {
failedCounts += 1;
}
else if (item.status === "done") {
doneCounts += 1;
}
else {
unknownCounts += 1;
}
});
return ( return (
<div className="p-6 mt-20"> <div className="container mx-auto p-6 pt-20 space-y-6">
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1> <h1 className="text-3xl font-bold text-center">Created LXCs</h1>
<div className="mb-4 flex space-x-4">
<div> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<input <Card>
type="text" <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Search</CardTitle>
</CardHeader>
<CardContent>
<Input
placeholder="Search..." placeholder="Search..."
value={searchQuery} value={searchQuery}
onChange={e => setSearchQuery(e.target.value)} onChange={e => setSearchQuery(e.target.value)}
className="p-2 border"
/> />
<label className="text-sm text-gray-600 mt-1 block">Search by keyword</label> </CardContent>
</div> </Card>
<div>
<DatePicker
selected={startDate}
onChange={date => setStartDate(date)}
selectsStart
startDate={startDate}
endDate={endDate}
placeholderText="Start date"
className="p-2 border"
/>
<label className="text-sm text-gray-600 mt-1 block">Set a start date</label>
</div>
<div> <Card>
<DatePicker <CardHeader className="pb-2">
selected={endDate} <CardTitle className="text-sm font-medium">Start Date</CardTitle>
onChange={date => setEndDate(date)} </CardHeader>
selectsEnd <CardContent>
startDate={startDate} <Popover>
endDate={endDate} <PopoverTrigger asChild>
placeholderText="End date" <Button variant="outline" className="w-full justify-start text-left font-normal">
className="p-2 border" <CalendarIcon className="mr-2 h-4 w-4" />
{startDate ? format(startDate, "PPP") : "Pick a date"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={startDate || undefined}
onSelect={(date: Date | undefined) => setStartDate(date || null)}
initialFocus
/> />
<label className="text-sm text-gray-600 mt-1 block">Set a end date</label> </PopoverContent>
</div> </Popover>
</CardContent>
</Card>
<div className="mb-4 flex space-x-4"> <Card>
<div> <CardHeader className="pb-2">
<input <CardTitle className="text-sm font-medium">End Date</CardTitle>
</CardHeader>
<CardContent>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start text-left font-normal">
<CalendarIcon className="mr-2 h-4 w-4" />
{endDate ? format(endDate, "PPP") : "Pick a date"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={endDate || undefined}
onSelect={(date: Date | undefined) => setEndDate(date || null)}
initialFocus
/>
</PopoverContent>
</Popover>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Reload Interval</CardTitle>
</CardHeader>
<CardContent>
<Input
type="number" type="number"
value={interval} value={interval}
onChange={e => setIntervalTime(Number(e.target.value))} onChange={e => setIntervalTime(Number(e.target.value))}
className="p-2 border"
placeholder="Interval (seconds)" placeholder="Interval (seconds)"
/> />
<label className="text-sm text-gray-600 mt-1 block">Set reload interval (0 for no reload)</label> </CardContent>
</div> </Card>
</div>
</div> </div>
<ApplicationChart data={filteredData} /> <ApplicationChart data={filteredData} />
<div className="mb-4 flex justify-between items-center">
<p className="text-lg font-bold">{filteredData.length} results found</p> <div className="flex justify-between items-center">
<p className="text-lg font">Status Legend: 🔄 installing {installingCounts} | completetd {doneCounts} | failed {failedCounts} | unknown {unknownCounts}</p> <p className="text-lg font-medium">{filteredData.length} results found</p>
<select value={itemsPerPage} onChange={handleItemsPerPageChange} className="p-2 border"> <div className="flex gap-2 items-center">
<option value={25}>25</option> <span>🔄 Installing: {statusCounts.installing || 0}</span>
<option value={50}>50</option> <span> Completed: {statusCounts.done || 0}</span>
<option value={100}>100</option> <span> Failed: {statusCounts.failed || 0}</span>
<option value={200}>200</option> <span> Unknown: {statusCounts.unknown || 0}</span>
</select>
</div> </div>
<div className="overflow-x-auto"> <Select value={itemsPerPage.toString()} onValueChange={(value) => setItemsPerPage(Number(value))}>
<div className="overflow-y-auto lg:overflow-y-visible"> <SelectTrigger className="w-[180px]">
<table className="min-w-full table-auto border-collapse"> <SelectValue placeholder="Items per page" />
<thead> </SelectTrigger>
<tr> <SelectContent>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('status')}>Status</th> {[25, 50, 100, 200].map(value => (
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</th> <SelectItem key={value} value={value.toString()}>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</th> {value} items
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_version')}>OS Version</th> </SelectItem>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('disk_size')}>Disk Size</th> ))}
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('core_count')}>Core Count</th> </SelectContent>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ram_size')}>RAM Size</th> </Select>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('hn')}>Hostname</th> </div>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ssh')}>SSH</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('verbose')}>Verb</th> <div className="rounded-md border">
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('tags')}>Tags</th> <Table>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('method')}>Method</th> <TableHeader>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('pve_version')}>PVE Version</th> <TableRow>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</th> <TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('status')}>Status</TableHead>
</tr> <TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</TableHead>
</thead> <TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</TableHead>
<tbody> <TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_version')}>OS Version</TableHead>
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('disk_size')}>Disk Size</TableHead>
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('core_count')}>Core Count</TableHead>
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ram_size')}>RAM Size</TableHead>
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('hn')}>Hostname</TableHead>
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ssh')}>SSH</TableHead>
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('verbose')}>Verb</TableHead>
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('tags')}>Tags</TableHead>
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('method')}>Method</TableHead>
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('pve_version')}>PVE Version</TableHead>
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedData.map((item, index) => ( {paginatedData.map((item, index) => (
<tr key={index}> <TableRow key={index}>
<td className="px-4 py-2 border-b"> <TableCell className="px-4 py-2 border-b">{item.status === "done" ? (
{item.status === "done" ? (
"✔️" "✔️"
) : item.status === "failed" ? ( ) : item.status === "failed" ? (
"❌" "❌"
@ -269,45 +300,47 @@ const DataFetcher: React.FC = () => {
"🔄" "🔄"
) : ( ) : (
item.status item.status
)} )}</TableCell>
</td> <TableCell className="px-4 py-2 border-b">{item.nsapp}</TableCell>
<td className="px-4 py-2 border-b">{item.nsapp}</td> <TableCell className="px-4 py-2 border-b">{item.os_type}</TableCell>
<td className="px-4 py-2 border-b">{item.os_type}</td> <TableCell className="px-4 py-2 border-b">{item.os_version}</TableCell>
<td className="px-4 py-2 border-b">{item.os_version}</td> <TableCell className="px-4 py-2 border-b">{item.disk_size}</TableCell>
<td className="px-4 py-2 border-b">{item.disk_size}</td> <TableCell className="px-4 py-2 border-b">{item.core_count}</TableCell>
<td className="px-4 py-2 border-b">{item.core_count}</td> <TableCell className="px-4 py-2 border-b">{item.ram_size}</TableCell>
<td className="px-4 py-2 border-b">{item.ram_size}</td> <TableCell className="px-4 py-2 border-b">{item.hn}</TableCell>
<td className="px-4 py-2 border-b">{item.hn}</td> <TableCell className="px-4 py-2 border-b">{item.ssh}</TableCell>
<td className="px-4 py-2 border-b">{item.ssh}</td> <TableCell className="px-4 py-2 border-b">{item.verbose}</TableCell>
<td className="px-4 py-2 border-b">{item.verbose}</td> <TableCell className="px-4 py-2 border-b">{item.tags.replace(/;/g, ' ')}</TableCell>
<td className="px-4 py-2 border-b">{item.tags.replace(/;/g, ' ')}</td> <TableCell className="px-4 py-2 border-b">{item.method}</TableCell>
<td className="px-4 py-2 border-b">{item.method}</td> <TableCell className="px-4 py-2 border-b">{item.pve_version}</TableCell>
<td className="px-4 py-2 border-b">{item.pve_version}</td> <TableCell className="px-4 py-2 border-b">{formatDate(item.created_at)}</TableCell>
<td className="px-4 py-2 border-b">{formatDate(item.created_at)}</td> </TableRow>
</tr>
))} ))}
</tbody> </TableBody>
</table> </Table>
</div> </div>
</div>
<div className="mt-4 flex justify-between items-center"> <div className="flex items-center justify-center space-x-2">
<button <Button
variant="outline"
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1} disabled={currentPage === 1}
className="p-2 border"
> >
Previous Previous
</button> </Button>
<span>Page {currentPage}</span> <span className="text-sm">
<button Page {currentPage} of {Math.ceil(sortedData.length / itemsPerPage)}
</span>
<Button
variant="outline"
onClick={() => setCurrentPage(prev => (prev * itemsPerPage < sortedData.length ? prev + 1 : prev))} onClick={() => setCurrentPage(prev => (prev * itemsPerPage < sortedData.length ? prev + 1 : prev))}
disabled={currentPage * itemsPerPage >= sortedData.length} disabled={currentPage * itemsPerPage >= sortedData.length}
className="p-2 border"
> >
Next Next
</button> </Button>
</div> </div>
</div> </div>
); );
}; };
export default DataFetcher; export default DataFetcher;

View File

@ -1,132 +1,193 @@
"use client"; "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 React, { useState } from "react";
import { Pie } from "react-chartjs-2"; 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 { interface ApplicationChartProps {
data: { nsapp: string }[]; data: { nsapp: string }[];
} }
const ApplicationChart: React.FC<ApplicationChartProps> = ({ data }) => { const ITEMS_PER_PAGE = 20;
const [isChartOpen, setIsChartOpen] = useState(false); const CHART_COLORS = [
const [isTableOpen, setIsTableOpen] = useState(false);
const [chartStartIndex, setChartStartIndex] = useState(0);
const [tableLimit, setTableLimit] = useState(20);
const appCounts: Record<string, number> = {};
data.forEach((item) => {
appCounts[item.nsapp] = (appCounts[item.nsapp] || 0) + 1;
});
const sortedApps = Object.entries(appCounts).sort(([, a], [, b]) => b - a);
const chartApps = sortedApps.slice(chartStartIndex, chartStartIndex + 20);
const chartData = {
labels: chartApps.map(([name]) => name),
datasets: [
{
label: "Applications",
data: chartApps.map(([, count]) => count),
backgroundColor: [
"#ff6384", "#ff6384",
"#36a2eb", "#36a2eb",
"#ffce56", "#ffce56",
"#4bc0c0", "#4bc0c0",
"#9966ff", "#9966ff",
"#ff9f40", "#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(ITEMS_PER_PAGE);
// 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 + ITEMS_PER_PAGE
);
const chartData = {
labels: chartApps.map(([name]) => name),
datasets: [
{
data: chartApps.map(([, count]) => count),
backgroundColor: CHART_COLORS,
}, },
], ],
}; };
return ( const chartOptions = {
<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>
<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: { plugins: {
legend: { display: false }, legend: { display: false },
datalabels: { datalabels: {
color: "white", color: "white",
font: { weight: "bold" }, font: { weight: "bold" as const },
formatter: (value, context) => formatter: (value: number, context: any) => {
context.chart.data.labels?.[context.dataIndex] || "", const label = context.chart.data.labels?.[context.dataIndex];
return `${label}\n(${value})`;
}, },
}, },
}} },
/> responsive: true,
maintainAspectRatio: false,
};
return (
<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>
<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>
<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>
<div className="flex justify-center space-x-4 mt-4"> <div className="flex justify-center gap-4">
<button <Button
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - 20))} variant="outline"
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))}
disabled={chartStartIndex === 0} disabled={chartStartIndex === 0}
className="p-2 border rounded bg-blue-500 text-white"
> >
Last 20 Previous {ITEMS_PER_PAGE}
</button> </Button>
<button <Button
onClick={() => setChartStartIndex(chartStartIndex + 20)} variant="outline"
disabled={chartStartIndex + 20 >= sortedApps.length} onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)}
className="p-2 border rounded bg-blue-500 text-white" disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length}
> >
Next 20 Next {ITEMS_PER_PAGE}
</button> </Button>
</div> </div>
</Modal> </DialogContent>
</Dialog>
<Modal isOpen={isTableOpen} onClose={() => setIsTableOpen(false)}> <Dialog open={isTableOpen} onOpenChange={setIsTableOpen}>
<h2 className="text-xl font-bold text-black dark:text-white mb-4">Application Count Table</h2> <DialogContent className="max-w-2xl">
<table className="w-full border-collapse border border-gray-600 dark:border-gray-500"> <DialogHeader>
<thead> <DialogTitle>Applications Count</DialogTitle>
<tr className="bg-gray-800 text-white"> </DialogHeader>
<th className="p-2 border">Application</th> <div className="max-h-[60vh] overflow-y-auto">
<th className="p-2 border">Count</th> <Table>
</tr> <TableHeader>
</thead> <TableRow>
<tbody> <TableHead>Application</TableHead>
<TableHead className="text-right">Count</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedApps.slice(0, tableLimit).map(([name, count]) => ( {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"> <TableRow key={name}>
<td className="p-2 border">{name}</td> <TableCell>{name}</TableCell>
<td className="p-2 border">{count}</td> <TableCell className="text-right">{count}</TableCell>
</tr> </TableRow>
))} ))}
</tbody> </TableBody>
</table> </Table>
</div>
{tableLimit < sortedApps.length && ( {tableLimit < sortedApps.length && (
<div className="text-center mt-4"> <Button
<button variant="outline"
onClick={() => setTableLimit(tableLimit + 20)} className="w-full"
className="p-2 bg-green-500 text-white rounded" onClick={() => setTableLimit(prev => prev + ITEMS_PER_PAGE)}
> >
Load More Load More
</button> </Button>
</div>
)} )}
</Modal> </DialogContent>
</Dialog>
</div> </div>
); );
}; }
export default ApplicationChart;

View File

@ -0,0 +1,120 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}