Skip to content

Commit

Permalink
feat: proxy traffic pie chart
Browse files Browse the repository at this point in the history
  • Loading branch information
VaalaCat committed Dec 1, 2024
1 parent e88c18c commit 4edc320
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 21 deletions.
4 changes: 2 additions & 2 deletions biz/master/proxy/task_collect_daily_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func CollectDailyStats() error {
Type: item.Type,
UserID: item.UserID,
TenantID: item.TenantID,
TrafficIn: item.TodayTrafficIn,
TrafficOut: item.TodayTrafficOut,
TrafficIn: item.HistoryTrafficIn,
TrafficOut: item.HistoryTrafficOut,
}
})

Expand Down
2 changes: 1 addition & 1 deletion cmd/frpp/master.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func runMaster() {
defer r.Stop()

tasks := watcher.NewClient()
tasks.AddCronTask("0 59 23 * * *", proxy.CollectDailyStats)
tasks.AddCronTask("0 0 3 * * *", proxy.CollectDailyStats)
defer tasks.Stop()

var wg conc.WaitGroup
Expand Down
Binary file modified doc/traffic_statistics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 26 additions & 8 deletions www/components/charts/proxy-traffic-overview.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ProxyInfo } from "@/lib/pb/common"
import { formatBytes } from "@/lib/utils"
import { CloudDownload, CloudUpload } from "lucide-react"

export function ProxyTrafficOverview({ proxyInfo }: { proxyInfo: ProxyInfo }) {
const todayTotal = Number(proxyInfo.todayTrafficIn) + Number(proxyInfo.todayTrafficOut)
const historyTotal = Number(proxyInfo.historyTrafficIn) + Number(proxyInfo.historyTrafficOut)

return (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="tracking-tight text-sm font-medium">今日入站流量</CardTitle>
<CloudUpload className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatBytes(Number(proxyInfo.todayTrafficIn))}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="tracking-tight text-sm font-medium">今日出站流量</CardTitle>
<CloudDownload className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatBytes(Number(proxyInfo.todayTrafficOut))}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>今日总流量</CardTitle>
<CardTitle className="tracking-tight text-sm font-medium">历史入站流量</CardTitle>
<CloudUpload className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatBytes(todayTotal)}</div>
<div className="text-2xl font-bold">{formatBytes(Number(proxyInfo.historyTrafficIn))}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>历史总流量</CardTitle>
<CardTitle className="tracking-tight text-sm font-medium">历史出站流量</CardTitle>
<CloudDownload className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatBytes(historyTotal)}</div>
<div className="text-2xl font-bold">{formatBytes(Number(proxyInfo.historyTrafficOut))}</div>
</CardContent>
</Card>
</div>
Expand Down
74 changes: 74 additions & 0 deletions www/components/charts/proxy-traffic-pie-chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use client"

import { Label, Pie, PieChart } from "recharts"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { formatBytes } from "@/lib/utils"
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart"

const chartConfig = {
trafficIn: {
label: "入站",
},
trafficOut: {
label: "出站",
},
} satisfies ChartConfig

export function ProxyTrafficPieChart({ trafficIn, trafficOut, title, chartLabel }:
{ trafficIn: bigint,
trafficOut: bigint,
title: string,
chartLabel: string,
}) {
const data = [
{ type: "trafficIn", data: Number(trafficIn), fill: "hsl(var(--chart-1))" },
{ type: "trafficOut", data: Number(trafficOut), fill: "hsl(var(--chart-2))" }]

return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent className="font-mono">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel valueFormatter={(value) => formatBytes(Number(value))} />}
/>
<Pie data={data}
dataKey="data"
nameKey="type"
innerRadius={55} strokeWidth={10}>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
<tspan x={viewBox.cx} y={viewBox.cy} className="fill-foreground text-xl font-bold">
{formatBytes(Number(trafficIn) + Number(trafficOut))}
</tspan>
<tspan x={viewBox.cx} y={(viewBox.cy || 0) + 24} className="fill-muted-foreground" >
{chartLabel}
</tspan>
</text>
)
}
}}
/>
</Pie>
</PieChart>
</ChartContainer>
</CardContent>
</Card>
)
}

4 changes: 2 additions & 2 deletions www/components/frps_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ export const FRPSEditor: React.FC<FRPSFormProps> = ({ server, serverID }) => {
} catch (error) {
toast({ title: '更新失败' })
}
refetchServer()
}

