mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-02-01 11:31:51 +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
@ -1,9 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import ApplicationChart from "../../components/ApplicationChart";
|
||||
import ApplicationChart from "@/components/ApplicationChart";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
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 {
|
||||
id: number;
|
||||
@ -40,23 +64,41 @@ const DataFetcher: React.FC = () => {
|
||||
const [reloadInterval, setReloadInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
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();
|
||||
setData(result);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
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 matchesSearchQuery = Object.values(item).some(value =>
|
||||
@ -111,157 +153,146 @@ const DataFetcher: React.FC = () => {
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
const storedInterval = localStorage.getItem('reloadInterval');
|
||||
if (storedInterval) {
|
||||
setIntervalTime(Number(storedInterval));
|
||||
}
|
||||
}, []);
|
||||
const statusCounts = data.reduce((acc, item) => {
|
||||
const status = item.status;
|
||||
acc[status] = (acc[status] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
if (loading) return <div className="flex justify-center items-center h-screen">Loading...</div>;
|
||||
if (error) return <div className="flex justify-center items-center h-screen text-red-500">Error: {error}</div>;
|
||||
|
||||
return (
|
||||
<div className="p-6 mt-20">
|
||||
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
|
||||
<div className="mb-4 flex space-x-4">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
<div className="container mx-auto p-6 pt-20 space-y-6">
|
||||
<h1 className="text-3xl font-bold text-center">Created LXCs</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Search</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
className="p-2 border"
|
||||
/>
|
||||
<label className="text-sm text-gray-600 mt-1 block">Search by keyword</label>
|
||||
</div>
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div>
|
||||
<DatePicker
|
||||
selected={endDate}
|
||||
onChange={date => setEndDate(date)}
|
||||
selectsEnd
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
placeholderText="End date"
|
||||
className="p-2 border"
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Start 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" />
|
||||
{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>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="mb-4 flex space-x-4">
|
||||
<div>
|
||||
<input
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<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"
|
||||
value={interval}
|
||||
onChange={e => setIntervalTime(Number(e.target.value))}
|
||||
className="p-2 border"
|
||||
placeholder="Interval (seconds)"
|
||||
/>
|
||||
<label className="text-sm text-gray-600 mt-1 block">Set reload interval (0 for no reload)</label>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<ApplicationChart data={filteredData} />
|
||||
<div className="mb-4 flex justify-between items-center">
|
||||
<p className="text-lg font-bold">{filteredData.length} results found</p>
|
||||
<p className="text-lg font">Status Legend: 🔄 installing {installingCounts} | ✔️ completetd {doneCounts} | ❌ failed {failedCounts} | ❓ unknown {unknownCounts}</p>
|
||||
<select value={itemsPerPage} onChange={handleItemsPerPageChange} className="p-2 border">
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={200}>200</option>
|
||||
</select>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-lg font-medium">{filteredData.length} results found</p>
|
||||
<div className="flex gap-2 items-center">
|
||||
<span>🔄 Installing: {statusCounts.installing || 0}</span>
|
||||
<span>✔️ Completed: {statusCounts.done || 0}</span>
|
||||
<span>❌ Failed: {statusCounts.failed || 0}</span>
|
||||
<span>❓ Unknown: {statusCounts.unknown || 0}</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-y-auto lg:overflow-y-visible">
|
||||
<table className="min-w-full table-auto border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('status')}>Status</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_version')}>OS Version</th>
|
||||
<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>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ram_size')}>RAM Size</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('hn')}>Hostname</th>
|
||||
<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>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('tags')}>Tags</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('method')}>Method</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('pve_version')}>PVE Version</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Select value={itemsPerPage.toString()} onValueChange={(value) => setItemsPerPage(Number(value))}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Items per page" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[25, 50, 100, 200].map(value => (
|
||||
<SelectItem key={value} value={value.toString()}>
|
||||
{value} items
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('status')}>Status</TableHead>
|
||||
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</TableHead>
|
||||
<TableHead className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</TableHead>
|
||||
<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) => (
|
||||
<tr key={index}>
|
||||
<td className="px-4 py-2 border-b">
|
||||
{item.status === "done" ? (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="px-4 py-2 border-b">{item.status === "done" ? (
|
||||
"✔️"
|
||||
) : item.status === "failed" ? (
|
||||
"❌"
|
||||
@ -269,45 +300,47 @@ const DataFetcher: React.FC = () => {
|
||||
"🔄"
|
||||
) : (
|
||||
item.status
|
||||
)}
|
||||
</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.os_version}</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.hn}</td>
|
||||
<td className="px-4 py-2 border-b">{item.ssh}</td>
|
||||
<td className="px-4 py-2 border-b">{item.verbose}</td>
|
||||
<td className="px-4 py-2 border-b">{item.tags.replace(/;/g, ' ')}</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">{formatDate(item.created_at)}</td>
|
||||
</tr>
|
||||
)}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.nsapp}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.os_type}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.os_version}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.disk_size}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.core_count}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.ram_size}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.hn}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.ssh}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.verbose}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.tags.replace(/;/g, ' ')}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.method}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{item.pve_version}</TableCell>
|
||||
<TableCell className="px-4 py-2 border-b">{formatDate(item.created_at)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-between items-center">
|
||||
<button
|
||||
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="p-2 border"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span>Page {currentPage}</span>
|
||||
<button
|
||||
</Button>
|
||||
<span className="text-sm">
|
||||
Page {currentPage} of {Math.ceil(sortedData.length / itemsPerPage)}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentPage(prev => (prev * itemsPerPage < sortedData.length ? prev + 1 : prev))}
|
||||
disabled={currentPage * itemsPerPage >= sortedData.length}
|
||||
className="p-2 border"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataFetcher;
|
||||
|
@ -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 [isChartOpen, setIsChartOpen] = useState(false);
|
||||
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: [
|
||||
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(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 (
|
||||
<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={{
|
||||
const chartOptions = {
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
datalabels: {
|
||||
color: "white",
|
||||
font: { weight: "bold" },
|
||||
formatter: (value, context) =>
|
||||
context.chart.data.labels?.[context.dataIndex] || "",
|
||||
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 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 className="flex justify-center space-x-4 mt-4">
|
||||
<button
|
||||
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - 20))}
|
||||
<div className="flex justify-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))}
|
||||
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"
|
||||
Previous {ITEMS_PER_PAGE}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)}
|
||||
disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length}
|
||||
>
|
||||
Next 20 ▶
|
||||
</button>
|
||||
Next {ITEMS_PER_PAGE}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<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>
|
||||
<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]) => (
|
||||
<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>
|
||||
<TableRow key={name}>
|
||||
<TableCell>{name}</TableCell>
|
||||
<TableCell className="text-right">{count}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{tableLimit < sortedApps.length && (
|
||||
<div className="text-center mt-4">
|
||||
<button
|
||||
onClick={() => setTableLimit(tableLimit + 20)}
|
||||
className="p-2 bg-green-500 text-white rounded"
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => setTableLimit(prev => prev + ITEMS_PER_PAGE)}
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationChart;
|
||||
}
|
120
frontend/src/components/ui/table.tsx
Normal file
120
frontend/src/components/ui/table.tsx
Normal 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,
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user