From de91d7a8821f93adba232f074e95ee9f8b7f7792 Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 3 Sep 2026 17:16:08 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=89=8D=E7=AB=AF=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=A3=8E=E6=A0=BC=E7=BB=9F=E4=B8=80=E5=B9=B6=E6=B8=85=E7=90=86?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E5=AF=BC=E5=85=A5=EF=BC=9B=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E8=BF=87=E6=97=B6=E7=9A=84=20PR=20=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 - web/src/BrowserImagesPage.jsx | 446 +++++++++++++++++------ web/src/BrowserImagesPage.test.jsx | 216 +++++++---- web/src/GatewaysPage.jsx | 16 +- web/src/GatewaysPage.test.jsx | 43 ++- web/src/NetworkExitsPage.jsx | 551 ++++++++++++++++++++++------- web/src/NetworkExitsPage.test.jsx | 243 +++++++++---- 7 files changed, 1120 insertions(+), 397 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 620d565..e8d6c2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,5 +90,3 @@ - 后端变更必须通过 `go test ./...`、`go vet ./...`,并完成 `./cmd/control-plane` 和 `./cmd/docker-gateway` 双端构建;涉及并发、生命周期或共享状态的变更须运行 `go test -race ./...`。Docker 或 Compose 变更还须通过 `docker compose config --quiet`。 - 前端变更必须从 lockfile 安装、通过仓库的非交互式测试命令,并通过 `npm --prefix web run build`。主题、Layout、导航、资源动作或 data provider 的变更需要聚焦的交互覆盖,包括适用的错误与禁用状态。 - 除非 issue 明确批准契约变更,保持既有 API 与 Docker 生命周期行为不变。在 PR 中文档化任何状态码、载荷、配置、迁移、安全或重试方面的影响。 -- 每个 PR 限定于单个 issue,标题、正文或分支名中包含 issue 编号,并报告确切的验证命令与结果。除非 issue 明确要求,不使用 close intent。 -- 每个代码 PR 均需独立评审。P0/P1 问题必须修复并复审;P2 问题记录为后续工作。实现 agent 不得合并、部署、发布或修改生产配置;将已批准的 PR 移交指定交付角色。 diff --git a/web/src/BrowserImagesPage.jsx b/web/src/BrowserImagesPage.jsx index 79637fb..04e81f5 100644 --- a/web/src/BrowserImagesPage.jsx +++ b/web/src/BrowserImagesPage.jsx @@ -1,187 +1,419 @@ -import { useState } from 'react' -import { Link, useNavigate } from 'react-router' -import { useDataProvider, useList } from '@refinedev/core' +import { useState } from "react"; +import { useDataProvider, useList } from "@refinedev/core"; import { - Alert, Button, Card, CardContent, ConfirmDialog, Field, Input, Modal, PageHeader, PageState, - Select, StatusPill, Switch, Table, Textarea, conflictMessage, -} from './lib/ui.jsx' -import { useTitle } from './lib/hooks.js' + Alert, + Button, + ConfirmDialog, + Field, + Input, + Modal, + PageHeader, + PageState, + Switch, + Table, +} from "./lib/ui.jsx"; +import { useTitle } from "./lib/hooks.js"; -const versionPattern = /^\d[A-Za-z0-9._-]{0,63}$/ -const refPattern = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,300}$/ +const versionPattern = /^\d[A-Za-z0-9._-]{0,63}$/; +const refPattern = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,300}$/; function ImageCreateModal({ open, onClose, onSubmit, busy, error }) { - const [form, setForm] = useState({ version: '', image_ref: '', note: '' }) - const update = (key, value) => setForm(current => ({ ...current, [key]: value })) - const valid = versionPattern.test(form.version) && refPattern.test(form.image_ref) - const versionInvalid = form.version !== '' && !versionPattern.test(form.version) - const refInvalid = form.image_ref !== '' && !refPattern.test(form.image_ref) + const [form, setForm] = useState({ version: "", image_ref: "", note: "" }); + const update = (key, value) => + setForm((current) => ({ ...current, [key]: value })); + const valid = + versionPattern.test(form.version) && refPattern.test(form.image_ref); + const versionInvalid = + form.version !== "" && !versionPattern.test(form.version); + const refInvalid = form.image_ref !== "" && !refPattern.test(form.image_ref); async function submit(event) { - event.preventDefault() - if (!valid) return - const created = await onSubmit({ version: form.version, image_ref: form.image_ref, note: form.note, enabled: true }) + event.preventDefault(); + if (!valid) return; + const created = await onSubmit({ + version: form.version, + image_ref: form.image_ref, + note: form.note, + enabled: true, + }); if (created) { - setForm({ version: '', image_ref: '', note: '' }) - onClose() + setForm({ version: "", image_ref: "", note: "" }); + onClose(); } } return ( - - - - }> + + + + + } + >
- {error ? {error.message} : null} + {error ? ( + + {error.message} + + ) : null}
- - update('version', event.target.value)} invalid={versionInvalid} /> + + update("version", event.target.value)} + invalid={versionInvalid} + /> - - update('image_ref', event.target.value)} placeholder="registry/repo:tag 或 @sha256 摘要" invalid={refInvalid} /> + + update("image_ref", event.target.value)} + placeholder="registry/repo:tag 或 @sha256 摘要" + invalid={refInvalid} + /> - update('note', event.target.value)} /> + update("note", event.target.value)} + />
- ) + ); } function ImageEditForm({ editing, onChange, onSave, busy }) { - const refInvalid = editing.image_ref !== '' && !refPattern.test(editing.image_ref) + const refInvalid = + editing.image_ref !== "" && !refPattern.test(editing.image_ref); return (
- - onChange({ ...editing, image_ref: event.target.value })} /> + + + onChange({ ...editing, image_ref: event.target.value }) + } + /> - onChange({ ...editing, note: event.target.value })} /> + + onChange({ ...editing, note: event.target.value }) + } + />

