Files
creator-hub/web/src/BrowserVersionsPage.jsx
T

429 lines
12 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}