429 lines
12 KiB
React
429 lines
12 KiB
React
import { useState } from "react";
|
||
import { useDataProvider, useList } from "@refinedev/core";
|
||
import {
|
||
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 pathPattern = /^\/[A-Za-z0-9._+~/-]{1,4095}$/;
|
||
|
||
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) && pathPattern.test(form.browser_path);
|
||
const versionInvalid =
|
||
form.version !== "" && !versionPattern.test(form.version);
|
||
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,
|
||
browser_path: form.browser_path,
|
||
note: form.note,
|
||
enabled: true,
|
||
});
|
||
if (created) {
|
||
setForm({ version: "", browser_path: "", note: "" });
|
||
onClose();
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Modal
|
||
open={open}
|
||
onClose={onClose}
|
||
title="添加浏览器版本"
|
||
labelledBy="browser-version-create-title"
|
||
footer={
|
||
<>
|
||
<Button onClick={onClose} disabled={busy}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
type="submit"
|
||
form="browser-version-create-form"
|
||
busy={busy}
|
||
busyText="保存中…"
|
||
disabled={!valid}
|
||
>
|
||
添加版本
|
||
</Button>
|
||
</>
|
||
}
|
||
>
|
||
<form id="browser-version-create-form" onSubmit={submit} noValidate>
|
||
{error ? (
|
||
<Alert variant="destructive" className="mb-4">
|
||
{error.message}
|
||
</Alert>
|
||
) : null}
|
||
<div className="space-y-4">
|
||
<Field
|
||
id="browser-version"
|
||
label="版本"
|
||
required
|
||
error={versionInvalid ? "数字开头,最长 64 字符" : undefined}
|
||
helper="浏览器版本号,如 148.0.7778.215"
|
||
>
|
||
<Input
|
||
id="browser-version"
|
||
required
|
||
maxLength={64}
|
||
value={form.version}
|
||
onChange={(event) => update("version", event.target.value)}
|
||
invalid={versionInvalid}
|
||
/>
|
||
</Field>
|
||
<Field
|
||
id="browser-path"
|
||
label="浏览器路径"
|
||
required
|
||
error={pathInvalid ? "格式不正确" : undefined}
|
||
helper="gateway 节点上的预安装浏览器绝对路径"
|
||
>
|
||
<Input
|
||
id="browser-path"
|
||
required
|
||
maxLength={4096}
|
||
value={form.browser_path}
|
||
onChange={(event) => update("browser_path", event.target.value)}
|
||
placeholder="gateway 节点上的预安装浏览器绝对路径"
|
||
invalid={pathInvalid}
|
||
/>
|
||
</Field>
|
||
<Field
|
||
id="browser-version-note"
|
||
label="备注"
|
||
helper="可选,最长 200 字符"
|
||
>
|
||
<Input
|
||
id="browser-version-note"
|
||
maxLength={200}
|
||
value={form.note}
|
||
onChange={(event) => update("note", event.target.value)}
|
||
/>
|
||
</Field>
|
||
</div>
|
||
</form>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
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-browser-path"
|
||
label="浏览器路径"
|
||
required
|
||
error={pathInvalid ? "格式不正确" : undefined}
|
||
helper="gateway 节点上的预安装浏览器绝对路径"
|
||
>
|
||
<Input
|
||
id="edit-browser-path"
|
||
value={editing.browser_path}
|
||
invalid={pathInvalid}
|
||
onChange={(event) =>
|
||
onChange({ ...editing, browser_path: event.target.value })
|
||
}
|
||
/>
|
||
</Field>
|
||
<Field
|
||
id="edit-browser-version-note"
|
||
label="备注"
|
||
helper="可选,最长 200 字符"
|
||
>
|
||
<Input
|
||
id="edit-browser-version-note"
|
||
maxLength={200}
|
||
value={editing.note}
|
||
onChange={(event) =>
|
||
onChange({ ...editing, note: event.target.value })
|
||
}
|
||
/>
|
||
</Field>
|
||
<div className="space-y-1.5">
|
||
<p className="text-sm font-medium">启用(可用于创建与升级)</p>
|
||
<Switch
|
||
id="edit-browser-version-enabled"
|
||
checked={editing.enabled}
|
||
onCheckedChange={(checked) =>
|
||
onChange({ ...editing, enabled: checked })
|
||
}
|
||
/>
|
||
</div>
|
||
<Button
|
||
variant="primary"
|
||
busy={busy}
|
||
busyText="保存中…"
|
||
disabled={pathInvalid}
|
||
onClick={onSave}
|
||
>
|
||
保存
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function BrowserVersionList() {
|
||
const dataProvider = useDataProvider()("default");
|
||
const { result, query } = useList({ resource: "browser-versions" });
|
||
const error = query.error;
|
||
const isPending = query.isPending;
|
||
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 · 浏览器版本");
|
||
|
||
async function createBrowserVersion(data) {
|
||
setBusyAlias("create");
|
||
setCreateError(null);
|
||
try {
|
||
await dataProvider.create({
|
||
resource: "browser-versions",
|
||
variables: data,
|
||
});
|
||
await query.refetch();
|
||
return true;
|
||
} catch (reason) {
|
||
setCreateError(reason);
|
||
return false;
|
||
} finally {
|
||
setBusyAlias("");
|
||
}
|
||
}
|
||
|
||
async function toggleBrowserVersion(version) {
|
||
setBusyAlias(version.version);
|
||
setNotice(null);
|
||
try {
|
||
await dataProvider.update({
|
||
resource: "browser-versions",
|
||
id: version.version,
|
||
variables: {
|
||
browser_path: version.browser_path,
|
||
note: version.note,
|
||
enabled: !version.enabled,
|
||
},
|
||
});
|
||
await query.refetch();
|
||
} catch (reason) {
|
||
setNotice({ variant: "destructive", text: reason.message });
|
||
} finally {
|
||
setBusyAlias("");
|
||
}
|
||
}
|
||
|
||
async function removeBrowserVersion(version) {
|
||
setBusyAlias(version.version);
|
||
setNotice(null);
|
||
try {
|
||
await dataProvider.deleteOne({
|
||
resource: "browser-versions",
|
||
id: version.version,
|
||
});
|
||
await query.refetch();
|
||
setDeleting(null);
|
||
} catch (reason) {
|
||
setNotice({ variant: "destructive", text: reason.message });
|
||
} finally {
|
||
setBusyAlias("");
|
||
}
|
||
}
|
||
|
||
async function saveEdit() {
|
||
if (!pathPattern.test(editing.browser_path)) {
|
||
setEditing({ ...editing, error: "浏览器路径格式不正确" });
|
||
return;
|
||
}
|
||
setBusyAlias(editing.version);
|
||
setNotice(null);
|
||
try {
|
||
await dataProvider.update({
|
||
resource: "browser-versions",
|
||
id: editing.version,
|
||
variables: {
|
||
browser_path: editing.browser_path,
|
||
note: editing.note,
|
||
enabled: editing.enabled,
|
||
},
|
||
});
|
||
setEditing(null);
|
||
await query.refetch();
|
||
} catch (reason) {
|
||
setEditing({ ...editing, error: reason.message });
|
||
} finally {
|
||
setBusyAlias("");
|
||
}
|
||
}
|
||
|
||
const message =
|
||
notice?.text ||
|
||
(error ? null : notice?.message) ||
|
||
(notice ? notice.message : null);
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader
|
||
icon="ri-window-line"
|
||
title="浏览器版本"
|
||
description="维护可用浏览器版本;创建与升级环境时从中选择"
|
||
>
|
||
<Button
|
||
variant="primary"
|
||
icon="ri-add-line"
|
||
onClick={() => setCreateOpen(true)}
|
||
>
|
||
添加版本
|
||
</Button>
|
||
</PageHeader>
|
||
{message ? (
|
||
<Alert variant="destructive" className="mb-4">
|
||
{message}
|
||
</Alert>
|
||
) : null}
|
||
<PageState
|
||
pending={isPending}
|
||
error={error}
|
||
empty={versions.length === 0}
|
||
emptyText="暂无浏览器版本;请先登记 gateway 节点上的预安装浏览器路径。"
|
||
>
|
||
<Table
|
||
columns={[
|
||
{
|
||
header: "版本",
|
||
width: "22%",
|
||
render: (version) => (
|
||
<span className="font-semibold">{version.version}</span>
|
||
),
|
||
},
|
||
{
|
||
header: "浏览器路径",
|
||
width: "38%",
|
||
render: (version) => (
|
||
<code className="anywhere font-mono text-[13px]">
|
||
{version.browser_path}
|
||
</code>
|
||
),
|
||
},
|
||
{
|
||
header: "备注",
|
||
width: "20%",
|
||
render: (version) => (
|
||
<span className={version.note ? "" : "text-muted"}>
|
||
{version.note || "—"}
|
||
</span>
|
||
),
|
||
},
|
||
{
|
||
header: "启用",
|
||
width: "8%",
|
||
render: (version) => (
|
||
<Switch
|
||
id={`browser-version-enabled-${version.version}`}
|
||
aria-label={`启用 ${version.version}`}
|
||
checked={version.enabled}
|
||
disabled={busyAlias === version.version}
|
||
onCheckedChange={() => toggleBrowserVersion(version)}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
header: "操作",
|
||
width: "12%",
|
||
align: "right",
|
||
render: (version) => (
|
||
<span className="inline-flex gap-1">
|
||
<button
|
||
type="button"
|
||
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={`删除 ${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" />
|
||
</button>
|
||
</span>
|
||
),
|
||
},
|
||
]}
|
||
rows={versions}
|
||
rowKey={(version) => version.version}
|
||
minWidth="900px"
|
||
/>
|
||
</PageState>
|
||
<BrowserVersionCreateModal
|
||
open={createOpen}
|
||
onClose={() => {
|
||
setCreateOpen(false);
|
||
setCreateError(null);
|
||
}}
|
||
onSubmit={createBrowserVersion}
|
||
busy={busyAlias === "create"}
|
||
error={createError}
|
||
/>
|
||
{editing === null ? null : (
|
||
<Modal
|
||
open
|
||
onClose={() => setEditing(null)}
|
||
title={`编辑 ${editing.version}`}
|
||
labelledBy="browser-version-edit-title"
|
||
footer={<Button onClick={() => setEditing(null)}>关闭</Button>}
|
||
>
|
||
{editing.error ? (
|
||
<Alert variant="destructive" className="mb-4">
|
||
{editing.error}
|
||
</Alert>
|
||
) : null}
|
||
<BrowserVersionEditForm
|
||
editing={editing}
|
||
onChange={(next) => setEditing({ ...next, error: editing.error })}
|
||
onSave={saveEdit}
|
||
busy={busyAlias === editing.version}
|
||
/>
|
||
</Modal>
|
||
)}
|
||
<ConfirmDialog
|
||
open={deleting !== null}
|
||
onClose={() => setDeleting(null)}
|
||
onConfirm={() => removeBrowserVersion(deleting)}
|
||
title="删除浏览器版本"
|
||
confirmLabel="确认删除"
|
||
busy={busyAlias === deleting?.version}
|
||
body={`删除浏览器版本 ${deleting?.version}?仍被环境引用时删除会被拒绝。`}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|