refactor: migrate browser gateway to native xvfb
This commit is contained in:
@@ -240,7 +240,7 @@ export function AccountEditPage() {
|
||||
variant: "destructive",
|
||||
text: conflictMessage(
|
||||
qrError,
|
||||
"登录二维码获取失败;请检查运行环境、网关和镜像配置",
|
||||
"登录二维码获取失败;请检查运行环境、网关和浏览器版本配置",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,29 +15,30 @@ import {
|
||||
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 pathPattern = /^\/[A-Za-z0-9._+~/-]{1,4095}$/;
|
||||
|
||||
function ImageCreateModal({ open, onClose, onSubmit, busy, error }) {
|
||||
const [form, setForm] = useState({ version: "", image_ref: "", note: "" });
|
||||
function BrowserVersionCreateModal({ open, onClose, onSubmit, busy, error }) {
|
||||
const [form, setForm] = useState({ version: "", browser_path: "", note: "" });
|
||||
const update = (key, value) =>
|
||||
setForm((current) => ({ ...current, [key]: value }));
|
||||
const valid =
|
||||
versionPattern.test(form.version) && refPattern.test(form.image_ref);
|
||||
versionPattern.test(form.version) && pathPattern.test(form.browser_path);
|
||||
const versionInvalid =
|
||||
form.version !== "" && !versionPattern.test(form.version);
|
||||
const refInvalid = form.image_ref !== "" && !refPattern.test(form.image_ref);
|
||||
const pathInvalid =
|
||||
form.browser_path !== "" && !pathPattern.test(form.browser_path);
|
||||
|
||||
async function submit(event) {
|
||||
event.preventDefault();
|
||||
if (!valid) return;
|
||||
const created = await onSubmit({
|
||||
version: form.version,
|
||||
image_ref: form.image_ref,
|
||||
browser_path: form.browser_path,
|
||||
note: form.note,
|
||||
enabled: true,
|
||||
});
|
||||
if (created) {
|
||||
setForm({ version: "", image_ref: "", note: "" });
|
||||
setForm({ version: "", browser_path: "", note: "" });
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
@@ -46,8 +47,8 @@ function ImageCreateModal({ open, onClose, onSubmit, busy, error }) {
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title="添加镜像版本"
|
||||
labelledBy="image-create-title"
|
||||
title="添加浏览器版本"
|
||||
labelledBy="browser-version-create-title"
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} disabled={busy}>
|
||||
@@ -56,7 +57,7 @@ function ImageCreateModal({ open, onClose, onSubmit, busy, error }) {
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
form="image-create-form"
|
||||
form="browser-version-create-form"
|
||||
busy={busy}
|
||||
busyText="保存中…"
|
||||
disabled={!valid}
|
||||
@@ -66,7 +67,7 @@ function ImageCreateModal({ open, onClose, onSubmit, busy, error }) {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form id="image-create-form" onSubmit={submit} noValidate>
|
||||
<form id="browser-version-create-form" onSubmit={submit} noValidate>
|
||||
{error ? (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
{error.message}
|
||||
@@ -74,14 +75,14 @@ function ImageCreateModal({ open, onClose, onSubmit, busy, error }) {
|
||||
) : null}
|
||||
<div className="space-y-4">
|
||||
<Field
|
||||
id="image-version"
|
||||
id="browser-version"
|
||||
label="版本"
|
||||
required
|
||||
error={versionInvalid ? "数字开头,最长 64 字符" : undefined}
|
||||
helper="镜像版本号,如 148.0.7778.215"
|
||||
helper="浏览器版本号,如 148.0.7778.215"
|
||||
>
|
||||
<Input
|
||||
id="image-version"
|
||||
id="browser-version"
|
||||
required
|
||||
maxLength={64}
|
||||
value={form.version}
|
||||
@@ -90,25 +91,29 @@ function ImageCreateModal({ open, onClose, onSubmit, busy, error }) {
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
id="image-ref"
|
||||
label="镜像引用"
|
||||
id="browser-path"
|
||||
label="浏览器路径"
|
||||
required
|
||||
error={refInvalid ? "格式不正确" : undefined}
|
||||
helper="registry/repo:tag 或 @sha256 摘要"
|
||||
error={pathInvalid ? "格式不正确" : undefined}
|
||||
helper="gateway 节点上的预安装浏览器绝对路径"
|
||||
>
|
||||
<Input
|
||||
id="image-ref"
|
||||
id="browser-path"
|
||||
required
|
||||
maxLength={301}
|
||||
value={form.image_ref}
|
||||
onChange={(event) => update("image_ref", event.target.value)}
|
||||
placeholder="registry/repo:tag 或 @sha256 摘要"
|
||||
invalid={refInvalid}
|
||||
maxLength={4096}
|
||||
value={form.browser_path}
|
||||
onChange={(event) => update("browser_path", event.target.value)}
|
||||
placeholder="gateway 节点上的预安装浏览器绝对路径"
|
||||
invalid={pathInvalid}
|
||||
/>
|
||||
</Field>
|
||||
<Field id="image-note" label="备注" helper="可选,最长 200 字符">
|
||||
<Field
|
||||
id="browser-version-note"
|
||||
label="备注"
|
||||
helper="可选,最长 200 字符"
|
||||
>
|
||||
<Input
|
||||
id="image-note"
|
||||
id="browser-version-note"
|
||||
maxLength={200}
|
||||
value={form.note}
|
||||
onChange={(event) => update("note", event.target.value)}
|
||||
@@ -120,30 +125,34 @@ function ImageCreateModal({ open, onClose, onSubmit, busy, error }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ImageEditForm({ editing, onChange, onSave, busy }) {
|
||||
const refInvalid =
|
||||
editing.image_ref !== "" && !refPattern.test(editing.image_ref);
|
||||
function BrowserVersionEditForm({ editing, onChange, onSave, busy }) {
|
||||
const pathInvalid =
|
||||
editing.browser_path !== "" && !pathPattern.test(editing.browser_path);
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Field
|
||||
id="edit-image-ref"
|
||||
label="镜像引用"
|
||||
id="edit-browser-path"
|
||||
label="浏览器路径"
|
||||
required
|
||||
error={refInvalid ? "格式不正确" : undefined}
|
||||
helper="registry/repo:tag 或 @sha256 摘要"
|
||||
error={pathInvalid ? "格式不正确" : undefined}
|
||||
helper="gateway 节点上的预安装浏览器绝对路径"
|
||||
>
|
||||
<Input
|
||||
id="edit-image-ref"
|
||||
value={editing.image_ref}
|
||||
invalid={refInvalid}
|
||||
id="edit-browser-path"
|
||||
value={editing.browser_path}
|
||||
invalid={pathInvalid}
|
||||
onChange={(event) =>
|
||||
onChange({ ...editing, image_ref: event.target.value })
|
||||
onChange({ ...editing, browser_path: event.target.value })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field id="edit-image-note" label="备注" helper="可选,最长 200 字符">
|
||||
<Field
|
||||
id="edit-browser-version-note"
|
||||
label="备注"
|
||||
helper="可选,最长 200 字符"
|
||||
>
|
||||
<Input
|
||||
id="edit-image-note"
|
||||
id="edit-browser-version-note"
|
||||
maxLength={200}
|
||||
value={editing.note}
|
||||
onChange={(event) =>
|
||||
@@ -154,7 +163,7 @@ function ImageEditForm({ editing, onChange, onSave, busy }) {
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-sm font-medium">启用(可用于创建与升级)</p>
|
||||
<Switch
|
||||
id="edit-image-enabled"
|
||||
id="edit-browser-version-enabled"
|
||||
checked={editing.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...editing, enabled: checked })
|
||||
@@ -165,7 +174,7 @@ function ImageEditForm({ editing, onChange, onSave, busy }) {
|
||||
variant="primary"
|
||||
busy={busy}
|
||||
busyText="保存中…"
|
||||
disabled={refInvalid}
|
||||
disabled={pathInvalid}
|
||||
onClick={onSave}
|
||||
>
|
||||
保存
|
||||
@@ -174,26 +183,26 @@ function ImageEditForm({ editing, onChange, onSave, busy }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function BrowserImageList() {
|
||||
export function BrowserVersionList() {
|
||||
const dataProvider = useDataProvider()("default");
|
||||
const { result, query } = useList({ resource: "browser-images" });
|
||||
const { result, query } = useList({ resource: "browser-versions" });
|
||||
const error = query.error;
|
||||
const isPending = query.isPending;
|
||||
const images = result.data ?? [];
|
||||
const versions = 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 · 镜像版本");
|
||||
useTitle("CreatorHub · 浏览器版本");
|
||||
|
||||
async function createImage(data) {
|
||||
async function createBrowserVersion(data) {
|
||||
setBusyAlias("create");
|
||||
setCreateError(null);
|
||||
try {
|
||||
await dataProvider.create({
|
||||
resource: "browser-images",
|
||||
resource: "browser-versions",
|
||||
variables: data,
|
||||
});
|
||||
await query.refetch();
|
||||
@@ -206,17 +215,17 @@ export function BrowserImageList() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleEnabled(image) {
|
||||
setBusyAlias(image.version);
|
||||
async function toggleBrowserVersion(version) {
|
||||
setBusyAlias(version.version);
|
||||
setNotice(null);
|
||||
try {
|
||||
await dataProvider.update({
|
||||
resource: "browser-images",
|
||||
id: image.version,
|
||||
resource: "browser-versions",
|
||||
id: version.version,
|
||||
variables: {
|
||||
image_ref: image.image_ref,
|
||||
note: image.note,
|
||||
enabled: !image.enabled,
|
||||
browser_path: version.browser_path,
|
||||
note: version.note,
|
||||
enabled: !version.enabled,
|
||||
},
|
||||
});
|
||||
await query.refetch();
|
||||
@@ -227,13 +236,13 @@ export function BrowserImageList() {
|
||||
}
|
||||
}
|
||||
|
||||
async function removeImage(image) {
|
||||
setBusyAlias(image.version);
|
||||
async function removeBrowserVersion(version) {
|
||||
setBusyAlias(version.version);
|
||||
setNotice(null);
|
||||
try {
|
||||
await dataProvider.deleteOne({
|
||||
resource: "browser-images",
|
||||
id: image.version,
|
||||
resource: "browser-versions",
|
||||
id: version.version,
|
||||
});
|
||||
await query.refetch();
|
||||
setDeleting(null);
|
||||
@@ -245,18 +254,18 @@ export function BrowserImageList() {
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!refPattern.test(editing.image_ref)) {
|
||||
setEditing({ ...editing, error: "镜像引用格式不正确" });
|
||||
if (!pathPattern.test(editing.browser_path)) {
|
||||
setEditing({ ...editing, error: "浏览器路径格式不正确" });
|
||||
return;
|
||||
}
|
||||
setBusyAlias(editing.version);
|
||||
setNotice(null);
|
||||
try {
|
||||
await dataProvider.update({
|
||||
resource: "browser-images",
|
||||
resource: "browser-versions",
|
||||
id: editing.version,
|
||||
variables: {
|
||||
image_ref: editing.image_ref,
|
||||
browser_path: editing.browser_path,
|
||||
note: editing.note,
|
||||
enabled: editing.enabled,
|
||||
},
|
||||
@@ -278,9 +287,9 @@ export function BrowserImageList() {
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
icon="ri-inbox-line"
|
||||
title="镜像版本"
|
||||
description="维护可用浏览器镜像版本;创建与升级环境时从中选择"
|
||||
icon="ri-window-line"
|
||||
title="浏览器版本"
|
||||
description="维护可用浏览器版本;创建与升级环境时从中选择"
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -298,46 +307,46 @@ export function BrowserImageList() {
|
||||
<PageState
|
||||
pending={isPending}
|
||||
error={error}
|
||||
empty={images.length === 0}
|
||||
emptyText="暂无镜像版本;网关会在镜像缺失时按引用自动拉取。"
|
||||
empty={versions.length === 0}
|
||||
emptyText="暂无浏览器版本;请先登记 gateway 节点上的预安装浏览器路径。"
|
||||
>
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
header: "版本",
|
||||
width: "22%",
|
||||
render: (image) => (
|
||||
<span className="font-semibold">{image.version}</span>
|
||||
render: (version) => (
|
||||
<span className="font-semibold">{version.version}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "镜像引用",
|
||||
header: "浏览器路径",
|
||||
width: "38%",
|
||||
render: (image) => (
|
||||
render: (version) => (
|
||||
<code className="anywhere font-mono text-[13px]">
|
||||
{image.image_ref}
|
||||
{version.browser_path}
|
||||
</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "备注",
|
||||
width: "20%",
|
||||
render: (image) => (
|
||||
<span className={image.note ? "" : "text-muted"}>
|
||||
{image.note || "—"}
|
||||
render: (version) => (
|
||||
<span className={version.note ? "" : "text-muted"}>
|
||||
{version.note || "—"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "启用",
|
||||
width: "8%",
|
||||
render: (image) => (
|
||||
render: (version) => (
|
||||
<Switch
|
||||
id={`image-enabled-${image.version}`}
|
||||
aria-label={`启用 ${image.version}`}
|
||||
checked={image.enabled}
|
||||
disabled={busyAlias === image.version}
|
||||
onCheckedChange={() => toggleEnabled(image)}
|
||||
id={`browser-version-enabled-${version.version}`}
|
||||
aria-label={`启用 ${version.version}`}
|
||||
checked={version.enabled}
|
||||
disabled={busyAlias === version.version}
|
||||
onCheckedChange={() => toggleBrowserVersion(version)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -345,22 +354,22 @@ export function BrowserImageList() {
|
||||
header: "操作",
|
||||
width: "12%",
|
||||
align: "right",
|
||||
render: (image) => (
|
||||
render: (version) => (
|
||||
<span className="inline-flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`编辑 ${image.version}`}
|
||||
disabled={busyAlias === image.version}
|
||||
onClick={() => setEditing({ ...image, error: "" })}
|
||||
aria-label={`编辑 ${version.version}`}
|
||||
disabled={busyAlias === version.version}
|
||||
onClick={() => setEditing({ ...version, error: "" })}
|
||||
className="rounded p-1.5 text-muted hover:bg-[#f5f7fa] hover:text-primary disabled:opacity-50"
|
||||
>
|
||||
<i className="ri-edit-line" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`删除 ${image.version}`}
|
||||
disabled={busyAlias === image.version}
|
||||
onClick={() => setDeleting(image)}
|
||||
aria-label={`删除 ${version.version}`}
|
||||
disabled={busyAlias === version.version}
|
||||
onClick={() => setDeleting(version)}
|
||||
className="rounded p-1.5 text-danger hover:bg-[#fdf1f0] disabled:opacity-50"
|
||||
>
|
||||
<i className="ri-delete-bin-line" aria-hidden="true" />
|
||||
@@ -369,18 +378,18 @@ export function BrowserImageList() {
|
||||
),
|
||||
},
|
||||
]}
|
||||
rows={images}
|
||||
rowKey={(image) => image.version}
|
||||
rows={versions}
|
||||
rowKey={(version) => version.version}
|
||||
minWidth="900px"
|
||||
/>
|
||||
</PageState>
|
||||
<ImageCreateModal
|
||||
<BrowserVersionCreateModal
|
||||
open={createOpen}
|
||||
onClose={() => {
|
||||
setCreateOpen(false);
|
||||
setCreateError(null);
|
||||
}}
|
||||
onSubmit={createImage}
|
||||
onSubmit={createBrowserVersion}
|
||||
busy={busyAlias === "create"}
|
||||
error={createError}
|
||||
/>
|
||||
@@ -389,7 +398,7 @@ export function BrowserImageList() {
|
||||
open
|
||||
onClose={() => setEditing(null)}
|
||||
title={`编辑 ${editing.version}`}
|
||||
labelledBy="image-edit-title"
|
||||
labelledBy="browser-version-edit-title"
|
||||
footer={<Button onClick={() => setEditing(null)}>关闭</Button>}
|
||||
>
|
||||
{editing.error ? (
|
||||
@@ -397,7 +406,7 @@ export function BrowserImageList() {
|
||||
{editing.error}
|
||||
</Alert>
|
||||
) : null}
|
||||
<ImageEditForm
|
||||
<BrowserVersionEditForm
|
||||
editing={editing}
|
||||
onChange={(next) => setEditing({ ...next, error: editing.error })}
|
||||
onSave={saveEdit}
|
||||
@@ -408,11 +417,11 @@ export function BrowserImageList() {
|
||||
<ConfirmDialog
|
||||
open={deleting !== null}
|
||||
onClose={() => setDeleting(null)}
|
||||
onConfirm={() => removeImage(deleting)}
|
||||
title="删除镜像版本"
|
||||
onConfirm={() => removeBrowserVersion(deleting)}
|
||||
title="删除浏览器版本"
|
||||
confirmLabel="确认删除"
|
||||
busy={busyAlias === deleting?.version}
|
||||
body={`删除镜像版本 ${deleting?.version}?仍被环境引用时删除会被拒绝。`}
|
||||
body={`删除浏览器版本 ${deleting?.version}?仍被环境引用时删除会被拒绝。`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -10,25 +10,25 @@ import {
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Refine } from "@refinedev/core";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { BrowserImageList } from "./BrowserImagesPage";
|
||||
import { BrowserVersionList } from "./BrowserVersionsPage";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const images = [
|
||||
const versions = [
|
||||
{
|
||||
id: "148.0.0.1",
|
||||
version: "148.0.0.1",
|
||||
image_ref: "reg/img:148",
|
||||
browser_path: "/opt/creatorhub/browsers/148/chrome",
|
||||
note: "",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "144.0.0.1",
|
||||
version: "144.0.0.1",
|
||||
image_ref: "reg/img:144",
|
||||
browser_path: "/opt/creatorhub/browsers/144/chrome",
|
||||
note: "旧版",
|
||||
enabled: false,
|
||||
},
|
||||
@@ -37,10 +37,10 @@ const images = [
|
||||
function provider(overrides = {}) {
|
||||
return {
|
||||
getList: vi.fn(() =>
|
||||
Promise.resolve({ data: images, total: images.length }),
|
||||
Promise.resolve({ data: versions, total: versions.length }),
|
||||
),
|
||||
create: vi.fn().mockResolvedValue({ data: images[0] }),
|
||||
update: vi.fn().mockResolvedValue({ data: images[0] }),
|
||||
create: vi.fn().mockResolvedValue({ data: versions[0] }),
|
||||
update: vi.fn().mockResolvedValue({ data: versions[0] }),
|
||||
deleteOne: vi.fn().mockResolvedValue({ data: { id: "144.0.0.1" } }),
|
||||
getOne: vi.fn(),
|
||||
getMany: vi.fn(),
|
||||
@@ -52,7 +52,7 @@ function provider(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const renderImages = (dataProvider) =>
|
||||
const renderVersions = (dataProvider) =>
|
||||
render(
|
||||
<QueryClientProvider
|
||||
client={
|
||||
@@ -61,35 +61,35 @@ const renderImages = (dataProvider) =>
|
||||
>
|
||||
<Refine dataProvider={dataProvider}>
|
||||
<MemoryRouter>
|
||||
<BrowserImageList />
|
||||
<BrowserVersionList />
|
||||
</MemoryRouter>
|
||||
</Refine>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
describe("BrowserImageList", () => {
|
||||
it("adds an image version", async () => {
|
||||
describe("BrowserVersionList", () => {
|
||||
it("adds a browser version", async () => {
|
||||
const dataProvider = provider();
|
||||
renderImages(dataProvider);
|
||||
renderVersions(dataProvider);
|
||||
|
||||
await screen.findAllByText("reg/img:148");
|
||||
await screen.findAllByText("/opt/creatorhub/browsers/148/chrome");
|
||||
fireEvent.click(screen.getByRole("button", { name: "添加版本" }));
|
||||
const dialog = screen.getByRole("dialog");
|
||||
fireEvent.change(within(dialog).getByRole("textbox", { name: "版本" }), {
|
||||
target: { value: "150.0.0.1" },
|
||||
});
|
||||
fireEvent.change(
|
||||
within(dialog).getByRole("textbox", { name: "镜像引用" }),
|
||||
{ target: { value: "reg/img:150" } },
|
||||
within(dialog).getByRole("textbox", { name: "浏览器路径" }),
|
||||
{ target: { value: "/opt/creatorhub/browsers/150/chrome" } },
|
||||
);
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "添加版本" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(dataProvider.create).toHaveBeenCalledWith({
|
||||
resource: "browser-images",
|
||||
resource: "browser-versions",
|
||||
variables: {
|
||||
version: "150.0.0.1",
|
||||
image_ref: "reg/img:150",
|
||||
browser_path: "/opt/creatorhub/browsers/150/chrome",
|
||||
note: "",
|
||||
enabled: true,
|
||||
},
|
||||
@@ -100,31 +100,31 @@ describe("BrowserImageList", () => {
|
||||
|
||||
it("keeps the create modal open after a failure", async () => {
|
||||
const dataProvider = provider({
|
||||
create: vi.fn().mockRejectedValue(new Error("镜像创建失败")),
|
||||
create: vi.fn().mockRejectedValue(new Error("浏览器版本创建失败")),
|
||||
});
|
||||
renderImages(dataProvider);
|
||||
renderVersions(dataProvider);
|
||||
|
||||
await screen.findAllByText("reg/img:148");
|
||||
await screen.findAllByText("/opt/creatorhub/browsers/148/chrome");
|
||||
fireEvent.click(screen.getByRole("button", { name: "添加版本" }));
|
||||
const dialog = screen.getByRole("dialog");
|
||||
fireEvent.change(within(dialog).getByRole("textbox", { name: "版本" }), {
|
||||
target: { value: "150.0.0.1" },
|
||||
});
|
||||
fireEvent.change(
|
||||
within(dialog).getByRole("textbox", { name: "镜像引用" }),
|
||||
{ target: { value: "reg/img:150" } },
|
||||
within(dialog).getByRole("textbox", { name: "浏览器路径" }),
|
||||
{ target: { value: "/opt/creatorhub/browsers/150/chrome" } },
|
||||
);
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "添加版本" }));
|
||||
|
||||
expect((await within(dialog).findByRole("alert")).textContent).toContain(
|
||||
"镜像创建失败",
|
||||
"浏览器版本创建失败",
|
||||
);
|
||||
expect(screen.getByRole("dialog")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("toggles enabled through update", async () => {
|
||||
const dataProvider = provider();
|
||||
renderImages(dataProvider);
|
||||
renderVersions(dataProvider);
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole("switch", { name: "启用 144.0.0.1" }),
|
||||
@@ -132,18 +132,22 @@ describe("BrowserImageList", () => {
|
||||
|
||||
await waitFor(() =>
|
||||
expect(dataProvider.update).toHaveBeenCalledWith({
|
||||
resource: "browser-images",
|
||||
resource: "browser-versions",
|
||||
id: "144.0.0.1",
|
||||
variables: { image_ref: "reg/img:144", note: "旧版", enabled: true },
|
||||
variables: {
|
||||
browser_path: "/opt/creatorhub/browsers/144/chrome",
|
||||
note: "旧版",
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks invalid input before submit", async () => {
|
||||
const dataProvider = provider();
|
||||
renderImages(dataProvider);
|
||||
renderVersions(dataProvider);
|
||||
|
||||
await screen.findAllByText("reg/img:148");
|
||||
await screen.findAllByText("/opt/creatorhub/browsers/148/chrome");
|
||||
fireEvent.click(screen.getByRole("button", { name: "添加版本" }));
|
||||
const dialog = screen.getByRole("dialog");
|
||||
fireEvent.change(within(dialog).getByRole("textbox", { name: "版本" }), {
|
||||
@@ -159,7 +163,7 @@ describe("BrowserImageList", () => {
|
||||
const dataProvider = provider({
|
||||
deleteOne: vi.fn().mockRejectedValue(new Error("该版本正被环境引用")),
|
||||
});
|
||||
renderImages(dataProvider);
|
||||
renderVersions(dataProvider);
|
||||
|
||||
fireEvent.click(await screen.findByLabelText("删除 148.0.0.1"));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "确认删除" }));
|
||||
+74
-51
@@ -34,9 +34,13 @@ const spoofingFields = ["font", "audio", "canvas", "clientrects", "gpu"];
|
||||
const stateText = {
|
||||
created: "已创建",
|
||||
running: "运行中",
|
||||
exited: "已停止",
|
||||
restarting: "重启中",
|
||||
paused: "已暂停",
|
||||
stopped: "已停止",
|
||||
degraded: "异常",
|
||||
failed: "失败待清理",
|
||||
cleanup_pending: "待清理",
|
||||
gateway_unreachable: "网关不可达",
|
||||
runtime_missing: "运行实例缺失",
|
||||
recorded: "已记录",
|
||||
missing: "未部署",
|
||||
};
|
||||
const blockText = {
|
||||
@@ -60,7 +64,7 @@ const createInitial = {
|
||||
name: "",
|
||||
alias: "",
|
||||
gateway: "",
|
||||
image_version: "",
|
||||
browser_version: "",
|
||||
account_id: "",
|
||||
network_exit_id: "",
|
||||
seed: "",
|
||||
@@ -114,7 +118,7 @@ function StatePill({ state }) {
|
||||
tone={
|
||||
state === "running"
|
||||
? "success"
|
||||
: state === "exited"
|
||||
: state === "stopped"
|
||||
? "warning"
|
||||
: "neutral"
|
||||
}
|
||||
@@ -126,6 +130,14 @@ function StatePill({ state }) {
|
||||
|
||||
function RuntimeActions({ runtime, busy, onAction }) {
|
||||
const running = runtime.state === "running";
|
||||
const stopping = [
|
||||
"running",
|
||||
"degraded",
|
||||
"starting",
|
||||
"failed",
|
||||
"cleanup_pending",
|
||||
].includes(runtime.state);
|
||||
const gatewayUnavailable = runtime.gateway_reachable === false;
|
||||
const block =
|
||||
runtime.schedule_block_reason === "runtime_missing"
|
||||
? ""
|
||||
@@ -137,7 +149,7 @@ function RuntimeActions({ runtime, busy, onAction }) {
|
||||
size="sm"
|
||||
icon="ri-play-line"
|
||||
aria-label={startLabel}
|
||||
disabled={busy || running || block !== ""}
|
||||
disabled={busy || running || gatewayUnavailable || block !== ""}
|
||||
onClick={() => onAction(runtime, "start")}
|
||||
>
|
||||
启动
|
||||
@@ -146,7 +158,7 @@ function RuntimeActions({ runtime, busy, onAction }) {
|
||||
size="sm"
|
||||
icon="ri-stop-line"
|
||||
aria-label={`停止 ${runtime.name}`}
|
||||
disabled={busy || !running}
|
||||
disabled={busy || !stopping || gatewayUnavailable}
|
||||
onClick={() => onAction(runtime, "stop")}
|
||||
>
|
||||
停止
|
||||
@@ -155,7 +167,7 @@ function RuntimeActions({ runtime, busy, onAction }) {
|
||||
size="sm"
|
||||
icon="ri-arrow-up-circle-line"
|
||||
aria-label={`升级 ${runtime.name}`}
|
||||
disabled={busy}
|
||||
disabled={busy || gatewayUnavailable}
|
||||
onClick={() => onAction(runtime, "upgrade")}
|
||||
>
|
||||
升级
|
||||
@@ -165,7 +177,7 @@ function RuntimeActions({ runtime, busy, onAction }) {
|
||||
variant="danger"
|
||||
icon="ri-delete-bin-line"
|
||||
aria-label={`回收 ${runtime.name}`}
|
||||
disabled={busy}
|
||||
disabled={busy || gatewayUnavailable}
|
||||
onClick={() => onAction(runtime, "recycle")}
|
||||
>
|
||||
回收
|
||||
@@ -184,9 +196,9 @@ function SchedulePill({ runtime }) {
|
||||
);
|
||||
}
|
||||
|
||||
function UpgradeDialog({ target, images, busy, onClose, onConfirm }) {
|
||||
function UpgradeDialog({ target, versions, busy, onClose, onConfirm }) {
|
||||
const [version, setVersion] = useState("");
|
||||
const enabled = images.filter((image) => image.enabled);
|
||||
const enabled = versions.filter((item) => item.enabled);
|
||||
useEffect(() => {
|
||||
setVersion("");
|
||||
}, [target]);
|
||||
@@ -206,7 +218,7 @@ function UpgradeDialog({ target, images, busy, onClose, onConfirm }) {
|
||||
variant="primary"
|
||||
busy={busy}
|
||||
busyText="升级中…"
|
||||
disabled={version === "" || version === target?.image_version}
|
||||
disabled={version === "" || version === target?.browser_version}
|
||||
onClick={() => onConfirm(version)}
|
||||
>
|
||||
确认升级
|
||||
@@ -216,16 +228,16 @@ function UpgradeDialog({ target, images, busy, onClose, onConfirm }) {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted">
|
||||
容器将按新镜像重建,Profile 数据卷与指纹参数保持不变。
|
||||
环境将按目标浏览器版本重启,Profile 目录与指纹参数保持不变。
|
||||
</p>
|
||||
<Field id="upgrade-version" label="目标镜像版本" required>
|
||||
<Field id="upgrade-version" label="目标浏览器版本" required>
|
||||
<Select
|
||||
id="upgrade-version"
|
||||
value={version}
|
||||
onChange={(event) => setVersion(event.target.value)}
|
||||
options={enabled.map((image) => ({
|
||||
value: image.version,
|
||||
label: `${image.version}${image.version === target?.image_version ? "(当前)" : ""}`,
|
||||
options={enabled.map((item) => ({
|
||||
value: item.version,
|
||||
label: `${item.version}${item.version === target?.browser_version ? "(当前)" : ""}`,
|
||||
}))}
|
||||
placeholder="选择版本"
|
||||
/>
|
||||
@@ -270,7 +282,7 @@ function RuntimeTable({ runtimes, busy, onAction, onRowClick }) {
|
||||
<p className="text-sm">{runtime.account_id || "未绑定账号"}</p>
|
||||
<p className="anywhere text-xs text-muted">
|
||||
{runtime.network_exit_id || "当前机器直连"} ·{" "}
|
||||
{runtime.image_version}
|
||||
{runtime.browser_version}
|
||||
</p>
|
||||
<div className="mt-1">
|
||||
<SchedulePill runtime={runtime} />
|
||||
@@ -307,7 +319,7 @@ function RuntimeCards({ runtimes, busy, onAction }) {
|
||||
<div>
|
||||
<p className="font-semibold">{runtime.name}</p>
|
||||
<p className="anywhere text-xs text-muted">
|
||||
{runtime.alias} · {runtime.image_version} · seed{" "}
|
||||
{runtime.alias} · {runtime.browser_version} · seed{" "}
|
||||
{runtime.fingerprint?.seed}
|
||||
</p>
|
||||
</div>
|
||||
@@ -340,17 +352,17 @@ export function BrowserList() {
|
||||
const error = query.error;
|
||||
const isPending = query.isPending;
|
||||
const runtimes = result.data ?? [];
|
||||
const { result: imageResult } = useList({
|
||||
resource: "browser-images",
|
||||
const { result: versionsResult } = useList({
|
||||
resource: "browser-versions",
|
||||
});
|
||||
const images = imageResult.data ?? [];
|
||||
const versions = versionsResult.data ?? [];
|
||||
useTitle("CreatorHub · 运行环境");
|
||||
|
||||
async function handleAction(runtime, action) {
|
||||
if (
|
||||
action === "recycle" &&
|
||||
!window.confirm(
|
||||
`回收 ${runtime.name}(${runtime.alias})?容器将被删除,Profile 数据卷保留。`,
|
||||
`回收 ${runtime.name}(${runtime.alias})?运行资源将被回收,Profile 目录保留。`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
@@ -384,7 +396,7 @@ export function BrowserList() {
|
||||
<PageHeader
|
||||
icon="ri-server-line"
|
||||
title="运行环境"
|
||||
description="启动、停止、升级并回收隔离的指纹浏览器"
|
||||
description="启动、停止、升级并回收隔离的指纹浏览器运行环境"
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -420,7 +432,7 @@ export function BrowserList() {
|
||||
</PageState>
|
||||
<UpgradeDialog
|
||||
target={upgradeTarget}
|
||||
images={images}
|
||||
versions={versions}
|
||||
busy={busy === upgradeTarget?.alias}
|
||||
onClose={() => setUpgradeTarget(null)}
|
||||
onConfirm={upgrade}
|
||||
@@ -442,15 +454,15 @@ export function BrowserCreatePage() {
|
||||
const { result: gatewayResult } = useList({ resource: "gateways" });
|
||||
const gateways = gatewayResult.data ?? [];
|
||||
const runtimes = runtimesResult.data ?? [];
|
||||
const { result: imageResult } = useList({ resource: "browser-images" });
|
||||
const images = imageResult.data ?? [];
|
||||
const { result: versionsResult } = useList({ resource: "browser-versions" });
|
||||
const versions = versionsResult.data ?? [];
|
||||
const { result: accountResult } = useList({ resource: "accounts" });
|
||||
const accounts = accountResult.data ?? [];
|
||||
const { result: exitResult } = useList({ resource: "network-exits" });
|
||||
const networkExits = exitResult.data ?? [];
|
||||
useTitle("CreatorHub · 创建运行环境");
|
||||
|
||||
const enabledImages = images.filter((image) => image.enabled);
|
||||
const enabledVersions = versions.filter((item) => item.enabled);
|
||||
const pausedAccounts = accounts.filter(
|
||||
(account) =>
|
||||
account.authorization_status === "authorized" &&
|
||||
@@ -475,9 +487,9 @@ export function BrowserCreatePage() {
|
||||
update("gateway", gateways[0].name);
|
||||
}, [gateways, form.gateway]);
|
||||
useEffect(() => {
|
||||
if (form.image_version === "" && enabledImages.length > 0)
|
||||
update("image_version", enabledImages[0].version);
|
||||
}, [enabledImages, form.image_version]);
|
||||
if (form.browser_version === "" && enabledVersions.length > 0)
|
||||
update("browser_version", enabledVersions[0].version);
|
||||
}, [enabledVersions, form.browser_version]);
|
||||
useEffect(() => {
|
||||
if (form.account_id === "" && pausedAccounts[0]?.id)
|
||||
update("account_id", pausedAccounts[0].id);
|
||||
@@ -495,7 +507,7 @@ export function BrowserCreatePage() {
|
||||
form.name.trim() !== "" &&
|
||||
aliasPattern.test(form.alias) &&
|
||||
form.gateway !== "" &&
|
||||
form.image_version !== "" &&
|
||||
form.browser_version !== "" &&
|
||||
form.account_id !== "" &&
|
||||
Number.isInteger(seed) &&
|
||||
seed >= 1 &&
|
||||
@@ -513,7 +525,7 @@ export function BrowserCreatePage() {
|
||||
alias: form.alias,
|
||||
name: form.name.trim(),
|
||||
gateway: form.gateway,
|
||||
image_version: form.image_version,
|
||||
browser_version: form.browser_version,
|
||||
account_id: form.account_id,
|
||||
network_exit_id: form.network_exit_id,
|
||||
fingerprint: fingerprintPayload(form),
|
||||
@@ -539,7 +551,7 @@ export function BrowserCreatePage() {
|
||||
<div className="max-w-[960px]">
|
||||
<PageHeader
|
||||
title="创建运行环境"
|
||||
description="为已授权且暂停的账号绑定网关、镜像、指纹与网络出口"
|
||||
description="为已授权且暂停的账号绑定网关、浏览器版本、指纹与网络出口"
|
||||
>
|
||||
<Button icon="ri-arrow-left-line" onClick={() => navigate("/browsers")}>
|
||||
返回运行环境
|
||||
@@ -570,14 +582,14 @@ export function BrowserCreatePage() {
|
||||
</Field>
|
||||
<Field
|
||||
id="env-alias"
|
||||
label="别名(容器名)"
|
||||
label="别名"
|
||||
required
|
||||
error={
|
||||
form.alias !== "" && !aliasPattern.test(form.alias)
|
||||
? "小写字母、数字与中划线,最长 32 字符"
|
||||
: undefined
|
||||
}
|
||||
helper="用于容器名、数据卷与 API 路径"
|
||||
helper="用于 Profile 标识与 API 路径"
|
||||
>
|
||||
<Input
|
||||
id="env-alias"
|
||||
@@ -597,7 +609,7 @@ export function BrowserCreatePage() {
|
||||
? "请先在「网关管理」注册网关"
|
||||
: undefined
|
||||
}
|
||||
helper="容器调度到该网关"
|
||||
helper="环境由该 native gateway 管理"
|
||||
>
|
||||
<Select
|
||||
id="env-gateway"
|
||||
@@ -613,27 +625,27 @@ export function BrowserCreatePage() {
|
||||
</Field>
|
||||
<Field
|
||||
id="env-version"
|
||||
label="镜像版本"
|
||||
label="浏览器版本"
|
||||
required
|
||||
error={
|
||||
enabledImages.length === 0
|
||||
? "请先在「镜像版本」添加并启用"
|
||||
enabledVersions.length === 0
|
||||
? "请先在「浏览器版本」添加并启用"
|
||||
: undefined
|
||||
}
|
||||
helper="环境使用的浏览器镜像"
|
||||
helper="环境使用的预安装浏览器版本"
|
||||
>
|
||||
<Select
|
||||
id="env-version"
|
||||
value={form.image_version}
|
||||
value={form.browser_version}
|
||||
onChange={(event) =>
|
||||
update("image_version", event.target.value)
|
||||
update("browser_version", event.target.value)
|
||||
}
|
||||
options={enabledImages.map((item) => ({
|
||||
options={enabledVersions.map((item) => ({
|
||||
value: item.version,
|
||||
label: item.version,
|
||||
}))}
|
||||
placeholder="选择版本"
|
||||
disabled={enabledImages.length === 0}
|
||||
disabled={enabledVersions.length === 0}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
@@ -884,16 +896,16 @@ export function BrowserDetail() {
|
||||
<DetailList
|
||||
rows={[
|
||||
[
|
||||
"账号与镜像",
|
||||
"账号与浏览器版本",
|
||||
<span className="anywhere">
|
||||
{runtime.account_id} · {runtime.image_version}
|
||||
{runtime.account_id} · {runtime.browser_version}
|
||||
</span>,
|
||||
],
|
||||
[
|
||||
"固定出口",
|
||||
<span className="anywhere">
|
||||
{runtime.network_exit?.id || "未绑定"} · 健康:
|
||||
{runtime.network_exit?.health_status || "未知"}
|
||||
{runtime.network_exit_id || "未绑定"} · 健康:
|
||||
{runtime.network_exit_health || "未知"}
|
||||
</span>,
|
||||
],
|
||||
[
|
||||
@@ -905,6 +917,17 @@ export function BrowserDetail() {
|
||||
],
|
||||
]}
|
||||
/>
|
||||
<div className="space-y-1 text-sm text-muted">
|
||||
<p>网关:{runtime.gateway_reachable ? "可达" : "不可达"}</p>
|
||||
{runtime.runtime_node_id ? (
|
||||
<p>节点:{runtime.runtime_node_id}</p>
|
||||
) : null}
|
||||
{runtime.runtime_cleanup_state === "pending" ? (
|
||||
<p className="text-danger">
|
||||
资源待清理:{runtime.runtime_cleanup_error || "原因未返回"}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{runtime.endpoint ? (
|
||||
<div className="flex min-w-0 flex-col gap-0.5 text-sm">
|
||||
<dt className="text-muted">CDP</dt>
|
||||
@@ -920,10 +943,10 @@ export function BrowserDetail() {
|
||||
>
|
||||
查看账号
|
||||
</Button>
|
||||
{runtime.network_exit?.id ? (
|
||||
{runtime.network_exit_id ? (
|
||||
<Button
|
||||
as={Link}
|
||||
to={`/network-exits/${encodeURIComponent(runtime.network_exit.id)}`}
|
||||
to={`/network-exits/${encodeURIComponent(runtime.network_exit_id)}`}
|
||||
>
|
||||
查看出口
|
||||
</Button>
|
||||
|
||||
@@ -27,7 +27,7 @@ const runtimes = [
|
||||
name: "店铺一号",
|
||||
state: "running",
|
||||
status: "Up",
|
||||
image_version: "148.0.0.1",
|
||||
browser_version: "148.0.0.1",
|
||||
fingerprint: { seed: 1000 },
|
||||
endpoint: "http://account-a:9222",
|
||||
account_id: "social-a",
|
||||
@@ -41,7 +41,7 @@ const runtimes = [
|
||||
name: "店铺二号",
|
||||
state: "exited",
|
||||
status: "Exited",
|
||||
image_version: "148.0.0.1",
|
||||
browser_version: "148.0.0.1",
|
||||
fingerprint: { seed: 2000 },
|
||||
endpoint: "http://account-b:9222",
|
||||
},
|
||||
@@ -53,19 +53,19 @@ const images = [
|
||||
{
|
||||
id: "148.0.0.1",
|
||||
version: "148.0.0.1",
|
||||
image_ref: "reg/img:148",
|
||||
browser_path: "reg/img:148",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "144.0.0.1",
|
||||
version: "144.0.0.1",
|
||||
image_ref: "reg/img:144",
|
||||
browser_path: "reg/img:144",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "139.0.0.1",
|
||||
version: "139.0.0.1",
|
||||
image_ref: "reg/img:139",
|
||||
browser_path: "reg/img:139",
|
||||
enabled: false,
|
||||
},
|
||||
];
|
||||
@@ -94,7 +94,7 @@ function provider(overrides = {}) {
|
||||
return Promise.resolve({ data: runtimes, total: runtimes.length });
|
||||
if (resource === "gateways")
|
||||
return Promise.resolve({ data: gateways, total: gateways.length });
|
||||
if (resource === "browser-images")
|
||||
if (resource === "browser-versions")
|
||||
return Promise.resolve({ data: images, total: images.length });
|
||||
if (resource === "accounts")
|
||||
return Promise.resolve({ data: accounts, total: accounts.length });
|
||||
@@ -373,7 +373,7 @@ describe("BrowserList", () => {
|
||||
alias: "shop-03",
|
||||
name: "店铺三号",
|
||||
gateway: "gw-1",
|
||||
image_version: "148.0.0.1",
|
||||
browser_version: "148.0.0.1",
|
||||
account_id: "social-a",
|
||||
network_exit_id: "exit-1",
|
||||
fingerprint: { seed: 1000 },
|
||||
@@ -446,7 +446,7 @@ describe("BrowserList", () => {
|
||||
fireEvent.click(screen.getAllByLabelText("升级 店铺一号")[0]);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole("combobox", { name: /目标镜像版本/ }),
|
||||
within(dialog).getByRole("combobox", { name: /目标浏览器版本/ }),
|
||||
);
|
||||
fireEvent.click(
|
||||
await within(document.body).findByRole("option", { name: "144.0.0.1" }),
|
||||
|
||||
@@ -150,7 +150,7 @@ function GatewayFormModal({ open, onClose, onSubmit, busy, error, initial }) {
|
||||
required
|
||||
value={form.endpoint}
|
||||
onChange={(event) => update("endpoint", event.target.value)}
|
||||
placeholder="http://docker-gateway:8081"
|
||||
placeholder="http://127.0.0.1:8081"
|
||||
invalid={endpointInvalid}
|
||||
/>
|
||||
</Field>
|
||||
@@ -253,7 +253,7 @@ export function GatewayList() {
|
||||
<PageHeader
|
||||
icon="ri-git-branch-line"
|
||||
title="网关管理"
|
||||
description="注册 Docker 网关;令牌须与网关进程的 GATEWAY_TOKEN 一致"
|
||||
description="注册宿主机 native browser gateway;令牌须与 gateway 进程的 GATEWAY_TOKEN 一致"
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -21,7 +21,7 @@ const gateways = [
|
||||
{
|
||||
id: "gw-1",
|
||||
name: "gw-1",
|
||||
endpoint: "http://docker-gateway:8081",
|
||||
endpoint: "http://127.0.0.1:8081",
|
||||
token: "unit-test-gateway-token",
|
||||
},
|
||||
{
|
||||
@@ -163,7 +163,7 @@ describe("GatewayList", () => {
|
||||
const name = within(dialog).getByRole("textbox", { name: "名称" });
|
||||
const endpoint = within(dialog).getByRole("textbox", { name: "Endpoint" });
|
||||
expect(name.value).toBe("gw-1");
|
||||
expect(endpoint.value).toBe("http://docker-gateway:8081");
|
||||
expect(endpoint.value).toBe("http://127.0.0.1:8081");
|
||||
fireEvent.change(name, { target: { value: "gw-main" } });
|
||||
fireEvent.change(endpoint, { target: { value: "http://gw-main:8081" } });
|
||||
fireEvent.change(
|
||||
|
||||
+11
-3
@@ -10,9 +10,17 @@ const menu = [
|
||||
section: "运营",
|
||||
items: [
|
||||
{ to: "/accounts", icon: "ri-account-circle-line", label: "账号管理" },
|
||||
{ to: "/creator/competitors", icon: "ri-line-chart-line", label: "竞品分析" },
|
||||
{
|
||||
to: "/creator/competitors",
|
||||
icon: "ri-line-chart-line",
|
||||
label: "竞品分析",
|
||||
},
|
||||
{ to: "/creator/workbench", icon: "ri-chat-3-line", label: "运营工作台" },
|
||||
{ to: "/creator/settings", icon: "ri-settings-3-line", label: "采集设置" },
|
||||
{
|
||||
to: "/creator/settings",
|
||||
icon: "ri-settings-3-line",
|
||||
label: "采集设置",
|
||||
},
|
||||
{ to: "/tasks", icon: "ri-task-line", label: "任务中心" },
|
||||
{ to: "/audit", icon: "ri-history-line", label: "审计记录" },
|
||||
],
|
||||
@@ -22,7 +30,7 @@ const menu = [
|
||||
items: [
|
||||
{ to: "/browsers", icon: "ri-hard-drive-3-line", label: "运行环境" },
|
||||
{ to: "/network-exits", icon: "ri-earth-line", label: "网络出口" },
|
||||
{ to: "/browser-images", icon: "ri-inbox-line", label: "镜像版本" },
|
||||
{ to: "/browser-versions", icon: "ri-window-line", label: "浏览器版本" },
|
||||
{ to: "/gateways", icon: "ri-router-line", label: "网关管理" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ const unsupported = (resource, operation) =>
|
||||
|
||||
const resourcePaths = {
|
||||
browsers: "/browsers",
|
||||
"browser-images": "/browser-images",
|
||||
"browser-versions": "/browser-versions",
|
||||
gateways: "/gateways",
|
||||
accounts: "/phase-a/accounts",
|
||||
drafts: "/phase-a/drafts",
|
||||
@@ -152,7 +152,7 @@ export const dataProvider = {
|
||||
const path = resourcePaths[resource];
|
||||
if (!path || resource === "browsers")
|
||||
return unsupported(resource, "update");
|
||||
// browser-images 的 PUT 不接受 version 字段(路径已携带),透传其余字段。
|
||||
// browser-versions 的 PUT 不接受 version 字段(路径已携带),透传其余字段。
|
||||
const { version: _ignored, ...rest } = variables ?? {};
|
||||
await request(
|
||||
`${path}/${encodeURIComponent(id)}`,
|
||||
@@ -164,7 +164,7 @@ export const dataProvider = {
|
||||
const path = resourcePaths[resource];
|
||||
if (
|
||||
!path ||
|
||||
!["browser-images", "accounts", "creator-competitors"].includes(resource)
|
||||
!["browser-versions", "accounts", "creator-competitors"].includes(resource)
|
||||
)
|
||||
return unsupported(resource, "deleteOne");
|
||||
await request(`${path}/${encodeURIComponent(id)}`, { method: "DELETE" });
|
||||
|
||||
+7
-11
@@ -5,14 +5,10 @@ import { Refine } from "@refinedev/core";
|
||||
import { dataProvider } from "./dataProvider";
|
||||
import { CreatorHubLayout } from "./Layout";
|
||||
import { LoginPage } from "./LoginPage";
|
||||
import {
|
||||
AccountCreatePage,
|
||||
AccountDetail,
|
||||
AccountList,
|
||||
} from "./AccountsPage";
|
||||
import { AccountCreatePage, AccountDetail, AccountList } from "./AccountsPage";
|
||||
import { AuditPage } from "./AuditPage";
|
||||
import { BrowserCreatePage, BrowserDetail, BrowserList } from "./BrowsersPage";
|
||||
import { BrowserImageList } from "./BrowserImagesPage";
|
||||
import { BrowserVersionList } from "./BrowserVersionsPage";
|
||||
import { DraftPage } from "./DraftPage";
|
||||
import { GatewayList } from "./GatewaysPage";
|
||||
import { NetworkExitDetail, NetworkExitList } from "./NetworkExitsPage";
|
||||
@@ -52,10 +48,7 @@ createRoot(document.getElementById("root")).render(
|
||||
<Route path="/accounts" element={<AccountList />} />
|
||||
<Route path="/accounts/new" element={<AccountCreatePage />} />
|
||||
<Route path="/accounts/:id" element={<AccountDetail />} />
|
||||
<Route
|
||||
path="/accounts/:id/edit"
|
||||
element={<AccountEditPage />}
|
||||
/>
|
||||
<Route path="/accounts/:id/edit" element={<AccountEditPage />} />
|
||||
<Route
|
||||
path="/creator/competitors"
|
||||
element={<CreatorCompetitorsPage />}
|
||||
@@ -89,7 +82,10 @@ createRoot(document.getElementById("root")).render(
|
||||
<Route path="/browsers" element={<BrowserList />} />
|
||||
<Route path="/browsers/new" element={<BrowserCreatePage />} />
|
||||
<Route path="/browsers/:id" element={<BrowserDetail />} />
|
||||
<Route path="/browser-images" element={<BrowserImageList />} />
|
||||
<Route
|
||||
path="/browser-versions"
|
||||
element={<BrowserVersionList />}
|
||||
/>
|
||||
<Route path="/gateways" element={<GatewayList />} />
|
||||
<Route path="/drafts/:id" element={<DraftPage />} />
|
||||
<Route path="/attempts/:id" element={<AttemptDetail />} />
|
||||
|
||||
Reference in New Issue
Block a user