Skip to content
Clone

feat(admin): add act and repository administration

ZhenYi committed
parent 9054d68 commit b200b73 Browse files

16 files changed

+3225 −271

File tree 16
Modified admin/src/app/layout.tsx View admin/src/app/layout.tsx
@@ -1,6 +1,7 @@
11import {
22 Activity,
33 Database,
4 FolderGit2,
45 HardDrive,
56 LayoutDashboard,
67 ListChecks,
@@ -12,6 +13,7 @@
1213 ShieldCheck,
1314 Sun,
1415 Users,
16 Workflow,
1517} from 'lucide-react'
1618import { useEffect, useState } from 'react'
1719import { Menu, X } from 'lucide-react'
@@ -25,9 +27,11 @@
2527 { to: '/', label: '系统状态', icon: LayoutDashboard, end: true },
2628 { to: '/users', label: '用户', icon: Users },
2729 { to: '/spaces', label: 'Space', icon: Database },
30 { to: '/repos', label: '仓库', icon: FolderGit2 },
2831 { to: '/chat', label: '聊天记录', icon: MessagesSquare },
2932 { to: '/storage', label: '存储', icon: HardDrive },
3033 { to: '/queues', label: '队列', icon: Activity },
34 { to: '/act', label: 'ACT', icon: Workflow },
3135 { to: '/runners', label: 'Runner', icon: Cpu },
3236 { to: '/tasks', label: '任务', icon: ListChecks },
3337 { to: '/logs', label: '日志', icon: ScrollText },
Modified admin/src/app/routes.tsx View admin/src/app/routes.tsx
@@ -1,5 +1,6 @@
11import { createBrowserRouter, Navigate } from 'react-router-dom'
22import { AdminShell } from '@/app/layout'
3import { ActPage } from '@/pages/act'
34import { AdminsPage } from '@/pages/admins'
45import { ChatRoomDetailPage } from '@/pages/chat-room-detail'
56import { ChatRoomsPage } from '@/pages/chat-rooms'
@@ -10,6 +11,7 @@
1011import { NotFoundPage } from '@/pages/not-found'
1112import { PasswordPage } from '@/pages/password'
1213import { QueuesPage } from '@/pages/queues'
14import { ReposPage } from '@/pages/repos'
1315import { RunnersPage } from '@/pages/runners'
1416import { SpaceDetailPage } from '@/pages/space-detail'
1517import { SpacesPage } from '@/pages/spaces'
@@ -31,11 +33,13 @@
3133 { path: 'users/:uid', element: <UserDetailPage /> },
3234 { path: 'spaces', element: <SpacesPage /> },
3335 { path: 'spaces/:id', element: <SpaceDetailPage /> },
36 { path: 'repos', element: <ReposPage /> },
3437 { path: 'chat', element: <ChatRoomsPage /> },
3538 { path: 'chat/rooms/:id', element: <ChatRoomDetailPage /> },
3639 { path: 'chat/messages', element: <ChatMessagesPage /> },
3740 { path: 'storage', element: <StoragePage /> },
3841 { path: 'queues', element: <QueuesPage /> },
42 { path: 'act', element: <ActPage /> },
3943 { path: 'runners', element: <RunnersPage /> },
4044 { path: 'tasks', element: <TasksPage /> },
4145 { path: 'logs', element: <LogsPage /> },
Added admin/src/pages/act.tsx View admin/src/pages/act.tsx
@@ -0,0 +1,297 @@
1import { useCallback, useEffect, useState } from 'react'
2import { useNavigate } from 'react-router-dom'
3import { adminAct, adminRunners } from '@/shared/api'
4import type { AdminActRun, AdminActRunPage, AdminActSummary, RunnerSummary } from '@/shared/api'
5import { ApiError } from '@/shared/api'
6import { formatDateTime, shortId } from '@/shared/format'
7import {
8 Badge,
9 Button,
10 Card,
11 ConfirmDialog,
12 DataTable,
13 ErrorBanner,
14 Field,
15 Loading,
16 Pagination,
17 Select,
18 statusTone,
19 TextInput,
20 useToast,
21 type Column,
22} from '@/shared/ui'
23
24const PAGE_SIZE = 20
25
26export function ActPage() {
27 const navigate = useNavigate()
28 const toast = useToast()
29 const [summary, setSummary] = useState<AdminActSummary | null>(null)
30 const [runners, setRunners] = useState<RunnerSummary[] | null>(null)
31 const [runs, setRuns] = useState<AdminActRunPage | null>(null)
32 const [error, setError] = useState<string | null>(null)
33 const [busy, setBusy] = useState(false)
34 const [query, setQuery] = useState('')
35 const [state, setState] = useState('')
36 const [applied, setApplied] = useState({ query: '', state: '' })
37 const [offset, setOffset] = useState(0)
38 const [cancelTarget, setCancelTarget] = useState<AdminActRun | null>(null)
39
40 const load = useCallback(async () => {
41 setBusy(true)
42 setError(null)
43 try {
44 const [nextSummary, nextRuns, nextRunners] = await Promise.all([
45 adminAct.summary(),
46 adminAct.runs({
47 q: applied.query || undefined,
48 state: applied.state || undefined,
49 limit: PAGE_SIZE,
50 offset,
51 }),
52 adminRunners.list({ limit: 50, offset: 0 }),
53 ])
54 setSummary(nextSummary)
55 setRuns(nextRuns)
56 setRunners(nextRunners.items)
57 } catch (err) {
58 setError(err instanceof ApiError ? err.message : 'ACT 数据加载失败')
59 } finally {
60 setBusy(false)
61 }
62 }, [applied, offset])
63
64 useEffect(() => {
65 void load()
66 }, [load])
67
68 const runAction = async (action: () => Promise<unknown>, success: string) => {
69 try {
70 await action()
71 toast(success)
72 void load()
73 } catch (err) {
74 toast(err instanceof ApiError ? err.message : '操作失败', 'danger')
75 }
76 }
77
78 const runnerColumns: Column<RunnerSummary>[] = [
79 { key: 'name', title: '名称', render: (row) => row.name },
80 {
81 key: 'protocol',
82 title: '协议',
83 render: (row) => (row.protocol_version ? `v${row.protocol_version}` : '—'),
84 },
85 {
86 key: 'capabilities',
87 title: '能力',
88 render: (row) => (row.capabilities.length > 0 ? row.capabilities.join(', ') : '—'),
89 },
90 {
91 key: 'load',
92 title: '负载',
93 render: (row) => `${row.active_jobs}/${row.capacity}`,
94 },
95 {
96 key: 'state',
97 title: '状态',
98 render: (row) => (
99 <Badge tone={row.state === 'active' ? 'success' : 'warning'}>
100 {row.state === 'active' ? '启用' : '停用'}
101 </Badge>
102 ),
103 },
104 {
105 key: 'last_seen',
106 title: '最近心跳',
107 render: (row) => (row.last_seen_at ? formatDateTime(row.last_seen_at) : '—'),
108 },
109 ]
110
111 const runColumns: Column<AdminActRun>[] = [
112 {
113 key: 'repo',
114 title: '仓库',
115 render: (row) => <span className="font-medium">{row.repo_full_name}</span>,
116 },
117 {
118 key: 'workflow',
119 title: '工作流',
120 render: (row) => (
121 <div>
122 <div>{row.display_name ?? row.workflow_name}</div>
123 <div className="text-xs text-slate-500">{row.workflow_file}</div>
124 </div>
125 ),
126 },
127 {
128 key: 'run',
129 title: '运行',
130 render: (row) => (
131 <div>
132 <div>{row.run_number ? `#${row.run_number}` : shortId(row.id)}</div>
133 <div className="text-xs text-slate-500">{row.event} · {row.branch ?? row.tag ?? '—'}</div>
134 </div>
135 ),
136 },
137 {
138 key: 'state',
139 title: '状态',
140 render: (row) => <Badge tone={statusTone(row.state)}>{row.state}</Badge>,
141 },
142 {
143 key: 'jobs',
144 title: 'Job',
145 render: (row) => `${row.jobs_active}/${row.jobs_total}`,
146 },
147 {
148 key: 'created',
149 title: '创建时间',
150 render: (row) => <span className="text-xs text-slate-500">{formatDateTime(row.created_at)}</span>,
151 },
152 {
153 key: 'actions',
154 title: '操作',
155 render: (row) => {
156 const active = ['queued', 'running', 'cancel_requested'].includes(row.state)
157 const finished = ['success', 'failure', 'cancelled'].includes(row.state)
158 return (
159 <div className="flex flex-wrap gap-1">
160 {active ? (
161 <Button size="sm" variant="danger" onClick={() => setCancelTarget(row)}>
162 取消
163 </Button>
164 ) : null}
165 {finished ? (
166 <Button
167 size="sm"
168 variant="secondary"
169 onClick={() => void runAction(() => adminAct.rerunRun(row.id), '已创建重跑任务')}
170 >
171 重跑
172 </Button>
173 ) : null}
174 </div>
175 )
176 },
177 },
178 ]
179
180 return (
181 <div className="space-y-5">
182 <div className="flex flex-wrap items-center justify-between gap-3">
183 <div>
184 <h1 className="text-xl font-semibold">ACT 管理</h1>
185 <p className="mt-1 text-xs text-slate-500">监控 act runner,并管理 Actions 工作流运行。</p>
186 </div>
187 <div className="flex gap-2">
188 <Button variant="secondary" onClick={() => navigate('/runners')}>
189 Runner 与令牌
190 </Button>
191 <Button disabled={busy} onClick={() => void load()}>
192 刷新
193 </Button>
194 </div>
195 </div>
196
197 <ErrorBanner message={error} />
198
199 <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
200 <Card title="Runner">
201 <div className="text-2xl font-semibold">
202 {summary?.runners_active ?? '—'}/{summary?.runners_total ?? '—'}
203 </div>
204 <div className="mt-1 text-xs text-slate-500">启用 / 总数,忙碌 {summary?.runners_busy ?? '—'}</div>
205 </Card>
206 <Card title="作业">
207 <div className="text-2xl font-semibold">{summary?.jobs_running ?? '—'}</div>
208 <div className="mt-1 text-xs text-slate-500">运行中,排队 {summary?.jobs_queued ?? '—'}</div>
209 </Card>
210 <Card title="运行">
211 <div className="text-2xl font-semibold">{summary?.runs_running ?? '—'}</div>
212 <div className="mt-1 text-xs text-slate-500">运行中,排队 {summary?.runs_queued ?? '—'}</div>
213 </Card>
214 <Card title="累计结果">
215 <div className="text-2xl font-semibold">
216 <span className="text-emerald-600">{summary?.runs_success ?? '—'}</span>
217 <span className="mx-1 text-slate-400">/</span>
218 <span className="text-red-600">{summary?.runs_failure ?? '—'}</span>
219 </div>
220 <div className="mt-1 text-xs text-slate-500">成功 / 失败</div>
221 </Card>
222 </div>
223
224 <Card title="ACT Runner">
225 {runners === null ? (
226 <Loading />
227 ) : (
228 <DataTable
229 columns={runnerColumns}
230 rows={runners}
231 rowKey={(row) => row.id}
232 empty="还没有 act runner 注册"
233 />
234 )}
235 </Card>
236
237 <Card title="工作流运行">
238 <form
239 className="mb-4 flex flex-wrap items-end gap-2"
240 onSubmit={(event) => {
241 event.preventDefault()
242 setOffset(0)
243 setApplied({ query, state })
244 }}
245 >
246 <Field label="关键字">
247 <TextInput value={query} onChange={setQuery} placeholder="工作流 / 提交 SHA" />
248 </Field>
249 <Field label="状态">
250 <Select
251 value={state}
252 onChange={setState}
253 options={[
254 { value: '', label: '全部' },
255 { value: 'queued', label: '排队' },
256 { value: 'running', label: '运行中' },
257 { value: 'success', label: '成功' },
258 { value: 'failure', label: '失败' },
259 { value: 'cancelled', label: '已取消' },
260 ]}
261 />
262 </Field>
263 <Button type="submit" disabled={busy}>查询</Button>
264 </form>
265
266 {runs === null ? (
267 <Loading />
268 ) : (
269 <>
270 <DataTable
271 columns={runColumns}
272 rows={runs.items}
273 rowKey={(row) => row.id}
274 empty="没有匹配的工作流运行"
275 />
276 <Pagination total={runs.total} limit={runs.limit} offset={runs.offset} onChange={setOffset} />
277 </>
278 )}
279 </Card>
280
281 <ConfirmDialog
282 open={cancelTarget !== null}
283 title="取消 ACT 运行"
284 description={`确定取消运行「${cancelTarget?.display_name ?? cancelTarget?.workflow_name ?? ''}」吗?`}
285 confirmLabel="取消运行"
286 onClose={() => setCancelTarget(null)}
287 onConfirm={() => {
288 const target = cancelTarget
289 setCancelTarget(null)
290 if (target) {
291 void runAction(() => adminAct.cancelRun(target.id), '已请求取消')
292 }
293 }}
294 />
295 </div>
296 )
297}
Added admin/src/pages/repos.tsx View admin/src/pages/repos.tsx
@@ -0,0 +1,287 @@
1import { useCallback, useEffect, useState } from 'react'
2import { adminRepos } from '@/shared/api'
3import type { AdminRepoPage, AdminRepoSummary, UpdateAdminRepoCmd } from '@/shared/api'
4import { ApiError } from '@/shared/api'
5import { formatBytes, formatDateTime } from '@/shared/format'
6import {
7 Badge,
8 Button,
9 Card,
10 DataTable,
11 ErrorBanner,
12 Field,
13 Loading,
14 Modal,
15 Pagination,
16 Select,
17 TextInput,
18 useToast,
19 type Column,
20} from '@/shared/ui'
21
22const PAGE_SIZE = 20
23
24interface RepoDraft {
25 description: string
26 default_branch: string
27 is_private: boolean
28 is_archived: boolean
29 enable_lfs: boolean
30 storage_limit_size: string
31}
32
33function draftFromRepo(repo: AdminRepoSummary): RepoDraft {
34 return {
35 description: repo.description ?? '',
36 default_branch: repo.default_branch,
37 is_private: repo.is_private,
38 is_archived: repo.is_archived,
39 enable_lfs: repo.enable_lfs,
40 storage_limit_size: repo.storage_limit_size === null ? '' : String(repo.storage_limit_size),
41 }
42}
43
44export function ReposPage() {
45 const toast = useToast()
46 const [data, setData] = useState<AdminRepoPage | null>(null)
47 const [error, setError] = useState<string | null>(null)
48 const [busy, setBusy] = useState(false)
49 const [query, setQuery] = useState('')
50 const [archived, setArchived] = useState('')
51 const [privateRepo, setPrivateRepo] = useState('')
52 const [applied, setApplied] = useState({ query: '', archived: '', privateRepo: '' })
53 const [offset, setOffset] = useState(0)
54 const [target, setTarget] = useState<AdminRepoSummary | null>(null)
55 const [draft, setDraft] = useState<RepoDraft | null>(null)
56
57 const load = useCallback(async () => {
58 setBusy(true)
59 setError(null)
60 try {
61 setData(
62 await adminRepos.list({
63 q: applied.query || undefined,
64 archived: applied.archived ? applied.archived === 'true' : undefined,
65 private: applied.privateRepo ? applied.privateRepo === 'true' : undefined,
66 limit: PAGE_SIZE,
67 offset,
68 }),
69 )
70 } catch (err) {
71 setError(err instanceof ApiError ? err.message : '仓库列表加载失败')
72 } finally {
73 setBusy(false)
74 }
75 }, [applied, offset])
76
77 useEffect(() => {
78 void load()
79 }, [load])
80
81 const openEdit = (repo: AdminRepoSummary) => {
82 setTarget(repo)
83 setDraft(draftFromRepo(repo))
84 }
85
86 const save = async () => {
87 if (!target || !draft) return
88 const body: UpdateAdminRepoCmd = {
89 description: draft.description.trim() ? draft.description.trim() : null,
90 default_branch: draft.default_branch.trim(),
91 is_private: draft.is_private,
92 is_archived: draft.is_archived,
93 enable_lfs: draft.enable_lfs,
94 storage_limit_size: draft.storage_limit_size.trim() ? Number(draft.storage_limit_size) : null,
95 }
96 try {
97 await adminRepos.update(target.id, body)
98 toast('仓库设置已更新')
99 setTarget(null)
100 setDraft(null)
101 void load()
102 } catch (err) {
103 toast(err instanceof ApiError ? err.message : '保存失败', 'danger')
104 }
105 }
106
107 const toggleArchived = async (repo: AdminRepoSummary) => {
108 try {
109 await adminRepos.update(repo.id, { is_archived: !repo.is_archived })
110 toast(repo.is_archived ? '仓库已取消归档' : '仓库已归档')
111 void load()
112 } catch (err) {
113 toast(err instanceof ApiError ? err.message : '操作失败', 'danger')
114 }
115 }
116
117 const columns: Column<AdminRepoSummary>[] = [
118 {
119 key: 'name',
120 title: '仓库',
121 render: (row) => (
122 <div>
123 <div className="font-medium">{row.full_name}</div>
124 <div className="text-xs text-slate-500">{row.description ?? '—'}</div>
125 </div>
126 ),
127 },
128 { key: 'space', title: 'Space', render: (row) => row.space_name ?? '—' },
129 { key: 'branch', title: '默认分支', render: (row) => row.default_branch },
130 {
131 key: 'visibility',
132 title: '可见性',
133 render: (row) => (
134 <div className="flex flex-wrap gap-1">
135 <Badge tone={row.is_private ? 'warning' : 'neutral'}>{row.is_private ? '私有' : '公开'}</Badge>
136 {row.is_archived ? <Badge tone="danger">已归档</Badge> : null}
137 {row.enable_lfs ? <Badge tone="info">LFS</Badge> : null}
138 </div>
139 ),
140 },
141 {
142 key: 'size',
143 title: '体积',
144 render: (row) => (
145 <div>
146 <div>{formatBytes(row.size)}</div>
147 <div className="text-xs text-slate-500">Git {formatBytes(row.git_size)} · LFS {formatBytes(row.lfs_size)}</div>
148 </div>
149 ),
150 },
151 {
152 key: 'stats',
153 title: '统计',
154 render: (row) => `${row.nums_commits} commits · ${row.nums_branches} branches`,
155 },
156 {
157 key: 'updated',
158 title: '更新时间',
159 render: (row) => <span className="text-xs text-slate-500">{formatDateTime(row.updated_at)}</span>,
160 },
161 {
162 key: 'actions',
163 title: '操作',
164 render: (row) => (
165 <div className="flex flex-wrap gap-1">
166 <Button size="sm" variant="secondary" onClick={() => openEdit(row)}>编辑</Button>
167 <Button size="sm" variant={row.is_archived ? 'secondary' : 'danger'} onClick={() => void toggleArchived(row)}>
168 {row.is_archived ? '取消归档' : '归档'}
169 </Button>
170 </div>
171 ),
172 },
173 ]
174
175 return (
176 <div className="space-y-5">
177 <div className="flex flex-wrap items-center justify-between gap-3">
178 <div>
179 <h1 className="text-xl font-semibold">仓库管理</h1>
180 <p className="mt-1 text-xs text-slate-500">跨 Space 查看并调整仓库设置,不执行物理删除。</p>
181 </div>
182 <Button disabled={busy} onClick={() => void load()}>刷新</Button>
183 </div>
184
185 <ErrorBanner message={error} />
186
187 <Card title="筛选">
188 <form
189 className="flex flex-wrap items-end gap-2"
190 onSubmit={(event) => {
191 event.preventDefault()
192 setOffset(0)
193 setApplied({ query, archived, privateRepo })
194 }}
195 >
196 <Field label="关键字">
197 <TextInput value={query} onChange={setQuery} placeholder="仓库名 / 描述" />
198 </Field>
199 <Field label="归档">
200 <Select
201 value={archived}
202 onChange={setArchived}
203 options={[
204 { value: '', label: '全部' },
205 { value: 'false', label: '未归档' },
206 { value: 'true', label: '已归档' },
207 ]}
208 />
209 </Field>
210 <Field label="可见性">
211 <Select
212 value={privateRepo}
213 onChange={setPrivateRepo}
214 options={[
215 { value: '', label: '全部' },
216 { value: 'false', label: '公开' },
217 { value: 'true', label: '私有' },
218 ]}
219 />
220 </Field>
221 <Button type="submit" disabled={busy}>查询</Button>
222 </form>
223 </Card>
224
225 <Card title="仓库列表">
226 {data === null ? (
227 <Loading />
228 ) : (
229 <>
230 <DataTable columns={columns} rows={data.items} rowKey={(row) => row.id} empty="没有匹配的仓库" />
231 <Pagination total={data.total} limit={data.limit} offset={data.offset} onChange={setOffset} />
232 </>
233 )}
234 </Card>
235
236 <Modal
237 open={target !== null && draft !== null}
238 title={`编辑仓库 ${target?.full_name ?? ''}`}
239 onClose={() => {
240 setTarget(null)
241 setDraft(null)
242 }}
243 footer={
244 <>
245 <Button variant="secondary" onClick={() => { setTarget(null); setDraft(null) }}>取消</Button>
246 <Button disabled={!draft?.default_branch.trim()} onClick={() => void save()}>保存</Button>
247 </>
248 }
249 >
250 {draft ? (
251 <div className="space-y-3">
252 <label className="block">
253 <span className="mb-1.5 block text-xs font-medium">描述</span>
254 <textarea
255 className="min-h-20 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-sm dark:border-slate-600 dark:bg-slate-800"
256 value={draft.description}
257 onChange={(event) => setDraft({ ...draft, description: event.target.value })}
258 />
259 </label>
260 <label className="block">
261 <span className="mb-1.5 block text-xs font-medium">默认分支</span>
262 <TextInput value={draft.default_branch} onChange={(value) => setDraft({ ...draft, default_branch: value })} />
263 </label>
264 <label className="block">
265 <span className="mb-1.5 block text-xs font-medium">存储配额(字节,留空不限)</span>
266 <TextInput value={draft.storage_limit_size} onChange={(value) => setDraft({ ...draft, storage_limit_size: value })} />
267 </label>
268 <div className="grid gap-2 sm:grid-cols-3">
269 <label className="flex items-center gap-2 text-sm">
270 <input type="checkbox" checked={draft.is_private} onChange={(event) => setDraft({ ...draft, is_private: event.target.checked })} />
271 私有仓库
272 </label>
273 <label className="flex items-center gap-2 text-sm">
274 <input type="checkbox" checked={draft.is_archived} onChange={(event) => setDraft({ ...draft, is_archived: event.target.checked })} />
275 已归档
276 </label>
277 <label className="flex items-center gap-2 text-sm">
278 <input type="checkbox" checked={draft.enable_lfs} onChange={(event) => setDraft({ ...draft, enable_lfs: event.target.checked })} />
279 启用 LFS
280 </label>
281 </div>
282 </div>
283 ) : null}
284 </Modal>
285 </div>
286 )
287}
Modified admin/src/shared/api-types.ts View admin/src/shared/api-types.ts
@@ -599,12 +599,103 @@
599599 removed: boolean
600600}
601601
602export interface AdminActSummary {
603 runners_total: number
604 runners_active: number
605 runners_busy: number
606 jobs_queued: number
607 jobs_running: number
608 runs_queued: number
609 runs_running: number
610 runs_success: number
611 runs_failure: number
612 generated_at: string
613}
614
615export interface AdminActRun {
616 id: string
617 repo_id: string
618 repo_name: string
619 repo_full_name: string
620 space_id: string | null
621 space_name: string | null
622 workflow_file: string
623 workflow_name: string
624 display_name: string | null
625 actions_id: number | null
626 run_number: number | null
627 event: string
628 state: string
629 commit_sha: string
630 branch: string | null
631 tag: string | null
632 pr_number: number | null
633 jobs_total: number
634 jobs_active: number
635 jobs_failed: number
636 created_at: string
637 started_at: string | null
638 finished_at: string | null
639}
640
641export interface AdminActRunPage {
642 items: AdminActRun[]
643 total: number
644 limit: number
645 offset: number
646}
647
648export interface AdminRepoSummary {
649 id: string
650 dir_id: string
651 space_id: string | null
652 space_name: string | null
653 space_lower_name: string | null
654 name: string
655 lower_name: string
656 full_name: string
657 description: string | null
658 default_branch: string
659 is_private: boolean
660 is_archived: boolean
661 enable_lfs: boolean
662 storage_limit_size: number | null
663 size: number
664 git_size: number
665 lfs_size: number
666 nums_commits: number
667 nums_branches: number
668 nums_tags: number
669 primary_language: string | null
670 created_at: string
671 updated_at: string
672 last_pushed_at: string | null
673}
674
675export interface AdminRepoPage {
676 items: AdminRepoSummary[]
677 total: number
678 limit: number
679 offset: number
680}
681
682export interface UpdateAdminRepoCmd {
683 description?: string | null
684 default_branch?: string
685 is_private?: boolean
686 is_archived?: boolean
687 enable_lfs?: boolean
688 storage_limit_size?: number | null
689}
690
602691export interface RunnerSummary {
603692 id: string
604693 name: string
605694 labels: string[]
606695 capacity: number
607696 version: string | null
697 protocol_version: number | null
698 capabilities: string[]
608699 state: string
609700 space_id: string | null
610701 active_jobs: number
Modified admin/src/shared/api.ts View admin/src/shared/api.ts
@@ -1,4 +1,6 @@
11import type {
2 AdminActRunPage,
3 AdminActSummary,
24 AdminChatAttachment,
35 AdminChatMessage,
46 AdminChatMessageDetail,
@@ -8,6 +10,8 @@
810 AdminChatStats,
911 AdminDto,
1012 AdminLoginResp,
13 AdminRepoPage,
14 AdminRepoSummary,
1115 AdminSpaceDetail,
1216 AdminSpaceMember,
1317 AdminSpaceSummary,
@@ -43,6 +47,7 @@
4347 TaskDetail,
4448 TaskPage,
4549 TaskStats,
50 UpdateAdminRepoCmd,
4651} from './api-types'
4752import { ApiError } from './api-types'
4853
@@ -373,6 +378,33 @@
373378 exportAuditCsv: (params: AuditQuery) => csvDownload('/admin/logs/audit/export', params),
374379}
375380
381export const adminAct = {
382 summary: () => request<AdminActSummary>('GET', '/admin/act/summary'),
383 runs: (params: {
384 q?: string
385 state?: string
386 repo_id?: string
387 limit?: number
388 offset?: number
389 }) => request<AdminActRunPage>('GET', '/admin/act/runs', { params }),
390 cancelRun: (id: string) => request<string>('POST', `/admin/act/runs/${id}/cancel`),
391 rerunRun: (id: string) => request<string>('POST', `/admin/act/runs/${id}/rerun`),
392}
393
394export const adminRepos = {
395 list: (params: {
396 q?: string
397 space_id?: string
398 archived?: boolean
399 private?: boolean
400 limit?: number
401 offset?: number
402 }) => request<AdminRepoPage>('GET', '/admin/repos', { params }),
403 get: (id: string) => request<AdminRepoSummary>('GET', `/admin/repos/${id}`),
404 update: (id: string, body: UpdateAdminRepoCmd) =>
405 request<AdminRepoSummary>('PATCH', `/admin/repos/${id}`, { body }),
406}
407
376408export const adminSystem = {
377409 status: () => request<SystemStatus>('GET', '/admin/system/status'),
378410 dashboard: () => request<Dashboard>('GET', '/admin/dashboard'),
Modified admin/openapi.json View admin/openapi.json
@@ -14,6 +14,178 @@
1414 }
1515 ],
1616 "paths": {
17 "/admin/act/runs": {
18 "get": {
19 "tags": [
20 "admin-act"
21 ],
22 "description": "分页列出 act 工作流运行。",
23 "operationId": "list_runs",
24 "parameters": [
25 {
26 "name": "q",
27 "in": "query",
28 "required": false,
29 "schema": {
30 "type": [
31 "string",
32 "null"
33 ]
34 }
35 },
36 {
37 "name": "state",
38 "in": "query",
39 "required": false,
40 "schema": {
41 "oneOf": [
42 {
43 "type": "null"
44 },
45 {
46 "$ref": "#/components/schemas/WorkflowRunState"
47 }
48 ]
49 }
50 },
51 {
52 "name": "repo_id",
53 "in": "query",
54 "required": false,
55 "schema": {
56 "type": [
57 "string",
58 "null"
59 ],
60 "format": "uuid"
61 }
62 },
63 {
64 "name": "limit",
65 "in": "query",
66 "required": false,
67 "schema": {
68 "type": [
69 "integer",
70 "null"
71 ],
72 "format": "int64",
73 "minimum": 0
74 }
75 },
76 {
77 "name": "offset",
78 "in": "query",
79 "required": false,
80 "schema": {
81 "type": [
82 "integer",
83 "null"
84 ],
85 "format": "int64",
86 "minimum": 0
87 }
88 }
89 ],
90 "responses": {
91 "200": {
92 "description": "act runs",
93 "content": {
94 "application/json": {
95 "schema": {
96 "$ref": "#/components/schemas/AppResponseData_AdminActRunPage"
97 }
98 }
99 }
100 }
101 }
102 }
103 },
104 "/admin/act/runs/{id}/cancel": {
105 "post": {
106 "tags": [
107 "admin-act"
108 ],
109 "description": "请求取消 act 工作流运行。",
110 "operationId": "cancel_run",
111 "parameters": [
112 {
113 "name": "id",
114 "in": "path",
115 "required": true,
116 "schema": {
117 "type": "string",
118 "format": "uuid"
119 }
120 }
121 ],
122 "responses": {
123 "200": {
124 "description": "cancel requested",
125 "content": {
126 "application/json": {
127 "schema": {
128 "$ref": "#/components/schemas/AppResponseData_String"
129 }
130 }
131 }
132 }
133 }
134 }
135 },
136 "/admin/act/runs/{id}/rerun": {
137 "post": {
138 "tags": [
139 "admin-act"
140 ],
141 "description": "重新运行 act 工作流。",
142 "operationId": "rerun_run",
143 "parameters": [
144 {
145 "name": "id",
146 "in": "path",
147 "required": true,
148 "schema": {
149 "type": "string",
150 "format": "uuid"
151 }
152 }
153 ],
154 "responses": {
155 "200": {
156 "description": "rerun created",
157 "content": {
158 "application/json": {
159 "schema": {
160 "$ref": "#/components/schemas/AppResponseData_String"
161 }
162 }
163 }
164 }
165 }
166 }
167 },
168 "/admin/act/summary": {
169 "get": {
170 "tags": [
171 "admin-act"
172 ],
173 "description": "读取 act runner、作业和运行概览。",
174 "operationId": "summary",
175 "responses": {
176 "200": {
177 "description": "act summary",
178 "content": {
179 "application/json": {
180 "schema": {
181 "$ref": "#/components/schemas/AppResponseData_AdminActSummary"
182 }
183 }
184 }
185 }
186 }
187 }
188 },
17189 "/admin/admins": {
18190 "get": {
19191 "tags": [
@@ -2203,49 +2375,93 @@
22032375 }
22042376 }
22052377 },
2206 "/admin/runner-tokens": {
2378 "/admin/repos": {
22072379 "get": {
22082380 "tags": [
2209 "admin-runners"
2381 "admin-repos"
22102382 ],
2211 "description": "列出注册令牌(不含明文)。",
2212 "operationId": "list_runner_tokens",
2213 "responses": {
2214 "200": {
2215 "description": "token list",
2216 "content": {
2217 "application/json": {
2218 "schema": {
2219 "$ref": "#/components/schemas/AppResponseData_Vec_RunnerTokenDto"
2220 }
2221 }
2383 "description": "分页列出仓库及其归属 Space。",
2384 "operationId": "list_repos",
2385 "parameters": [
2386 {
2387 "name": "q",
2388 "in": "query",
2389 "required": false,
2390 "schema": {
2391 "type": [
2392 "string",
2393 "null"
2394 ]
2395 }
2396 },
2397 {
2398 "name": "space_id",
2399 "in": "query",
2400 "required": false,
2401 "schema": {
2402 "type": [
2403 "string",
2404 "null"
2405 ],
2406 "format": "uuid"
2407 }
2408 },
2409 {
2410 "name": "archived",
2411 "in": "query",
2412 "required": false,
2413 "schema": {
2414 "type": [
2415 "boolean",
2416 "null"
2417 ]
2418 }
2419 },
2420 {
2421 "name": "private",
2422 "in": "query",
2423 "required": false,
2424 "schema": {
2425 "type": [
2426 "boolean",
2427 "null"
2428 ]
2429 }
2430 },
2431 {
2432 "name": "limit",
2433 "in": "query",
2434 "required": false,
2435 "schema": {
2436 "type": [
2437 "integer",
2438 "null"
2439 ],
2440 "format": "int64",
2441 "minimum": 0
2442 }
2443 },
2444 {
2445 "name": "offset",
2446 "in": "query",
2447 "required": false,
2448 "schema": {
2449 "type": [
2450 "integer",
2451 "null"
2452 ],
2453 "format": "int64",
2454 "minimum": 0
22222455 }
22232456 }
2224 }
2225 },
2226 "post": {
2227 "tags": [
2228 "admin-runners"
22292457 ],
2230 "description": "创建一次性 runner 注册令牌;明文只在本次响应中返回。",
2231 "operationId": "create_runner_token",
2232 "requestBody": {
2233 "content": {
2234 "application/json": {
2235 "schema": {
2236 "$ref": "#/components/schemas/CreateRunnerTokenCmd"
2237 }
2238 }
2239 },
2240 "required": true
2241 },
22422458 "responses": {
22432459 "200": {
2244 "description": "token created",
2460 "description": "repo list",
22452461 "content": {
22462462 "application/json": {
22472463 "schema": {
2248 "$ref": "#/components/schemas/AppResponseData_CreatedRunnerTokenDto"
2464 "$ref": "#/components/schemas/AppResponseData_AdminRepoPage"
22492465 }
22502466 }
22512467 }
@@ -2253,13 +2469,13 @@
22532469 }
22542470 }
22552471 },
2256 "/admin/runner-tokens/{id}/revoke": {
2257 "post": {
2472 "/admin/repos/{id}": {
2473 "get": {
22582474 "tags": [
2259 "admin-runners"
2475 "admin-repos"
22602476 ],
2261 "description": "吊销注册令牌或 runner 凭据。",
2262 "operationId": "revoke_runner_token",
2477 "description": "读取仓库管理详情。",
2478 "operationId": "get_repo",
22632479 "parameters": [
22642480 {
22652481 "name": "id",
@@ -2273,20 +2489,142 @@
22732489 ],
22742490 "responses": {
22752491 "200": {
2276 "description": "token revoked",
2492 "description": "repo detail",
22772493 "content": {
22782494 "application/json": {
22792495 "schema": {
2280 "$ref": "#/components/schemas/AppResponseData_Vec_RunnerTokenDto"
2496 "$ref": "#/components/schemas/AppResponseData_AdminRepoSummary"
22812497 }
22822498 }
22832499 }
22842500 }
22852501 }
2286 }
2287 },
2288 "/admin/runners": {
2289 "get": {
2502 },
2503 "patch": {
2504 "tags": [
2505 "admin-repos"
2506 ],
2507 "description": "更新仓库默认分支、可见性、归档、LFS 或配额。",
2508 "operationId": "update_repo",
2509 "parameters": [
2510 {
2511 "name": "id",
2512 "in": "path",
2513 "required": true,
2514 "schema": {
2515 "type": "string",
2516 "format": "uuid"
2517 }
2518 }
2519 ],
2520 "requestBody": {
2521 "content": {
2522 "application/json": {
2523 "schema": {
2524 "$ref": "#/components/schemas/UpdateRepoCmd"
2525 }
2526 }
2527 },
2528 "required": true
2529 },
2530 "responses": {
2531 "200": {
2532 "description": "repo updated",
2533 "content": {
2534 "application/json": {
2535 "schema": {
2536 "$ref": "#/components/schemas/AppResponseData_AdminRepoSummary"
2537 }
2538 }
2539 }
2540 }
2541 }
2542 }
2543 },
2544 "/admin/runner-tokens": {
2545 "get": {
2546 "tags": [
2547 "admin-runners"
2548 ],
2549 "description": "列出注册令牌(不含明文)。",
2550 "operationId": "list_runner_tokens",
2551 "responses": {
2552 "200": {
2553 "description": "token list",
2554 "content": {
2555 "application/json": {
2556 "schema": {
2557 "$ref": "#/components/schemas/AppResponseData_Vec_RunnerTokenDto"
2558 }
2559 }
2560 }
2561 }
2562 }
2563 },
2564 "post": {
2565 "tags": [
2566 "admin-runners"
2567 ],
2568 "description": "创建一次性 runner 注册令牌;明文只在本次响应中返回。",
2569 "operationId": "create_runner_token",
2570 "requestBody": {
2571 "content": {
2572 "application/json": {
2573 "schema": {
2574 "$ref": "#/components/schemas/CreateRunnerTokenCmd"
2575 }
2576 }
2577 },
2578 "required": true
2579 },
2580 "responses": {
2581 "200": {
2582 "description": "token created",
2583 "content": {
2584 "application/json": {
2585 "schema": {
2586 "$ref": "#/components/schemas/AppResponseData_CreatedRunnerTokenDto"
2587 }
2588 }
2589 }
2590 }
2591 }
2592 }
2593 },
2594 "/admin/runner-tokens/{id}/revoke": {
2595 "post": {
2596 "tags": [
2597 "admin-runners"
2598 ],
2599 "description": "吊销注册令牌或 runner 凭据。",
2600 "operationId": "revoke_runner_token",
2601 "parameters": [
2602 {
2603 "name": "id",
2604 "in": "path",
2605 "required": true,
2606 "schema": {
2607 "type": "string",
2608 "format": "uuid"
2609 }
2610 }
2611 ],
2612 "responses": {
2613 "200": {
2614 "description": "token revoked",
2615 "content": {
2616 "application/json": {
2617 "schema": {
2618 "$ref": "#/components/schemas/AppResponseData_Vec_RunnerTokenDto"
2619 }
2620 }
2621 }
2622 }
2623 }
2624 }
2625 },
2626 "/admin/runners": {
2627 "get": {
22902628 "tags": [
22912629 "admin-runners"
22922630 ],
@@ -4521,121 +4859,353 @@
45214859 }
45224860 }
45234861 },
4524 "AdminChatAttachmentDto": {
4862 "AdminActRunDto": {
45254863 "type": "object",
45264864 "required": [
45274865 "id",
4528 "user_id",
4529 "filename",
4530 "content_type",
4531 "size",
4532 "bucket",
4533 "s3_key",
4534 "status",
4866 "repo_id",
4867 "repo_name",
4868 "repo_full_name",
4869 "workflow_file",
4870 "workflow_name",
4871 "event",
4872 "state",
4873 "commit_sha",
4874 "jobs_total",
4875 "jobs_active",
4876 "jobs_failed",
45354877 "created_at"
45364878 ],
45374879 "properties": {
4538 "bucket": {
4539 "type": "string"
4880 "actions_id": {
4881 "type": [
4882 "integer",
4883 "null"
4884 ],
4885 "format": "int64"
4886 },
4887 "branch": {
4888 "type": [
4889 "string",
4890 "null"
4891 ]
45404892 },
4541 "content_type": {
4893 "commit_sha": {
45424894 "type": "string"
45434895 },
45444896 "created_at": {
45454897 "type": "string",
45464898 "format": "date-time"
45474899 },
4548 "description": {
4900 "display_name": {
45494901 "type": [
45504902 "string",
45514903 "null"
45524904 ]
45534905 },
4554 "display_name": {
4906 "event": {
4907 "type": "string"
4908 },
4909 "finished_at": {
45554910 "type": [
45564911 "string",
45574912 "null"
4558 ]
4913 ],
4914 "format": "date-time"
4915 },
4916 "id": {
4917 "type": "string",
4918 "format": "uuid"
4919 },
4920 "jobs_active": {
4921 "type": "integer",
4922 "format": "int64",
4923 "minimum": 0
4924 },
4925 "jobs_failed": {
4926 "type": "integer",
4927 "format": "int64",
4928 "minimum": 0
4929 },
4930 "jobs_total": {
4931 "type": "integer",
4932 "format": "int64",
4933 "minimum": 0
4934 },
4935 "pr_number": {
4936 "type": [
4937 "integer",
4938 "null"
4939 ],
4940 "format": "int64"
45594941 },
4560 "filename": {
4942 "repo_full_name": {
45614943 "type": "string"
45624944 },
4563 "id": {
4945 "repo_id": {
45644946 "type": "string",
45654947 "format": "uuid"
45664948 },
4567 "message_id": {
4949 "repo_name": {
4950 "type": "string"
4951 },
4952 "run_number": {
45684953 "type": [
4569 "string",
4954 "integer",
45704955 "null"
45714956 ],
4572 "format": "uuid"
4957 "format": "int64"
45734958 },
4574 "room_id": {
4959 "space_id": {
45754960 "type": [
45764961 "string",
45774962 "null"
45784963 ],
45794964 "format": "uuid"
45804965 },
4581 "s3_key": {
4582 "type": "string"
4966 "space_name": {
4967 "type": [
4968 "string",
4969 "null"
4970 ]
45834971 },
4584 "size": {
4585 "type": "integer",
4586 "format": "int64"
4972 "started_at": {
4973 "type": [
4974 "string",
4975 "null"
4976 ],
4977 "format": "date-time"
45874978 },
4588 "status": {
4979 "state": {
45894980 "type": "string"
45904981 },
4591 "user_id": {
4592 "type": "string",
4593 "format": "uuid"
4594 },
4595 "username": {
4982 "tag": {
45964983 "type": [
45974984 "string",
45984985 "null"
45994986 ]
4987 },
4988 "workflow_file": {
4989 "type": "string"
4990 },
4991 "workflow_name": {
4992 "type": "string"
46004993 }
46014994 }
46024995 },
4603 "AdminChatKindCount": {
4996 "AdminActRunPage": {
46044997 "type": "object",
46054998 "required": [
4606 "kind",
4607 "count"
4999 "items",
5000 "total",
5001 "limit",
5002 "offset"
46085003 ],
46095004 "properties": {
4610 "count": {
5005 "items": {
5006 "type": "array",
5007 "items": {
5008 "$ref": "#/components/schemas/AdminActRunDto"
5009 }
5010 },
5011 "limit": {
5012 "type": "integer",
5013 "format": "int64",
5014 "minimum": 0
5015 },
5016 "offset": {
46115017 "type": "integer",
4612 "format": "int64"
5018 "format": "int64",
5019 "minimum": 0
46135020 },
4614 "kind": {
4615 "type": "string"
5021 "total": {
5022 "type": "integer",
5023 "format": "int64",
5024 "minimum": 0
46165025 }
46175026 }
46185027 },
4619 "AdminChatMessageDetail": {
5028 "AdminActSummary": {
46205029 "type": "object",
46215030 "required": [
4622 "message",
4623 "edits",
4624 "reactions",
4625 "attachments"
5031 "runners_total",
5032 "runners_active",
5033 "runners_busy",
5034 "jobs_queued",
5035 "jobs_running",
5036 "runs_queued",
5037 "runs_running",
5038 "runs_success",
5039 "runs_failure",
5040 "generated_at"
46265041 ],
46275042 "properties": {
4628 "attachments": {
4629 "type": "array",
4630 "items": {
4631 "$ref": "#/components/schemas/AdminChatAttachmentDto"
4632 }
4633 },
4634 "edits": {
4635 "type": "array",
4636 "items": {
4637 "$ref": "#/components/schemas/AdminChatMessageEditDto"
4638 }
5043 "generated_at": {
5044 "type": "string",
5045 "format": "date-time"
5046 },
5047 "jobs_queued": {
5048 "type": "integer",
5049 "format": "int64",
5050 "minimum": 0
5051 },
5052 "jobs_running": {
5053 "type": "integer",
5054 "format": "int64",
5055 "minimum": 0
5056 },
5057 "runners_active": {
5058 "type": "integer",
5059 "format": "int64",
5060 "minimum": 0
5061 },
5062 "runners_busy": {
5063 "type": "integer",
5064 "format": "int64",
5065 "minimum": 0
5066 },
5067 "runners_total": {
5068 "type": "integer",
5069 "format": "int64",
5070 "minimum": 0
5071 },
5072 "runs_failure": {
5073 "type": "integer",
5074 "format": "int64",
5075 "minimum": 0
5076 },
5077 "runs_queued": {
5078 "type": "integer",
5079 "format": "int64",
5080 "minimum": 0
5081 },
5082 "runs_running": {
5083 "type": "integer",
5084 "format": "int64",
5085 "minimum": 0
5086 },
5087 "runs_success": {
5088 "type": "integer",
5089 "format": "int64",
5090 "minimum": 0
5091 }
5092 }
5093 },
5094 "AdminChatAttachmentDto": {
5095 "type": "object",
5096 "required": [
5097 "id",
5098 "user_id",
5099 "filename",
5100 "content_type",
5101 "size",
5102 "bucket",
5103 "s3_key",
5104 "status",
5105 "created_at"
5106 ],
5107 "properties": {
5108 "bucket": {
5109 "type": "string"
5110 },
5111 "content_type": {
5112 "type": "string"
5113 },
5114 "created_at": {
5115 "type": "string",
5116 "format": "date-time"
5117 },
5118 "description": {
5119 "type": [
5120 "string",
5121 "null"
5122 ]
5123 },
5124 "display_name": {
5125 "type": [
5126 "string",
5127 "null"
5128 ]
5129 },
5130 "filename": {
5131 "type": "string"
5132 },
5133 "id": {
5134 "type": "string",
5135 "format": "uuid"
5136 },
5137 "message_id": {
5138 "type": [
5139 "string",
5140 "null"
5141 ],
5142 "format": "uuid"
5143 },
5144 "room_id": {
5145 "type": [
5146 "string",
5147 "null"
5148 ],
5149 "format": "uuid"
5150 },
5151 "s3_key": {
5152 "type": "string"
5153 },
5154 "size": {
5155 "type": "integer",
5156 "format": "int64"
5157 },
5158 "status": {
5159 "type": "string"
5160 },
5161 "user_id": {
5162 "type": "string",
5163 "format": "uuid"
5164 },
5165 "username": {
5166 "type": [
5167 "string",
5168 "null"
5169 ]
5170 }
5171 }
5172 },
5173 "AdminChatKindCount": {
5174 "type": "object",
5175 "required": [
5176 "kind",
5177 "count"
5178 ],
5179 "properties": {
5180 "count": {
5181 "type": "integer",
5182 "format": "int64"
5183 },
5184 "kind": {
5185 "type": "string"
5186 }
5187 }
5188 },
5189 "AdminChatMessageDetail": {
5190 "type": "object",
5191 "required": [
5192 "message",
5193 "edits",
5194 "reactions",
5195 "attachments"
5196 ],
5197 "properties": {
5198 "attachments": {
5199 "type": "array",
5200 "items": {
5201 "$ref": "#/components/schemas/AdminChatAttachmentDto"
5202 }
5203 },
5204 "edits": {
5205 "type": "array",
5206 "items": {
5207 "$ref": "#/components/schemas/AdminChatMessageEditDto"
5208 }
46395209 },
46405210 "message": {
46415211 "$ref": "#/components/schemas/AdminChatMessageDto"
@@ -5240,101 +5810,263 @@
52405810 }
52415811 }
52425812 },
5243 "AdminRole": {
5244 "type": "string",
5245 "enum": [
5246 "super_admin",
5247 "operator",
5248 "viewer"
5249 ]
5250 },
5251 "AdminSpaceDetail": {
5813 "AdminRepoPage": {
52525814 "type": "object",
52535815 "required": [
5254 "space",
5255 "repos_count",
5256 "repos_size",
5257 "rooms_count",
5258 "dirs_count"
5816 "items",
5817 "total",
5818 "limit",
5819 "offset"
52595820 ],
52605821 "properties": {
5261 "dirs_count": {
5262 "type": "integer",
5263 "format": "int64"
5822 "items": {
5823 "type": "array",
5824 "items": {
5825 "$ref": "#/components/schemas/AdminRepoSummary"
5826 }
52645827 },
5265 "repos_count": {
5828 "limit": {
52665829 "type": "integer",
5267 "format": "int64"
5830 "format": "int64",
5831 "minimum": 0
52685832 },
5269 "repos_size": {
5833 "offset": {
52705834 "type": "integer",
5271 "format": "int64"
5835 "format": "int64",
5836 "minimum": 0
52725837 },
5273 "rooms_count": {
5838 "total": {
52745839 "type": "integer",
5275 "format": "int64"
5276 },
5277 "space": {
5278 "$ref": "#/components/schemas/AdminSpaceSummary"
5840 "format": "int64",
5841 "minimum": 0
52795842 }
52805843 }
52815844 },
5282 "AdminSpaceMemberDto": {
5845 "AdminRepoSummary": {
52835846 "type": "object",
52845847 "required": [
5285 "user_id",
5286 "status",
5287 "joined_at"
5848 "id",
5849 "dir_id",
5850 "name",
5851 "lower_name",
5852 "full_name",
5853 "default_branch",
5854 "is_private",
5855 "is_archived",
5856 "enable_lfs",
5857 "size",
5858 "git_size",
5859 "lfs_size",
5860 "nums_commits",
5861 "nums_branches",
5862 "nums_tags",
5863 "created_at",
5864 "updated_at"
52885865 ],
52895866 "properties": {
5290 "alias_name": {
5291 "type": [
5292 "string",
5293 "null"
5294 ]
5867 "created_at": {
5868 "type": "string",
5869 "format": "date-time"
5870 },
5871 "default_branch": {
5872 "type": "string"
52955873 },
5296 "display_name": {
5874 "description": {
52975875 "type": [
52985876 "string",
52995877 "null"
53005878 ]
53015879 },
5302 "joined_at": {
5880 "dir_id": {
53035881 "type": "string",
5304 "format": "date-time"
5882 "format": "uuid"
53055883 },
5306 "last_active_at": {
5307 "type": [
5308 "string",
5309 "null"
5310 ],
5311 "format": "date-time"
5884 "enable_lfs": {
5885 "type": "boolean"
53125886 },
5313 "status": {
5887 "full_name": {
53145888 "type": "string"
53155889 },
5316 "user_id": {
5890 "git_size": {
5891 "type": "integer",
5892 "format": "int64"
5893 },
5894 "id": {
53175895 "type": "string",
53185896 "format": "uuid"
53195897 },
5320 "username": {
5898 "is_archived": {
5899 "type": "boolean"
5900 },
5901 "is_private": {
5902 "type": "boolean"
5903 },
5904 "last_pushed_at": {
53215905 "type": [
53225906 "string",
53235907 "null"
5324 ]
5325 }
5326 }
5327 },
5328 "AdminSpaceSummary": {
5329 "type": "object",
5330 "required": [
5331 "id",
5332 "name",
5333 "lower_name",
5334 "owner_id",
5335 "is_private",
5336 "is_active",
5337 "allow_join",
5908 ],
5909 "format": "date-time"
5910 },
5911 "lfs_size": {
5912 "type": "integer",
5913 "format": "int64"
5914 },
5915 "lower_name": {
5916 "type": "string"
5917 },
5918 "name": {
5919 "type": "string"
5920 },
5921 "nums_branches": {
5922 "type": "integer",
5923 "format": "int32"
5924 },
5925 "nums_commits": {
5926 "type": "integer",
5927 "format": "int32"
5928 },
5929 "nums_tags": {
5930 "type": "integer",
5931 "format": "int32"
5932 },
5933 "primary_language": {
5934 "type": [
5935 "string",
5936 "null"
5937 ]
5938 },
5939 "size": {
5940 "type": "integer",
5941 "format": "int64"
5942 },
5943 "space_id": {
5944 "type": [
5945 "string",
5946 "null"
5947 ],
5948 "format": "uuid"
5949 },
5950 "space_lower_name": {
5951 "type": [
5952 "string",
5953 "null"
5954 ]
5955 },
5956 "space_name": {
5957 "type": [
5958 "string",
5959 "null"
5960 ]
5961 },
5962 "storage_limit_size": {
5963 "type": [
5964 "integer",
5965 "null"
5966 ],
5967 "format": "int64"
5968 },
5969 "updated_at": {
5970 "type": "string",
5971 "format": "date-time"
5972 }
5973 }
5974 },
5975 "AdminRole": {
5976 "type": "string",
5977 "enum": [
5978 "super_admin",
5979 "operator",
5980 "viewer"
5981 ]
5982 },
5983 "AdminSpaceDetail": {
5984 "type": "object",
5985 "required": [
5986 "space",
5987 "repos_count",
5988 "repos_size",
5989 "rooms_count",
5990 "dirs_count"
5991 ],
5992 "properties": {
5993 "dirs_count": {
5994 "type": "integer",
5995 "format": "int64"
5996 },
5997 "repos_count": {
5998 "type": "integer",
5999 "format": "int64"
6000 },
6001 "repos_size": {
6002 "type": "integer",
6003 "format": "int64"
6004 },
6005 "rooms_count": {
6006 "type": "integer",
6007 "format": "int64"
6008 },
6009 "space": {
6010 "$ref": "#/components/schemas/AdminSpaceSummary"
6011 }
6012 }
6013 },
6014 "AdminSpaceMemberDto": {
6015 "type": "object",
6016 "required": [
6017 "user_id",
6018 "status",
6019 "joined_at"
6020 ],
6021 "properties": {
6022 "alias_name": {
6023 "type": [
6024 "string",
6025 "null"
6026 ]
6027 },
6028 "display_name": {
6029 "type": [
6030 "string",
6031 "null"
6032 ]
6033 },
6034 "joined_at": {
6035 "type": "string",
6036 "format": "date-time"
6037 },
6038 "last_active_at": {
6039 "type": [
6040 "string",
6041 "null"
6042 ],
6043 "format": "date-time"
6044 },
6045 "status": {
6046 "type": "string"
6047 },
6048 "user_id": {
6049 "type": "string",
6050 "format": "uuid"
6051 },
6052 "username": {
6053 "type": [
6054 "string",
6055 "null"
6056 ]
6057 }
6058 }
6059 },
6060 "AdminSpaceSummary": {
6061 "type": "object",
6062 "required": [
6063 "id",
6064 "name",
6065 "lower_name",
6066 "owner_id",
6067 "is_private",
6068 "is_active",
6069 "allow_join",
53386070 "allow_invite",
53396071 "member_must_enable_2fa",
53406072 "nums_members",
@@ -5839,7 +6571,7 @@
58396571 }
58406572 }
58416573 },
5842 "AppResponseData_AdminChatMessageDetail": {
6574 "AppResponseData_AdminActRunPage": {
58436575 "type": "object",
58446576 "required": [
58456577 "code",
@@ -5855,42 +6587,32 @@
58556587 "data": {
58566588 "type": "object",
58576589 "required": [
5858 "message",
5859 "edits",
5860 "reactions",
5861 "attachments"
6590 "items",
6591 "total",
6592 "limit",
6593 "offset"
58626594 ],
58636595 "properties": {
5864 "attachments": {
6596 "items": {
58656597 "type": "array",
58666598 "items": {
5867 "$ref": "#/components/schemas/AdminChatAttachmentDto"
6599 "$ref": "#/components/schemas/AdminActRunDto"
58686600 }
58696601 },
5870 "edits": {
5871 "type": "array",
5872 "items": {
5873 "$ref": "#/components/schemas/AdminChatMessageEditDto"
5874 }
6602 "limit": {
6603 "type": "integer",
6604 "format": "int64",
6605 "minimum": 0
58756606 },
5876 "message": {
5877 "$ref": "#/components/schemas/AdminChatMessageDto"
6607 "offset": {
6608 "type": "integer",
6609 "format": "int64",
6610 "minimum": 0
58786611 },
5879 "reactions": {
5880 "type": "array",
5881 "items": {
5882 "$ref": "#/components/schemas/AdminChatReactionGroupDto"
5883 }
5884 },
5885 "thread": {
5886 "oneOf": [
5887 {
5888 "type": "null"
5889 },
5890 {
5891 "$ref": "#/components/schemas/AdminChatThreadDto"
5892 }
5893 ]
6612 "total": {
6613 "type": "integer",
6614 "format": "int64",
6615 "minimum": 0
58946616 }
58956617 }
58966618 },
@@ -5917,7 +6639,7 @@
59176639 }
59186640 }
59196641 },
5920 "AppResponseData_AdminChatRoomDetail": {
6642 "AppResponseData_AdminActSummary": {
59216643 "type": "object",
59226644 "required": [
59236645 "code",
@@ -5933,41 +6655,66 @@
59336655 "data": {
59346656 "type": "object",
59356657 "required": [
5936 "room",
5937 "deleted_messages",
5938 "threads",
5939 "attachments",
5940 "attachment_bytes",
5941 "bans",
5942 "invites"
6658 "runners_total",
6659 "runners_active",
6660 "runners_busy",
6661 "jobs_queued",
6662 "jobs_running",
6663 "runs_queued",
6664 "runs_running",
6665 "runs_success",
6666 "runs_failure",
6667 "generated_at"
59436668 ],
59446669 "properties": {
5945 "attachment_bytes": {
6670 "generated_at": {
6671 "type": "string",
6672 "format": "date-time"
6673 },
6674 "jobs_queued": {
6675 "type": "integer",
6676 "format": "int64",
6677 "minimum": 0
6678 },
6679 "jobs_running": {
6680 "type": "integer",
6681 "format": "int64",
6682 "minimum": 0
6683 },
6684 "runners_active": {
59466685 "type": "integer",
5947 "format": "int64"
6686 "format": "int64",
6687 "minimum": 0
59486688 },
5949 "attachments": {
6689 "runners_busy": {
59506690 "type": "integer",
5951 "format": "int64"
6691 "format": "int64",
6692 "minimum": 0
59526693 },
5953 "bans": {
6694 "runners_total": {
59546695 "type": "integer",
5955 "format": "int64"
6696 "format": "int64",
6697 "minimum": 0
59566698 },
5957 "deleted_messages": {
6699 "runs_failure": {
59586700 "type": "integer",
5959 "format": "int64"
6701 "format": "int64",
6702 "minimum": 0
59606703 },
5961 "invites": {
6704 "runs_queued": {
59626705 "type": "integer",
5963 "format": "int64"
6706 "format": "int64",
6707 "minimum": 0
59646708 },
5965 "room": {
5966 "$ref": "#/components/schemas/AdminChatRoomSummary"
6709 "runs_running": {
6710 "type": "integer",
6711 "format": "int64",
6712 "minimum": 0
59676713 },
5968 "threads": {
6714 "runs_success": {
59696715 "type": "integer",
5970 "format": "int64"
6716 "format": "int64",
6717 "minimum": 0
59716718 }
59726719 }
59736720 },
@@ -5994,7 +6741,7 @@
59946741 }
59956742 }
59966743 },
5997 "AppResponseData_AdminChatStatsDto": {
6744 "AppResponseData_AdminChatMessageDetail": {
59986745 "type": "object",
59996746 "required": [
60006747 "code",
@@ -6010,9 +6757,164 @@
60106757 "data": {
60116758 "type": "object",
60126759 "required": [
6013 "rooms",
6014 "active_rooms",
6015 "private_rooms",
6760 "message",
6761 "edits",
6762 "reactions",
6763 "attachments"
6764 ],
6765 "properties": {
6766 "attachments": {
6767 "type": "array",
6768 "items": {
6769 "$ref": "#/components/schemas/AdminChatAttachmentDto"
6770 }
6771 },
6772 "edits": {
6773 "type": "array",
6774 "items": {
6775 "$ref": "#/components/schemas/AdminChatMessageEditDto"
6776 }
6777 },
6778 "message": {
6779 "$ref": "#/components/schemas/AdminChatMessageDto"
6780 },
6781 "reactions": {
6782 "type": "array",
6783 "items": {
6784 "$ref": "#/components/schemas/AdminChatReactionGroupDto"
6785 }
6786 },
6787 "thread": {
6788 "oneOf": [
6789 {
6790 "type": "null"
6791 },
6792 {
6793 "$ref": "#/components/schemas/AdminChatThreadDto"
6794 }
6795 ]
6796 }
6797 }
6798 },
6799 "error": {
6800 "type": [
6801 "string",
6802 "null"
6803 ]
6804 },
6805 "error_code": {
6806 "type": [
6807 "string",
6808 "null"
6809 ]
6810 },
6811 "log_id": {
6812 "type": "string"
6813 },
6814 "message": {
6815 "type": "string"
6816 },
6817 "request_id": {
6818 "type": "string"
6819 }
6820 }
6821 },
6822 "AppResponseData_AdminChatRoomDetail": {
6823 "type": "object",
6824 "required": [
6825 "code",
6826 "message",
6827 "request_id",
6828 "log_id"
6829 ],
6830 "properties": {
6831 "code": {
6832 "type": "integer",
6833 "format": "int32"
6834 },
6835 "data": {
6836 "type": "object",
6837 "required": [
6838 "room",
6839 "deleted_messages",
6840 "threads",
6841 "attachments",
6842 "attachment_bytes",
6843 "bans",
6844 "invites"
6845 ],
6846 "properties": {
6847 "attachment_bytes": {
6848 "type": "integer",
6849 "format": "int64"
6850 },
6851 "attachments": {
6852 "type": "integer",
6853 "format": "int64"
6854 },
6855 "bans": {
6856 "type": "integer",
6857 "format": "int64"
6858 },
6859 "deleted_messages": {
6860 "type": "integer",
6861 "format": "int64"
6862 },
6863 "invites": {
6864 "type": "integer",
6865 "format": "int64"
6866 },
6867 "room": {
6868 "$ref": "#/components/schemas/AdminChatRoomSummary"
6869 },
6870 "threads": {
6871 "type": "integer",
6872 "format": "int64"
6873 }
6874 }
6875 },
6876 "error": {
6877 "type": [
6878 "string",
6879 "null"
6880 ]
6881 },
6882 "error_code": {
6883 "type": [
6884 "string",
6885 "null"
6886 ]
6887 },
6888 "log_id": {
6889 "type": "string"
6890 },
6891 "message": {
6892 "type": "string"
6893 },
6894 "request_id": {
6895 "type": "string"
6896 }
6897 }
6898 },
6899 "AppResponseData_AdminChatStatsDto": {
6900 "type": "object",
6901 "required": [
6902 "code",
6903 "message",
6904 "request_id",
6905 "log_id"
6906 ],
6907 "properties": {
6908 "code": {
6909 "type": "integer",
6910 "format": "int32"
6911 },
6912 "data": {
6913 "type": "object",
6914 "required": [
6915 "rooms",
6916 "active_rooms",
6917 "private_rooms",
60166918 "kinds",
60176919 "messages",
60186920 "deleted_messages",
@@ -6160,66 +7062,300 @@
61607062 "must_change_password": {
61617063 "type": "boolean"
61627064 },
6163 "role": {
6164 "$ref": "#/components/schemas/AdminRole"
7065 "role": {
7066 "$ref": "#/components/schemas/AdminRole"
7067 },
7068 "updated_at": {
7069 "type": "string",
7070 "format": "date-time"
7071 },
7072 "username": {
7073 "type": "string"
7074 }
7075 }
7076 },
7077 "error": {
7078 "type": [
7079 "string",
7080 "null"
7081 ]
7082 },
7083 "error_code": {
7084 "type": [
7085 "string",
7086 "null"
7087 ]
7088 },
7089 "log_id": {
7090 "type": "string"
7091 },
7092 "message": {
7093 "type": "string"
7094 },
7095 "request_id": {
7096 "type": "string"
7097 }
7098 }
7099 },
7100 "AppResponseData_AdminLoginResp": {
7101 "type": "object",
7102 "required": [
7103 "code",
7104 "message",
7105 "request_id",
7106 "log_id"
7107 ],
7108 "properties": {
7109 "code": {
7110 "type": "integer",
7111 "format": "int32"
7112 },
7113 "data": {
7114 "type": "object",
7115 "required": [
7116 "admin",
7117 "must_change_password"
7118 ],
7119 "properties": {
7120 "admin": {
7121 "$ref": "#/components/schemas/AdminDto"
7122 },
7123 "must_change_password": {
7124 "type": "boolean"
7125 }
7126 }
7127 },
7128 "error": {
7129 "type": [
7130 "string",
7131 "null"
7132 ]
7133 },
7134 "error_code": {
7135 "type": [
7136 "string",
7137 "null"
7138 ]
7139 },
7140 "log_id": {
7141 "type": "string"
7142 },
7143 "message": {
7144 "type": "string"
7145 },
7146 "request_id": {
7147 "type": "string"
7148 }
7149 }
7150 },
7151 "AppResponseData_AdminRepoPage": {
7152 "type": "object",
7153 "required": [
7154 "code",
7155 "message",
7156 "request_id",
7157 "log_id"
7158 ],
7159 "properties": {
7160 "code": {
7161 "type": "integer",
7162 "format": "int32"
7163 },
7164 "data": {
7165 "type": "object",
7166 "required": [
7167 "items",
7168 "total",
7169 "limit",
7170 "offset"
7171 ],
7172 "properties": {
7173 "items": {
7174 "type": "array",
7175 "items": {
7176 "$ref": "#/components/schemas/AdminRepoSummary"
7177 }
7178 },
7179 "limit": {
7180 "type": "integer",
7181 "format": "int64",
7182 "minimum": 0
7183 },
7184 "offset": {
7185 "type": "integer",
7186 "format": "int64",
7187 "minimum": 0
7188 },
7189 "total": {
7190 "type": "integer",
7191 "format": "int64",
7192 "minimum": 0
7193 }
7194 }
7195 },
7196 "error": {
7197 "type": [
7198 "string",
7199 "null"
7200 ]
7201 },
7202 "error_code": {
7203 "type": [
7204 "string",
7205 "null"
7206 ]
7207 },
7208 "log_id": {
7209 "type": "string"
7210 },
7211 "message": {
7212 "type": "string"
7213 },
7214 "request_id": {
7215 "type": "string"
7216 }
7217 }
7218 },
7219 "AppResponseData_AdminRepoSummary": {
7220 "type": "object",
7221 "required": [
7222 "code",
7223 "message",
7224 "request_id",
7225 "log_id"
7226 ],
7227 "properties": {
7228 "code": {
7229 "type": "integer",
7230 "format": "int32"
7231 },
7232 "data": {
7233 "type": "object",
7234 "required": [
7235 "id",
7236 "dir_id",
7237 "name",
7238 "lower_name",
7239 "full_name",
7240 "default_branch",
7241 "is_private",
7242 "is_archived",
7243 "enable_lfs",
7244 "size",
7245 "git_size",
7246 "lfs_size",
7247 "nums_commits",
7248 "nums_branches",
7249 "nums_tags",
7250 "created_at",
7251 "updated_at"
7252 ],
7253 "properties": {
7254 "created_at": {
7255 "type": "string",
7256 "format": "date-time"
7257 },
7258 "default_branch": {
7259 "type": "string"
7260 },
7261 "description": {
7262 "type": [
7263 "string",
7264 "null"
7265 ]
7266 },
7267 "dir_id": {
7268 "type": "string",
7269 "format": "uuid"
7270 },
7271 "enable_lfs": {
7272 "type": "boolean"
7273 },
7274 "full_name": {
7275 "type": "string"
7276 },
7277 "git_size": {
7278 "type": "integer",
7279 "format": "int64"
7280 },
7281 "id": {
7282 "type": "string",
7283 "format": "uuid"
7284 },
7285 "is_archived": {
7286 "type": "boolean"
7287 },
7288 "is_private": {
7289 "type": "boolean"
7290 },
7291 "last_pushed_at": {
7292 "type": [
7293 "string",
7294 "null"
7295 ],
7296 "format": "date-time"
7297 },
7298 "lfs_size": {
7299 "type": "integer",
7300 "format": "int64"
7301 },
7302 "lower_name": {
7303 "type": "string"
7304 },
7305 "name": {
7306 "type": "string"
7307 },
7308 "nums_branches": {
7309 "type": "integer",
7310 "format": "int32"
7311 },
7312 "nums_commits": {
7313 "type": "integer",
7314 "format": "int32"
7315 },
7316 "nums_tags": {
7317 "type": "integer",
7318 "format": "int32"
7319 },
7320 "primary_language": {
7321 "type": [
7322 "string",
7323 "null"
7324 ]
7325 },
7326 "size": {
7327 "type": "integer",
7328 "format": "int64"
7329 },
7330 "space_id": {
7331 "type": [
7332 "string",
7333 "null"
7334 ],
7335 "format": "uuid"
7336 },
7337 "space_lower_name": {
7338 "type": [
7339 "string",
7340 "null"
7341 ]
7342 },
7343 "space_name": {
7344 "type": [
7345 "string",
7346 "null"
7347 ]
7348 },
7349 "storage_limit_size": {
7350 "type": [
7351 "integer",
7352 "null"
7353 ],
7354 "format": "int64"
61657355 },
61667356 "updated_at": {
61677357 "type": "string",
61687358 "format": "date-time"
6169 },
6170 "username": {
6171 "type": "string"
6172 }
6173 }
6174 },
6175 "error": {
6176 "type": [
6177 "string",
6178 "null"
6179 ]
6180 },
6181 "error_code": {
6182 "type": [
6183 "string",
6184 "null"
6185 ]
6186 },
6187 "log_id": {
6188 "type": "string"
6189 },
6190 "message": {
6191 "type": "string"
6192 },
6193 "request_id": {
6194 "type": "string"
6195 }
6196 }
6197 },
6198 "AppResponseData_AdminLoginResp": {
6199 "type": "object",
6200 "required": [
6201 "code",
6202 "message",
6203 "request_id",
6204 "log_id"
6205 ],
6206 "properties": {
6207 "code": {
6208 "type": "integer",
6209 "format": "int32"
6210 },
6211 "data": {
6212 "type": "object",
6213 "required": [
6214 "admin",
6215 "must_change_password"
6216 ],
6217 "properties": {
6218 "admin": {
6219 "$ref": "#/components/schemas/AdminDto"
6220 },
6221 "must_change_password": {
6222 "type": "boolean"
62237359 }
62247360 }
62257361 },
@@ -9028,6 +10164,7 @@
902810164 "name",
902910165 "labels",
903010166 "capacity",
10167 "capabilities",
903110168 "state",
903210169 "active_jobs",
903310170 "created_at"
@@ -9037,6 +10174,12 @@
903710174 "type": "integer",
903810175 "format": "int64"
903910176 },
10177 "capabilities": {
10178 "type": "array",
10179 "items": {
10180 "type": "string"
10181 }
10182 },
904010183 "capacity": {
904110184 "type": "integer",
904210185 "format": "int32"
@@ -9065,6 +10208,13 @@
906510208 "name": {
906610209 "type": "string"
906710210 },
10211 "protocol_version": {
10212 "type": [
10213 "integer",
10214 "null"
10215 ],
10216 "format": "int32"
10217 },
906810218 "space_id": {
906910219 "type": [
907010220 "string",
@@ -9309,6 +10459,45 @@
930910459 }
931010460 }
931110461 },
10462 "AppResponseData_String": {
10463 "type": "object",
10464 "required": [
10465 "code",
10466 "message",
10467 "request_id",
10468 "log_id"
10469 ],
10470 "properties": {
10471 "code": {
10472 "type": "integer",
10473 "format": "int32"
10474 },
10475 "data": {
10476 "type": "string"
10477 },
10478 "error": {
10479 "type": [
10480 "string",
10481 "null"
10482 ]
10483 },
10484 "error_code": {
10485 "type": [
10486 "string",
10487 "null"
10488 ]
10489 },
10490 "log_id": {
10491 "type": "string"
10492 },
10493 "message": {
10494 "type": "string"
10495 },
10496 "request_id": {
10497 "type": "string"
10498 }
10499 }
10500 },
931210501 "AppResponseData_SystemConfigDto": {
931310502 "type": "object",
931410503 "required": [
@@ -9921,6 +11110,7 @@
992111110 "name",
992211111 "labels",
992311112 "capacity",
11113 "capabilities",
992411114 "state",
992511115 "active_jobs",
992611116 "created_at"
@@ -9930,6 +11120,12 @@
993011120 "type": "integer",
993111121 "format": "int64"
993211122 },
11123 "capabilities": {
11124 "type": "array",
11125 "items": {
11126 "type": "string"
11127 }
11128 },
993311129 "capacity": {
993411130 "type": "integer",
993511131 "format": "int32"
@@ -9958,6 +11154,13 @@
995811154 "name": {
995911155 "type": "string"
996011156 },
11157 "protocol_version": {
11158 "type": [
11159 "integer",
11160 "null"
11161 ],
11162 "format": "int32"
11163 },
996111164 "space_id": {
996211165 "type": [
996311166 "string",
@@ -11149,6 +12352,96 @@
1114912352 }
1115012353 }
1115112354 },
12355 "ListActRunsQuery": {
12356 "type": "object",
12357 "properties": {
12358 "limit": {
12359 "type": [
12360 "integer",
12361 "null"
12362 ],
12363 "format": "int64",
12364 "minimum": 0
12365 },
12366 "offset": {
12367 "type": [
12368 "integer",
12369 "null"
12370 ],
12371 "format": "int64",
12372 "minimum": 0
12373 },
12374 "q": {
12375 "type": [
12376 "string",
12377 "null"
12378 ]
12379 },
12380 "repo_id": {
12381 "type": [
12382 "string",
12383 "null"
12384 ],
12385 "format": "uuid"
12386 },
12387 "state": {
12388 "oneOf": [
12389 {
12390 "type": "null"
12391 },
12392 {
12393 "$ref": "#/components/schemas/WorkflowRunState"
12394 }
12395 ]
12396 }
12397 }
12398 },
12399 "ListReposQuery": {
12400 "type": "object",
12401 "properties": {
12402 "archived": {
12403 "type": [
12404 "boolean",
12405 "null"
12406 ]
12407 },
12408 "limit": {
12409 "type": [
12410 "integer",
12411 "null"
12412 ],
12413 "format": "int64",
12414 "minimum": 0
12415 },
12416 "offset": {
12417 "type": [
12418 "integer",
12419 "null"
12420 ],
12421 "format": "int64",
12422 "minimum": 0
12423 },
12424 "private": {
12425 "type": [
12426 "boolean",
12427 "null"
12428 ]
12429 },
12430 "q": {
12431 "type": [
12432 "string",
12433 "null"
12434 ]
12435 },
12436 "space_id": {
12437 "type": [
12438 "string",
12439 "null"
12440 ],
12441 "format": "uuid"
12442 }
12443 }
12444 },
1115212445 "NoData": {
1115312446 "default": null
1115412447 },
@@ -11407,6 +12700,7 @@
1140712700 "name",
1140812701 "labels",
1140912702 "capacity",
12703 "capabilities",
1141012704 "state",
1141112705 "active_jobs",
1141212706 "created_at"
@@ -11416,6 +12710,12 @@
1141612710 "type": "integer",
1141712711 "format": "int64"
1141812712 },
12713 "capabilities": {
12714 "type": "array",
12715 "items": {
12716 "type": "string"
12717 }
12718 },
1141912719 "capacity": {
1142012720 "type": "integer",
1142112721 "format": "int32"
@@ -11444,6 +12744,13 @@
1144412744 "name": {
1144512745 "type": "string"
1144612746 },
12747 "protocol_version": {
12748 "type": [
12749 "integer",
12750 "null"
12751 ],
12752 "format": "int32"
12753 },
1144712754 "space_id": {
1144812755 "type": [
1144912756 "string",
@@ -12165,6 +13472,48 @@
1216513472 }
1216613473 }
1216713474 },
13475 "UpdateRepoCmd": {
13476 "type": "object",
13477 "properties": {
13478 "default_branch": {
13479 "type": [
13480 "string",
13481 "null"
13482 ]
13483 },
13484 "description": {
13485 "type": [
13486 "string",
13487 "null"
13488 ]
13489 },
13490 "enable_lfs": {
13491 "type": [
13492 "boolean",
13493 "null"
13494 ]
13495 },
13496 "is_archived": {
13497 "type": [
13498 "boolean",
13499 "null"
13500 ]
13501 },
13502 "is_private": {
13503 "type": [
13504 "boolean",
13505 "null"
13506 ]
13507 },
13508 "storage_limit_size": {
13509 "type": [
13510 "integer",
13511 "null"
13512 ],
13513 "format": "int64"
13514 }
13515 }
13516 },
1216813517 "UpdateRunnerCmd": {
1216913518 "type": "object",
1217013519 "properties": {
@@ -12305,6 +13654,17 @@
1230513654 "type": "string"
1230613655 }
1230713656 }
13657 },
13658 "WorkflowRunState": {
13659 "type": "string",
13660 "enum": [
13661 "queued",
13662 "running",
13663 "success",
13664 "failure",
13665 "cancel_requested",
13666 "cancelled"
13667 ]
1230813668 }
1230913669 },
1231013670 "securitySchemes": {
Added lib/admin/src/handlers/act.rs View lib/admin/src/handlers/act.rs
@@ -0,0 +1,97 @@
1use axum::extract::{Extension, Path, Query, State};
2use axum::http::HeaderMap;
3use export::middleware::RequestId;
4use export::rest::AppResponseData;
5use models::admin::admin_user::AdminRole;
6use uuid::Uuid;
7
8use crate::errors::AdminApiError;
9use crate::services::act::{AdminActRunPage, AdminActSummary, ListActRunsQuery};
10use crate::services::service::AdminService;
11use crate::share::roles::CurrentAdmin;
12
13use super::audit_ctx;
14
15#[utoipa::path(
16 get,
17 path = "/admin/act/summary",
18 tag = "admin-act",
19 description = "读取 act runner、作业和运行概览。",
20 responses((status = 200, description = "act summary", body = AppResponseData<AdminActSummary>))
21)]
22pub async fn summary(
23 State(service): State<AdminService>,
24) -> Result<AppResponseData<AdminActSummary>, AdminApiError> {
25 Ok(AppResponseData::ok(service.act_summary().await?))
26}
27
28#[utoipa::path(
29 get,
30 path = "/admin/act/runs",
31 tag = "admin-act",
32 description = "分页列出 act 工作流运行。",
33 params(ListActRunsQuery),
34 responses((status = 200, description = "act runs", body = AppResponseData<AdminActRunPage>))
35)]
36pub async fn list_runs(
37 State(service): State<AdminService>,
38 Query(query): Query<ListActRunsQuery>,
39) -> Result<AppResponseData<AdminActRunPage>, AdminApiError> {
40 Ok(AppResponseData::ok(service.list_act_runs(&query).await?))
41}
42
43#[utoipa::path(
44 post,
45 path = "/admin/act/runs/{id}/cancel",
46 tag = "admin-act",
47 description = "请求取消 act 工作流运行。",
48 params(("id" = Uuid, Path)),
49 responses((status = 200, description = "cancel requested", body = AppResponseData<Uuid>))
50)]
51pub async fn cancel_run(
52 State(service): State<AdminService>,
53 CurrentAdmin(actor): CurrentAdmin,
54 Path(id): Path<Uuid>,
55 headers: HeaderMap,
56 request_id: Option<Extension<RequestId>>,
57) -> Result<AppResponseData<Uuid>, AdminApiError> {
58 actor.require(AdminRole::Operator)?;
59 service.cancel_act_run(id).await?;
60 service
61 .audit_action(
62 &actor,
63 "act.run.cancel",
64 serde_json::json!({ "run_id": id }),
65 &audit_ctx(&headers, request_id),
66 )
67 .await;
68 Ok(AppResponseData::ok(id))
69}
70
71#[utoipa::path(
72 post,
73 path = "/admin/act/runs/{id}/rerun",
74 tag = "admin-act",
75 description = "重新运行 act 工作流。",
76 params(("id" = Uuid, Path)),
77 responses((status = 200, description = "rerun created", body = AppResponseData<Uuid>))
78)]
79pub async fn rerun_run(
80 State(service): State<AdminService>,
81 CurrentAdmin(actor): CurrentAdmin,
82 Path(id): Path<Uuid>,
83 headers: HeaderMap,
84 request_id: Option<Extension<RequestId>>,
85) -> Result<AppResponseData<Uuid>, AdminApiError> {
86 actor.require(AdminRole::Operator)?;
87 let new_id = service.rerun_act_run(id).await?;
88 service
89 .audit_action(
90 &actor,
91 "act.run.rerun",
92 serde_json::json!({ "run_id": id, "new_run_id": new_id }),
93 &audit_ctx(&headers, request_id),
94 )
95 .await;
96 Ok(AppResponseData::ok(new_id))
97}
Modified lib/admin/src/handlers/mod.rs View lib/admin/src/handlers/mod.rs
@@ -1,8 +1,10 @@
1pub mod act;
12pub mod admins;
23pub mod auth;
34pub mod chat;
45pub mod logs;
56pub mod queues;
7pub mod repos;
68pub mod runners;
79pub mod spaces;
810pub mod storage;
Added lib/admin/src/handlers/repos.rs View lib/admin/src/handlers/repos.rs
@@ -0,0 +1,80 @@
1use axum::extract::{Extension, Path, Query, State};
2use axum::http::HeaderMap;
3use export::middleware::RequestId;
4use export::rest::AppResponseData;
5use models::admin::admin_user::AdminRole;
6use uuid::Uuid;
7
8use crate::errors::AdminApiError;
9use crate::services::repos::{AdminRepoPage, AdminRepoSummary, ListReposQuery, UpdateRepoCmd};
10use crate::services::service::AdminService;
11use crate::share::roles::CurrentAdmin;
12
13use super::audit_ctx;
14
15#[utoipa::path(
16 get,
17 path = "/admin/repos",
18 tag = "admin-repos",
19 description = "分页列出仓库及其归属 Space。",
20 params(ListReposQuery),
21 responses((status = 200, description = "repo list", body = AppResponseData<AdminRepoPage>))
22)]
23pub async fn list_repos(
24 State(service): State<AdminService>,
25 Query(query): Query<ListReposQuery>,
26) -> Result<AppResponseData<AdminRepoPage>, AdminApiError> {
27 Ok(AppResponseData::ok(service.list_repos(&query).await?))
28}
29
30#[utoipa::path(
31 get,
32 path = "/admin/repos/{id}",
33 tag = "admin-repos",
34 description = "读取仓库管理详情。",
35 params(("id" = Uuid, Path)),
36 responses((status = 200, description = "repo detail", body = AppResponseData<AdminRepoSummary>))
37)]
38pub async fn get_repo(
39 State(service): State<AdminService>,
40 Path(id): Path<Uuid>,
41) -> Result<AppResponseData<AdminRepoSummary>, AdminApiError> {
42 Ok(AppResponseData::ok(service.get_repo(id).await?))
43}
44
45#[utoipa::path(
46 patch,
47 path = "/admin/repos/{id}",
48 tag = "admin-repos",
49 description = "更新仓库默认分支、可见性、归档、LFS 或配额。",
50 params(("id" = Uuid, Path)),
51 request_body = UpdateRepoCmd,
52 responses((status = 200, description = "repo updated", body = AppResponseData<AdminRepoSummary>))
53)]
54pub async fn update_repo(
55 State(service): State<AdminService>,
56 CurrentAdmin(actor): CurrentAdmin,
57 Path(id): Path<Uuid>,
58 headers: HeaderMap,
59 request_id: Option<Extension<RequestId>>,
60 axum::Json(cmd): axum::Json<UpdateRepoCmd>,
61) -> Result<AppResponseData<AdminRepoSummary>, AdminApiError> {
62 actor.require(AdminRole::Operator)?;
63 let repo = service.update_repo(id, &cmd).await?;
64 service
65 .audit_action(
66 &actor,
67 "repo.update",
68 serde_json::json!({
69 "repo_id": id,
70 "default_branch": repo.default_branch,
71 "is_private": repo.is_private,
72 "is_archived": repo.is_archived,
73 "enable_lfs": repo.enable_lfs,
74 "storage_limit_size": repo.storage_limit_size,
75 }),
76 &audit_ctx(&headers, request_id),
77 )
78 .await;
79 Ok(AppResponseData::ok(repo))
80}
Added lib/admin/src/services/act.rs View lib/admin/src/services/act.rs
@@ -0,0 +1,370 @@
1use std::collections::{BTreeMap, BTreeSet};
2
3use models::ci::runner::{self, RunnerState};
4use models::ci::workflow_job::{self, WorkflowJobState};
5use models::ci::workflow_run::{self, WorkflowRunState};
6use models::dir::dirs;
7use models::repo::repos;
8use models::space::spaces;
9use sea_orm::{
10 ColumnTrait, Condition, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect,
11};
12use serde::{Deserialize, Serialize};
13use time::OffsetDateTime;
14use utoipa::{IntoParams, ToSchema};
15use uuid::Uuid;
16
17use crate::errors::AdminError;
18use crate::services::runners::map_ci_error;
19use crate::services::service::AdminService;
20
21#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
22pub struct ListActRunsQuery {
23 pub q: Option<String>,
24 pub state: Option<WorkflowRunState>,
25 pub repo_id: Option<Uuid>,
26 pub limit: Option<u64>,
27 pub offset: Option<u64>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
31pub struct AdminActSummary {
32 pub runners_total: u64,
33 pub runners_active: u64,
34 pub runners_busy: u64,
35 pub jobs_queued: u64,
36 pub jobs_running: u64,
37 pub runs_queued: u64,
38 pub runs_running: u64,
39 pub runs_success: u64,
40 pub runs_failure: u64,
41 pub generated_at: OffsetDateTime,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
45pub struct AdminActRunDto {
46 pub id: Uuid,
47 pub repo_id: Uuid,
48 pub repo_name: String,
49 pub repo_full_name: String,
50 pub space_id: Option<Uuid>,
51 pub space_name: Option<String>,
52 pub workflow_file: String,
53 pub workflow_name: String,
54 pub display_name: Option<String>,
55 pub actions_id: Option<i64>,
56 pub run_number: Option<i64>,
57 pub event: String,
58 pub state: String,
59 pub commit_sha: String,
60 pub branch: Option<String>,
61 pub tag: Option<String>,
62 pub pr_number: Option<i64>,
63 pub jobs_total: u64,
64 pub jobs_active: u64,
65 pub jobs_failed: u64,
66 pub created_at: OffsetDateTime,
67 pub started_at: Option<OffsetDateTime>,
68 pub finished_at: Option<OffsetDateTime>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
72pub struct AdminActRunPage {
73 pub items: Vec<AdminActRunDto>,
74 pub total: u64,
75 pub limit: u64,
76 pub offset: u64,
77}
78
79#[derive(Default)]
80struct RunJobCounts {
81 total: u64,
82 active: u64,
83 failed: u64,
84}
85
86struct RunContext {
87 repo: Option<repos::Model>,
88 space: Option<spaces::Model>,
89}
90
91impl AdminService {
92 pub async fn act_summary(&self) -> Result<AdminActSummary, AdminError> {
93 let runners_total = runner::Entity::find().count(self.db().read()).await?;
94 let runners_active = runner::Entity::find()
95 .filter(runner::Column::State.eq(RunnerState::Active))
96 .count(self.db().read())
97 .await?;
98 let active_jobs = workflow_job::Entity::find()
99 .filter(
100 workflow_job::Column::State
101 .is_in([WorkflowJobState::Claimed, WorkflowJobState::Running]),
102 )
103 .all(self.db().read())
104 .await?;
105 let runners_busy = active_jobs
106 .iter()
107 .filter_map(|job| job.runner_id)
108 .collect::<BTreeSet<_>>()
109 .len() as u64;
110 let jobs_queued = workflow_job::Entity::find()
111 .filter(workflow_job::Column::State.eq(WorkflowJobState::Queued))
112 .count(self.db().read())
113 .await?;
114 let jobs_running = active_jobs.len() as u64;
115 let runs_queued = workflow_run::Entity::find()
116 .filter(workflow_run::Column::State.eq(WorkflowRunState::Queued))
117 .count(self.db().read())
118 .await?;
119 let runs_running = workflow_run::Entity::find()
120 .filter(
121 workflow_run::Column::State
122 .is_in([WorkflowRunState::Running, WorkflowRunState::CancelRequested]),
123 )
124 .count(self.db().read())
125 .await?;
126 let runs_success = workflow_run::Entity::find()
127 .filter(workflow_run::Column::State.eq(WorkflowRunState::Success))
128 .count(self.db().read())
129 .await?;
130 let runs_failure = workflow_run::Entity::find()
131 .filter(workflow_run::Column::State.eq(WorkflowRunState::Failure))
132 .count(self.db().read())
133 .await?;
134 Ok(AdminActSummary {
135 runners_total,
136 runners_active,
137 runners_busy,
138 jobs_queued,
139 jobs_running,
140 runs_queued,
141 runs_running,
142 runs_success,
143 runs_failure,
144 generated_at: OffsetDateTime::now_utc(),
145 })
146 }
147
148 pub async fn list_act_runs(
149 &self,
150 query: &ListActRunsQuery,
151 ) -> Result<AdminActRunPage, AdminError> {
152 let limit = query.limit.unwrap_or(20).clamp(1, 100);
153 let offset = query.offset.unwrap_or(0).min(50_000);
154 let mut q = workflow_run::Entity::find().order_by_desc(workflow_run::Column::CreatedAt);
155 if let Some(term) = query
156 .q
157 .as_deref()
158 .map(str::trim)
159 .filter(|term| !term.is_empty())
160 {
161 q = q.filter(
162 Condition::any()
163 .add(workflow_run::Column::WorkflowName.contains(term))
164 .add(workflow_run::Column::WorkflowFile.contains(term))
165 .add(workflow_run::Column::CommitSha.contains(term)),
166 );
167 }
168 if let Some(state) = query.state {
169 q = q.filter(workflow_run::Column::State.eq(state));
170 }
171 if let Some(repo_id) = query.repo_id {
172 q = q.filter(workflow_run::Column::RepoId.eq(repo_id));
173 }
174 let total = q.clone().count(self.db().read()).await?;
175 let rows = q.limit(limit).offset(offset).all(self.db().read()).await?;
176 let run_ids: Vec<Uuid> = rows.iter().map(|run| run.id).collect();
177 let jobs = if run_ids.is_empty() {
178 Vec::new()
179 } else {
180 workflow_job::Entity::find()
181 .filter(workflow_job::Column::RunId.is_in(run_ids))
182 .all(self.db().read())
183 .await?
184 };
185 let mut job_counts: BTreeMap<Uuid, RunJobCounts> = BTreeMap::new();
186 for job in jobs {
187 let entry = job_counts.entry(job.run_id).or_default();
188 entry.total += 1;
189 if job.state.is_active() {
190 entry.active += 1;
191 }
192 if matches!(
193 job.state,
194 WorkflowJobState::Failure | WorkflowJobState::TimedOut
195 ) {
196 entry.failed += 1;
197 }
198 }
199 let contexts = self.run_contexts(&rows).await?;
200 Ok(AdminActRunPage {
201 items: rows
202 .into_iter()
203 .map(|run| {
204 let counts = job_counts.remove(&run.id).unwrap_or_default();
205 run_summary(run, &contexts, counts)
206 })
207 .collect(),
208 total,
209 limit,
210 offset,
211 })
212 }
213
214 pub async fn cancel_act_run(&self, id: Uuid) -> Result<(), AdminError> {
215 self.require_ci_enabled()?;
216 let service = ci::CiService::new(self.state.clone());
217 service
218 .mark_run_cancel_requested(id)
219 .await
220 .map_err(map_ci_error)?;
221 service.apply_cancellations().await.map_err(map_ci_error)?;
222 Ok(())
223 }
224
225 pub async fn rerun_act_run(&self, id: Uuid) -> Result<Uuid, AdminError> {
226 self.require_ci_enabled()?;
227 let run = workflow_run::Entity::find_by_id(id)
228 .one(self.db().read())
229 .await?
230 .ok_or(AdminError::NotFound)?;
231 let caller = if let Some(caller) = run.trigger_user_id {
232 caller
233 } else {
234 self.run_space_owner(run.repo_id).await?
235 };
236 ci::CiService::new(self.state.clone())
237 .rerun(id, caller)
238 .await
239 .map_err(map_ci_error)
240 }
241
242 fn require_ci_enabled(&self) -> Result<(), AdminError> {
243 if self.config().ci.enabled {
244 Ok(())
245 } else {
246 Err(AdminError::BadRequest("ci is disabled".to_string()))
247 }
248 }
249
250 async fn run_space_owner(&self, repo_id: Uuid) -> Result<Uuid, AdminError> {
251 let repo = repos::Entity::find_by_id(repo_id)
252 .one(self.db().read())
253 .await?
254 .ok_or(AdminError::NotFound)?;
255 let dir = dirs::Entity::find_by_id(repo.dir_id)
256 .one(self.db().read())
257 .await?
258 .ok_or(AdminError::NotFound)?;
259 let space_id = dir.space_id.ok_or(AdminError::NotFound)?;
260 let space = spaces::Entity::find_by_id(space_id)
261 .one(self.db().read())
262 .await?
263 .ok_or(AdminError::NotFound)?;
264 Ok(space.owner_id)
265 }
266
267 async fn run_contexts(
268 &self,
269 rows: &[workflow_run::Model],
270 ) -> Result<BTreeMap<Uuid, RunContext>, AdminError> {
271 let repo_ids: BTreeSet<Uuid> = rows.iter().map(|run| run.repo_id).collect();
272 let repo_rows = if repo_ids.is_empty() {
273 Vec::new()
274 } else {
275 repos::Entity::find()
276 .filter(repos::Column::Id.is_in(repo_ids))
277 .all(self.db().read())
278 .await?
279 };
280 let repo_by_id: BTreeMap<Uuid, repos::Model> =
281 repo_rows.into_iter().map(|repo| (repo.id, repo)).collect();
282 let dir_ids: BTreeSet<Uuid> = repo_by_id.values().map(|repo| repo.dir_id).collect();
283 let dir_rows = if dir_ids.is_empty() {
284 Vec::new()
285 } else {
286 dirs::Entity::find()
287 .filter(dirs::Column::Id.is_in(dir_ids))
288 .all(self.db().read())
289 .await?
290 };
291 let dir_by_id: BTreeMap<Uuid, dirs::Model> =
292 dir_rows.into_iter().map(|dir| (dir.id, dir)).collect();
293 let space_ids: BTreeSet<Uuid> = dir_by_id.values().filter_map(|dir| dir.space_id).collect();
294 let space_rows = if space_ids.is_empty() {
295 Vec::new()
296 } else {
297 spaces::Entity::find()
298 .filter(spaces::Column::Id.is_in(space_ids))
299 .all(self.db().read())
300 .await?
301 };
302 let space_by_id: BTreeMap<Uuid, spaces::Model> = space_rows
303 .into_iter()
304 .map(|space| (space.id, space))
305 .collect();
306 Ok(repo_by_id
307 .into_iter()
308 .map(|(repo_id, repo)| {
309 let space = dir_by_id
310 .get(&repo.dir_id)
311 .and_then(|dir| dir.space_id)
312 .and_then(|space_id| space_by_id.get(&space_id).cloned());
313 (
314 repo_id,
315 RunContext {
316 repo: Some(repo),
317 space,
318 },
319 )
320 })
321 .collect())
322 }
323}
324
325fn run_summary(
326 run: workflow_run::Model,
327 contexts: &BTreeMap<Uuid, RunContext>,
328 counts: RunJobCounts,
329) -> AdminActRunDto {
330 let context = contexts.get(&run.repo_id);
331 let repo = context.and_then(|context| context.repo.as_ref());
332 let space = context.and_then(|context| context.space.as_ref());
333 let repo_name = repo
334 .map(|repo| repo.name.clone())
335 .unwrap_or_else(|| run.repo_id.to_string());
336 let repo_full_name = match (space, repo) {
337 (Some(space), Some(repo)) => format!("{}/{}", space.lower_name, repo.lower_name),
338 (_, Some(repo)) => repo.lower_name.clone(),
339 _ => repo_name.clone(),
340 };
341 AdminActRunDto {
342 id: run.id,
343 repo_id: run.repo_id,
344 repo_name,
345 repo_full_name,
346 space_id: space.map(|space| space.id),
347 space_name: space.map(|space| space.name.clone()),
348 workflow_file: run.workflow_file,
349 workflow_name: run.workflow_name,
350 display_name: run
351 .definition
352 .get("_display_name")
353 .and_then(|value| value.as_str())
354 .map(str::to_owned),
355 actions_id: run.actions_id,
356 run_number: run.run_number,
357 event: run.event.as_str().to_string(),
358 state: ci::share::states::run_state_str(run.state).to_string(),
359 commit_sha: run.commit_sha,
360 branch: run.branch,
361 tag: run.tag,
362 pr_number: run.pr_number,
363 jobs_total: counts.total,
364 jobs_active: counts.active,
365 jobs_failed: counts.failed,
366 created_at: run.created_at,
367 started_at: run.started_at,
368 finished_at: run.finished_at,
369 }
370}
Modified lib/admin/src/services/mod.rs View lib/admin/src/services/mod.rs
@@ -1,8 +1,10 @@
1pub mod act;
12pub mod audit;
23pub mod auth;
34pub mod chat;
45pub mod logs;
56pub mod queues;
7pub mod repos;
68pub mod runners;
79pub mod service;
810pub mod spaces;
Added lib/admin/src/services/repos.rs View lib/admin/src/services/repos.rs
@@ -0,0 +1,297 @@
1use std::collections::{BTreeMap, BTreeSet};
2
3use git::service::GitService;
4use models::dir::dirs;
5use models::repo::repos;
6use models::space::spaces;
7use sea_orm::{
8 ActiveModelTrait, ColumnTrait, Condition, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
9 QuerySelect, Set,
10};
11use serde::{Deserialize, Serialize};
12use time::OffsetDateTime;
13use utoipa::{IntoParams, ToSchema};
14use uuid::Uuid;
15
16use crate::errors::AdminError;
17use crate::services::service::AdminService;
18
19#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
20pub struct ListReposQuery {
21 pub q: Option<String>,
22 pub space_id: Option<Uuid>,
23 pub archived: Option<bool>,
24 pub private: Option<bool>,
25 pub limit: Option<u64>,
26 pub offset: Option<u64>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
30pub struct AdminRepoSummary {
31 pub id: Uuid,
32 pub dir_id: Uuid,
33 pub space_id: Option<Uuid>,
34 pub space_name: Option<String>,
35 pub space_lower_name: Option<String>,
36 pub name: String,
37 pub lower_name: String,
38 pub full_name: String,
39 pub description: Option<String>,
40 pub default_branch: String,
41 pub is_private: bool,
42 pub is_archived: bool,
43 pub enable_lfs: bool,
44 pub storage_limit_size: Option<i64>,
45 pub size: i64,
46 pub git_size: i64,
47 pub lfs_size: i64,
48 pub nums_commits: i32,
49 pub nums_branches: i32,
50 pub nums_tags: i32,
51 pub primary_language: Option<String>,
52 pub created_at: OffsetDateTime,
53 pub updated_at: OffsetDateTime,
54 pub last_pushed_at: Option<OffsetDateTime>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
58pub struct AdminRepoPage {
59 pub items: Vec<AdminRepoSummary>,
60 pub total: u64,
61 pub limit: u64,
62 pub offset: u64,
63}
64
65#[derive(Debug, Clone, Deserialize, ToSchema)]
66pub struct UpdateRepoCmd {
67 #[serde(default, deserialize_with = "export::serde_patch::deserialize_opt_opt")]
68 pub description: Option<Option<String>>,
69 #[serde(default)]
70 pub default_branch: Option<String>,
71 #[serde(default)]
72 pub is_private: Option<bool>,
73 #[serde(default)]
74 pub is_archived: Option<bool>,
75 #[serde(default)]
76 pub enable_lfs: Option<bool>,
77 #[serde(default, deserialize_with = "export::serde_patch::deserialize_opt_opt")]
78 pub storage_limit_size: Option<Option<i64>>,
79}
80
81struct RepoContext {
82 space: Option<spaces::Model>,
83}
84
85impl AdminService {
86 pub async fn list_repos(&self, query: &ListReposQuery) -> Result<AdminRepoPage, AdminError> {
87 let limit = query.limit.unwrap_or(20).clamp(1, 100);
88 let offset = query.offset.unwrap_or(0).min(50_000);
89 let mut q = repos::Entity::find().order_by_desc(repos::Column::CreatedAt);
90 if let Some(term) = query
91 .q
92 .as_deref()
93 .map(str::trim)
94 .filter(|term| !term.is_empty())
95 {
96 q = q.filter(
97 Condition::any()
98 .add(repos::Column::Name.contains(term))
99 .add(repos::Column::LowerName.contains(term))
100 .add(repos::Column::Description.contains(term)),
101 );
102 }
103 if let Some(space_id) = query.space_id {
104 let dir_ids: Vec<Uuid> = dirs::Entity::find()
105 .filter(dirs::Column::SpaceId.eq(space_id))
106 .all(self.db().read())
107 .await?
108 .into_iter()
109 .map(|dir| dir.id)
110 .collect();
111 if dir_ids.is_empty() {
112 return Ok(AdminRepoPage {
113 items: Vec::new(),
114 total: 0,
115 limit,
116 offset,
117 });
118 }
119 q = q.filter(repos::Column::DirId.is_in(dir_ids));
120 }
121 if let Some(archived) = query.archived {
122 q = q.filter(repos::Column::IsArchived.eq(archived));
123 }
124 if let Some(private) = query.private {
125 q = q.filter(repos::Column::IsPrivate.eq(private));
126 }
127 let total = q.clone().count(self.db().read()).await?;
128 let rows = q.limit(limit).offset(offset).all(self.db().read()).await?;
129 let contexts = self.repo_contexts(&rows).await?;
130 Ok(AdminRepoPage {
131 items: rows
132 .into_iter()
133 .map(|repo| repo_summary(repo, &contexts))
134 .collect(),
135 total,
136 limit,
137 offset,
138 })
139 }
140
141 pub async fn get_repo(&self, id: Uuid) -> Result<AdminRepoSummary, AdminError> {
142 let repo = repos::Entity::find_by_id(id)
143 .one(self.db().read())
144 .await?
145 .ok_or(AdminError::NotFound)?;
146 let contexts = self.repo_contexts(std::slice::from_ref(&repo)).await?;
147 Ok(repo_summary(repo, &contexts))
148 }
149
150 pub async fn update_repo(
151 &self,
152 id: Uuid,
153 cmd: &UpdateRepoCmd,
154 ) -> Result<AdminRepoSummary, AdminError> {
155 let repo = repos::Entity::find_by_id(id)
156 .one(self.db().read())
157 .await?
158 .ok_or(AdminError::NotFound)?;
159 let mut active: repos::ActiveModel = repo.clone().into();
160 if let Some(description) = &cmd.description {
161 active.description = Set(description.clone());
162 }
163 if let Some(branch) = cmd.default_branch.as_deref() {
164 let branch = branch.trim();
165 if branch.is_empty() || branch.len() > 255 {
166 return Err(AdminError::BadRequest(
167 "default branch must be 1-255 characters".to_string(),
168 ));
169 }
170 if branch != repo.default_branch {
171 let contexts = self.repo_contexts(std::slice::from_ref(&repo)).await?;
172 let space = contexts
173 .get(&repo.dir_id)
174 .and_then(|context| context.space.as_ref())
175 .ok_or_else(|| {
176 AdminError::BadRequest("repository space not found".to_string())
177 })?;
178 GitService::new(self.state.clone())
179 .rev_parse(
180 space.lower_name.clone(),
181 repo.name.clone(),
182 branch.to_string(),
183 )
184 .await
185 .map_err(|_| {
186 AdminError::BadRequest(format!("branch `{branch}` was not found"))
187 })?;
188 }
189 active.default_branch = Set(branch.to_string());
190 }
191 if let Some(private) = cmd.is_private {
192 active.is_private = Set(private);
193 }
194 if let Some(enable_lfs) = cmd.enable_lfs {
195 active.enable_lfs = Set(enable_lfs);
196 }
197 if let Some(limit) = cmd.storage_limit_size {
198 if limit.is_some_and(|value| value < 0) {
199 return Err(AdminError::BadRequest(
200 "storage limit must not be negative".to_string(),
201 ));
202 }
203 active.storage_limit_size = Set(limit);
204 }
205 if let Some(archived) = cmd.is_archived {
206 active.is_archived = Set(archived);
207 active.archived_at = Set(if archived {
208 Some(repo.archived_at.unwrap_or_else(OffsetDateTime::now_utc))
209 } else {
210 None
211 });
212 }
213 active.updated_at = Set(OffsetDateTime::now_utc());
214 active.update(self.db().write()).await?;
215 self.get_repo(id).await
216 }
217
218 async fn repo_contexts(
219 &self,
220 rows: &[repos::Model],
221 ) -> Result<BTreeMap<Uuid, RepoContext>, AdminError> {
222 let dir_ids: BTreeSet<Uuid> = rows.iter().map(|repo| repo.dir_id).collect();
223 let dir_rows = if dir_ids.is_empty() {
224 Vec::new()
225 } else {
226 dirs::Entity::find()
227 .filter(dirs::Column::Id.is_in(dir_ids))
228 .all(self.db().read())
229 .await?
230 };
231 let dir_by_id: BTreeMap<Uuid, dirs::Model> =
232 dir_rows.into_iter().map(|dir| (dir.id, dir)).collect();
233 let space_ids: BTreeSet<Uuid> = dir_by_id.values().filter_map(|dir| dir.space_id).collect();
234 let space_rows = if space_ids.is_empty() {
235 Vec::new()
236 } else {
237 spaces::Entity::find()
238 .filter(spaces::Column::Id.is_in(space_ids))
239 .all(self.db().read())
240 .await?
241 };
242 let space_by_id: BTreeMap<Uuid, spaces::Model> = space_rows
243 .into_iter()
244 .map(|space| (space.id, space))
245 .collect();
246 Ok(dir_by_id
247 .into_iter()
248 .map(|(dir_id, dir)| {
249 (
250 dir_id,
251 RepoContext {
252 space: dir
253 .space_id
254 .and_then(|space_id| space_by_id.get(&space_id).cloned()),
255 },
256 )
257 })
258 .collect())
259 }
260}
261
262fn repo_summary(repo: repos::Model, contexts: &BTreeMap<Uuid, RepoContext>) -> AdminRepoSummary {
263 let space = contexts
264 .get(&repo.dir_id)
265 .and_then(|context| context.space.as_ref());
266 let space_lower_name = space.map(|space| space.lower_name.clone());
267 let full_name = space_lower_name
268 .as_ref()
269 .map(|space| format!("{space}/{}", repo.lower_name))
270 .unwrap_or_else(|| repo.lower_name.clone());
271 AdminRepoSummary {
272 id: repo.id,
273 dir_id: repo.dir_id,
274 space_id: space.map(|space| space.id),
275 space_name: space.map(|space| space.name.clone()),
276 space_lower_name,
277 name: repo.name,
278 lower_name: repo.lower_name,
279 full_name,
280 description: repo.description,
281 default_branch: repo.default_branch,
282 is_private: repo.is_private,
283 is_archived: repo.is_archived,
284 enable_lfs: repo.enable_lfs,
285 storage_limit_size: repo.storage_limit_size,
286 size: repo.size,
287 git_size: repo.git_size,
288 lfs_size: repo.lfs_size,
289 nums_commits: repo.nums_commits,
290 nums_branches: repo.nums_branches,
291 nums_tags: repo.nums_tags,
292 primary_language: repo.primary_language,
293 created_at: repo.created_at,
294 updated_at: repo.updated_at,
295 last_pushed_at: repo.last_pushed_at,
296 }
297}
Modified lib/admin/src/services/runners.rs View lib/admin/src/services/runners.rs
@@ -24,6 +24,8 @@
2424 pub labels: Vec<String>,
2525 pub capacity: i32,
2626 pub version: Option<String>,
27 pub protocol_version: Option<i32>,
28 pub capabilities: Vec<String>,
2729 pub state: String,
2830 pub space_id: Option<Uuid>,
2931 pub active_jobs: i64,
@@ -99,7 +101,7 @@
99101 pub value: String,
100102}
101103
102fn map_ci_error(error: ci::CiError) -> AdminError {
104pub(crate) fn map_ci_error(error: ci::CiError) -> AdminError {
103105 match error {
104106 ci::CiError::NotFound => AdminError::NotFound,
105107 ci::CiError::InvalidArgument(message) => AdminError::BadRequest(message),
@@ -117,6 +119,11 @@
117119 labels: serde_json::from_value(model.labels).unwrap_or_default(),
118120 capacity: model.capacity,
119121 version: model.version,
122 protocol_version: model.protocol_version,
123 capabilities: model
124 .capabilities
125 .and_then(|value| serde_json::from_value(value).ok())
126 .unwrap_or_default(),
120127 state: match model.state {
121128 RunnerState::Active => "active",
122129 RunnerState::Disabled => "disabled",
Modified lib/admin/src/openapi.rs View lib/admin/src/openapi.rs
@@ -71,6 +71,13 @@
7171 crate::handlers::tasks::delete_task,
7272 crate::handlers::tasks::task_stats,
7373 crate::handlers::tasks::bulk_tasks,
74 crate::handlers::act::summary,
75 crate::handlers::act::list_runs,
76 crate::handlers::act::cancel_run,
77 crate::handlers::act::rerun_run,
78 crate::handlers::repos::list_repos,
79 crate::handlers::repos::get_repo,
80 crate::handlers::repos::update_repo,
7481 crate::handlers::runners::list_runners,
7582 crate::handlers::runners::update_runner,
7683 crate::handlers::runners::delete_runner,
@@ -90,6 +97,14 @@
9097 crate::handlers::system::dashboard
9198 ),
9299 components(schemas(
100 crate::services::act::AdminActSummary,
101 crate::services::act::AdminActRunDto,
102 crate::services::act::AdminActRunPage,
103 crate::services::act::ListActRunsQuery,
104 crate::services::repos::AdminRepoSummary,
105 crate::services::repos::AdminRepoPage,
106 crate::services::repos::ListReposQuery,
107 crate::services::repos::UpdateRepoCmd,
93108 crate::services::runners::RunnerDto,
94109 crate::services::runners::RunnerPageDto,
95110 crate::services::runners::UpdateRunnerCmd,
Modified lib/admin/src/routes.rs View lib/admin/src/routes.rs
@@ -2,7 +2,7 @@
22use axum::routing::{delete, get, patch, post, put};
33
44use crate::handlers::{
5 admins, auth, chat, logs, queues, runners, spaces, storage, system, tasks, users,
5 act, admins, auth, chat, logs, queues, repos, runners, spaces, storage, system, tasks, users,
66};
77use crate::services::service::AdminService;
88use crate::share::guard::admin_guard;
@@ -13,6 +13,15 @@
1313 .route("/admin/auth/logout", post(auth::logout));
1414
1515 let protected = Router::new()
16 .route("/admin/act/summary", get(act::summary))
17 .route("/admin/act/runs", get(act::list_runs))
18 .route("/admin/act/runs/{id}/cancel", post(act::cancel_run))
19 .route("/admin/act/runs/{id}/rerun", post(act::rerun_run))
20 .route("/admin/repos", get(repos::list_repos))
21 .route(
22 "/admin/repos/{id}",
23 get(repos::get_repo).patch(repos::update_repo),
24 )
1625 .route("/admin/runners", get(runners::list_runners))
1726 .route(
1827 "/admin/runners/{id}",