HH-738: add constrained browser container control (#4)
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import { StrictMode, useEffect, useState } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './styles.css'
|
||||
|
||||
const statusLabels = {
|
||||
created: '已创建',
|
||||
running: '运行中',
|
||||
exited: '已停止',
|
||||
restarting: '重启中',
|
||||
paused: '已暂停',
|
||||
}
|
||||
|
||||
async function api(path = '', options) {
|
||||
const response = await fetch(`/api/browsers${path}`, options)
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}))
|
||||
throw new Error(body.error || `请求失败 (${response.status})`)
|
||||
}
|
||||
return response.status === 204 ? null : response.json()
|
||||
}
|
||||
|
||||
function Icon({ name }) {
|
||||
const paths = {
|
||||
runtime: <><rect x="3" y="4" width="18" height="16" rx="2"/><path d="m8 9 3 3-3 3M13 15h3"/></>,
|
||||
audit: <><path d="M9 5H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-3"/><rect x="9" y="3" width="6" height="4" rx="1"/></>,
|
||||
shield: <path d="M12 3 5 6v5c0 4.6 2.9 8.1 7 10 4.1-1.9 7-5.4 7-10V6l-7-3Z"/>,
|
||||
play: <path d="m9 7 8 5-8 5V7Z"/>,
|
||||
stop: <rect x="7" y="7" width="10" height="10" rx="1"/>,
|
||||
trash: <><path d="M4 7h16M9 7V4h6v3M7 7l1 14h8l1-14M10 11v6M14 11v6"/></>,
|
||||
}
|
||||
return <svg aria-hidden="true" viewBox="0 0 24 24" className="icon">{paths[name]}</svg>
|
||||
}
|
||||
|
||||
function RuntimeRow({ runtime, busy, onAction }) {
|
||||
const running = runtime.state === 'running'
|
||||
return (
|
||||
<tr>
|
||||
<td data-label="环境"><strong>{runtime.name}</strong><small>{runtime.status}</small></td>
|
||||
<td data-label="状态"><span className={`state state-${runtime.state}`}>{statusLabels[runtime.state] || runtime.state}</span></td>
|
||||
<td data-label="Profile"><code>{runtime.profile}</code></td>
|
||||
<td data-label="CDP"><code>{runtime.endpoint}</code></td>
|
||||
<td data-label="操作" className="actions">
|
||||
<button className="button button-outline" disabled={busy || running} onClick={() => onAction(runtime.name, 'start')}><Icon name="play" />启动</button>
|
||||
<button className="button button-neutral" disabled={busy || !running} onClick={() => onAction(runtime.name, 'stop')}><Icon name="stop" />停止</button>
|
||||
<button className="button button-danger" disabled={busy} onClick={() => onAction(runtime.name, 'delete')}><Icon name="trash" />回收</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [runtimes, setRuntimes] = useState([])
|
||||
const [name, setName] = useState('')
|
||||
const [seed, setSeed] = useState('1000')
|
||||
const [busy, setBusy] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
async function refresh(signal) {
|
||||
try {
|
||||
setRuntimes(await api('', { signal }))
|
||||
setError('')
|
||||
} catch (reason) {
|
||||
if (reason.name !== 'AbortError') setError(reason.message)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
refresh(controller.signal)
|
||||
const timer = window.setInterval(() => refresh(controller.signal), 3000)
|
||||
return () => {
|
||||
window.clearInterval(timer)
|
||||
controller.abort()
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function createRuntime(event) {
|
||||
event.preventDefault()
|
||||
setBusy('create')
|
||||
setError('')
|
||||
try {
|
||||
await api('', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, seed: Number(seed) }),
|
||||
})
|
||||
setName('')
|
||||
await refresh()
|
||||
} catch (reason) {
|
||||
setError(reason.message)
|
||||
} finally {
|
||||
setBusy('')
|
||||
}
|
||||
}
|
||||
|
||||
async function act(runtimeName, action) {
|
||||
if (action === 'delete' && !window.confirm(`回收 ${runtimeName}?Profile 数据卷将保留。`)) return
|
||||
setBusy(runtimeName)
|
||||
setError('')
|
||||
try {
|
||||
await api(`/${runtimeName}${action === 'delete' ? '' : `/${action}`}`, { method: action === 'delete' ? 'DELETE' : 'POST' })
|
||||
await refresh()
|
||||
} catch (reason) {
|
||||
setError(reason.message)
|
||||
} finally {
|
||||
setBusy('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<aside className="sidebar">
|
||||
<a className="brand" href="/">CreatorHub</a>
|
||||
<nav aria-label="主导航">
|
||||
<a className="nav-item active" href="/"><Icon name="runtime" />运行环境</a>
|
||||
<span className="nav-item disabled"><Icon name="audit" />审计记录</span>
|
||||
</nav>
|
||||
</aside>
|
||||
<main>
|
||||
<header>
|
||||
<h1>运行环境</h1>
|
||||
<p>启动、停止并回收隔离的浏览器 Profile</p>
|
||||
</header>
|
||||
|
||||
<form className="create-form" onSubmit={createRuntime}>
|
||||
<label>环境名称<input required pattern="[a-z0-9](?:[a-z0-9]|-){0,31}" value={name} onChange={(event) => setName(event.target.value)} placeholder="例如:account-a" /></label>
|
||||
<label>Fingerprint Seed<input required min="1" max="2147483647" type="number" value={seed} onChange={(event) => setSeed(event.target.value)} /></label>
|
||||
<button className="button button-primary" disabled={busy === 'create'}>{busy === 'create' ? '创建中…' : '创建环境'}</button>
|
||||
</form>
|
||||
|
||||
<section className="runtime-list" aria-labelledby="runtime-list-title">
|
||||
<h2 id="runtime-list-title" className="sr-only">浏览器运行环境</h2>
|
||||
<table>
|
||||
<thead><tr><th>环境</th><th>状态</th><th>Profile</th><th>CDP</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
{runtimes.map((runtime) => <RuntimeRow key={runtime.id} runtime={runtime} busy={busy === runtime.name} onAction={act} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
{runtimes.length === 0 ? <p className="empty">暂无运行环境,创建第一个隔离 Profile。</p> : null}
|
||||
</section>
|
||||
|
||||
{error ? <p className="error" role="alert">{error}</p> : null}
|
||||
<p className="security-note"><Icon name="shield" />Docker socket 仅由受限网关访问</p>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')).render(<StrictMode><App /></StrictMode>)
|
||||
Reference in New Issue
Block a user