mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-08 18:56:37 +08:00
feat(forward): display tunnel traffic ratio on forward page
This commit is contained in:
@@ -703,25 +703,26 @@ func (r *Repository) ListForwards() ([]map[string]interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fwdRow struct {
|
type fwdRow struct {
|
||||||
ID int64
|
ID int64
|
||||||
UserID int64
|
UserID int64
|
||||||
UserName string
|
UserName string
|
||||||
Name string
|
Name string
|
||||||
TunnelID int64
|
TunnelID int64
|
||||||
TunnelName string
|
TunnelName string
|
||||||
RemoteAddr string
|
TrafficRatio float64
|
||||||
Strategy string
|
RemoteAddr string
|
||||||
InFlow int64
|
Strategy string
|
||||||
OutFlow int64
|
InFlow int64
|
||||||
CreatedTime int64
|
OutFlow int64
|
||||||
Status int
|
CreatedTime int64
|
||||||
Inx int
|
Status int
|
||||||
SpeedID sql.NullInt64
|
Inx int
|
||||||
|
SpeedID sql.NullInt64
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows []fwdRow
|
var rows []fwdRow
|
||||||
err := r.db.Model(&model.Forward{}).
|
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").
|
Joins("LEFT JOIN tunnel ON tunnel.id = forward.tunnel_id").
|
||||||
Order("forward.inx ASC, forward.id ASC").
|
Order("forward.inx ASC, forward.id ASC").
|
||||||
Find(&rows).Error
|
Find(&rows).Error
|
||||||
@@ -738,7 +739,8 @@ func (r *Repository) ListForwards() ([]map[string]interface{}, error) {
|
|||||||
item := map[string]interface{}{
|
item := map[string]interface{}{
|
||||||
"id": row.ID, "userId": row.UserID, "userName": row.UserName,
|
"id": row.ID, "userId": row.UserID, "userName": row.UserName,
|
||||||
"name": row.Name, "tunnelId": row.TunnelID, "tunnelName": row.TunnelName,
|
"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,
|
"remoteAddr": row.RemoteAddr, "strategy": row.Strategy,
|
||||||
"inFlow": row.InFlow, "outFlow": row.OutFlow,
|
"inFlow": row.InFlow, "outFlow": row.OutFlow,
|
||||||
"createdTime": row.CreatedTime, "status": row.Status, "inx": int64(row.Inx),
|
"createdTime": row.CreatedTime, "status": row.Status, "inx": int64(row.Inx),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func TestForwardOwnershipAndScopeContracts(t *testing.T) {
|
|||||||
if err := repo.DB().Exec(`
|
if err := repo.DB().Exec(`
|
||||||
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
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)
|
t.Fatalf("insert tunnel: %v", err)
|
||||||
}
|
}
|
||||||
tunnelID := mustLastInsertID(t, repo, "contract-tunnel")
|
tunnelID := mustLastInsertID(t, repo, "contract-tunnel")
|
||||||
@@ -118,6 +118,13 @@ func TestForwardOwnershipAndScopeContracts(t *testing.T) {
|
|||||||
if got := int64(idFloat); got != userForwardID {
|
if got := int64(idFloat); got != userForwardID {
|
||||||
t.Fatalf("expected forward id %d, got %d", userForwardID, got)
|
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) {
|
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
|
||||||
@@ -47,6 +47,7 @@ export interface ForwardApiItem {
|
|||||||
name: string;
|
name: string;
|
||||||
status: number;
|
status: number;
|
||||||
tunnelName?: string;
|
tunnelName?: string;
|
||||||
|
tunnelTrafficRatio?: number;
|
||||||
inIp?: string;
|
inIp?: string;
|
||||||
inPort?: number;
|
inPort?: number;
|
||||||
remoteAddr?: string;
|
remoteAddr?: string;
|
||||||
|
|||||||
@@ -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 { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
@@ -104,6 +104,7 @@ interface Forward {
|
|||||||
name: string;
|
name: string;
|
||||||
tunnelId: number;
|
tunnelId: number;
|
||||||
tunnelName: string;
|
tunnelName: string;
|
||||||
|
tunnelTrafficRatio?: number;
|
||||||
inIp: string;
|
inIp: string;
|
||||||
inPort: number;
|
inPort: number;
|
||||||
remoteAddr: string;
|
remoteAddr: string;
|
||||||
@@ -164,6 +165,7 @@ interface ForwardUserGroup {
|
|||||||
interface ForwardTunnelGroup {
|
interface ForwardTunnelGroup {
|
||||||
tunnelKey: string;
|
tunnelKey: string;
|
||||||
tunnelName: string;
|
tunnelName: string;
|
||||||
|
tunnelTrafficRatio?: number;
|
||||||
items: Forward[];
|
items: Forward[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +247,32 @@ const compareForwardTunnelGroupKeyAsc = (a: string, b: string): number => {
|
|||||||
return compareForwardTunnelNameAsc(a, b);
|
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 => {
|
const buildForwardGroupOrderLocalKey = (tokenUserId: number): string => {
|
||||||
return `${FORWARD_GROUP_ORDER_LOCAL_STORAGE_PREFIX}:u:${tokenUserId}`;
|
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) => ({
|
return (items || []).map((forward) => ({
|
||||||
...forward,
|
id: forward.id,
|
||||||
|
name: forward.name,
|
||||||
tunnelId: forward.tunnelId ?? 0,
|
tunnelId: forward.tunnelId ?? 0,
|
||||||
tunnelName: forward.tunnelName || "",
|
tunnelName: forward.tunnelName || "",
|
||||||
|
tunnelTrafficRatio: normalizeTunnelTrafficRatio(
|
||||||
|
forward.tunnelTrafficRatio,
|
||||||
|
),
|
||||||
inIp: forward.inIp || "",
|
inIp: forward.inIp || "",
|
||||||
inPort: forward.inPort ?? 0,
|
inPort: forward.inPort ?? 0,
|
||||||
remoteAddr: forward.remoteAddr || "",
|
remoteAddr: forward.remoteAddr || "",
|
||||||
strategy: forward.strategy || "fifo",
|
strategy: typeof forward.strategy === "string" ? forward.strategy : "fifo",
|
||||||
status: typeof forward.status === "number" ? forward.status : 0,
|
status: typeof forward.status === "number" ? forward.status : 0,
|
||||||
inFlow: forward.inFlow ?? 0,
|
inFlow: forward.inFlow ?? 0,
|
||||||
outFlow: forward.outFlow ?? 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,
|
serviceRunning: forward.status === 1,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -2727,6 +2768,9 @@ export default function ForwardPage() {
|
|||||||
existingGroup.tunnelMap.set(tunnelKey, {
|
existingGroup.tunnelMap.set(tunnelKey, {
|
||||||
tunnelKey,
|
tunnelKey,
|
||||||
tunnelName,
|
tunnelName,
|
||||||
|
tunnelTrafficRatio: normalizeTunnelTrafficRatio(
|
||||||
|
forward.tunnelTrafficRatio,
|
||||||
|
),
|
||||||
items: [forward],
|
items: [forward],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2741,6 +2785,15 @@ export default function ForwardPage() {
|
|||||||
) {
|
) {
|
||||||
existingTunnelGroup.tunnelName = tunnelName;
|
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) => {
|
const groups = Array.from(userGroupMap.values()).map((group) => {
|
||||||
@@ -2917,6 +2970,14 @@ export default function ForwardPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
<span className={titleClassName}>{tunnel.tunnelName}</span>
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={countClassName}>{tunnel.items.length} 条规则</span>
|
<span className={countClassName}>{tunnel.items.length} 条规则</span>
|
||||||
@@ -3312,6 +3373,9 @@ export default function ForwardPage() {
|
|||||||
<span className="font-medium text-secondary-700">
|
<span className="font-medium text-secondary-700">
|
||||||
{forward.tunnelName}
|
{forward.tunnelName}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="ml-1 text-secondary-600/80">
|
||||||
|
{formatTunnelTrafficRatio(forward.tunnelTrafficRatio)}
|
||||||
|
</span>
|
||||||
</Chip>
|
</Chip>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-[220px]">
|
<TableCell className="max-w-[220px]">
|
||||||
@@ -3467,7 +3531,7 @@ export default function ForwardPage() {
|
|||||||
{forward.name}
|
{forward.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-default-500 truncate">
|
<p className="text-xs text-default-500 truncate">
|
||||||
{normalizeForwardTunnelName(forward.tunnelName)}
|
{`${normalizeForwardTunnelName(forward.tunnelName)} · ${formatTunnelTrafficRatio(forward.tunnelTrafficRatio)}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 ml-2">
|
<div className="flex items-center gap-1.5 ml-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user