mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
feat(clients): restore reset traffic button in edit client form
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
Tooltip,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { EyeOutlined, ReloadOutlined, RetweetOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import { HttpUtil, RandomUtil } from '@/utils';
|
||||
@@ -39,6 +40,7 @@ const CLIENT_IP_LOG_MODAL_Z_INDEX = CLIENT_FORM_MODAL_Z_INDEX + 1;
|
||||
|
||||
interface ApiMsg<T = unknown> {
|
||||
success?: boolean;
|
||||
msg?: string;
|
||||
obj?: T;
|
||||
}
|
||||
|
||||
@@ -72,6 +74,7 @@ interface ClientFormModalProps {
|
||||
payload: Record<string, unknown> | SaveCreatePayload,
|
||||
meta: SaveMetaEdit | SaveMetaCreate,
|
||||
) => Promise<ApiMsg | null>;
|
||||
resetTraffic?: (client: ClientRecord) => Promise<ApiMsg | null>;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -140,6 +143,7 @@ export default function ClientFormModal({
|
||||
tgBotEnable = false,
|
||||
groups = [],
|
||||
save,
|
||||
resetTraffic,
|
||||
onOpenChange,
|
||||
}: ClientFormModalProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -148,6 +152,7 @@ export default function ClientFormModal({
|
||||
|
||||
const [form, setForm] = useState<FormState>(emptyForm);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [resetting, setResetting] = useState(false);
|
||||
const [clientIps, setClientIps] = useState<string[]>([]);
|
||||
const [ipsLoading, setIpsLoading] = useState(false);
|
||||
const [ipsClearing, setIpsClearing] = useState(false);
|
||||
@@ -328,6 +333,21 @@ export default function ClientFormModal({
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
async function onResetTraffic() {
|
||||
if (!isEdit || !client?.email || !resetTraffic) return;
|
||||
setResetting(true);
|
||||
try {
|
||||
const msg = await resetTraffic(client);
|
||||
if (msg?.success) {
|
||||
messageApi.success(t('pages.clients.toasts.trafficReset'));
|
||||
} else {
|
||||
messageApi.error(msg?.msg || t('somethingWentWrong'));
|
||||
}
|
||||
} finally {
|
||||
setResetting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const schema = isEdit ? ClientFormSchema : ClientCreateFormSchema;
|
||||
const validated = schema.safeParse({
|
||||
@@ -413,15 +433,35 @@ export default function ClientFormModal({
|
||||
open={open}
|
||||
title={isEdit ? t('pages.clients.editClient') : t('pages.clients.addClient')}
|
||||
destroyOnHidden
|
||||
okText={isEdit ? t('save') : t('create')}
|
||||
cancelText={t('cancel')}
|
||||
okButtonProps={{ loading: submitting }}
|
||||
width={720}
|
||||
zIndex={CLIENT_FORM_MODAL_Z_INDEX}
|
||||
style={{ top: 20 }}
|
||||
styles={{ body: { maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', overflowX: 'hidden' } }}
|
||||
onOk={onSubmit}
|
||||
onCancel={close}
|
||||
footer={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
{isEdit && resetTraffic && (
|
||||
<Popconfirm
|
||||
title={t('pages.inbounds.resetTraffic')}
|
||||
description={t('pages.inbounds.resetTrafficContent')}
|
||||
okText={t('reset')}
|
||||
cancelText={t('cancel')}
|
||||
zIndex={CLIENT_IP_LOG_MODAL_Z_INDEX}
|
||||
onConfirm={onResetTraffic}
|
||||
>
|
||||
<Button color="danger" variant="filled" icon={<RetweetOutlined />} loading={resetting}>
|
||||
{t('pages.inbounds.resetTraffic')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
<div style={{ marginInlineStart: 'auto', display: 'flex', gap: 8 }}>
|
||||
<Button onClick={close}>{t('cancel')}</Button>
|
||||
<Button type="primary" loading={submitting} onClick={onSubmit}>
|
||||
{isEdit ? t('save') : t('create')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Tabs
|
||||
|
||||
@@ -165,15 +165,15 @@ function gbToBytes(gb: number | undefined): number {
|
||||
}
|
||||
|
||||
const SORT_OPTIONS: { value: string; column: string; order: 'ascend' | 'descend'; labelKey: string }[] = [
|
||||
{ value: 'createdAt:ascend', column: 'createdAt', order: 'ascend', labelKey: 'pages.clients.sortOldest' },
|
||||
{ value: 'createdAt:descend', column: 'createdAt', order: 'descend', labelKey: 'pages.clients.sortNewest' },
|
||||
{ value: 'updatedAt:descend', column: 'updatedAt', order: 'descend', labelKey: 'pages.clients.sortRecentlyUpdated' },
|
||||
{ value: 'lastOnline:descend', column: 'lastOnline', order: 'descend', labelKey: 'pages.clients.sortRecentlyOnline' },
|
||||
{ value: 'email:ascend', column: 'email', order: 'ascend', labelKey: 'pages.clients.sortEmailAZ' },
|
||||
{ value: 'email:descend', column: 'email', order: 'descend', labelKey: 'pages.clients.sortEmailZA' },
|
||||
{ value: 'traffic:descend', column: 'traffic', order: 'descend', labelKey: 'pages.clients.sortMostTraffic' },
|
||||
{ value: 'remaining:descend', column: 'remaining', order: 'descend', labelKey: 'pages.clients.sortHighestRemaining' },
|
||||
{ value: 'expiryTime:ascend', column: 'expiryTime', order: 'ascend', labelKey: 'pages.clients.sortExpiringSoonest' },
|
||||
{ value: 'createdAt:ascend', column: 'createdAt', order: 'ascend', labelKey: 'pages.clients.sortOldest' },
|
||||
{ value: 'createdAt:descend', column: 'createdAt', order: 'descend', labelKey: 'pages.clients.sortNewest' },
|
||||
{ value: 'updatedAt:descend', column: 'updatedAt', order: 'descend', labelKey: 'pages.clients.sortRecentlyUpdated' },
|
||||
{ value: 'lastOnline:descend', column: 'lastOnline', order: 'descend', labelKey: 'pages.clients.sortRecentlyOnline' },
|
||||
{ value: 'email:ascend', column: 'email', order: 'ascend', labelKey: 'pages.clients.sortEmailAZ' },
|
||||
{ value: 'email:descend', column: 'email', order: 'descend', labelKey: 'pages.clients.sortEmailZA' },
|
||||
{ value: 'traffic:descend', column: 'traffic', order: 'descend', labelKey: 'pages.clients.sortMostTraffic' },
|
||||
{ value: 'remaining:descend', column: 'remaining', order: 'descend', labelKey: 'pages.clients.sortHighestRemaining' },
|
||||
{ value: 'expiryTime:ascend', column: 'expiryTime', order: 'ascend', labelKey: 'pages.clients.sortExpiringSoonest' },
|
||||
];
|
||||
|
||||
const DEFAULT_SORT = SORT_OPTIONS[0];
|
||||
@@ -743,6 +743,7 @@ export default function ClientsPage() {
|
||||
{
|
||||
title: t('pages.clients.traffic'),
|
||||
key: 'traffic',
|
||||
width: 300,
|
||||
render: (_v, record) => (
|
||||
<ClientTrafficCell
|
||||
up={record.traffic?.up}
|
||||
@@ -924,40 +925,40 @@ export default function ClientsPage() {
|
||||
menu={{
|
||||
items: selectedRowKeys.length > 0
|
||||
? [
|
||||
{
|
||||
key: 'adjust',
|
||||
icon: <ClockCircleOutlined />,
|
||||
label: t('pages.clients.adjust'),
|
||||
onClick: () => setBulkAdjustOpen(true),
|
||||
},
|
||||
{
|
||||
key: 'subLinks',
|
||||
icon: <LinkOutlined />,
|
||||
label: t('pages.clients.subLinks'),
|
||||
onClick: () => setSubLinksOpen(true),
|
||||
},
|
||||
]
|
||||
{
|
||||
key: 'adjust',
|
||||
icon: <ClockCircleOutlined />,
|
||||
label: t('pages.clients.adjust'),
|
||||
onClick: () => setBulkAdjustOpen(true),
|
||||
},
|
||||
{
|
||||
key: 'subLinks',
|
||||
icon: <LinkOutlined />,
|
||||
label: t('pages.clients.subLinks'),
|
||||
onClick: () => setSubLinksOpen(true),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
key: 'bulk',
|
||||
icon: <UsergroupAddOutlined />,
|
||||
label: t('pages.clients.bulk'),
|
||||
onClick: () => setBulkAddOpen(true),
|
||||
},
|
||||
{
|
||||
key: 'resetAll',
|
||||
icon: <RetweetOutlined />,
|
||||
label: t('pages.clients.resetAllTraffics'),
|
||||
onClick: onResetAllTraffics,
|
||||
},
|
||||
{
|
||||
key: 'delDepleted',
|
||||
icon: <RestOutlined />,
|
||||
label: t('pages.clients.delDepleted'),
|
||||
danger: true,
|
||||
onClick: onDelDepleted,
|
||||
},
|
||||
],
|
||||
{
|
||||
key: 'bulk',
|
||||
icon: <UsergroupAddOutlined />,
|
||||
label: t('pages.clients.bulk'),
|
||||
onClick: () => setBulkAddOpen(true),
|
||||
},
|
||||
{
|
||||
key: 'resetAll',
|
||||
icon: <RetweetOutlined />,
|
||||
label: t('pages.clients.resetAllTraffics'),
|
||||
onClick: onResetAllTraffics,
|
||||
},
|
||||
{
|
||||
key: 'delDepleted',
|
||||
icon: <RestOutlined />,
|
||||
label: t('pages.clients.delDepleted'),
|
||||
danger: true,
|
||||
onClick: onDelDepleted,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Button icon={<MoreOutlined />}>
|
||||
@@ -1246,6 +1247,7 @@ export default function ClientsPage() {
|
||||
tgBotEnable={tgBotEnable}
|
||||
groups={allGroups}
|
||||
save={onSave}
|
||||
resetTraffic={resetTraffic}
|
||||
onOpenChange={setFormOpen}
|
||||
/>
|
||||
</LazyMount>
|
||||
|
||||
Reference in New Issue
Block a user