feat(web): 账号即环境 UI 收敛——删 /browsers 页面,环境并入账号详情
- 删除 /browsers 列表/创建/详情页与菜单项,运行环境不再是独立资源 - 账号详情:GET /api/phase-a/accounts/:id 内联 environment 字段 (网关/出口/绑定版本/运行实例/不可调度原因),固定资源卡直接展示 - 状态卡两态开关:暂停→「启动账号」(start=resume+启动环境), 运行中→「暂停账号」(联动停环境);出口重绑卡片内完成 (POST /network-exits/:id/rebind 返回内联 environment,出口切换不再跳环境页) - 删除 creator/settings 残留的转写服务字段(后端 settings 无此列) - helpers.ts accountReadiness 改用 runtime_id/runtime_cleanup_pending - network-exits 列表绑定账号列改由账号列表内联数据驱动 - 全量验证:go test ./...(PG)全绿、tsc --noEmit 通过、max build 通过、 node --test 12/12 通过
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// 账号环境出口重绑:账号即环境下唯一的换绑入口(环境跟随账号,不再有独立环境页)。
|
||||
// 仅列举检测通过的出口;重绑走 POST /network-exits/:id/rebind(后端联动账号环境),成功后由父级刷新。
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Flex, Select, Typography } from 'antd';
|
||||
import { getList } from '@/services/api';
|
||||
import { jsonOptions, request } from '@/requestErrorConfig';
|
||||
|
||||
interface RebindEnvironmentProps {
|
||||
binding: any;
|
||||
disabled?: boolean;
|
||||
onRebound: () => void | Promise<void>;
|
||||
onError: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export default function RebindEnvironment({ binding, disabled, onRebound, onError }: RebindEnvironmentProps) {
|
||||
const [exits, setExits] = useState<any[]>([]);
|
||||
const [exitsError, setExitsError] = useState(false);
|
||||
const [exitID, setExitID] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
getList({ resource: 'network-exits' })
|
||||
.then((result) => {
|
||||
if (!cancelled) setExits((result.data ?? []).filter((item: any) => item.health_status === 'healthy'));
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setExitsError(true);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 直连(当前状态)或任一健康出口;已在用的出口不重复出现在选项里。
|
||||
const options = useMemo(() => {
|
||||
const choices = [{ value: '', label: '当前机器直连' }];
|
||||
for (const exit of exits) {
|
||||
if (exit.id === binding.network_exit_id) continue;
|
||||
choices.push({ value: exit.id, label: `${exit.id}${exit.observed_public_ip ? ` · ${exit.observed_public_ip}` : ''}` });
|
||||
}
|
||||
return choices;
|
||||
}, [exits, binding.network_exit_id]);
|
||||
|
||||
const unchanged = exitID === (binding.network_exit_id ?? '');
|
||||
|
||||
async function rebind() {
|
||||
setBusy(true);
|
||||
try {
|
||||
await request(`/network-exits/${encodeURIComponent(binding.network_exit_id ?? '')}/rebind`, jsonOptions('POST', { network_exit_id: exitID }));
|
||||
await onRebound();
|
||||
} catch (reason) {
|
||||
onError(reason);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex vertical gap={8}>
|
||||
<Flex align="center" gap={8} wrap="wrap">
|
||||
<Typography.Text type="secondary">切换出口</Typography.Text>
|
||||
<Select
|
||||
style={{ minWidth: 220 }}
|
||||
value={exitID}
|
||||
onChange={setExitID}
|
||||
options={options}
|
||||
placeholder={exitsError ? '出口列表不可用' : '选择目标出口'}
|
||||
disabled={disabled || exitsError}
|
||||
size="small"
|
||||
/>
|
||||
<Button size="small" type="primary" disabled={disabled || unchanged || busy} loading={busy} onClick={rebind}>
|
||||
重绑
|
||||
</Button>
|
||||
</Flex>
|
||||
{exitsError ? <Typography.Text type="secondary" style={{ fontSize: 12 }}>出口状态未知,请稍后重试。</Typography.Text> : null}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user