useEffect(() => {
refetchServer()
try {
setConfigContent(
JSON.stringify(
Expand Down Expand Up @@ -71,7 +71,7 @@ export const FRPSEditor: React.FC<FRPSFormProps> = ({ server, serverID }) => {
setEditorValue('{}')
setServerComment('')
}
}, [serverResp, refetchServer])
}, [serverResp])

return (
<div className="grid w-full gap-1.5">
Expand Down
55 changes: 47 additions & 8 deletions www/components/stats/client_stats_card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Label } from '@/components/ui/label'
import { useQuery } from '@tanstack/react-query'
import { useSearchParams } from 'next/navigation'
import { getProxyStatsByClientID } from '@/api/stats'
import { ProxyTrafficBarChart } from '../charts/proxy-traffic-bar-chart'
import { ProxyTrafficPieChart } from '../charts/proxy-traffic-pie-chart'
import { ProxyTrafficOverview } from '../charts/proxy-traffic-overview'
import { ClientSelector } from '../base/client-selector'
import { ProxySelector } from '../base/proxy-selector'
Expand All @@ -19,6 +19,7 @@ export const ClientStatsCard: React.FC<ClientStatsCardProps> = ({ clientID: defa
const [clientID, setClientID] = useState<string | undefined>()
const [proxyName, setProxyName] = useState<string | undefined>()
const [status, setStatus] = useState<"loading" | "success" | "error" | undefined>()
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);

const searchParams = useSearchParams()
const paramClientID = searchParams.get('clientID')
Expand Down Expand Up @@ -46,6 +47,31 @@ export const ClientStatsCard: React.FC<ClientStatsCardProps> = ({ clientID: defa
setClientID(value)
}

const mergeProxyInfos = (proxyInfos: ProxyInfo[]): ProxyInfo[] => {
const mergedMap: Map<string, ProxyInfo> = new Map();

for (const proxyInfo of proxyInfos) {
const key = `${proxyInfo.clientId}:${proxyInfo.name}`;

if (!mergedMap.has(key)) {
mergedMap.set(key, { ...proxyInfo });
} else {
const existingProxyInfo = mergedMap.get(key)!;
existingProxyInfo.todayTrafficIn = (existingProxyInfo.todayTrafficIn || BigInt(0)) + (proxyInfo.todayTrafficIn || BigInt(0));
existingProxyInfo.todayTrafficOut = (existingProxyInfo.todayTrafficOut || BigInt(0)) + (proxyInfo.todayTrafficOut || BigInt(0));
existingProxyInfo.historyTrafficIn = (existingProxyInfo.historyTrafficIn || BigInt(0)) + (proxyInfo.historyTrafficIn || BigInt(0));
existingProxyInfo.historyTrafficOut = (existingProxyInfo.historyTrafficOut || BigInt(0)) + (proxyInfo.historyTrafficOut || BigInt(0));
}
}

return Array.from(mergedMap.values());
};

function removeDuplicateCharacters(input: string): string {
const uniqueChars = new Set(input);
return Array.from(uniqueChars).join('');
}

return (
<Card className="w-full">
<CardHeader>
Expand All @@ -65,12 +91,12 @@ export const ClientStatsCard: React.FC<ClientStatsCardProps> = ({ clientID: defa
<Label>隧道名称</Label>
<ProxySelector
// @ts-ignore
proxyNames={clientStatsList?.proxyInfos.map((proxyInfo) => proxyInfo.name).filter((value) => value !== undefined) || []}
proxyNames={Array.from(new Set(clientStatsList?.proxyInfos.map((proxyInfo) => proxyInfo.name).filter((value) => value !== undefined))) || []}
proxyName={proxyName}
setProxyname={setProxyName} />
<div className="w-full grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
<div className="w-full grid gap-4 grid-cols-1">
{clientStatsList && clientStatsList.proxyInfos.length > 0 &&
ProxyStatusCard(clientStatsList.proxyInfos.find((proxyInfo) => proxyInfo.name === proxyName))}
ProxyStatusCard(mergeProxyInfos(clientStatsList.proxyInfos).find((proxyInfo) => proxyInfo.name === proxyName))}
</div>
</CardContent>
<CardFooter>
Expand All @@ -81,10 +107,12 @@ export const ClientStatsCard: React.FC<ClientStatsCardProps> = ({ clientID: defa
}).catch(() => {
setStatus("error")
}).finally(() => {
const timer = setTimeout(() => {
if (timeoutId) { clearTimeout(timeoutId); }

const id = setTimeout(() => {
setStatus(undefined)
}, 3000)
return () => clearTimeout(timer)
setTimeoutId(id)
})
}}>
{status === "loading" && <RefreshCcw className="w-4 h-4 animate-spin" />}
Expand All @@ -98,10 +126,21 @@ export const ClientStatsCard: React.FC<ClientStatsCardProps> = ({ clientID: defa

const ProxyStatusCard = (proxyInfo: ProxyInfo | undefined) => {
return (<>{proxyInfo &&
<div key={proxyInfo.name} className="flex flex-col w-full space-y-4">
<div key={proxyInfo.name} className="flex flex-col space-y-4">
<Label>{`隧道 ${proxyInfo.name} 流量使用`}</Label>
<ProxyTrafficOverview proxyInfo={proxyInfo} />
<ProxyTrafficBarChart proxyInfo={proxyInfo} />
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4'>
<ProxyTrafficPieChart
title='今日流量统计'
chartLabel='今日总流量'
trafficIn={proxyInfo.todayTrafficIn || BigInt(0)}
trafficOut={proxyInfo.todayTrafficOut || BigInt(0)} />
<ProxyTrafficPieChart
title='历史流量统计'
chartLabel='历史总流量'
trafficIn={proxyInfo.historyTrafficIn || BigInt(0)}
trafficOut={proxyInfo.historyTrafficOut || BigInt(0)} />
</div>
</div>
}</>)
}
1 change: 1 addition & 0 deletions www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"sonner": "^1.3.1",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0",
"uuid": "^9.0.1",
"zod": "^3.22.4"
},
Expand Down
19 changes: 19 additions & 0 deletions www/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4edc320

Please sign in to comment.