import { useEffect, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Col, Input, InputNumber, Row, Select, Switch, message } from 'antd'; import { HttpUtil } from '@/utils'; // Item 7 B3: probe/public-page metadata for an agent node, edited in // place from the node edit dialog. The section is deliberately outside // the node form: it loads and saves on its own (dedicated endpoints, // effective immediately on save — no node save, no restart). interface ProbeMetaWire { price: number; currency: string; billingCycle: string; expiresAt: string | null; trafficLimit: number; // bytes trafficMode: string; trafficResetDay: number; public: boolean; sort: number; group: string; publicRemark: string; notify: boolean; countryPin: string; ipv4Pin: string; ipv6Pin: string; // V2.0.1: ledger corrections in GB (either sign) and the counted // interface pin ('' = automatic selection on the agent). trafficCalUpGb: number; trafficCalDownGb: number; statIface: string; } const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } }; const NAMED_CYCLES = [ 'once', 'monthly', 'quarterly', 'semiannual', 'yearly', 'biennial', 'triennial', ] as const; function Field({ label, children }: { label: string; children: ReactNode }) { return (
{label}
{children}
); } export default function ProbeMetaSection({ nodeId }: { nodeId: number }) { const { t } = useTranslation(); const [messageApi, messageContextHolder] = message.useMessage(); // Loaded meta, keyed by node id so a stale result for a previously // edited node is never shown (same discipline as the install block // in NodeFormModal); visibility derives during render. const [loaded, setLoaded] = useState<{ nodeId: number; meta: ProbeMetaWire } | null>(null); const [trafficGb, setTrafficGb] = useState(0); const [saving, setSaving] = useState(false); useEffect(() => { let cancelled = false; HttpUtil.get(`/panel/api/agent/probe-meta/${nodeId}`, { silent: true, }).then((msg) => { if (!cancelled && msg?.success && msg.obj) { setLoaded({ nodeId, meta: msg.obj }); setTrafficGb(msg.obj.trafficLimit / 1e9); } }); return () => { cancelled = true; }; }, [nodeId]); const meta = loaded && loaded.nodeId === nodeId ? loaded.meta : null; if (!meta) return null; const set = (key: K, value: ProbeMetaWire[K]) => setLoaded((l) => (l ? { nodeId: l.nodeId, meta: { ...l.meta, [key]: value } } : l)); const cycleLabel = (cycle: string) => { switch (cycle) { case 'once': return t('pages.nodes.probe.cycleOnce'); case 'monthly': return t('pages.nodes.probe.cycleMonthly'); case 'quarterly': return t('pages.nodes.probe.cycleQuarterly'); case 'semiannual': return t('pages.nodes.probe.cycleSemiannual'); case 'yearly': return t('pages.nodes.probe.cycleYearly'); case 'biennial': return t('pages.nodes.probe.cycleBiennial'); case 'triennial': return t('pages.nodes.probe.cycleTriennial'); default: return cycle; } }; const cycleOptions: { value: string; label: string }[] = NAMED_CYCLES.map((cycle) => ({ value: cycle, label: cycleLabel(cycle), })); if (!NAMED_CYCLES.includes(meta.billingCycle as (typeof NAMED_CYCLES)[number])) { cycleOptions.push({ value: meta.billingCycle, label: meta.billingCycle }); } async function onSave() { if (!meta) return; setSaving(true); try { const payload: ProbeMetaWire = { ...meta, trafficLimit: Math.round(trafficGb * 1e9), }; const msg = await HttpUtil.put(`/panel/api/agent/probe-meta/${nodeId}`, payload, { silent: true, ...JSON_HEADERS, }); if (msg?.success) { setLoaded({ nodeId, meta: payload }); messageApi.success(t('pages.nodes.probe.saved')); } else { messageApi.error(msg?.msg || t('pages.nodes.probe.saveFailed')); } } finally { setSaving(false); } } return (
{messageContextHolder}
{t('pages.nodes.probe.title')}

{t('pages.nodes.probe.hint')}

set('public', v)} /> set('notify', v)} /> set('group', e.target.value)} /> set('sort', v ?? 0)} style={{ width: '100%' }} /> set('publicRemark', e.target.value)} /> set('price', v ?? 0)} style={{ width: '100%' }} /> set('currency', e.target.value)} /> set('expiresAt', e.target.value || null)} /> set('trafficResetDay', v ?? 1)} style={{ width: '100%' }} /> setTrafficGb(v ?? 0)} style={{ width: '100%' }} /> set('statIface', e.target.value)} />

{t('pages.nodes.probe.calHint')}

set('countryPin', e.target.value)} /> set('ipv4Pin', e.target.value)} /> set('ipv6Pin', e.target.value)} />
); }