Initial commit: DSH Web Manager

This commit is contained in:
2026-10-13 09:34:47 +08:00
commit 85332bbc56
135 changed files with 19712 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
import { useState, type FormEvent } from 'react';
import { api } from './api';
import { Panel, ResourceState, Empty, Status, date, useAction, useResource } from './ui';
export function Images({admin = false}: {admin?:boolean}) {
const images = useResource(api.images), builds = useResource(api.builds, '', 5000); const action = useAction();
const [latest, setLatest] = useState<{version:string; checked_at?:string}>(), [version,setVersion] = useState(''), [selected,setSelected] = useState<string>();
function build(e: FormEvent) {e.preventDefault(); void action.run(async () => {await api.request('/builds','POST',{version}); await builds.refresh();}, '构建任务已提交,请查看任务的实际状态。', '确认在本机手动构建精确版本 ' + version + '?构建会消耗磁盘、网络与 CPU,不会自动升级实例。');}
const current = builds.data?.find(b => b.id === selected);
return <>{action.feedback}<Panel title="查询与手动构建"><div className="split"><div><p className="muted">从 npm 查询 dist-tags.latest,仅查询,不自动构建。</p><button disabled={action.busy} onClick={() => action.run(async () => {const result = await api.request<{version:string; checked_at?:string}>('/latest'); setLatest(result); setVersion(result.version);}, '最新版本查询完成')}>查询 latest</button>{latest && <p><strong>{latest.version}</strong> <small>查询时间 {date(latest.checked_at)}</small></p>}</div><form className="build-form" onSubmit={build}><label>精确 DSH 版本<input required value={version} onChange={e => setVersion(e.target.value)} placeholder="查询 latest,或填写精确版本" pattern="[0-9]+\.[0-9]+\.[0-9]+.*" /></label><button className="primary" disabled={action.busy || !version}>手动构建</button></form></div><p className="callout">镜像仅保存在本机。构建成功不等于实例升级;创建或升级前请检查可用状态。</p></Panel><Panel title="本地镜像" action={<button disabled={images.loading} onClick={images.refresh}>刷新</button>}><ResourceState resource={images}/>{images.data?.length === 0 && <Empty>本机暂无托管镜像,请先手动构建。</Empty>}{!!images.data?.length && <div className="table-scroll"><table><thead><tr><th>DSH 版本</th><th>镜像标识</th><th>标签</th><th>状态</th><th>使用/操作</th></tr></thead><tbody>{images.data.map(i => <tr key={i.image_id || i.id}><td><strong>{i.version}</strong></td><td className="mono wrap">{i.image_id || i.id}</td><td>{i.tag || '—'}</td><td><Status value={i.status}/></td><td>{i.usage_count ?? 0} 个实例 {admin && <button className="danger" disabled={action.busy || !!i.usage_count} onClick={() => action.run(async () => {await api.request('/images?image_id=' + encodeURIComponent(i.image_id || i.id), 'DELETE'); await images.refresh();}, '镜像已删除', '确认删除这个本地镜像?服务器将检查所有引用,不会强制删除。')}>清理镜像</button>}</td></tr>)}</tbody></table></div>}</Panel><Panel title="构建任务" action={<span className="muted">每 5 秒自动刷新</span>}><ResourceState resource={builds}/>{builds.data?.length === 0 && <Empty>没有构建任务记录</Empty>}{!!builds.data?.length && <div className="table-scroll"><table><thead><tr><th>任务</th><th>版本</th><th>状态</th><th>提交时间</th><th>日志</th></tr></thead><tbody>{builds.data.map(b => <tr key={b.id}><td className="mono">{b.id}</td><td>{b.version}</td><td><Status value={b.status}/></td><td>{date(b.created_at)}</td><td><button onClick={() => setSelected(b.id)}>查看日志</button></td></tr>)}</tbody></table></div>}{current && <div><h3>{current.version} · 构建日志</h3>{current.error && <p className="alert">{current.error}</p>}<pre className="logs">{current.logs || '任务尚未输出日志'}</pre></div>}</Panel></>;
}