启用(可用于创建与升级)

- onChange({ ...editing, enabled: checked })} /> + + onChange({ ...editing, enabled: checked }) + } + />
- +
- ) + ); } export function BrowserImageList() { - const dataProvider = useDataProvider()('default') - const { result, query } = useList({ resource: 'browser-images' }) - const error = query.error - const isPending = query.isPending - const images = result.data ?? [] - const [createOpen, setCreateOpen] = useState(false) - const [createError, setCreateError] = useState(null) - const [editing, setEditing] = useState(null) - const [busyAlias, setBusyAlias] = useState('') - const [notice, setNotice] = useState(null) - const [deleting, setDeleting] = useState(null) - useTitle('CreatorHub · 镜像版本') + const dataProvider = useDataProvider()("default"); + const { result, query } = useList({ resource: "browser-images" }); + const error = query.error; + const isPending = query.isPending; + const images = result.data ?? []; + const [createOpen, setCreateOpen] = useState(false); + const [createError, setCreateError] = useState(null); + const [editing, setEditing] = useState(null); + const [busyAlias, setBusyAlias] = useState(""); + const [notice, setNotice] = useState(null); + const [deleting, setDeleting] = useState(null); + useTitle("CreatorHub · 镜像版本"); async function createImage(data) { - setBusyAlias('create') - setCreateError(null) + setBusyAlias("create"); + setCreateError(null); try { - await dataProvider.create({ resource: 'browser-images', variables: data }) - await query.refetch() - return true + await dataProvider.create({ + resource: "browser-images", + variables: data, + }); + await query.refetch(); + return true; } catch (reason) { - setCreateError(reason) - return false + setCreateError(reason); + return false; } finally { - setBusyAlias('') + setBusyAlias(""); } } async function toggleEnabled(image) { - setBusyAlias(image.version) - setNotice(null) + setBusyAlias(image.version); + setNotice(null); try { - await dataProvider.update({ resource: 'browser-images', id: image.version, variables: { image_ref: image.image_ref, note: image.note, enabled: !image.enabled } }) - await query.refetch() + await dataProvider.update({ + resource: "browser-images", + id: image.version, + variables: { + image_ref: image.image_ref, + note: image.note, + enabled: !image.enabled, + }, + }); + await query.refetch(); } catch (reason) { - setNotice({ variant: 'destructive', text: reason.message }) + setNotice({ variant: "destructive", text: reason.message }); } finally { - setBusyAlias('') + setBusyAlias(""); } } async function removeImage(image) { - setBusyAlias(image.version) - setNotice(null) + setBusyAlias(image.version); + setNotice(null); try { - await dataProvider.deleteOne({ resource: 'browser-images', id: image.version }) - await query.refetch() - setDeleting(null) + await dataProvider.deleteOne({ + resource: "browser-images", + id: image.version, + }); + await query.refetch(); + setDeleting(null); } catch (reason) { - setNotice({ variant: 'destructive', text: reason.message }) + setNotice({ variant: "destructive", text: reason.message }); } finally { - setBusyAlias('') + setBusyAlias(""); } } async function saveEdit() { if (!refPattern.test(editing.image_ref)) { - setEditing({ ...editing, error: '镜像引用格式不正确' }) - return + setEditing({ ...editing, error: "镜像引用格式不正确" }); + return; } - setBusyAlias(editing.version) - setNotice(null) + setBusyAlias(editing.version); + setNotice(null); try { - await dataProvider.update({ resource: 'browser-images', id: editing.version, variables: { image_ref: editing.image_ref, note: editing.note, enabled: editing.enabled } }) - setEditing(null) - await query.refetch() + await dataProvider.update({ + resource: "browser-images", + id: editing.version, + variables: { + image_ref: editing.image_ref, + note: editing.note, + enabled: editing.enabled, + }, + }); + setEditing(null); + await query.refetch(); } catch (reason) { - setEditing({ ...editing, error: reason.message }) + setEditing({ ...editing, error: reason.message }); } finally { - setBusyAlias('') + setBusyAlias(""); } } - const message = notice?.text || (error ? null : notice?.message) || (notice ? notice.message : null) + const message = + notice?.text || + (error ? null : notice?.message) || + (notice ? notice.message : null); return (
- - + + - {message ? {message} : null} - + {message ? ( + + {message} + + ) : null} + {image.version} }, - { header: '镜像引用', width: '38%', render: image => {image.image_ref} }, - { header: '备注', width: '20%', render: image => {image.note || '—'} }, - { header: '启用', width: '8%', render: image => toggleEnabled(image)} /> }, - { header: '操作', width: '12%', align: 'right', render: image => ( - -