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:
@@ -27,6 +27,7 @@ import {
|
|||||||
InfoCircleOutlined,
|
InfoCircleOutlined,
|
||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
|
RadarChartOutlined,
|
||||||
RightOutlined,
|
RightOutlined,
|
||||||
SafetyCertificateOutlined,
|
SafetyCertificateOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
@@ -48,6 +49,7 @@ interface NodeListProps {
|
|||||||
onSelectionChange: (ids: number[]) => void;
|
onSelectionChange: (ids: number[]) => void;
|
||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
onMtls: () => void;
|
onMtls: () => void;
|
||||||
|
onProbeTasks: () => void;
|
||||||
onEdit: (node: NodeRecord) => void;
|
onEdit: (node: NodeRecord) => void;
|
||||||
onDelete: (node: NodeRecord) => void;
|
onDelete: (node: NodeRecord) => void;
|
||||||
onProbe: (node: NodeRecord) => void;
|
onProbe: (node: NodeRecord) => void;
|
||||||
@@ -218,6 +220,7 @@ export default function NodeList({
|
|||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
onAdd,
|
onAdd,
|
||||||
onMtls,
|
onMtls,
|
||||||
|
onProbeTasks,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onProbe,
|
onProbe,
|
||||||
@@ -618,6 +621,9 @@ export default function NodeList({
|
|||||||
<Button icon={<SafetyCertificateOutlined />} onClick={onMtls}>
|
<Button icon={<SafetyCertificateOutlined />} onClick={onMtls}>
|
||||||
{t('pages.nodes.mtls.title')}
|
{t('pages.nodes.mtls.title')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button icon={<RadarChartOutlined />} onClick={onProbeTasks}>
|
||||||
|
{t('pages.nodes.probeTasks.title')}
|
||||||
|
</Button>
|
||||||
{selectedIds.length > 0 && (
|
{selectedIds.length > 0 && (
|
||||||
<Button icon={<CloudDownloadOutlined />} onClick={onUpdateSelected}>
|
<Button icon={<CloudDownloadOutlined />} onClick={onUpdateSelected}>
|
||||||
{t('pages.nodes.updateSelected', { count: selectedIds.length })}
|
{t('pages.nodes.updateSelected', { count: selectedIds.length })}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { useNodeMutations } from '@/api/queries/useNodeMutations';
|
|||||||
import AppSidebar from '@/layouts/AppSidebar';
|
import AppSidebar from '@/layouts/AppSidebar';
|
||||||
import NodeList from './NodeList';
|
import NodeList from './NodeList';
|
||||||
import NodeFormModal from './NodeFormModal';
|
import NodeFormModal from './NodeFormModal';
|
||||||
|
import ProbeTasksModal from './ProbeTasksModal';
|
||||||
import { setMessageInstance } from '@/utils/messageBus';
|
import { setMessageInstance } from '@/utils/messageBus';
|
||||||
import { HttpUtil } from '@/utils';
|
import { HttpUtil } from '@/utils';
|
||||||
import type { PanelUpdateInfo } from '../index/PanelUpdateModal';
|
import type { PanelUpdateInfo } from '../index/PanelUpdateModal';
|
||||||
@@ -104,6 +105,7 @@ export default function NodesPage() {
|
|||||||
const [formNode, setFormNode] = useState<NodeRecord | null>(null);
|
const [formNode, setFormNode] = useState<NodeRecord | null>(null);
|
||||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||||
const [mtlsOpen, setMtlsOpen] = useState(false);
|
const [mtlsOpen, setMtlsOpen] = useState(false);
|
||||||
|
const [probeTasksOpen, setProbeTasksOpen] = useState(false);
|
||||||
const [trustCa, setTrustCa] = useState('');
|
const [trustCa, setTrustCa] = useState('');
|
||||||
const [copyingCa, setCopyingCa] = useState(false);
|
const [copyingCa, setCopyingCa] = useState(false);
|
||||||
const [savingTrustCa, setSavingTrustCa] = useState(false);
|
const [savingTrustCa, setSavingTrustCa] = useState(false);
|
||||||
@@ -367,6 +369,7 @@ export default function NodesPage() {
|
|||||||
onSelectionChange={setSelectedIds}
|
onSelectionChange={setSelectedIds}
|
||||||
onAdd={onAdd}
|
onAdd={onAdd}
|
||||||
onMtls={() => setMtlsOpen(true)}
|
onMtls={() => setMtlsOpen(true)}
|
||||||
|
onProbeTasks={() => setProbeTasksOpen(true)}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onProbe={onProbe}
|
onProbe={onProbe}
|
||||||
@@ -381,6 +384,12 @@ export default function NodesPage() {
|
|||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<ProbeTasksModal
|
||||||
|
open={probeTasksOpen}
|
||||||
|
onClose={() => setProbeTasksOpen(false)}
|
||||||
|
nodes={nodes}
|
||||||
|
/>
|
||||||
|
|
||||||
<NodeFormModal
|
<NodeFormModal
|
||||||
open={formOpen}
|
open={formOpen}
|
||||||
mode={formMode}
|
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}
|
onSelectionChange={noop}
|
||||||
onAdd={noop}
|
onAdd={noop}
|
||||||
onMtls={noop}
|
onMtls={noop}
|
||||||
|
onProbeTasks={noop}
|
||||||
onEdit={noop}
|
onEdit={noop}
|
||||||
onDelete={noop}
|
onDelete={noop}
|
||||||
onProbe={noop}
|
onProbe={noop}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ describe('NodeList desktop table row keys', () => {
|
|||||||
onSelectionChange={noop}
|
onSelectionChange={noop}
|
||||||
onAdd={noop}
|
onAdd={noop}
|
||||||
onMtls={noop}
|
onMtls={noop}
|
||||||
|
onProbeTasks={noop}
|
||||||
onEdit={noop}
|
onEdit={noop}
|
||||||
onDelete={noop}
|
onDelete={noop}
|
||||||
onProbe={noop}
|
onProbe={noop}
|
||||||
|
|||||||
@@ -959,6 +959,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -1099,6 +1099,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -959,6 +959,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -959,6 +959,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -959,6 +959,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -959,6 +959,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -959,6 +959,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -959,6 +959,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -959,6 +959,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -959,6 +959,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -959,6 +959,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -981,6 +981,33 @@
|
|||||||
"save": "保存状态页信息",
|
"save": "保存状态页信息",
|
||||||
"saved": "状态页信息已保存",
|
"saved": "状态页信息已保存",
|
||||||
"saveFailed": "状态页信息保存失败"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -959,6 +959,33 @@
|
|||||||
"save": "Save status page info",
|
"save": "Save status page info",
|
||||||
"saved": "Status page info saved",
|
"saved": "Status page info saved",
|
||||||
"saveFailed": "Saving status page info failed"
|
"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": {
|
"settings": {
|
||||||
|
|||||||
Reference in New Issue
Block a user