mirror of
				https://github.com/community-scripts/ProxmoxVE.git
				synced 2025-11-04 10:22:50 +00:00 
			
		
		
		
	feat: enhance DataFetcher with better UI components and add reactive data fetching intervals (#1901) (#1902)
This commit is contained in:
		@@ -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 () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await fetch("https://api.htl-braunau.at/data/json");
 | 
			
		||||
        if (!response.ok) throw new Error("Failed to fetch data: ${response.statusText}");
 | 
			
		||||
        const result: DataModel[] = await response.json();
 | 
			
		||||
        setData(result);
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        setError((err as Error).message);
 | 
			
		||||
      } finally {
 | 
			
		||||
        setLoading(false);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    fetchData();
 | 
			
		||||
  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}`);
 | 
			
		||||
      const result: DataModel[] = await response.json();
 | 
			
		||||
      setData(result);
 | 
			
		||||
      setLoading(false);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      setError((err as Error).message);
 | 
			
		||||
      setLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  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,203 +153,194 @@ 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"
 | 
			
		||||
            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>
 | 
			
		||||
    <div className="container mx-auto p-6 pt-20 space-y-6">
 | 
			
		||||
      <h1 className="text-3xl font-bold text-center">Created LXCs</h1>
 | 
			
		||||
      
 | 
			
		||||
        <div>
 | 
			
		||||
          <DatePicker
 | 
			
		||||
            selected={endDate}
 | 
			
		||||
            onChange={date => setEndDate(date)}
 | 
			
		||||
            selectsEnd
 | 
			
		||||
            startDate={startDate}
 | 
			
		||||
            endDate={endDate}
 | 
			
		||||
            placeholderText="End date"
 | 
			
		||||
            className="p-2 border"
 | 
			
		||||
          />
 | 
			
		||||
          <label className="text-sm text-gray-600 mt-1 block">Set a end date</label>
 | 
			
		||||
        </div>
 | 
			
		||||
      <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)}
 | 
			
		||||
            />
 | 
			
		||||
          </CardContent>
 | 
			
		||||
        </Card>
 | 
			
		||||
 | 
			
		||||
      <div className="mb-4 flex space-x-4">
 | 
			
		||||
        <div>
 | 
			
		||||
          <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>
 | 
			
		||||
        <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
 | 
			
		||||
                />
 | 
			
		||||
              </PopoverContent>
 | 
			
		||||
            </Popover>
 | 
			
		||||
          </CardContent>
 | 
			
		||||
        </Card>
 | 
			
		||||
 | 
			
		||||
        <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))}
 | 
			
		||||
              placeholder="Interval (seconds)"
 | 
			
		||||
            />
 | 
			
		||||
          </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>
 | 
			
		||||
      <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>
 | 
			
		||||
              {paginatedData.map((item, index) => (
 | 
			
		||||
                <tr key={index}>
 | 
			
		||||
                  <td className="px-4 py-2 border-b">
 | 
			
		||||
                    {item.status === "done" ? (
 | 
			
		||||
                      "✔️"
 | 
			
		||||
                    ) : item.status === "failed" ? (
 | 
			
		||||
                      "❌"
 | 
			
		||||
                    ) : item.status === "installing" ? (
 | 
			
		||||
                      "🔄"  
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      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>
 | 
			
		||||
              ))}
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
 | 
			
		||||
      <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>
 | 
			
		||||
        <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="mt-4 flex justify-between items-center">
 | 
			
		||||
        <button
 | 
			
		||||
 | 
			
		||||
      <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) => (
 | 
			
		||||
              <TableRow key={index}>
 | 
			
		||||
                <TableCell className="px-4 py-2 border-b">{item.status === "done" ? (
 | 
			
		||||
                  "✔️"
 | 
			
		||||
                ) : item.status === "failed" ? (
 | 
			
		||||
                  "❌"
 | 
			
		||||
                ) : item.status === "installing" ? (
 | 
			
		||||
                  "🔄"  
 | 
			
		||||
                ) : (
 | 
			
		||||
                  item.status
 | 
			
		||||
                )}</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>
 | 
			
		||||
            ))}
 | 
			
		||||
          </TableBody>
 | 
			
		||||
        </Table>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <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 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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,
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user