feat(forward): display tunnel traffic ratio on forward page

This commit is contained in:
sagitchu
2026-03-08 20:00:26 +08:00
parent 31ef861504
commit d60655045a
5 changed files with 103 additions and 23 deletions
+18 -16
View File
@@ -703,25 +703,26 @@ func (r *Repository) ListForwards() ([]map[string]interface{}, error) {
}
type fwdRow struct {
ID int64
UserID int64
UserName string
Name string
TunnelID int64
TunnelName string
RemoteAddr string
Strategy string
InFlow int64
OutFlow int64
CreatedTime int64
Status int
Inx int
SpeedID sql.NullInt64
ID int64
UserID int64
UserName string
Name string
TunnelID int64
TunnelName string
TrafficRatio float64
RemoteAddr string
Strategy string
InFlow int64
OutFlow int64
CreatedTime int64
Status int
Inx int
SpeedID sql.NullInt64
}
var rows []fwdRow
err := r.db.Model(&model.Forward{}).
Select("forward.id, forward.user_id, forward.user_name, forward.name, forward.tunnel_id, COALESCE(tunnel.name, '') AS tunnel_name, forward.remote_addr, COALESCE(forward.strategy, 'fifo') AS strategy, forward.in_flow, forward.out_flow, forward.created_time, forward.status, forward.inx, forward.speed_id").
Select("forward.id, forward.user_id, forward.user_name, forward.name, forward.tunnel_id, COALESCE(tunnel.name, '') AS tunnel_name, COALESCE(tunnel.traffic_ratio, 1.0) AS traffic_ratio, forward.remote_addr, COALESCE(forward.strategy, 'fifo') AS strategy, forward.in_flow, forward.out_flow, forward.created_time, forward.status, forward.inx, forward.speed_id").
Joins("LEFT JOIN tunnel ON tunnel.id = forward.tunnel_id").
Order("forward.inx ASC, forward.id ASC").
Find(&rows).Error
@@ -738,7 +739,8 @@ func (r *Repository) ListForwards() ([]map[string]interface{}, error) {
item := map[string]interface{}{
"id": row.ID, "userId": row.UserID, "userName": row.UserName,
"name": row.Name, "tunnelId": row.TunnelID, "tunnelName": row.TunnelName,
"inIp": nullableForwardIngress(inIP), "inPort": nullableInt64(inPort),
"tunnelTrafficRatio": row.TrafficRatio,
"inIp": nullableForwardIngress(inIP), "inPort": nullableInt64(inPort),
"remoteAddr": row.RemoteAddr, "strategy": row.Strategy,
"inFlow": row.InFlow, "outFlow": row.OutFlow,
"createdTime": row.CreatedTime, "status": row.Status, "inx": int64(row.Inx),
@@ -31,7 +31,7 @@ func TestForwardOwnershipAndScopeContracts(t *testing.T) {
if err := repo.DB().Exec(`
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, "contract-tunnel", 1.0, 1, "tls", 99999, now, now, 1, nil, 0).Error; err != nil {
`, "contract-tunnel", 2.5, 1, "tls", 99999, now, now, 1, nil, 0).Error; err != nil {
t.Fatalf("insert tunnel: %v", err)
}
tunnelID := mustLastInsertID(t, repo, "contract-tunnel")
@@ -118,6 +118,13 @@ func TestForwardOwnershipAndScopeContracts(t *testing.T) {
if got := int64(idFloat); got != userForwardID {
t.Fatalf("expected forward id %d, got %d", userForwardID, got)
}
ratioFloat, ok := item["tunnelTrafficRatio"].(float64)
if !ok {
t.Fatalf("expected tunnelTrafficRatio to be float64, got %T", item["tunnelTrafficRatio"])
}
if ratioFloat != 2.5 {
t.Fatalf("expected tunnelTrafficRatio 2.5, got %v", ratioFloat)
}
})
t.Run("forward diagnose returns structured payload", func(t *testing.T) {
@@ -0,0 +1,6 @@
# Forward Page Tunnel Traffic Ratio Plan
- [x] Review `/forward/list` data flow and rule page render points for tunnel ratio support
- [x] Extend backend forward list payload with tunnel traffic ratio and cover it with a contract test
- [x] Update forward page types, mapping, grouped metadata, and visible ratio UI across list modes
- [x] Verify targeted backend tests and frontend build, then mark the plan complete
+1
View File
@@ -47,6 +47,7 @@ export interface ForwardApiItem {
name: string;
status: number;
tunnelName?: string;
tunnelTrafficRatio?: number;
inIp?: string;
inPort?: number;
remoteAddr?: string;
+70 -6
View File
@@ -1,4 +1,4 @@
import type { SpeedLimitApiItem } from "@/api/types";
import type { ForwardApiItem, SpeedLimitApiItem } from "@/api/types";
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
import toast from "react-hot-toast";
@@ -104,6 +104,7 @@ interface Forward {
name: string;
tunnelId: number;
tunnelName: string;
tunnelTrafficRatio?: number;
inIp: string;
inPort: number;
remoteAddr: string;
@@ -164,6 +165,7 @@ interface ForwardUserGroup {
interface ForwardTunnelGroup {
tunnelKey: string;
tunnelName: string;
tunnelTrafficRatio?: number;
items: Forward[];
}
@@ -245,6 +247,32 @@ const compareForwardTunnelGroupKeyAsc = (a: string, b: string): number => {
return compareForwardTunnelNameAsc(a, b);
};
const normalizeTunnelTrafficRatio = (value: unknown): number => {
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
return value;
}
if (typeof value === "string") {
const parsed = Number(value);
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
}
return 1;
};
const formatTunnelTrafficRatio = (value?: number): string => {
const ratio = normalizeTunnelTrafficRatio(value);
if (Number.isInteger(ratio)) {
return `${ratio}x`;
}
return `${parseFloat(ratio.toFixed(2))}x`;
};
const buildForwardGroupOrderLocalKey = (tokenUserId: number): string => {
return `${FORWARD_GROUP_ORDER_LOCAL_STORAGE_PREFIX}:u:${tokenUserId}`;
};
@@ -504,19 +532,32 @@ const normalizeForwardItems = (items: Forward[]): Forward[] => {
}));
};
const mapForwardApiItems = (items: any[]): Forward[] => {
const mapForwardApiItems = (items: ForwardApiItem[]): Forward[] => {
return (items || []).map((forward) => ({
...forward,
id: forward.id,
name: forward.name,
tunnelId: forward.tunnelId ?? 0,
tunnelName: forward.tunnelName || "",
tunnelTrafficRatio: normalizeTunnelTrafficRatio(
forward.tunnelTrafficRatio,
),
inIp: forward.inIp || "",
inPort: forward.inPort ?? 0,
remoteAddr: forward.remoteAddr || "",
strategy: forward.strategy || "fifo",
strategy: typeof forward.strategy === "string" ? forward.strategy : "fifo",
status: typeof forward.status === "number" ? forward.status : 0,
inFlow: forward.inFlow ?? 0,
outFlow: forward.outFlow ?? 0,
createdTime: forward.createdTime || "",
createdTime:
typeof forward.createdTime === "string" ? forward.createdTime : "",
userName:
typeof forward.userName === "string" ? forward.userName : undefined,
userId: typeof forward.userId === "number" ? forward.userId : undefined,
inx: typeof forward.inx === "number" ? forward.inx : undefined,
speedId:
typeof forward.speedId === "number" || forward.speedId === null
? forward.speedId
: undefined,
serviceRunning: forward.status === 1,
}));
};
@@ -2727,6 +2768,9 @@ export default function ForwardPage() {
existingGroup.tunnelMap.set(tunnelKey, {
tunnelKey,
tunnelName,
tunnelTrafficRatio: normalizeTunnelTrafficRatio(
forward.tunnelTrafficRatio,
),
items: [forward],
});
@@ -2741,6 +2785,15 @@ export default function ForwardPage() {
) {
existingTunnelGroup.tunnelName = tunnelName;
}
if (
normalizeTunnelTrafficRatio(existingTunnelGroup.tunnelTrafficRatio) === 1 &&
normalizeTunnelTrafficRatio(forward.tunnelTrafficRatio) !== 1
) {
existingTunnelGroup.tunnelTrafficRatio = normalizeTunnelTrafficRatio(
forward.tunnelTrafficRatio,
);
}
});
const groups = Array.from(userGroupMap.values()).map((group) => {
@@ -2917,6 +2970,14 @@ export default function ForwardPage() {
</svg>
</Button>
<span className={titleClassName}>{tunnel.tunnelName}</span>
<Chip
className="h-5 border-none bg-secondary/15 px-1.5 text-[11px] font-semibold text-secondary-700"
color="secondary"
size="sm"
variant="flat"
>
{formatTunnelTrafficRatio(tunnel.tunnelTrafficRatio)}
</Chip>
</div>
<div className="flex items-center gap-2">
<span className={countClassName}>{tunnel.items.length} 条规则</span>
@@ -3312,6 +3373,9 @@ export default function ForwardPage() {
<span className="font-medium text-secondary-700">
{forward.tunnelName}
</span>
<span className="ml-1 text-secondary-600/80">
{formatTunnelTrafficRatio(forward.tunnelTrafficRatio)}
</span>
</Chip>
</TableCell>
<TableCell className="max-w-[220px]">
@@ -3467,7 +3531,7 @@ export default function ForwardPage() {
{forward.name}
</h3>
<p className="text-xs text-default-500 truncate">
{normalizeForwardTunnelName(forward.tunnelName)}
{`${normalizeForwardTunnelName(forward.tunnelName)} · ${formatTunnelTrafficRatio(forward.tunnelTrafficRatio)}`}
</p>
</div>
<div className="flex items-center gap-1.5 ml-2">