292 lines
8.9 KiB
TypeScript
292 lines
8.9 KiB
TypeScript
'use client';
|
|
|
|
import { Display } from '@/components/display';
|
|
import { ProTable, ProTableActions } from '@/components/pro-table';
|
|
import {
|
|
batchDeleteNode,
|
|
createNode,
|
|
deleteNode,
|
|
getNodeGroupList,
|
|
getNodeList,
|
|
nodeSort,
|
|
updateNode,
|
|
} from '@/services/admin/server';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { Badge } from '@workspace/ui/components/badge';
|
|
import { Button } from '@workspace/ui/components/button';
|
|
import { Switch } from '@workspace/ui/components/switch';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@workspace/ui/components/tooltip';
|
|
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
|
import { cn } from '@workspace/ui/lib/utils';
|
|
import { useTranslations } from 'next-intl';
|
|
import { useRef, useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
import NodeForm from './node-form';
|
|
import { NodeStatusCell } from './node-status';
|
|
|
|
export default function NodeTable() {
|
|
const t = useTranslations('server.node');
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const { data: groups } = useQuery({
|
|
queryKey: ['getNodeGroupList'],
|
|
queryFn: async () => {
|
|
const { data } = await getNodeGroupList();
|
|
return (data.data?.list || []) as API.ServerGroup[];
|
|
},
|
|
});
|
|
|
|
const ref = useRef<ProTableActions>(null);
|
|
|
|
return (
|
|
<ProTable<API.Server, { groupId: number; search: string }>
|
|
action={ref}
|
|
header={{
|
|
toolbar: (
|
|
<NodeForm<API.CreateNodeRequest>
|
|
trigger={t('create')}
|
|
title={t('createNode')}
|
|
loading={loading}
|
|
onSubmit={async (values) => {
|
|
setLoading(true);
|
|
try {
|
|
await createNode({ ...values, enable: false });
|
|
toast.success(t('createSuccess'));
|
|
ref.current?.refresh();
|
|
setLoading(false);
|
|
return true;
|
|
} catch (error) {
|
|
setLoading(false);
|
|
|
|
return false;
|
|
}
|
|
}}
|
|
/>
|
|
),
|
|
}}
|
|
columns={[
|
|
{
|
|
accessorKey: 'id',
|
|
header: 'ID',
|
|
cell: ({ row }) => (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<Badge
|
|
variant='outline'
|
|
className={cn('text-primary-foreground', {
|
|
'bg-green-500': row.original.protocol === 'shadowsocks',
|
|
'bg-rose-500': row.original.protocol === 'vmess',
|
|
'bg-blue-500': row.original.protocol === 'vless',
|
|
'bg-yellow-500': row.original.protocol === 'trojan',
|
|
'bg-purple-500': row.original.protocol === 'hysteria2',
|
|
'bg-cyan-500': row.original.protocol === 'tuic',
|
|
})}
|
|
>
|
|
{row.getValue('id')}
|
|
</Badge>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{row.original.protocol}</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'enable',
|
|
header: t('enable'),
|
|
cell: ({ row }) => {
|
|
return (
|
|
<Switch
|
|
checked={row.getValue('enable')}
|
|
onCheckedChange={async (checked) => {
|
|
await updateNode({
|
|
...row.original,
|
|
id: row.original.id!,
|
|
enable: checked,
|
|
} as API.UpdateNodeRequest);
|
|
ref.current?.refresh();
|
|
}}
|
|
/>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'status',
|
|
header: t('status'),
|
|
cell: ({ row }) => {
|
|
return <NodeStatusCell status={row.original?.status} />;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'name',
|
|
header: t('name'),
|
|
},
|
|
{
|
|
accessorKey: 'server_addr',
|
|
header: t('serverAddr'),
|
|
},
|
|
{
|
|
accessorKey: 'speed_limit',
|
|
header: t('speedLimit'),
|
|
cell: ({ row }) => (
|
|
<Display type='trafficSpeed' value={row.getValue('speed_limit')} unlimited />
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'traffic_ratio',
|
|
header: t('trafficRatio'),
|
|
cell: ({ row }) => <Badge variant='outline'>{row.getValue('traffic_ratio')} X</Badge>,
|
|
},
|
|
|
|
{
|
|
accessorKey: 'group_id',
|
|
header: t('nodeGroup'),
|
|
cell: ({ row }) => {
|
|
const name = groups?.find((group) => group.id === row.getValue('group_id'))?.name;
|
|
return name ? <Badge variant='outline'>{name}</Badge> : '--';
|
|
},
|
|
},
|
|
]}
|
|
params={[
|
|
{
|
|
key: 'search',
|
|
},
|
|
{
|
|
key: 'group_id',
|
|
placeholder: t('nodeGroup'),
|
|
options: groups?.map((item) => ({
|
|
label: item.name,
|
|
value: String(item.id),
|
|
})),
|
|
},
|
|
]}
|
|
request={async (pagination, filter) => {
|
|
const { data } = await getNodeList({
|
|
...pagination,
|
|
...filter,
|
|
});
|
|
return {
|
|
list: data.data?.list || [],
|
|
total: data.data?.total || 0,
|
|
};
|
|
}}
|
|
actions={{
|
|
render: (row) => [
|
|
<NodeForm<API.Server>
|
|
key='edit'
|
|
trigger={t('edit')}
|
|
title={t('editNode')}
|
|
loading={loading}
|
|
initialValues={row}
|
|
onSubmit={async (values) => {
|
|
setLoading(true);
|
|
try {
|
|
await updateNode({ ...row, ...values } as API.UpdateNodeRequest);
|
|
toast.success(t('updateSuccess'));
|
|
ref.current?.refresh();
|
|
setLoading(false);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
setLoading(false);
|
|
|
|
return false;
|
|
}
|
|
}}
|
|
/>,
|
|
<Button
|
|
key='copy'
|
|
variant='secondary'
|
|
onClick={async () => {
|
|
setLoading(true);
|
|
try {
|
|
const { id, sort, enable, updated_at, created_at, status, ...params } = row;
|
|
await createNode({
|
|
...params,
|
|
enable: false,
|
|
} as API.CreateNodeRequest);
|
|
toast.success(t('copySuccess'));
|
|
ref.current?.refresh();
|
|
setLoading(false);
|
|
return true;
|
|
} catch (error) {
|
|
setLoading(false);
|
|
return false;
|
|
}
|
|
}}
|
|
>
|
|
{t('copy')}
|
|
</Button>,
|
|
<ConfirmButton
|
|
key='delete'
|
|
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
|
title={t('confirmDelete')}
|
|
description={t('deleteWarning')}
|
|
onConfirm={async () => {
|
|
await deleteNode({
|
|
id: row.id,
|
|
});
|
|
toast.success(t('deleteSuccess'));
|
|
ref.current?.refresh();
|
|
}}
|
|
cancelText={t('cancel')}
|
|
confirmText={t('confirm')}
|
|
/>,
|
|
],
|
|
batchRender(rows) {
|
|
return [
|
|
<ConfirmButton
|
|
key='delete'
|
|
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
|
title={t('group.confirmDelete')}
|
|
description={t('group.deleteWarning')}
|
|
onConfirm={async () => {
|
|
await batchDeleteNode({
|
|
ids: rows.map((item) => item.id),
|
|
});
|
|
toast.success(t('group.deleteSuccess'));
|
|
ref.current?.refresh();
|
|
}}
|
|
cancelText={t('group.cancel')}
|
|
confirmText={t('group.confirm')}
|
|
/>,
|
|
];
|
|
},
|
|
}}
|
|
onSort={async (source, target, items) => {
|
|
const sourceIndex = items.findIndex((item) => String(item.id) === source);
|
|
const targetIndex = items.findIndex((item) => String(item.id) === target);
|
|
|
|
const originalSortMap = new Map(items.map((item) => [item.id, item.sort || item.id]));
|
|
|
|
const [movedItem] = items.splice(sourceIndex, 1);
|
|
items.splice(targetIndex, 0, movedItem!);
|
|
|
|
const updatedItems = items.map((item, index) => {
|
|
const originalSort = originalSortMap.get(item.id);
|
|
const newSort = originalSort !== undefined ? originalSort : item.sort;
|
|
return { ...item, sort: newSort };
|
|
});
|
|
|
|
const changedItems = updatedItems.filter(
|
|
(item) => originalSortMap.get(item.id) !== item.sort,
|
|
);
|
|
|
|
if (changedItems.length > 0) {
|
|
nodeSort({
|
|
sort: changedItems.map((item) => ({ id: item.id, sort: item.sort })),
|
|
});
|
|
}
|
|
|
|
return updatedItems;
|
|
}}
|
|
/>
|
|
);
|
|
}
|