Files
hi-frontend/apps/admin/src/sections/group/node-groups.tsx
T
shanshanzhong147 d6616c5859 merge: 同步 upstream/main 新功能到定制版本
- feat: Add slider verification code (bd67997)
- fix bug: Inventory cannot be zero (1f7a6ee)
- fix: resolve merge conflicts and lint errors
2026-03-23 21:50:10 -07:00

252 lines
8.8 KiB
TypeScript

"use client";
import { Badge } from "@workspace/ui/components/badge";
import { Button } from "@workspace/ui/components/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@workspace/ui/components/card";
import { ConfirmButton } from "@workspace/ui/composed/confirm-button";
import {
ProTable,
type ProTableActions,
} from "@workspace/ui/composed/pro-table/pro-table";
import {
createNodeGroup,
deleteNodeGroup,
getNodeGroupList,
updateNodeGroup,
} from "@workspace/ui/services/admin/group";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import NodeGroupForm from "./node-group-form";
export default function NodeGroups() {
const { t } = useTranslation("group");
const [loading, setLoading] = useState(false);
const [allNodeGroups, setAllNodeGroups] = useState<API.NodeGroup[]>([]);
const ref = useRef<ProTableActions>(null);
// 获取所有节点组数据(用于冲突检测)
useEffect(() => {
const fetchAllNodeGroups = async () => {
try {
const { data } = await getNodeGroupList({ page: 1, size: 1000 });
setAllNodeGroups(data.data?.list || []);
} catch (error) {
console.error("Failed to fetch node groups:", error);
}
};
fetchAllNodeGroups();
}, []);
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>{t("nodeGroups", "Node Groups")}</CardTitle>
<CardDescription>
{t(
"nodeGroupsDescription",
"Manage node groups for user access control"
)}
</CardDescription>
</CardHeader>
<CardContent>
<ProTable<API.NodeGroup, API.GetNodeGroupListRequest>
action={ref}
actions={{
render: (row: any) => [
<NodeGroupForm
allNodeGroups={allNodeGroups}
currentGroupId={row.id}
initialValues={row}
key={`edit-${row.id}`}
loading={loading}
onSubmit={async (values) => {
setLoading(true);
try {
await updateNodeGroup({
id: row.id,
...values,
} as API.UpdateNodeGroupRequest);
toast.success(t("updated", "Updated successfully"));
// 刷新节点组列表
const { data } = await getNodeGroupList({
page: 1,
size: 1000,
});
setAllNodeGroups(data.data?.list || []);
ref.current?.refresh();
setLoading(false);
return true;
} catch {
setLoading(false);
return false;
}
}}
title={t("editNodeGroup", "Edit Node Group")}
trigger={
<Button size="sm" variant="outline">
{t("edit", "Edit")}
</Button>
}
/>,
<ConfirmButton
cancelText={t("cancel", "Cancel")}
confirmText={t("confirm", "Confirm")}
description={t(
"deleteNodeGroupConfirm",
"This will delete the node group. Nodes in this group will be reassigned."
)}
key="delete"
onConfirm={async () => {
await deleteNodeGroup({ id: row.id });
toast.success(t("deleted", "Deleted successfully"));
// 刷新节点组列表
const { data } = await getNodeGroupList({
page: 1,
size: 1000,
});
setAllNodeGroups(data.data?.list || []);
ref.current?.refresh();
setLoading(false);
}}
title={t("confirmDelete", "Confirm Delete")}
trigger={
<Button size="sm" variant="destructive">
{t("delete", "Delete")}
</Button>
}
/>,
],
}}
columns={[
{
id: "id",
accessorKey: "id",
header: t("id", "ID"),
cell: ({ row }: { row: any }) => (
<span className="text-muted-foreground">
#{row.getValue("id")}
</span>
),
},
{
id: "name",
accessorKey: "name",
header: t("name", "Name"),
cell: ({ row }: { row: any }) => {
const isExpiredGroup = row.original.is_expired_group;
return (
<div className="flex items-center gap-2">
<span>{row.getValue("name")}</span>
{isExpiredGroup && (
<Badge variant="destructive">
{t("expiredGroup", "Expired")}
</Badge>
)}
</div>
);
},
},
{
id: "description",
accessorKey: "description",
header: t("description", "Description"),
cell: ({ row }: { row: any }) =>
row.getValue("description") || "--",
},
{
id: "for_calculation",
accessorKey: "for_calculation",
header: t("forCalculation", "For Calculation"),
cell: ({ row }: { row: any }) => {
const value = row.getValue("for_calculation");
return value ? (
<Badge variant="default">{t("yes", "Yes")}</Badge>
) : (
<Badge variant="secondary">{t("no", "No")}</Badge>
);
},
},
{
id: "traffic_range",
header: t("trafficRange", "Traffic Range (GB)"),
cell: ({ row }: { row: any }) => {
const min = row.original.min_traffic_gb;
const max = row.original.max_traffic_gb;
if (min !== undefined && max !== undefined) {
return `${min} - ${max}`;
}
if (min !== undefined) {
return `${min}`;
}
if (max !== undefined) {
return `${max}`;
}
return "--";
},
},
{
id: "sort",
accessorKey: "sort",
header: t("sort", "Sort"),
},
]}
header={{
title: t("nodeGroups", "Node Groups"),
toolbar: (
<NodeGroupForm
allNodeGroups={allNodeGroups}
currentGroupId={undefined}
initialValues={undefined}
key="create"
loading={loading}
onSubmit={async (values) => {
setLoading(true);
try {
await createNodeGroup(
values as API.CreateNodeGroupRequest
);
toast.success(t("created", "Created successfully"));
// 刷新节点组列表
const { data } = await getNodeGroupList({
page: 1,
size: 1000,
});
setAllNodeGroups(data.data?.list || []);
ref.current?.refresh();
setLoading(false);
return true;
} catch {
setLoading(false);
return false;
}
}}
title={t("createNodeGroup", "Create Node Group")}
trigger={<Button>{t("create", "Create")}</Button>}
/>
),
}}
request={async (params) => {
const { data } = await getNodeGroupList({
page: params.page || 1,
size: params.size || 10,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
/>
</CardContent>
</Card>
</div>
);
}