feat(clients): restore reset traffic button in edit client form

This commit is contained in:
MHSanaei
2026-06-12 20:15:31 +02:00
parent 08bc481ae3
commit 4eab37b66c
2 changed files with 89 additions and 47 deletions
+45 -5
View File
@@ -8,6 +8,7 @@ import {
Input, Input,
InputNumber, InputNumber,
Modal, Modal,
Popconfirm,
Row, Row,
Select, Select,
Space, Space,
@@ -17,7 +18,7 @@ import {
Tooltip, Tooltip,
message, message,
} from 'antd'; } from 'antd';
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons'; import { EyeOutlined, ReloadOutlined, RetweetOutlined } from '@ant-design/icons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import { HttpUtil, RandomUtil } from '@/utils'; 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> { interface ApiMsg<T = unknown> {
success?: boolean; success?: boolean;
msg?: string;
obj?: T; obj?: T;
} }
@@ -72,6 +74,7 @@ interface ClientFormModalProps {
payload: Record<string, unknown> | SaveCreatePayload, payload: Record<string, unknown> | SaveCreatePayload,
meta: SaveMetaEdit | SaveMetaCreate, meta: SaveMetaEdit | SaveMetaCreate,
) => Promise<ApiMsg | null>; ) => Promise<ApiMsg | null>;
resetTraffic?: (client: ClientRecord) => Promise<ApiMsg | null>;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
@@ -140,6 +143,7 @@ export default function ClientFormModal({
tgBotEnable = false, tgBotEnable = false,
groups = [], groups = [],
save, save,
resetTraffic,
onOpenChange, onOpenChange,
}: ClientFormModalProps) { }: ClientFormModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -148,6 +152,7 @@ export default function ClientFormModal({
const [form, setForm] = useState<FormState>(emptyForm); const [form, setForm] = useState<FormState>(emptyForm);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [resetting, setResetting] = useState(false);
const [clientIps, setClientIps] = useState<string[]>([]); const [clientIps, setClientIps] = useState<string[]>([]);
const [ipsLoading, setIpsLoading] = useState(false); const [ipsLoading, setIpsLoading] = useState(false);
const [ipsClearing, setIpsClearing] = useState(false); const [ipsClearing, setIpsClearing] = useState(false);
@@ -328,6 +333,21 @@ export default function ClientFormModal({
onOpenChange(false); 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() { async function onSubmit() {
const schema = isEdit ? ClientFormSchema : ClientCreateFormSchema; const schema = isEdit ? ClientFormSchema : ClientCreateFormSchema;
const validated = schema.safeParse({ const validated = schema.safeParse({
@@ -413,15 +433,35 @@ export default function ClientFormModal({
open={open} open={open}
title={isEdit ? t('pages.clients.editClient') : t('pages.clients.addClient')} title={isEdit ? t('pages.clients.editClient') : t('pages.clients.addClient')}
destroyOnHidden destroyOnHidden
okText={isEdit ? t('save') : t('create')}
cancelText={t('cancel')}
okButtonProps={{ loading: submitting }}
width={720} width={720}
zIndex={CLIENT_FORM_MODAL_Z_INDEX} zIndex={CLIENT_FORM_MODAL_Z_INDEX}
style={{ top: 20 }} style={{ top: 20 }}
styles={{ body: { maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', overflowX: 'hidden' } }} styles={{ body: { maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', overflowX: 'hidden' } }}
onOk={onSubmit}
onCancel={close} 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"> <Form layout="vertical">
<Tabs <Tabs
+44 -42
View File
@@ -165,15 +165,15 @@ function gbToBytes(gb: number | undefined): number {
} }
const SORT_OPTIONS: { value: string; column: string; order: 'ascend' | 'descend'; labelKey: string }[] = [ 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:ascend', column: 'createdAt', order: 'ascend', labelKey: 'pages.clients.sortOldest' },
{ value: 'createdAt:descend', column: 'createdAt', order: 'descend', labelKey: 'pages.clients.sortNewest' }, { value: 'createdAt:descend', column: 'createdAt', order: 'descend', labelKey: 'pages.clients.sortNewest' },
{ value: 'updatedAt:descend', column: 'updatedAt', order: 'descend', labelKey: 'pages.clients.sortRecentlyUpdated' }, { value: 'updatedAt:descend', column: 'updatedAt', order: 'descend', labelKey: 'pages.clients.sortRecentlyUpdated' },
{ value: 'lastOnline:descend', column: 'lastOnline', order: 'descend', labelKey: 'pages.clients.sortRecentlyOnline' }, { value: 'lastOnline:descend', column: 'lastOnline', order: 'descend', labelKey: 'pages.clients.sortRecentlyOnline' },
{ value: 'email:ascend', column: 'email', order: 'ascend', labelKey: 'pages.clients.sortEmailAZ' }, { value: 'email:ascend', column: 'email', order: 'ascend', labelKey: 'pages.clients.sortEmailAZ' },
{ value: 'email:descend', column: 'email', order: 'descend', labelKey: 'pages.clients.sortEmailZA' }, { value: 'email:descend', column: 'email', order: 'descend', labelKey: 'pages.clients.sortEmailZA' },
{ value: 'traffic:descend', column: 'traffic', order: 'descend', labelKey: 'pages.clients.sortMostTraffic' }, { value: 'traffic:descend', column: 'traffic', order: 'descend', labelKey: 'pages.clients.sortMostTraffic' },
{ value: 'remaining:descend', column: 'remaining', order: 'descend', labelKey: 'pages.clients.sortHighestRemaining' }, { value: 'remaining:descend', column: 'remaining', order: 'descend', labelKey: 'pages.clients.sortHighestRemaining' },
{ value: 'expiryTime:ascend', column: 'expiryTime', order: 'ascend', labelKey: 'pages.clients.sortExpiringSoonest' }, { value: 'expiryTime:ascend', column: 'expiryTime', order: 'ascend', labelKey: 'pages.clients.sortExpiringSoonest' },
]; ];
const DEFAULT_SORT = SORT_OPTIONS[0]; const DEFAULT_SORT = SORT_OPTIONS[0];
@@ -743,6 +743,7 @@ export default function ClientsPage() {
{ {
title: t('pages.clients.traffic'), title: t('pages.clients.traffic'),
key: 'traffic', key: 'traffic',
width: 300,
render: (_v, record) => ( render: (_v, record) => (
<ClientTrafficCell <ClientTrafficCell
up={record.traffic?.up} up={record.traffic?.up}
@@ -924,40 +925,40 @@ export default function ClientsPage() {
menu={{ menu={{
items: selectedRowKeys.length > 0 items: selectedRowKeys.length > 0
? [ ? [
{ {
key: 'adjust', key: 'adjust',
icon: <ClockCircleOutlined />, icon: <ClockCircleOutlined />,
label: t('pages.clients.adjust'), label: t('pages.clients.adjust'),
onClick: () => setBulkAdjustOpen(true), onClick: () => setBulkAdjustOpen(true),
}, },
{ {
key: 'subLinks', key: 'subLinks',
icon: <LinkOutlined />, icon: <LinkOutlined />,
label: t('pages.clients.subLinks'), label: t('pages.clients.subLinks'),
onClick: () => setSubLinksOpen(true), onClick: () => setSubLinksOpen(true),
}, },
] ]
: [ : [
{ {
key: 'bulk', key: 'bulk',
icon: <UsergroupAddOutlined />, icon: <UsergroupAddOutlined />,
label: t('pages.clients.bulk'), label: t('pages.clients.bulk'),
onClick: () => setBulkAddOpen(true), onClick: () => setBulkAddOpen(true),
}, },
{ {
key: 'resetAll', key: 'resetAll',
icon: <RetweetOutlined />, icon: <RetweetOutlined />,
label: t('pages.clients.resetAllTraffics'), label: t('pages.clients.resetAllTraffics'),
onClick: onResetAllTraffics, onClick: onResetAllTraffics,
}, },
{ {
key: 'delDepleted', key: 'delDepleted',
icon: <RestOutlined />, icon: <RestOutlined />,
label: t('pages.clients.delDepleted'), label: t('pages.clients.delDepleted'),
danger: true, danger: true,
onClick: onDelDepleted, onClick: onDelDepleted,
}, },
], ],
}} }}
> >
<Button icon={<MoreOutlined />}> <Button icon={<MoreOutlined />}>
@@ -1246,6 +1247,7 @@ export default function ClientsPage() {
tgBotEnable={tgBotEnable} tgBotEnable={tgBotEnable}
groups={allGroups} groups={allGroups}
save={onSave} save={onSave}
resetTraffic={resetTraffic}
onOpenChange={setFormOpen} onOpenChange={setFormOpen}
/> />
</LazyMount> </LazyMount>