feat: update controls in the admin, release token scope, build info in CI images [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
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
993dd5c41c
commit
251e0cee99
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>
|
||||
|
||||
+209
-1
@@ -310,6 +310,24 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/releases": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** The latest signed release manifest of each component */
|
||||
get: operations["releases_list"];
|
||||
put?: never;
|
||||
/** Register a CI-signed release manifest; online nodes get it right away (their updaters verify the signature). Admin or release tokens */
|
||||
post: operations["releases_publish"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/auth/logout": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -806,7 +824,7 @@ export interface paths {
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Version, tag, commit and build time of the backend and every agent and probe */
|
||||
/** Version, tag, commit, build time and update state of the backend and every agent and probe */
|
||||
get: operations["build_info_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
@@ -816,6 +834,23 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/nodes/{id}/update": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Ask an online agent or probe to install the release it has been offered */
|
||||
post: operations["nodes_update"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/healthz": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1277,6 +1312,7 @@ export interface components {
|
||||
tag: null | string;
|
||||
commit: null | string;
|
||||
built_at: null | string;
|
||||
update?: null | components["schemas"]["NodeUpdateDto"];
|
||||
};
|
||||
NodeInfoDto: {
|
||||
id: string;
|
||||
@@ -1290,6 +1326,15 @@ export interface components {
|
||||
version?: null | string;
|
||||
tag?: null | string;
|
||||
};
|
||||
NodeUpdateDto: {
|
||||
state: string;
|
||||
current_digest: null | string;
|
||||
available_tag: null | string;
|
||||
available_version: null | string;
|
||||
error: null | string;
|
||||
/** Format: date-time */
|
||||
at: unknown;
|
||||
};
|
||||
OverviewDto: {
|
||||
range: string;
|
||||
/** Format: date-time */
|
||||
@@ -1367,6 +1412,10 @@ export interface components {
|
||||
items: components["schemas"]["RawChunkMetaDto"][];
|
||||
next_cursor: null | string;
|
||||
};
|
||||
PageDtoOfReleaseDto: {
|
||||
items: components["schemas"]["ReleaseDto"][];
|
||||
next_cursor: null | string;
|
||||
};
|
||||
PageDtoOfSettingDto: {
|
||||
items: components["schemas"]["SettingDto"][];
|
||||
next_cursor: null | string;
|
||||
@@ -1465,6 +1514,10 @@ export interface components {
|
||||
[key: string]: string[];
|
||||
};
|
||||
};
|
||||
PublishReleaseRequest: {
|
||||
manifest: null | string;
|
||||
signature: null | string;
|
||||
};
|
||||
RawChunkMetaDto: {
|
||||
chunk_id: string;
|
||||
agent_id: string;
|
||||
@@ -1500,6 +1553,14 @@ export interface components {
|
||||
id: string;
|
||||
reason: string;
|
||||
};
|
||||
ReleaseDto: {
|
||||
component: string;
|
||||
/** Format: int64 */
|
||||
seq: number | string;
|
||||
tag: string;
|
||||
version: string;
|
||||
digest: string;
|
||||
};
|
||||
ScoreDto: {
|
||||
/** Format: double */
|
||||
score: null | number | string;
|
||||
@@ -2473,6 +2534,104 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
releases_list: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PageDtoOfReleaseDto"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
releases_publish: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["PublishReleaseRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Created */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ReleaseDto"];
|
||||
};
|
||||
};
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
/** @description Conflict */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
auth_logout: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -4552,6 +4711,55 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
nodes_update: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description No Content */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": unknown;
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
/** @description Conflict */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
health_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -17,6 +17,8 @@ export type AboutDto = Narrow<S['AboutDto']>
|
||||
export type BuildDto = Narrow<S['BuildDto']>
|
||||
export type BuildInfoDto = Narrow<S['BuildInfoDto']>
|
||||
export type NodeBuildDto = Narrow<S['NodeBuildDto']>
|
||||
export type NodeUpdateDto = Narrow<S['NodeUpdateDto']>
|
||||
export type ReleaseDto = Narrow<S['ReleaseDto']>
|
||||
export type OverviewDto = Narrow<S['OverviewDto']>
|
||||
export type OverviewLineDto = Narrow<S['OverviewLineDto']>
|
||||
export type SeriesDto = Narrow<S['SeriesDto']>
|
||||
|
||||
Reference in new issue
Block a user