probe: latency add row always visible, single-line with select-all nodes

- Drop the '+ add probe' toggle button; the add/edit form is now
  always shown, resetting to a blank add state after save/cancel.
- Single nowrap row: name, host:port (flex 6), interval (flex 1),
  auto-join switch, node multi-select (flex 2), select-all-nodes
  button (disabled with no agent nodes), save/cancel.
- Remove the region input from the task editor (region value is
  still kept in the draft and in the save payload, so editing an
  existing task never wipes its region; grouping display unchanged).
- i18n: drop probeTasks.add / probeTasks.region (13 locales),
  add probeTasks.selectAllNodes (13 locales, key sets in sync).
This commit is contained in:
Muse
2026-10-24 16:15:10 +00:00
parent 20db43210e
commit 2212604636
14 changed files with 102 additions and 106 deletions
+89 -80
View File
@@ -13,7 +13,7 @@ import {
Typography,
message,
} from 'antd';
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { HttpUtil } from '@/utils';
import type { NodeRecord } from '@/api/queries/useNodesQuery';
@@ -55,7 +55,20 @@ export default function ProbeTasksPanel({ nodes }: { nodes: NodeRecord[] }) {
const { t } = useTranslation();
const [messageApi, messageContextHolder] = message.useMessage();
const [tasks, setTasks] = useState<ProbeTaskWire[] | null>(null);
const [draft, setDraft] = useState<TaskDraft | null>(null);
// The add row is always visible (no expand button): a blank draft
// is the resting state, and editing a row loads that task into the
// same row (cancel / a successful save resets it to blank).
const blankDraft = (): TaskDraft => ({
id: 0,
name: '',
target: '',
interval: 60,
autoJoin: false,
region: '',
nodeIds: [],
baseNodeIds: null,
});
const [draft, setDraft] = useState<TaskDraft>(blankDraft);
const [saving, setSaving] = useState(false);
const agentNodes = useMemo(() => nodes.filter((n) => n.nodeType === 'agent'), [nodes]);
@@ -97,18 +110,6 @@ export default function ProbeTasksPanel({ nodes }: { nodes: NodeRecord[] }) {
.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,
@@ -122,7 +123,6 @@ export default function ProbeTasksPanel({ nodes }: { nodes: NodeRecord[] }) {
});
async function onSave() {
if (!draft) return;
setSaving(true);
try {
const payload: Record<string, unknown> = {
@@ -141,7 +141,7 @@ export default function ProbeTasksPanel({ nodes }: { nodes: NodeRecord[] }) {
});
if (msg?.success) {
messageApi.success(t('pages.nodes.probeTasks.saved'));
setDraft(null);
setDraft(blankDraft());
await load();
} else {
messageApi.error(msg?.msg || t('pages.nodes.probeTasks.saveFailed'));
@@ -155,7 +155,7 @@ export default function ProbeTasksPanel({ nodes }: { nodes: NodeRecord[] }) {
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);
if (draft.id === task.id) setDraft(blankDraft());
await load();
} else {
messageApi.error(msg?.msg || t('pages.nodes.probeTasks.saveFailed'));
@@ -225,7 +225,7 @@ export default function ProbeTasksPanel({ nodes }: { nodes: NodeRecord[] }) {
{tasks === null ? (
<Typography.Text type="secondary">{t('pages.nodes.probeTasks.loading')}</Typography.Text>
) : tasks.length === 0 && !draft ? (
) : tasks.length === 0 ? (
<Typography.Text type="secondary">{t('pages.nodes.probeTasks.empty')}</Typography.Text>
) : (
groups.map(({ region, list }) => (
@@ -245,70 +245,79 @@ export default function ProbeTasksPanel({ nodes }: { nodes: NodeRecord[] }) {
))
)}
{draft ? (
<div
style={{
borderTop: '1px solid var(--ant-color-border-secondary, #f0f0f0)',
paddingTop: 12,
marginTop: 4,
}}
{/* Always-visible single-line add row (no expand button): the
name field the redesign did not mention is kept, first, so
the specified order of the other elements is unchanged.
The region field is gone from the row; the value itself
still rides along in the payload, so editing an existing
task keeps its region (new tasks get ''). Widths are flex
ratios: target 6 : interval 1, matching the specified
1.5x target (230 -> ~345) against 0.5x interval (120 -> 60)
relationship (345:60 ~ 5.75:1, rounded to 6:1). */}
<div
style={{
borderTop: '1px solid var(--ant-color-border-secondary, #f0f0f0)',
paddingTop: 12,
marginTop: 4,
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
gap: 8,
}}
>
<Input
placeholder={t('pages.nodes.probeTasks.name')}
value={draft.name}
onChange={(e) => setDraftField('name', e.target.value)}
style={{ flex: '2 1 0', minWidth: 0 }}
/>
<Input
placeholder={t('pages.nodes.probeTasks.targetPlaceholder')}
value={draft.target}
onChange={(e) => setDraftField('target', e.target.value)}
style={{ flex: '6 1 0', minWidth: 0, fontFamily: 'monospace' }}
/>
<InputNumber
min={5}
max={3600}
value={draft.interval}
onChange={(v) => setDraftField('interval', v ?? 60)}
addonAfter="s"
style={{ flex: '1 1 0', minWidth: 0 }}
/>
<span style={{ flexShrink: 0 }}>
{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={{ flex: '2 1 0', minWidth: 120 }}
/>
<Button
disabled={agentNodes.length === 0}
onClick={() =>
setDraftField(
'nodeIds',
agentNodes.map((n) => n.id),
)
}
style={{ flexShrink: 0 }}
>
<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')}
{t('pages.nodes.probeTasks.selectAllNodes')}
</Button>
)}
<Space style={{ flexShrink: 0 }}>
<Button type="primary" loading={saving} onClick={() => void onSave()}>
{t('pages.nodes.probeTasks.save')}
</Button>
<Button onClick={() => setDraft(blankDraft())}>
{t('pages.nodes.probeTasks.cancel')}
</Button>
</Space>
</div>
</div>
);
}
+1 -2
View File
@@ -922,17 +922,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
+1 -2
View File
@@ -1040,17 +1040,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
+1 -2
View File
@@ -922,17 +922,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
+1 -2
View File
@@ -922,17 +922,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
+1 -2
View File
@@ -922,17 +922,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
+1 -2
View File
@@ -922,17 +922,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
+1 -2
View File
@@ -922,17 +922,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
+1 -2
View File
@@ -922,17 +922,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
+1 -2
View File
@@ -922,17 +922,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
+1 -2
View File
@@ -922,17 +922,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
+1 -2
View File
@@ -922,17 +922,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
+1 -2
View File
@@ -922,17 +922,16 @@
"intro": "由 agent 节点执行的 TCP 延迟探测。修改在各 agent 下一次拉取配置时生效(约 30 秒),无需重启。",
"loading": "加载中…",
"empty": "还没有探测任务,新增一个开始测延迟。",
"add": "新增探测",
"name": "名称",
"target": "目标",
"targetPlaceholder": "主机:端口,如 1.1.1.1:443",
"interval": "间隔(秒)",
"region": "地区",
"ungrouped": "未分组",
"autoJoin": "自动加入",
"autoJoinOn": "自动",
"nodes": "指派节点",
"nodesPlaceholder": "选择节点",
"selectAllNodes": "选择全部节点",
"actions": "操作",
"edit": "编辑",
"delete": "删除",
+1 -2
View File
@@ -922,17 +922,16 @@
"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",
"selectAllNodes": "Select all nodes",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",