feat: update controls in the admin, release token scope, build info in CI images [CI]
The versions page shows each node's update state with update buttons, API tokens get a release scope that can only register releases (used by CI), and the CI image and release builds now pass the tag, commit and build time.
This commit is contained in:
1 parent
8dd3209a65
commit
d449e50791
17 files changed
+470
-141
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { Download, KeyRound, Plus, Trash2 } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '@shared/api/client'
|
||||
import type { AgentDto, ApiTokenDto, BuildInfoDto, CreatedApiTokenDto, EventDto, Page, ProbeDto, RawChunkMetaDto, RecomputeDto, SettingDto, UserDto } from '@shared/api/types'
|
||||
import type { AgentDto, ApiTokenDto, BuildInfoDto, NodeUpdateDto, ReleaseDto, CreatedApiTokenDto, EventDto, Page, ProbeDto, RawChunkMetaDto, RecomputeDto, SettingDto, UserDto } from '@shared/api/types'
|
||||
import { fmtBuiltAt, frontendBuild } from '@shared/buildInfo'
|
||||
import { fmtNum, fmtTime } from '@shared/format'
|
||||
import { translateSetting } from '@shared/i18n'
|
||||
@@ -95,8 +95,36 @@ function BuildRow({ name, kind, version, tag, commit, builtAt, platform }: { nam
|
||||
)
|
||||
}
|
||||
|
||||
/** 每个程序的版本:后端、前端、每个 Agent 和 Probe 自己上报的(它们没升级就能看出来)。 */
|
||||
/** 节点宿主机上的更新器报告的状态;没装更新器(二进制安装、旧版本)的节点没有。 */
|
||||
const UPDATE_STATE: Record<string, { text: string; tone: '' | 'ok' | 'warn' | 'bad' }> = {
|
||||
up_to_date: { text: '已是最新', tone: 'ok' },
|
||||
available: { text: '有新版本', tone: 'warn' },
|
||||
applying: { text: '更新中…', tone: 'warn' },
|
||||
failed: { text: '更新失败', tone: 'bad' },
|
||||
rolled_back: { text: '已回滚', tone: 'bad' },
|
||||
invalid: { text: '清单无效', tone: 'bad' },
|
||||
}
|
||||
|
||||
function UpdateCell({ update, busy, onUpdate }: { update?: NodeUpdateDto | null; busy: boolean; onUpdate: () => void }) {
|
||||
if (!update) return <span className="faint">—</span>
|
||||
const s = UPDATE_STATE[update.state] ?? { text: update.state, tone: '' as const }
|
||||
return (
|
||||
<span className="row" style={{ gap: 8 }} title={update.error ?? undefined}>
|
||||
<Chip tone={s.tone}>{s.text}</Chip>
|
||||
{update.state === 'available' ? <span className="mono muted">{update.available_tag}</span> : null}
|
||||
{update.state === 'available' ? (
|
||||
<button type="button" className="btn small primary" disabled={busy} onClick={onUpdate}>
|
||||
更新
|
||||
</button>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/** 每个程序的版本:后端、前端、每个 Agent 和 Probe 自己上报的,以及 CI 发布的最新签名清单和各节点的更新状态。 */
|
||||
function Versions() {
|
||||
const { busy, run } = useAction()
|
||||
const releases = useQuery(() => api.get<Page<ReleaseDto>>('/api/admin/releases'), [], 30_000)
|
||||
const info = useQuery(() => api.get<BuildInfoDto>('/api/admin/build-info'), [], 15_000)
|
||||
const agents = useQuery(() => api.get<Page<AgentDto>>('/api/admin/agents'), [], 30_000)
|
||||
const probes = useQuery(() => api.get<Page<ProbeDto>>('/api/admin/probes'), [], 30_000)
|
||||
@@ -105,9 +133,33 @@ function Versions() {
|
||||
const nodes = info.data?.nodes ?? []
|
||||
// 以后端的版本为准,节点的版本不一样就标出来(升级了一半的时候一眼能看到)
|
||||
const mismatch = (v?: string | null) => Boolean(v && back && v !== back.version)
|
||||
const updatable = nodes.filter((n) => n.update?.state === 'available')
|
||||
const requestUpdate = async (ids: string[]) => {
|
||||
for (const id of ids) {
|
||||
await run(() => api.post(`/api/admin/nodes/${id}/update`), '已请求更新')
|
||||
}
|
||||
info.reload()
|
||||
}
|
||||
return (
|
||||
<div className="card">
|
||||
<ErrorNotice error={info.error} />
|
||||
<ErrorNotice error={info.error ?? releases.error} />
|
||||
<div className="row" style={{ marginBottom: 12 }}>
|
||||
<span className="muted">
|
||||
最新发布:
|
||||
{(releases.data?.items ?? []).length === 0
|
||||
? '还没有(CI 构建后会自动登记)'
|
||||
: (releases.data?.items ?? []).map((r) => (
|
||||
<span key={r.component} style={{ marginRight: 12 }}>
|
||||
{r.component} <span className="mono">{r.tag}</span>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{updatable.length > 0 ? (
|
||||
<button type="button" className="btn primary right" disabled={busy} onClick={() => requestUpdate(updatable.map((n) => n.id))}>
|
||||
全部更新({updatable.length})
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -118,6 +170,7 @@ function Versions() {
|
||||
<th>提交</th>
|
||||
<th>构建时间</th>
|
||||
<th>系统</th>
|
||||
<th>更新</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -136,6 +189,9 @@ function Versions() {
|
||||
<td className="mono">{n.commit || <span className="faint">—</span>}</td>
|
||||
<td className="muted">{fmtBuiltAt(n.built_at) || <span className="faint">—</span>}</td>
|
||||
<td className="muted">{[n.os_name, n.arch].filter(Boolean).join(' / ') || <span className="faint">—</span>}</td>
|
||||
<td>
|
||||
<UpdateCell update={n.update} busy={busy} onUpdate={() => requestUpdate([n.id])} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -143,6 +199,7 @@ function Versions() {
|
||||
{nodes.length === 0 && !info.loading ? <Empty>还没有节点上报过版本</Empty> : null}
|
||||
<p className="muted" style={{ marginTop: 12 }}>
|
||||
版本、tag 和构建时间是各个程序自己上报的。设置里打开「site.show_build」之后,展示页底部和节点卡片上才会给访客显示(提交号只在这里显示)。
|
||||
「更新」一列来自节点宿主机上的更新器:清单是 CI 签名的,面板只负责通知和请求,验签和拉取都在节点上完成;没装更新器的节点这一列是「—」。
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
@@ -153,7 +210,7 @@ function Tokens() {
|
||||
const { run } = useAction()
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
const [scope, setScope] = useState<'read' | 'admin'>('read')
|
||||
const [scope, setScope] = useState<'read' | 'admin' | 'release'>('read')
|
||||
const [created, setCreated] = useState<CreatedApiTokenDto>()
|
||||
const [revoking, setRevoking] = useState<ApiTokenDto>()
|
||||
return (
|
||||
@@ -237,7 +294,7 @@ function Tokens() {
|
||||
<input className="input" value={name} onChange={(e) => setName(e.target.value)} autoFocus />
|
||||
</Field>
|
||||
<Field label="权限">
|
||||
<Segmented options={[{ value: 'read' as const, label: 'read(只读)' }, { value: 'admin' as const, label: 'admin(全部)' }]} value={scope} onChange={setScope} />
|
||||
<Segmented options={[{ value: 'read' as const, label: 'read(只读)' }, { value: 'admin' as const, label: 'admin(全部)' }, { value: 'release' as const, label: 'release(只登记发布,给 CI)' }]} value={scope} onChange={setScope} />
|
||||
</Field>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
Reference in new issue
Block a user