diff --git a/web/src/pages/gateways/index.tsx b/web/src/pages/gateways/index.tsx index 2c635df..1c9db86 100644 --- a/web/src/pages/gateways/index.tsx +++ b/web/src/pages/gateways/index.tsx @@ -1,53 +1,101 @@ -// 网关管理:语义对齐 web.archived GatewaysPage.jsx(注册/编辑 Modal、令牌显隐复制、删除)。 -import { useCallback, useEffect, useRef, useState } from 'react'; -import { Alert, App, Button, Card, Flex, Form, Grid, Input, Modal, Popconfirm, Space, Table, Tag, Tooltip, Typography } from 'antd'; -import { CopyOutlined, EyeInvisibleOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons'; +// 网关管理:平台为每个网关签发 access key,网关以出站 WebSocket 连回 /v1/agent; +// 页面维护名称与 key 的生命周期(创建回显、重置回显),在线状态由通道会话维护。 +import { useCallback, useEffect, useState } from 'react'; +import { Alert, App, Button, Card, Drawer, Flex, Form, Grid, Input, Modal, Popconfirm, Space, Table, Tag, Tooltip, Typography } from 'antd'; +import { CopyOutlined, EyeInvisibleOutlined, EyeOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; -import { create, getList, remove, update } from '@/services/api'; +import { create, getList, gatewayAction, gatewayTasks, remove, update } from '@/services/api'; import { conflictMessage, dateTime } from '@/utils/helpers'; import { fixedLeft, fixedRight, useTablePagination, useTableSticky } from '@/utils/table'; const namePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/; -const tokenPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{15,127}$/; -// 健康状态列:只展示后台定时探活的最近一次结果,不保留历史。 -function HealthCell({ status, reason, checkedAt }: { status?: string; reason?: string; checkedAt?: string }) { - const label = status === 'healthy' ? '在线' : status === 'unhealthy' ? '离线' : '未检查'; - const color = status === 'healthy' ? 'success' : status === 'unhealthy' ? 'error' : 'default'; - const details = [ - checkedAt ? `最近检查:${dateTime(checkedAt)}` : null, - reason || null, - ].filter(Boolean).join(';'); +// 在线状态:会话维护 online/last_seen_at;从未连接不冒充离线。 +function OnlineCell({ online, lastSeenAt }: { online: boolean; lastSeenAt?: string | null }) { + const [label, color] = online + ? ['在线', 'success'] + : lastSeenAt + ? ['离线', 'error'] + : ['从未连接', 'default']; return ( - + {label} ); } +function AccessKeyCell({ accessKey, name }: { accessKey: string; name: string }) { + const [visible, setVisible] = useState(false); + return ( + + + {visible ? accessKey : '••••••••••••'} + + } /> + ) : null} + load(next), showSizeChanger: false }} + locale={{ emptyText: '暂无任务记录' }} + /> + + ); +} + export default function Page() { const [gateways, setGateways] = useState([]); const [pending, setPending] = useState(true); @@ -94,11 +202,12 @@ export default function Page() { const [editing, setEditing] = useState(null); const [busy, setBusy] = useState(''); const [created, setCreated] = useState(null); + const [resetKey, setResetKey] = useState(null); + const [tasksGateway, setTasksGateway] = useState(null); const { message: messageApi } = App.useApp(); const horizontal = !Grid.useBreakpoint().xl; const sticky = useTableSticky(); const pagination = useTablePagination(); - const healthReloadTimer = useRef>(undefined); const load = useCallback(async () => { setPending(true); @@ -113,41 +222,32 @@ export default function Page() { } }, []); - // 创建/编辑后的立即探活在后台异步进行:延迟刷新一次让健康状态列尽快显示最新结果。 - const scheduleHealthReload = useCallback(() => { - clearTimeout(healthReloadTimer.current); - healthReloadTimer.current = setTimeout(load, 2500); - }, [load]); - useEffect(() => () => clearTimeout(healthReloadTimer.current), []); - useEffect(() => { load(); }, [load]); - async function createGateway(values: { name: string; endpoint: string; token?: string }): Promise { + async function createGateway(values: { name: string }): Promise { setBusy('create'); try { const record = await create('gateways', values); setCreated(record?.data ?? record); await load(); - scheduleHealthReload(); setCreateOpen(false); return true; } catch (reason) { - messageApi.error(conflictMessage(reason, '网关注册失败;表单内容已保留。')); + messageApi.error(conflictMessage(reason, '网关创建失败;表单内容已保留。')); return false; } finally { setBusy(''); } } - async function updateGateway(values: { name: string; endpoint: string; token?: string }): Promise { + async function updateGateway(values: { name: string }): Promise { if (!editing) return false; setBusy('edit'); try { await update('gateways', editing.name, values); await load(); - scheduleHealthReload(); setEditing(null); return true; } catch (reason) { @@ -158,6 +258,19 @@ export default function Page() { } } + async function resetGatewayKey(gateway: any) { + setBusy(gateway.name); + try { + const record = await gatewayAction(gateway.name, 'reset-key'); + setResetKey(record?.data ?? record); + await load(); + } catch (reason) { + messageApi.error(conflictMessage(reason, '网关 key 重置失败')); + } finally { + setBusy(''); + } + } + async function removeGateway(gateway: any) { setBusy(gateway.name); try { @@ -172,17 +285,31 @@ export default function Page() { const columns: ColumnsType = [ { title: '名称', dataIndex: 'name', ...fixedLeft({}), render: (value: string) => {value} }, - { title: 'Endpoint', dataIndex: 'endpoint', render: (value: string) => {value} }, { - title: '健康状态', - dataIndex: 'health_status', + title: '在线状态', + key: 'online', width: 110, - render: (_, gateway) => , + render: (_, gateway) => , + }, + { title: '版本', dataIndex: 'version', width: 140, render: (value: string) => (value ? {value} : '—') }, + { + title: '任务队列', + dataIndex: 'pending_tasks', + width: 90, + align: 'right', + render: (value: number, gateway) => ( + + + + ), }, { - title: '令牌', - dataIndex: 'token', - render: (token: string, gateway) => }, + title: 'Access Key', + dataIndex: 'access_key', + render: (accessKey: string, gateway) => , + }, { title: '操作', key: 'actions', @@ -190,6 +317,14 @@ export default function Page() { ...fixedRight({}), render: (_, gateway) => ( + + + + @@ -206,42 +341,25 @@ export default function Page() { 共 {gateways.length} 个网关 - + + + + - {created ? ( - - 网关 {created.name} 已注册。令牌 - {created.token} - - - } - /> - ) : null} + {created ? setCreated(null)} /> : null} + {resetKey ? setResetKey(null)} /> : null} {error ? ( 重试} /> ) : null} -
+
{ setCreateOpen(false); }} @@ -251,37 +369,10 @@ export default function Page() { open={editing !== null} initial={editing} busy={busy === 'edit'} - error={null} onCancel={() => setEditing(null)} onSubmit={updateGateway} /> + setTasksGateway(null)} /> ); } - -function TokenCell({ token, name }: { token: string; name: string }) { - const [visible, setVisible] = useState(false); - return ( - - - {visible ? token : '••••••••••••'} - -