B4 frontend: probe task management modal on the nodes page

- ProbeTasksModal: self-loading/self-saving surface (B3 probe-meta
  pattern), tasks grouped by region label (ungrouped last), editor
  with name/region/target/interval/auto-join + agent-node multi
  select snapshotting baseNodeIds for the base-diff save; delete
  with confirm. Toolbar entry on NodeList (RadarChart icon), wired
  in NodesPage; NodeList call sites in two component tests updated
  for the new required prop.
- i18n pages.nodes.probeTasks.* (25 keys) in all 13 locales: zh-CN
  translated, en-US English, others English placeholder per repo
  convention; insertion verified pure-additive (351 insertions,
  0 deletions) and the i18n dead-key failures are byte-identical
  to the B3 baseline.
- tsc clean; oxlint 0/0 (baseline parity); oxfmt clean on touched
  files (3 pre-existing drift files untouched).
This commit is contained in:
Muse
2026-10-22 22:20:23 +00:00
parent 0d1607c358
commit 1763f94a5f
18 changed files with 699 additions and 0 deletions
+6
View File
@@ -27,6 +27,7 @@ import {
InfoCircleOutlined,
MoreOutlined,
PlusOutlined,
RadarChartOutlined,
RightOutlined,
SafetyCertificateOutlined,
TeamOutlined,
@@ -48,6 +49,7 @@ interface NodeListProps {
onSelectionChange: (ids: number[]) => void;
onAdd: () => void;
onMtls: () => void;
onProbeTasks: () => void;
onEdit: (node: NodeRecord) => void;
onDelete: (node: NodeRecord) => void;
onProbe: (node: NodeRecord) => void;
@@ -218,6 +220,7 @@ export default function NodeList({
onSelectionChange,
onAdd,
onMtls,
onProbeTasks,
onEdit,
onDelete,
onProbe,
@@ -618,6 +621,9 @@ export default function NodeList({
<Button icon={<SafetyCertificateOutlined />} onClick={onMtls}>
{t('pages.nodes.mtls.title')}
</Button>
<Button icon={<RadarChartOutlined />} onClick={onProbeTasks}>
{t('pages.nodes.probeTasks.title')}
</Button>
{selectedIds.length > 0 && (
<Button icon={<CloudDownloadOutlined />} onClick={onUpdateSelected}>
{t('pages.nodes.updateSelected', { count: selectedIds.length })}
+9
View File
@@ -33,6 +33,7 @@ import { useNodeMutations } from '@/api/queries/useNodeMutations';
import AppSidebar from '@/layouts/AppSidebar';
import NodeList from './NodeList';
import NodeFormModal from './NodeFormModal';
import ProbeTasksModal from './ProbeTasksModal';
import { setMessageInstance } from '@/utils/messageBus';
import { HttpUtil } from '@/utils';
import type { PanelUpdateInfo } from '../index/PanelUpdateModal';
@@ -104,6 +105,7 @@ export default function NodesPage() {
const [formNode, setFormNode] = useState<NodeRecord | null>(null);
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [mtlsOpen, setMtlsOpen] = useState(false);
const [probeTasksOpen, setProbeTasksOpen] = useState(false);
const [trustCa, setTrustCa] = useState('');
const [copyingCa, setCopyingCa] = useState(false);
const [savingTrustCa, setSavingTrustCa] = useState(false);
@@ -367,6 +369,7 @@ export default function NodesPage() {
onSelectionChange={setSelectedIds}
onAdd={onAdd}
onMtls={() => setMtlsOpen(true)}
onProbeTasks={() => setProbeTasksOpen(true)}
onEdit={onEdit}
onDelete={onDelete}
onProbe={onProbe}
@@ -381,6 +384,12 @@ export default function NodesPage() {
</Layout.Content>
</Layout>
<ProbeTasksModal
open={probeTasksOpen}
onClose={() => setProbeTasksOpen(false)}
nodes={nodes}
/>
<NodeFormModal
open={formOpen}
mode={formMode}
@@ -0,0 +1,331 @@
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Button,
Input,
InputNumber,
Modal,
Popconfirm,
Select,
Space,
Switch,
Table,
Tag,
Typography,
message,
} from 'antd';
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { HttpUtil } from '@/utils';
import type { NodeRecord } from '@/api/queries/useNodesQuery';
// Item 7 B4: probe (latency) task management. The operator maintains
// the target set here — grouped by the free-form region label, no
// preset list (user ruling Q4). Like ProbeMetaSection the surface is
// self-contained: it loads and saves on its own through the dedicated
// endpoints, effective immediately (the master pushes the set on each
// agent's next config poll; nothing restarts).
interface ProbeTaskWire {
id: number;
name: string;
target: string;
interval: number;
autoJoin: boolean;
sort: number;
region: string;
nodeIds: number[];
}
interface TaskDraft {
id: number; // 0 = new task
name: string;
target: string;
interval: number;
autoJoin: boolean;
region: string;
nodeIds: number[];
baseNodeIds: number[] | null; // editor-open snapshot (existing tasks)
}
const TASKS_URL = '/panel/api/agent/probe-tasks';
export default function ProbeTasksModal({
open,
onClose,
nodes,
}: {
open: boolean;
onClose: () => void;
nodes: NodeRecord[];
}) {
const { t } = useTranslation();
const [messageApi, messageContextHolder] = message.useMessage();
const [tasks, setTasks] = useState<ProbeTaskWire[] | null>(null);
const [draft, setDraft] = useState<TaskDraft | null>(null);
const [saving, setSaving] = useState(false);
const agentNodes = useMemo(() => nodes.filter((n) => n.nodeType === 'agent'), [nodes]);
const nodeName = useCallback(
(id: number) => nodes.find((n) => n.id === id)?.name ?? `#${id}`,
[nodes],
);
const load = useCallback(async () => {
const msg = await HttpUtil.get<ProbeTaskWire[]>(TASKS_URL, { silent: true });
if (msg?.success && msg.obj) {
setTasks(msg.obj);
} else {
messageApi.error(msg?.msg || t('pages.nodes.probeTasks.loadFailed'));
}
}, [messageApi, t]);
// Load on open (afterOpenChange is an event callback, so resetting
// the previous session's state here is not an effect write).
const onAfterOpenChange = (opened: boolean) => {
if (opened) {
setTasks(null);
setDraft(null);
void load();
}
};
// Group by region: region labels ascending, the ungrouped bucket
// last; inside a group the panel order (sort) rules.
const groups = useMemo(() => {
const byRegion = new Map<string, ProbeTaskWire[]>();
for (const task of tasks ?? []) {
const key = task.region ?? '';
const list = byRegion.get(key) ?? [];
list.push(task);
byRegion.set(key, list);
}
return [...byRegion.entries()]
.sort(([a], [b]) => {
if (a === b) return 0;
if (a === '') return 1;
if (b === '') return -1;
return a < b ? -1 : 1;
})
.map(([region, list]) => ({ region, list }));
}, [tasks]);
const startCreate = () =>
setDraft({
id: 0,
name: '',
target: '',
interval: 60,
autoJoin: false,
region: '',
nodeIds: [],
baseNodeIds: null,
});
const startEdit = (task: ProbeTaskWire) =>
setDraft({
id: task.id,
name: task.name,
target: task.target,
interval: task.interval,
autoJoin: task.autoJoin,
region: task.region ?? '',
nodeIds: [...task.nodeIds],
baseNodeIds: [...task.nodeIds],
});
async function onSave() {
if (!draft) return;
setSaving(true);
try {
const payload: Record<string, unknown> = {
id: draft.id,
name: draft.name,
target: draft.target,
interval: draft.interval,
autoJoin: draft.autoJoin,
region: draft.region,
nodeIds: draft.nodeIds,
};
if (draft.baseNodeIds) payload.baseNodeIds = draft.baseNodeIds;
const msg = await HttpUtil.put<{ id: number }>(TASKS_URL, payload, { silent: true });
if (msg?.success) {
messageApi.success(t('pages.nodes.probeTasks.saved'));
setDraft(null);
await load();
} else {
messageApi.error(msg?.msg || t('pages.nodes.probeTasks.saveFailed'));
}
} finally {
setSaving(false);
}
}
async function onDelete(task: ProbeTaskWire) {
const msg = await HttpUtil.delete(`${TASKS_URL}/${task.id}`, { silent: true });
if (msg?.success) {
messageApi.success(t('pages.nodes.probeTasks.deleted'));
if (draft?.id === task.id) setDraft(null);
await load();
} else {
messageApi.error(msg?.msg || t('pages.nodes.probeTasks.saveFailed'));
}
}
const columns: ColumnsType<ProbeTaskWire> = [
{ title: t('pages.nodes.probeTasks.name'), dataIndex: 'name', key: 'name' },
{
title: t('pages.nodes.probeTasks.target'),
dataIndex: 'target',
key: 'target',
render: (v: string) => <Typography.Text code>{v}</Typography.Text>,
},
{
title: t('pages.nodes.probeTasks.interval'),
dataIndex: 'interval',
key: 'interval',
width: 90,
render: (v: number) => `${v}s`,
},
{
title: t('pages.nodes.probeTasks.autoJoin'),
dataIndex: 'autoJoin',
key: 'autoJoin',
width: 90,
render: (v: boolean) =>
v ? <Tag color="blue">{t('pages.nodes.probeTasks.autoJoinOn')}</Tag> : null,
},
{
title: t('pages.nodes.probeTasks.nodes'),
dataIndex: 'nodeIds',
key: 'nodeIds',
render: (ids: number[]) => <span title={ids.map(nodeName).join(', ')}>{ids.length}</span>,
},
{
title: t('pages.nodes.probeTasks.actions'),
key: 'actions',
width: 110,
render: (_, task) => (
<Space size="small">
<Button size="small" icon={<EditOutlined />} onClick={() => startEdit(task)}>
{t('pages.nodes.probeTasks.edit')}
</Button>
<Popconfirm
title={t('pages.nodes.probeTasks.deleteConfirm')}
onConfirm={() => void onDelete(task)}
okText={t('pages.nodes.probeTasks.delete')}
cancelText={t('pages.nodes.probeTasks.cancel')}
>
<Button size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
];
const setDraftField = <K extends keyof TaskDraft>(key: K, value: TaskDraft[K]) =>
setDraft((d) => (d ? { ...d, [key]: value } : d));
return (
<Modal
open={open}
title={t('pages.nodes.probeTasks.title')}
onCancel={onClose}
afterOpenChange={onAfterOpenChange}
footer={null}
width={780}
destroyOnHidden
>
{messageContextHolder}
<Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
{t('pages.nodes.probeTasks.intro')}
</Typography.Paragraph>
{tasks === null ? (
<Typography.Text type="secondary">{t('pages.nodes.probeTasks.loading')}</Typography.Text>
) : tasks.length === 0 && !draft ? (
<Typography.Text type="secondary">{t('pages.nodes.probeTasks.empty')}</Typography.Text>
) : (
groups.map(({ region, list }) => (
<div key={region || '__ungrouped'} style={{ marginBottom: 16 }}>
<Typography.Text strong>
{region || t('pages.nodes.probeTasks.ungrouped')}
</Typography.Text>
<Table<ProbeTaskWire>
size="small"
rowKey="id"
columns={columns}
dataSource={list}
pagination={false}
style={{ marginTop: 6 }}
/>
</div>
))
)}
{draft ? (
<div
style={{
borderTop: '1px solid var(--ant-color-border-secondary, #f0f0f0)',
paddingTop: 12,
marginTop: 4,
}}
>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space wrap>
<Input
placeholder={t('pages.nodes.probeTasks.name')}
value={draft.name}
onChange={(e) => setDraftField('name', e.target.value)}
style={{ width: 180 }}
/>
<Input
placeholder={t('pages.nodes.probeTasks.region')}
value={draft.region}
onChange={(e) => setDraftField('region', e.target.value)}
style={{ width: 130 }}
/>
<Input
placeholder={t('pages.nodes.probeTasks.targetPlaceholder')}
value={draft.target}
onChange={(e) => setDraftField('target', e.target.value)}
style={{ width: 230, fontFamily: 'monospace' }}
/>
<InputNumber
min={5}
max={3600}
value={draft.interval}
onChange={(v) => setDraftField('interval', v ?? 60)}
addonAfter="s"
style={{ width: 120 }}
/>
</Space>
<Space wrap>
<span>
{t('pages.nodes.probeTasks.autoJoin')}{' '}
<Switch checked={draft.autoJoin} onChange={(v) => setDraftField('autoJoin', v)} />
</span>
<Select
mode="multiple"
placeholder={t('pages.nodes.probeTasks.nodesPlaceholder')}
value={draft.nodeIds}
onChange={(v) => setDraftField('nodeIds', v)}
options={agentNodes.map((n) => ({ value: n.id, label: n.name }))}
style={{ minWidth: 260 }}
/>
</Space>
<Space>
<Button type="primary" loading={saving} onClick={() => void onSave()}>
{t('pages.nodes.probeTasks.save')}
</Button>
<Button onClick={() => setDraft(null)}>{t('pages.nodes.probeTasks.cancel')}</Button>
</Space>
</Space>
</div>
) : (
<Button icon={<PlusOutlined />} onClick={startCreate} style={{ marginTop: 8 }}>
{t('pages.nodes.probeTasks.add')}
</Button>
)}
</Modal>
);
}
@@ -17,6 +17,7 @@ function renderNodes(nodes: NodeRecord[]) {
onSelectionChange={noop}
onAdd={noop}
onMtls={noop}
onProbeTasks={noop}
onEdit={noop}
onDelete={noop}
onProbe={noop}
@@ -29,6 +29,7 @@ describe('NodeList desktop table row keys', () => {
onSelectionChange={noop}
onAdd={noop}
onMtls={noop}
onProbeTasks={noop}
onEdit={noop}
onDelete={noop}
onProbe={noop}
+27
View File
@@ -959,6 +959,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {
+27
View File
@@ -1099,6 +1099,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {
+27
View File
@@ -959,6 +959,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {
+27
View File
@@ -959,6 +959,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {
+27
View File
@@ -959,6 +959,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {
+27
View File
@@ -959,6 +959,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {
+27
View File
@@ -959,6 +959,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {
+27
View File
@@ -959,6 +959,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {
+27
View File
@@ -959,6 +959,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {
+27
View File
@@ -959,6 +959,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {
+27
View File
@@ -959,6 +959,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {
+27
View File
@@ -981,6 +981,33 @@
"save": "保存状态页信息",
"saved": "状态页信息已保存",
"saveFailed": "状态页信息保存失败"
},
"probeTasks": {
"title": "延迟探测",
"intro": "由 agent 节点执行的 TCP 延迟探测。修改在各 agent 下一次拉取配置时生效(约 30 秒),无需重启。",
"loading": "加载中…",
"empty": "还没有探测任务,新增一个开始测延迟。",
"add": "新增探测",
"name": "名称",
"target": "目标",
"targetPlaceholder": "主机:端口,如 1.1.1.1:443",
"interval": "间隔(秒)",
"region": "地区",
"ungrouped": "未分组",
"autoJoin": "自动加入",
"autoJoinOn": "自动",
"nodes": "指派节点",
"nodesPlaceholder": "选择节点",
"actions": "操作",
"edit": "编辑",
"delete": "删除",
"deleteConfirm": "删除这个探测及其历史数据?",
"save": "保存",
"cancel": "取消",
"saved": "探测任务已保存",
"deleted": "探测任务已删除",
"loadFailed": "探测任务加载失败",
"saveFailed": "操作失败"
}
},
"settings": {
+27
View File
@@ -959,6 +959,33 @@
"save": "Save status page info",
"saved": "Status page info saved",
"saveFailed": "Saving status page info failed"
},
"probeTasks": {
"title": "Probe tasks",
"intro": "TCP latency probes run by agent nodes. Changes apply on each agent's next config poll (about 30 s); nothing restarts.",
"loading": "Loading…",
"empty": "No probe tasks yet. Add one to start measuring latency.",
"add": "Add probe",
"name": "Name",
"target": "Target",
"targetPlaceholder": "host:port, e.g. 1.1.1.1:443",
"interval": "Interval (s)",
"region": "Region",
"ungrouped": "Ungrouped",
"autoJoin": "Auto-join",
"autoJoinOn": "Auto",
"nodes": "Nodes",
"nodesPlaceholder": "Assign nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Delete this probe and its history?",
"save": "Save",
"cancel": "Cancel",
"saved": "Probe task saved",
"deleted": "Probe task deleted",
"loadFailed": "Failed to load probe tasks",
"saveFailed": "Operation failed"
}
},
"settings": {