mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-07 05:14:27 +00:00
feat(groups): show used traffic per group in groups table
Sum up+down across each group's clients via a LEFT JOIN on email in ListGroups, expose it as trafficUsed on GroupSummary, and render it as a new column plus a "Total traffic" summary card. Drops the unused "Empty groups" card and its translation key.
This commit is contained in:
@@ -41,7 +41,7 @@ import { useTheme } from '@/hooks/useTheme';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { usePageTitle } from '@/hooks/usePageTitle';
|
||||
import { useClients } from '@/hooks/useClients';
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { HttpUtil, SizeFormatter } from '@/utils';
|
||||
import { setMessageInstance } from '@/utils/messageBus';
|
||||
import AppSidebar from '@/layouts/AppSidebar';
|
||||
import { LazyMount } from '@/components/utility';
|
||||
@@ -161,8 +161,8 @@ export default function GroupsPage() {
|
||||
() => groups.reduce((acc, g) => acc + (g.clientCount || 0), 0),
|
||||
[groups],
|
||||
);
|
||||
const emptyGroups = useMemo(
|
||||
() => groups.filter((g) => (g.clientCount || 0) === 0).length,
|
||||
const totalTraffic = useMemo(
|
||||
() => groups.reduce((acc, g) => acc + (g.trafficUsed || 0), 0),
|
||||
[groups],
|
||||
);
|
||||
|
||||
@@ -417,6 +417,13 @@ export default function GroupsPage() {
|
||||
width: 180,
|
||||
render: (count: number) => <span>{count || 0}</span>,
|
||||
},
|
||||
{
|
||||
title: t('pages.groups.trafficUsed'),
|
||||
dataIndex: 'trafficUsed',
|
||||
key: 'trafficUsed',
|
||||
width: 160,
|
||||
render: (bytes: number) => <span>{SizeFormatter.sizeFormat(bytes || 0)}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
const pageClass = useMemo(() => {
|
||||
@@ -465,8 +472,9 @@ export default function GroupsPage() {
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={6}>
|
||||
<Statistic
|
||||
title={t('pages.groups.emptyGroups')}
|
||||
value={String(emptyGroups)}
|
||||
title={t('pages.groups.totalTraffic')}
|
||||
value={SizeFormatter.sizeFormat(totalTraffic)}
|
||||
prefix={<RetweetOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -126,6 +126,7 @@ export const ActiveInboundsByNodeSchema = z
|
||||
export const GroupSummarySchema = z.object({
|
||||
name: z.string(),
|
||||
clientCount: z.number(),
|
||||
trafficUsed: z.number().nullable().transform((v) => v ?? 0),
|
||||
});
|
||||
|
||||
export const GroupSummaryListSchema = z.array(GroupSummarySchema).nullable().transform((v) => v ?? []);
|
||||
|
||||
+17
-9
@@ -1714,15 +1714,19 @@ func (s *ClientService) ListPaged(inboundSvc *InboundService, settingSvc *Settin
|
||||
type GroupSummary struct {
|
||||
Name string `json:"name"`
|
||||
ClientCount int `json:"clientCount"`
|
||||
TrafficUsed int64 `json:"trafficUsed"`
|
||||
}
|
||||
|
||||
func (s *ClientService) ListGroups() ([]GroupSummary, error) {
|
||||
db := database.GetDB()
|
||||
// email is unique in both clients and client_traffics, so the LEFT JOIN
|
||||
// never double-counts a client's traffic.
|
||||
var derived []GroupSummary
|
||||
if err := db.Model(&model.ClientRecord{}).
|
||||
Select("group_name AS name, COUNT(*) AS client_count").
|
||||
Where("group_name <> ''").
|
||||
Group("group_name").
|
||||
if err := db.Table("clients AS c").
|
||||
Select("c.group_name AS name, COUNT(*) AS client_count, COALESCE(SUM(ct.up + ct.down), 0) AS traffic_used").
|
||||
Joins("LEFT JOIN client_traffics ct ON ct.email = c.email").
|
||||
Where("c.group_name <> ''").
|
||||
Group("c.group_name").
|
||||
Scan(&derived).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1730,16 +1734,20 @@ func (s *ClientService) ListGroups() ([]GroupSummary, error) {
|
||||
if err := db.Find(&stored).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
merged := make(map[string]int, len(derived)+len(stored))
|
||||
type groupAgg struct {
|
||||
count int
|
||||
traffic int64
|
||||
}
|
||||
merged := make(map[string]groupAgg, len(derived)+len(stored))
|
||||
for _, g := range stored {
|
||||
merged[g.Name] = 0
|
||||
merged[g.Name] = groupAgg{}
|
||||
}
|
||||
for _, g := range derived {
|
||||
merged[g.Name] = g.ClientCount
|
||||
merged[g.Name] = groupAgg{count: g.ClientCount, traffic: g.TrafficUsed}
|
||||
}
|
||||
out := make([]GroupSummary, 0, len(merged))
|
||||
for name, count := range merged {
|
||||
out = append(out, GroupSummary{Name: name, ClientCount: count})
|
||||
for name, agg := range merged {
|
||||
out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: agg.traffic})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "عملاء في المجموعة",
|
||||
"totalGroups": "إجمالي المجموعات",
|
||||
"totalGroupedClients": "العملاء بمجموعة",
|
||||
"emptyGroups": "مجموعات فارغة",
|
||||
"trafficUsed": "حركة المرور المستخدمة",
|
||||
"totalTraffic": "إجمالي حركة المرور",
|
||||
"addGroup": "إضافة مجموعة",
|
||||
"createSuccess": "تم إنشاء المجموعة «{name}».",
|
||||
"rename": "إعادة تسمية",
|
||||
|
||||
@@ -813,7 +813,8 @@
|
||||
"clientCount": "Clients in group",
|
||||
"totalGroups": "Total groups",
|
||||
"totalGroupedClients": "Clients with a group",
|
||||
"emptyGroups": "Empty groups",
|
||||
"trafficUsed": "Traffic used",
|
||||
"totalTraffic": "Total traffic",
|
||||
"addGroup": "Add Group",
|
||||
"createSuccess": "Group \"{name}\" created.",
|
||||
"rename": "Rename",
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "Clientes en el grupo",
|
||||
"totalGroups": "Total de grupos",
|
||||
"totalGroupedClients": "Clientes con grupo",
|
||||
"emptyGroups": "Grupos vacíos",
|
||||
"trafficUsed": "Tráfico usado",
|
||||
"totalTraffic": "Tráfico total",
|
||||
"addGroup": "Añadir grupo",
|
||||
"createSuccess": "Grupo «{name}» creado.",
|
||||
"rename": "Renombrar",
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "کاربران در گروه",
|
||||
"totalGroups": "تعداد گروهها",
|
||||
"totalGroupedClients": "کاربران دارای گروه",
|
||||
"emptyGroups": "گروههای خالی",
|
||||
"trafficUsed": "ترافیک مصرفشده",
|
||||
"totalTraffic": "مجموع ترافیک",
|
||||
"addGroup": "افزودن گروه",
|
||||
"createSuccess": "گروه «{name}» ایجاد شد.",
|
||||
"rename": "تغییر نام",
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "Klien di grup",
|
||||
"totalGroups": "Total grup",
|
||||
"totalGroupedClients": "Klien dengan grup",
|
||||
"emptyGroups": "Grup kosong",
|
||||
"trafficUsed": "Trafik terpakai",
|
||||
"totalTraffic": "Total trafik",
|
||||
"addGroup": "Tambah grup",
|
||||
"createSuccess": "Grup «{name}» dibuat.",
|
||||
"rename": "Ubah nama",
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "グループ内のクライアント",
|
||||
"totalGroups": "グループ合計",
|
||||
"totalGroupedClients": "グループのあるクライアント",
|
||||
"emptyGroups": "空のグループ",
|
||||
"trafficUsed": "使用済みトラフィック",
|
||||
"totalTraffic": "合計トラフィック",
|
||||
"addGroup": "グループ追加",
|
||||
"createSuccess": "グループ「{name}」を作成しました。",
|
||||
"rename": "名前変更",
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "Clientes no grupo",
|
||||
"totalGroups": "Total de grupos",
|
||||
"totalGroupedClients": "Clientes com grupo",
|
||||
"emptyGroups": "Grupos vazios",
|
||||
"trafficUsed": "Tráfego usado",
|
||||
"totalTraffic": "Tráfego total",
|
||||
"addGroup": "Adicionar grupo",
|
||||
"createSuccess": "Grupo «{name}» criado.",
|
||||
"rename": "Renomear",
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "Клиентов в группе",
|
||||
"totalGroups": "Всего групп",
|
||||
"totalGroupedClients": "Клиенты с группой",
|
||||
"emptyGroups": "Пустые группы",
|
||||
"trafficUsed": "Использованный трафик",
|
||||
"totalTraffic": "Общий трафик",
|
||||
"addGroup": "Добавить группу",
|
||||
"createSuccess": "Группа «{name}» создана.",
|
||||
"rename": "Переименовать",
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "Gruptaki kullanıcılar",
|
||||
"totalGroups": "Toplam grup",
|
||||
"totalGroupedClients": "Grubu olan kullanıcılar",
|
||||
"emptyGroups": "Boş gruplar",
|
||||
"trafficUsed": "Kullanılan trafik",
|
||||
"totalTraffic": "Toplam trafik",
|
||||
"addGroup": "Grup ekle",
|
||||
"createSuccess": "«{name}» grubu oluşturuldu.",
|
||||
"rename": "Yeniden adlandır",
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "Клієнтів у групі",
|
||||
"totalGroups": "Всього груп",
|
||||
"totalGroupedClients": "Клієнти з групою",
|
||||
"emptyGroups": "Порожні групи",
|
||||
"trafficUsed": "Використаний трафік",
|
||||
"totalTraffic": "Загальний трафік",
|
||||
"addGroup": "Додати групу",
|
||||
"createSuccess": "Групу «{name}» створено.",
|
||||
"rename": "Перейменувати",
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "Client trong nhóm",
|
||||
"totalGroups": "Tổng số nhóm",
|
||||
"totalGroupedClients": "Client có nhóm",
|
||||
"emptyGroups": "Nhóm trống",
|
||||
"trafficUsed": "Lưu lượng đã dùng",
|
||||
"totalTraffic": "Tổng lưu lượng",
|
||||
"addGroup": "Thêm nhóm",
|
||||
"createSuccess": "Đã tạo nhóm «{name}».",
|
||||
"rename": "Đổi tên",
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "分组中的客户端",
|
||||
"totalGroups": "分组总数",
|
||||
"totalGroupedClients": "有分组的客户端",
|
||||
"emptyGroups": "空分组",
|
||||
"trafficUsed": "已用流量",
|
||||
"totalTraffic": "总流量",
|
||||
"addGroup": "添加分组",
|
||||
"createSuccess": "已创建分组 “{name}”。",
|
||||
"rename": "重命名",
|
||||
|
||||
@@ -812,7 +812,8 @@
|
||||
"clientCount": "群組中的客戶端",
|
||||
"totalGroups": "群組總數",
|
||||
"totalGroupedClients": "有群組的客戶端",
|
||||
"emptyGroups": "空群組",
|
||||
"trafficUsed": "已用流量",
|
||||
"totalTraffic": "總流量",
|
||||
"addGroup": "新增群組",
|
||||
"createSuccess": "已建立群組「{name}」。",
|
||||
"rename": "重新命名",
|
||||
|
||||
Reference in New Issue
Block a user