Refactor DataPage for cleaner imports and UI improvements

Consolidated and reordered import statements for better readability. Simplified JSX formatting and conditional rendering throughout the file. Added error tooltips for failed installations in the log table, improved table cell formatting, and fixed disk size unit from MB to GB. Minor code style and consistency improvements applied across the component.
This commit is contained in:
CanbiZ 2025-12-04 09:23:28 +01:00
parent 8e93f5cb1d
commit 62201a0872

View File

@ -11,18 +11,16 @@ import {
Trophy, Trophy,
XCircle, XCircle,
} from "lucide-react"; } from "lucide-react";
import { import { useEffect, useMemo, useState } from "react";
Bar, import { Bar, BarChart, CartesianGrid, Cell, LabelList, XAxis } from "recharts";
BarChart,
CartesianGrid,
Cell,
LabelList,
XAxis,
} from "recharts";
import React, { useEffect, useMemo, useState } from "react";
import type { ChartConfig } from "@/components/ui/chart"; import type { ChartConfig } from "@/components/ui/chart";
import { formattedBadge } from "@/components/command-menu";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -31,37 +29,10 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { formattedBadge } from "@/components/command-menu";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
type DataModel = { type DataModel = {
id: number; id: number;
@ -141,7 +112,8 @@ export default function DataPage() {
const [summaryRes, dataRes] = await Promise.all([ const [summaryRes, dataRes] = await Promise.all([
fetch("https://api.htl-braunau.at/data/summary"), fetch("https://api.htl-braunau.at/data/summary"),
fetch( fetch(
`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? "" : itemsPerPage `https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${
itemsPerPage === 0 ? "" : itemsPerPage
}`, }`,
), ),
]); ]);
@ -158,11 +130,9 @@ export default function DataPage() {
setSummary(summaryData); setSummary(summaryData);
setData(pageData); setData(pageData);
} } catch (err) {
catch (err) {
setError((err as Error).message); setError((err as Error).message);
} } finally {
finally {
setLoading(false); setLoading(false);
} }
}; };
@ -171,8 +141,7 @@ export default function DataPage() {
}, [currentPage, itemsPerPage]); }, [currentPage, itemsPerPage]);
const sortedData = useMemo(() => { const sortedData = useMemo(() => {
if (!sortConfig) if (!sortConfig) return data;
return data;
return [...data].sort((a, b) => { return [...data].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) { if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === "ascending" ? -1 : 1; return sortConfig.direction === "ascending" ? -1 : 1;
@ -186,11 +155,7 @@ export default function DataPage() {
const requestSort = (key: string) => { const requestSort = (key: string) => {
let direction: "ascending" | "descending" = "ascending"; let direction: "ascending" | "descending" = "ascending";
if ( if (sortConfig && sortConfig.key === key && sortConfig.direction === "ascending") {
sortConfig
&& sortConfig.key === key
&& sortConfig.direction === "ascending"
) {
direction = "descending"; direction = "descending";
} }
setSortConfig({ key, direction }); setSortConfig({ key, direction });
@ -205,10 +170,8 @@ export default function DataPage() {
}; };
const getTypeBadge = (type: string) => { const getTypeBadge = (type: string) => {
if (type === "lxc") if (type === "lxc") return formattedBadge("ct");
return formattedBadge("ct"); if (type === "vm") return formattedBadge("vm");
if (type === "vm")
return formattedBadge("vm");
return null; return null;
}; };
@ -219,8 +182,7 @@ export default function DataPage() {
const successRate = totalCount > 0 ? (successCount / totalCount) * 100 : 0; const successRate = totalCount > 0 ? (successCount / totalCount) * 100 : 0;
const allApps = useMemo(() => { const allApps = useMemo(() => {
if (!summary?.nsapp_count) if (!summary?.nsapp_count) return [];
return [];
return Object.entries(summary.nsapp_count).sort(([, a], [, b]) => b - a); return Object.entries(summary.nsapp_count).sort(([, a], [, b]) => b - a);
}, [summary]); }, [summary]);
@ -255,9 +217,7 @@ export default function DataPage() {
{/* Header */} {/* Header */}
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Analytics</h1> <h1 className="text-3xl font-bold tracking-tight">Analytics</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">Overview of container installations and system statistics.</p>
Overview of container installations and system statistics.
</p>
</div> </div>
{/* Widgets */} {/* Widgets */}
@ -269,9 +229,7 @@ export default function DataPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{nf.format(totalCount)}</div> <div className="text-2xl font-bold">{nf.format(totalCount)}</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">Total LXC/VM entries found</p>
Total LXC/VM entries found
</p>
</CardContent> </CardContent>
</Card> </Card>
@ -281,15 +239,8 @@ export default function DataPage() {
<CheckCircle2 className="h-4 w-4 text-green-500" /> <CheckCircle2 className="h-4 w-4 text-green-500" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">{successRate.toFixed(1)}%</div>
{successRate.toFixed(1)} <p className="text-xs text-muted-foreground">{nf.format(successCount)} successful installations</p>
%
</div>
<p className="text-xs text-muted-foreground">
{nf.format(successCount)}
{" "}
successful installations
</p>
</CardContent> </CardContent>
</Card> </Card>
@ -300,9 +251,7 @@ export default function DataPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{nf.format(failureCount)}</div> <div className="text-2xl font-bold">{nf.format(failureCount)}</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">Installations encountered errors</p>
Installations encountered errors
</p>
</CardContent> </CardContent>
</Card> </Card>
@ -312,13 +261,9 @@ export default function DataPage() {
<Trophy className="h-4 w-4 text-yellow-500" /> <Trophy className="h-4 w-4 text-yellow-500" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="truncate text-2xl font-bold"> <div className="truncate text-2xl font-bold">{mostPopularApp ? mostPopularApp[0] : "N/A"}</div>
{mostPopularApp ? mostPopularApp[0] : "N/A"}
</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{mostPopularApp ? nf.format(mostPopularApp[1]) : 0} {mostPopularApp ? nf.format(mostPopularApp[1]) : 0} installations
{" "}
installations
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@ -329,9 +274,7 @@ export default function DataPage() {
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<div className="space-y-1.5"> <div className="space-y-1.5">
<CardTitle>Top Applications</CardTitle> <CardTitle>Top Applications</CardTitle>
<CardDescription> <CardDescription>The most frequently installed applications.</CardDescription>
The most frequently installed applications.
</CardDescription>
</div> </div>
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -343,26 +286,14 @@ export default function DataPage() {
<DialogContent className="max-h-[80vh] sm:max-w-md"> <DialogContent className="max-h-[80vh] sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Application Statistics</DialogTitle> <DialogTitle>Application Statistics</DialogTitle>
<DialogDescription> <DialogDescription>Installation counts for all {allApps.length} applications.</DialogDescription>
Installation counts for all
{" "}
{allApps.length}
{" "}
applications.
</DialogDescription>
</DialogHeader> </DialogHeader>
<ScrollArea className="h-[60vh] w-full rounded-md border p-4"> <ScrollArea className="h-[60vh] w-full rounded-md border p-4">
<div className="space-y-4"> <div className="space-y-4">
{allApps.map(([name, count], index) => ( {allApps.map(([name, count], index) => (
<div <div key={name} className="flex items-center justify-between text-sm">
key={name}
className="flex items-center justify-between text-sm"
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="w-8 font-mono text-muted-foreground"> <span className="w-8 font-mono text-muted-foreground">{index + 1}.</span>
{index + 1}
.
</span>
<span className="font-medium">{name}</span> <span className="font-medium">{name}</span>
</div> </div>
<span className="font-mono">{nf.format(count)}</span> <span className="font-mono">{nf.format(count)}</span>
@ -375,47 +306,37 @@ export default function DataPage() {
</CardHeader> </CardHeader>
<CardContent className="pl-2"> <CardContent className="pl-2">
<div className="h-[300px] w-full"> <div className="h-[300px] w-full">
{loading {loading ? (
? ( <div className="flex h-full w-full items-center justify-center">
<div className="flex h-full w-full items-center justify-center"> <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> </div>
</div> ) : (
) <ChartContainer config={chartConfigApps} className="h-full w-full">
: ( <BarChart
<ChartContainer config={chartConfigApps} className="h-full w-full"> accessibilityLayer
<BarChart data={appsChartData}
accessibilityLayer margin={{
data={appsChartData} top: 20,
margin={{ }}
top: 20, >
}} <CartesianGrid vertical={false} />
> <XAxis
<CartesianGrid vertical={false} /> dataKey="app"
<XAxis tickLine={false}
dataKey="app" tickMargin={10}
tickLine={false} axisLine={false}
tickMargin={10} tickFormatter={(value) => (value.length > 8 ? `${value.slice(0, 8)}...` : value)}
axisLine={false} />
tickFormatter={value => (value.length > 8 ? `${value.slice(0, 8)}...` : value)} <ChartTooltip cursor={false} content={<ChartTooltipContent nameKey="app" />} />
/> <Bar dataKey="count" radius={8}>
<ChartTooltip {appsChartData.map((entry, index) => (
cursor={false} <Cell key={`cell-${index}`} fill={entry.fill} />
content={<ChartTooltipContent nameKey="app" />} ))}
/> <LabelList position="top" offset={12} className="fill-foreground" fontSize={12} />
<Bar dataKey="count" radius={8}> </Bar>
{appsChartData.map((entry, index) => ( </BarChart>
<Cell key={`cell-${index}`} fill={entry.fill} /> </ChartContainer>
))} )}
<LabelList
position="top"
offset={12}
className="fill-foreground"
fontSize={12}
/>
</Bar>
</BarChart>
</ChartContainer>
)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -425,15 +346,10 @@ export default function DataPage() {
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<div> <div>
<CardTitle>Installation Log</CardTitle> <CardTitle>Installation Log</CardTitle>
<CardDescription> <CardDescription>Detailed records of all container creation attempts.</CardDescription>
Detailed records of all container creation attempts.
</CardDescription>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Select <Select value={String(itemsPerPage)} onValueChange={(val) => setItemsPerPage(Number(val))}>
value={String(itemsPerPage)}
onValueChange={val => setItemsPerPage(Number(val))}
>
<SelectTrigger className="w-[80px]"> <SelectTrigger className="w-[80px]">
<SelectValue placeholder="Limit" /> <SelectValue placeholder="Limit" />
</SelectTrigger> </SelectTrigger>
@ -451,161 +367,108 @@ export default function DataPage() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead <TableHead className="w-[100px] cursor-pointer" onClick={() => requestSort("status")}>
className="w-[100px] cursor-pointer"
onClick={() => requestSort("status")}
>
Status Status
{sortConfig?.key === "status" && ( {sortConfig?.key === "status" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead> </TableHead>
<TableHead <TableHead className="cursor-pointer" onClick={() => requestSort("type")}>
className="cursor-pointer"
onClick={() => requestSort("type")}
>
Type Type
{sortConfig?.key === "type" && ( {sortConfig?.key === "type" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead> </TableHead>
<TableHead <TableHead className="cursor-pointer" onClick={() => requestSort("nsapp")}>
className="cursor-pointer"
onClick={() => requestSort("nsapp")}
>
Application Application
{sortConfig?.key === "nsapp" && ( {sortConfig?.key === "nsapp" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead> </TableHead>
<TableHead <TableHead className="hidden cursor-pointer md:table-cell" onClick={() => requestSort("os_type")}>
className="hidden cursor-pointer md:table-cell"
onClick={() => requestSort("os_type")}
>
OS OS
{sortConfig?.key === "os_type" && ( {sortConfig?.key === "os_type" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead> </TableHead>
<TableHead <TableHead
className="hidden cursor-pointer md:table-cell" className="hidden cursor-pointer md:table-cell"
onClick={() => requestSort("disk_size")} onClick={() => requestSort("disk_size")}
> >
Disk Size Disk Size
{sortConfig?.key === "disk_size" && ( {sortConfig?.key === "disk_size" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead> </TableHead>
<TableHead <TableHead
className="hidden cursor-pointer lg:table-cell" className="hidden cursor-pointer lg:table-cell"
onClick={() => requestSort("core_count")} onClick={() => requestSort("core_count")}
> >
Core Count Core Count
{sortConfig?.key === "core_count" && ( {sortConfig?.key === "core_count" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead> </TableHead>
<TableHead <TableHead
className="hidden cursor-pointer lg:table-cell" className="hidden cursor-pointer lg:table-cell"
onClick={() => requestSort("ram_size")} onClick={() => requestSort("ram_size")}
> >
RAM Size RAM Size
{sortConfig?.key === "ram_size" && ( {sortConfig?.key === "ram_size" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead> </TableHead>
<TableHead <TableHead className="cursor-pointer text-right" onClick={() => requestSort("created_at")}>
className="cursor-pointer text-right"
onClick={() => requestSort("created_at")}
>
Created At Created At
{sortConfig?.key === "created_at" && ( {sortConfig?.key === "created_at" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead> </TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{loading {loading ? (
? ( <TableRow>
<TableRow> <TableCell colSpan={8} className="h-24 text-center">
<TableCell colSpan={8} className="h-24 text-center"> <div className="flex items-center justify-center gap-2">
<div className="flex items-center justify-center gap-2"> <Loader2 className="h-4 w-4 animate-spin" /> Loading data...
<Loader2 className="h-4 w-4 animate-spin" /> </div>
{" "} </TableCell>
Loading data... </TableRow>
</div> ) : sortedData.length > 0 ? (
</TableCell> sortedData.map((item, idx) => (
</TableRow> <TableRow key={`${item.id}-${idx}`}>
) <TableCell>
: sortedData.length > 0 {item.status === "done" ? (
? ( <Badge className="text-green-500/75 border-green-500/75">Success</Badge>
sortedData.map((item, idx) => ( ) : item.status === "failed" ? (
<TableRow key={`${item.id}-${idx}`}> <TooltipProvider>
<TableCell> <Tooltip>
{item.status === "done" <TooltipTrigger asChild>
? ( <Badge className="text-red-500/75 border-red-500/75 cursor-help">Failed</Badge>
<Badge className="text-green-500/75 border-green-500/75"> </TooltipTrigger>
Success <TooltipContent className="max-w-xs">
</Badge> <p className="font-semibold">Error:</p>
) <p className="text-sm">{item.error || "Unknown error"}</p>
: item.status === "failed" </TooltipContent>
? ( </Tooltip>
<Badge className="text-red-500/75 border-red-500/75"> </TooltipProvider>
Failed ) : item.status === "installing" ? (
</Badge> <Badge className="text-blue-500/75 border-blue-500/75">Installing</Badge>
) ) : (
: item.status === "installing" <Badge variant="outline">{item.status}</Badge>
? ( )}
<Badge className="text-blue-500/75 border-blue-500/75"> </TableCell>
Installing <TableCell>
</Badge> {getTypeBadge(item.type) || <Badge variant="outline">{item.type}</Badge>}
) </TableCell>
: ( <TableCell className="font-medium">{item.nsapp}</TableCell>
<Badge variant="outline"> <TableCell className="hidden md:table-cell">
{item.status} {item.os_type} {item.os_version}
</Badge> </TableCell>
)} <TableCell className="hidden md:table-cell">
</TableCell> {item.disk_size}
<TableCell> GB
{getTypeBadge(item.type) || ( </TableCell>
<Badge variant="outline"> <TableCell className="hidden lg:table-cell">{item.core_count}</TableCell>
{item.type} <TableCell className="hidden lg:table-cell">
</Badge> {item.ram_size}
)} MB
</TableCell> </TableCell>
<TableCell className="font-medium"> <TableCell className="text-right">{formatDate(item.created_at)}</TableCell>
{item.nsapp} </TableRow>
</TableCell> ))
<TableCell className="hidden md:table-cell"> ) : (
{item.os_type} <TableRow>
{" "} <TableCell colSpan={8} className="h-24 text-center">
{item.os_version} No results found.
</TableCell> </TableCell>
<TableCell className="hidden md:table-cell"> </TableRow>
{item.disk_size} )}
MB
</TableCell>
<TableCell className="hidden lg:table-cell">
{item.core_count}
</TableCell>
<TableCell className="hidden lg:table-cell">
{item.ram_size}
MB
</TableCell>
<TableCell className="text-right">
{formatDate(item.created_at)}
</TableCell>
</TableRow>
))
)
: (
<TableRow>
<TableCell colSpan={8} className="h-24 text-center">
No results found.
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
@ -614,21 +477,17 @@ export default function DataPage() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1 || loading} disabled={currentPage === 1 || loading}
> >
<ChevronLeft className="mr-2 h-4 w-4" /> <ChevronLeft className="mr-2 h-4 w-4" />
Previous Previous
</Button> </Button>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">Page {currentPage}</div>
Page
{" "}
{currentPage}
</div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage(prev => prev + 1)} onClick={() => setCurrentPage((prev) => prev + 1)}
disabled={loading || sortedData.length < itemsPerPage} disabled={loading || sortedData.length < itemsPerPage}
> >
Next Next