Files
hi-frontend/apps/admin/src/sections/dashboard/components/statistics.tsx
T
web@ppanel 116f6e5360 fix(admin): prioritize follow-up tickets (#18)
* 🐛 fix(admin): prioritize follow-up tickets by updated time

*  feat(dashboard): show node/user traffic ranks side-by-side
2026-03-04 08:16:31 -08:00

299 lines
10 KiB
TypeScript

"use client";
import { useQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@workspace/ui/components/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@workspace/ui/components/chart";
// (Select imports removed)
import { Separator } from "@workspace/ui/components/separator";
import { Tabs, TabsList, TabsTrigger } from "@workspace/ui/components/tabs";
import Empty from "@workspace/ui/composed/empty";
import { Icon } from "@workspace/ui/composed/icon";
import {
queryServerTotalData,
queryTicketWaitReply,
} from "@workspace/ui/services/admin/console";
import { formatBytes } from "@workspace/ui/utils/formatting";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Bar,
BarChart,
CartesianGrid,
LabelList,
XAxis,
YAxis,
} from "recharts";
import { UserSubscribeDetail } from "@/sections/user/user-detail";
import { RevenueStatisticsCard } from "./revenue-statistics-card";
import SystemVersionCard from "./system-version-card";
import { UserStatisticsCard } from "./user-statistics-card";
export default function Statistics() {
const { t } = useTranslation("dashboard");
const { data: TicketTotal } = useQuery({
queryKey: ["queryTicketWaitReply"],
queryFn: async () => {
const { data } = await queryTicketWaitReply();
return data.data?.count;
},
});
const { data: ServerTotal } = useQuery({
queryKey: ["queryServerTotalData"],
queryFn: async () => {
const { data } = await queryServerTotalData();
return data.data;
},
});
const [timeFrame, setTimeFrame] = useState<string | "today" | "yesterday">(
"today"
);
const trafficData = {
nodes: {
today:
ServerTotal?.server_traffic_ranking_today?.map((item) => ({
name: item.name,
traffic: item.download + item.upload,
})) || [],
yesterday:
ServerTotal?.server_traffic_ranking_yesterday?.map((item) => ({
name: item.name,
traffic: item.download + item.upload,
})) || [],
},
users: {
today:
ServerTotal?.user_traffic_ranking_today?.map((item) => ({
name: item.sid,
traffic: item.download + item.upload,
})) || [],
yesterday:
ServerTotal?.user_traffic_ranking_yesterday?.map((item) => ({
name: item.sid,
traffic: item.download + item.upload,
})) || [],
},
};
const TrafficRankCard = ({ type }: { type: "nodes" | "users" }) => {
const currentData = trafficData[type][timeFrame as "today" | "yesterday"];
return (
<Card>
<CardHeader className="!flex-row flex items-center justify-between">
<CardTitle>
{type === "nodes"
? t("nodeTraffic", "Node Traffic")
: t("userTraffic", "User Traffic")}
</CardTitle>
<Tabs onValueChange={setTimeFrame} value={timeFrame}>
<TabsList>
<TabsTrigger value="today">{t("today", "Today")}</TabsTrigger>
<TabsTrigger value="yesterday">
{t("yesterday", "Yesterday")}
</TabsTrigger>
</TabsList>
</Tabs>
</CardHeader>
<CardContent className="h-80">
{currentData.length > 0 ? (
<ChartContainer
className="max-h-80"
config={{
traffic: {
label: t("traffic", "Traffic"),
color: "var(--primary)",
},
type: {
label: t("type", "Type"),
color: "var(--muted-foreground)",
},
label: {
color: "var(--foreground)",
},
}}
>
<BarChart data={currentData} height={400} layout="vertical">
<CartesianGrid strokeDasharray="3 3" />
<XAxis
axisLine={false}
tickFormatter={(value) => formatBytes(value || 0)}
tickLine={false}
type="number"
/>
<YAxis
axisLine={false}
dataKey="name"
interval={0}
tickFormatter={(_value, index) => String(index + 1)}
tickLine={false}
tickMargin={0}
type="category"
width={15}
/>
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value) => formatBytes(Number(value) || 0)}
label={true}
labelFormatter={(label, [payload]) =>
type === "nodes" ? (
`${t("nodes", "Nodes")}: ${label}`
) : (
<>
<div className="w-80">
<UserSubscribeDetail
enabled={true}
id={payload?.payload.name}
/>
</div>
<Separator className="my-2" />
<div>{`${t("users", "Users")}: ${label}`}</div>
</>
)
}
/>
}
trigger="hover"
/>
<Bar
dataKey="traffic"
fill="var(--primary)"
radius={[0, 4, 4, 0]}
>
<LabelList
className="fill-foreground"
dataKey="name"
fontSize={12}
offset={8}
position="insideLeft"
/>
</Bar>
</BarChart>
</ChartContainer>
) : (
<div className="flex h-full items-center justify-center">
<Empty />
</div>
)}
</CardContent>
</Card>
);
};
return (
<>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{[
{
title: t("onlineUsersCount", "Online Users"),
value: ServerTotal?.online_users || 0,
subtitle: t("currentlyOnline", "Currently Online"),
icon: "uil:users-alt",
href: "/dashboard/user",
color: "text-blue-600 dark:text-blue-400",
iconBg: "bg-blue-100 dark:bg-blue-900/30",
},
{
title: t("todayTraffic", "Today Traffic"),
value: formatBytes(
(ServerTotal?.today_upload || 0) +
(ServerTotal?.today_download || 0)
),
subtitle: `${formatBytes(ServerTotal?.today_upload || 0)}${formatBytes(ServerTotal?.today_download || 0)}`,
icon: "uil:exchange-alt",
color: "text-purple-600 dark:text-purple-400",
iconBg: "bg-purple-100 dark:bg-purple-900/30",
},
{
title: t("monthTraffic", "Month Traffic"),
value: formatBytes(
(ServerTotal?.monthly_upload || 0) +
(ServerTotal?.monthly_download || 0)
),
subtitle: `${formatBytes(ServerTotal?.monthly_upload || 0)}${formatBytes(ServerTotal?.monthly_download || 0)}`,
icon: "uil:cloud-data-connection",
color: "text-orange-600 dark:text-orange-400",
iconBg: "bg-orange-100 dark:bg-orange-900/30",
},
{
title: t("totalServers", "Total Servers"),
value:
(ServerTotal?.online_servers || 0) +
(ServerTotal?.offline_servers || 0),
subtitle: `${t("online", "Online")} ${ServerTotal?.online_servers || 0} ${t("offline", "Offline")} ${ServerTotal?.offline_servers || 0}`,
icon: "uil:server-network",
href: "/dashboard/servers",
color: "text-green-600 dark:text-green-400",
iconBg: "bg-green-100 dark:bg-green-900/30",
},
{
title: t("pendingTickets", "Pending Tickets"),
value: TicketTotal || 0,
subtitle: t("pending", "Pending"),
icon: "uil:clipboard-notes",
href: "/dashboard/ticket",
color: "text-red-600 dark:text-red-400",
iconBg: "bg-red-100 dark:bg-red-900/30",
},
].map((item, index) => (
<Link
className={item.href ? "" : "pointer-events-none"}
key={index}
to={item.href || "#"}
>
<Card className={`group ${item.href ? "cursor-pointer" : ""}`}>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="mb-2 font-medium text-muted-foreground text-sm">
{item.title}
</p>
<div className={`mb-1 font-bold text-2xl ${item.color}`}>
{item.value}
</div>
<div className="h-4 text-muted-foreground text-xs">
{item.subtitle}
</div>
</div>
<div
className={`rounded-full p-3 ${item.iconBg} transition-transform duration-300 group-hover:scale-110`}
>
<Icon
className={`h-6 w-6 ${item.color}`}
icon={item.icon}
/>
</div>
</div>
</CardContent>
</Card>
</Link>
))}
<SystemVersionCard />
</div>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-2">
<RevenueStatisticsCard />
<UserStatisticsCard />
</div>
<div className="grid gap-3 md:grid-cols-2">
<TrafficRankCard type="nodes" />
<TrafficRankCard type="users" />
</div>
</>
);
}