@kernel-sig/console 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/.env.example +50 -0
  2. package/LICENSE +127 -0
  3. package/Makefile +66 -0
  4. package/README.md +279 -0
  5. package/backend/.dockerignore +12 -0
  6. package/backend/Dockerfile +43 -0
  7. package/backend/alembic/env.py +75 -0
  8. package/backend/alembic/script.py.mako +25 -0
  9. package/backend/alembic/versions/0001_initial.py +88 -0
  10. package/backend/alembic/versions/350e2d9b6553_add_pr_comment_table.py +48 -0
  11. package/backend/alembic/versions/4f8342e727fe_sig_info_roster_fields_and_branch_.py +39 -0
  12. package/backend/alembic/versions/5a31ade90136_add_repository_credential_pull_request_.py +293 -0
  13. package/backend/alembic/versions/87a008142f17_add_classification_attention_and_ai_.py +436 -0
  14. package/backend/alembic/versions/a1c7d90e4b52_branch_belongs_to_a_repository.py +72 -0
  15. package/backend/alembic/versions/b41c9d7e5f28_extend_pr_kind_categories.py +43 -0
  16. package/backend/alembic/versions/c8e4f2a71b93_add_release_kind.py +40 -0
  17. package/backend/alembic/versions/d1275091dfdc_add_needs_detail_flag_to_pull_request.py +44 -0
  18. package/backend/alembic/versions/e5b27c9d3a41_downgrade_cve_without_ids.py +54 -0
  19. package/backend/alembic/versions/ee3159a4eff0_add_sig_meeting_member_and_release_.py +164 -0
  20. package/backend/alembic/versions/fcf3c186d63b_attention_rule_subscriptions.py +42 -0
  21. package/backend/alembic.ini +40 -0
  22. package/backend/app/__init__.py +0 -0
  23. package/backend/app/api/__init__.py +0 -0
  24. package/backend/app/api/deps.py +74 -0
  25. package/backend/app/api/v1/__init__.py +0 -0
  26. package/backend/app/api/v1/ai.py +386 -0
  27. package/backend/app/api/v1/analytics.py +48 -0
  28. package/backend/app/api/v1/attention.py +142 -0
  29. package/backend/app/api/v1/auth.py +106 -0
  30. package/backend/app/api/v1/classification.py +192 -0
  31. package/backend/app/api/v1/health.py +29 -0
  32. package/backend/app/api/v1/issues.py +143 -0
  33. package/backend/app/api/v1/pulls.py +388 -0
  34. package/backend/app/api/v1/repositories.py +273 -0
  35. package/backend/app/api/v1/router.py +32 -0
  36. package/backend/app/api/v1/sig.py +565 -0
  37. package/backend/app/api/v1/users.py +87 -0
  38. package/backend/app/api/v1/webhooks.py +135 -0
  39. package/backend/app/core/__init__.py +0 -0
  40. package/backend/app/core/config.py +72 -0
  41. package/backend/app/core/crypto.py +74 -0
  42. package/backend/app/core/db.py +79 -0
  43. package/backend/app/core/exceptions.py +60 -0
  44. package/backend/app/core/logging.py +69 -0
  45. package/backend/app/core/permissions.py +87 -0
  46. package/backend/app/core/queue.py +57 -0
  47. package/backend/app/core/security.py +69 -0
  48. package/backend/app/domain/__init__.py +0 -0
  49. package/backend/app/domain/attention.py +618 -0
  50. package/backend/app/domain/classification.py +689 -0
  51. package/backend/app/domain/meeting.py +408 -0
  52. package/backend/app/domain/release.py +163 -0
  53. package/backend/app/domain/review.py +416 -0
  54. package/backend/app/domain/sig.py +178 -0
  55. package/backend/app/domain/sig_info.py +201 -0
  56. package/backend/app/integrations/__init__.py +0 -0
  57. package/backend/app/integrations/atomgit/__init__.py +0 -0
  58. package/backend/app/integrations/atomgit/client.py +505 -0
  59. package/backend/app/integrations/atomgit/models.py +311 -0
  60. package/backend/app/integrations/llm/__init__.py +0 -0
  61. package/backend/app/integrations/llm/prompts.py +222 -0
  62. package/backend/app/integrations/llm/provider.py +326 -0
  63. package/backend/app/main.py +242 -0
  64. package/backend/app/middleware/__init__.py +0 -0
  65. package/backend/app/middleware/audit.py +129 -0
  66. package/backend/app/middleware/request_context.py +42 -0
  67. package/backend/app/models/__init__.py +112 -0
  68. package/backend/app/models/ai.py +214 -0
  69. package/backend/app/models/attention.py +131 -0
  70. package/backend/app/models/attention_settings.py +61 -0
  71. package/backend/app/models/audit.py +53 -0
  72. package/backend/app/models/base.py +35 -0
  73. package/backend/app/models/classification.py +144 -0
  74. package/backend/app/models/credential.py +56 -0
  75. package/backend/app/models/issue.py +132 -0
  76. package/backend/app/models/meeting.py +189 -0
  77. package/backend/app/models/pull_request.py +288 -0
  78. package/backend/app/models/repository.py +196 -0
  79. package/backend/app/models/sig.py +194 -0
  80. package/backend/app/models/user.py +44 -0
  81. package/backend/app/schemas/__init__.py +0 -0
  82. package/backend/app/schemas/ai.py +120 -0
  83. package/backend/app/schemas/attention.py +43 -0
  84. package/backend/app/schemas/auth.py +26 -0
  85. package/backend/app/schemas/classification.py +58 -0
  86. package/backend/app/schemas/common.py +43 -0
  87. package/backend/app/schemas/pull_request.py +214 -0
  88. package/backend/app/schemas/repository.py +94 -0
  89. package/backend/app/schemas/sig.py +246 -0
  90. package/backend/app/schemas/user.py +79 -0
  91. package/backend/app/services/__init__.py +0 -0
  92. package/backend/app/services/ai_service.py +569 -0
  93. package/backend/app/services/analytics_service.py +390 -0
  94. package/backend/app/services/attention_queue.py +451 -0
  95. package/backend/app/services/attention_service.py +432 -0
  96. package/backend/app/services/auth_service.py +74 -0
  97. package/backend/app/services/classification_service.py +658 -0
  98. package/backend/app/services/credential_service.py +105 -0
  99. package/backend/app/services/pull_query.py +273 -0
  100. package/backend/app/services/release_service.py +446 -0
  101. package/backend/app/services/repository_service.py +132 -0
  102. package/backend/app/services/sig_service.py +385 -0
  103. package/backend/app/services/sync_service.py +752 -0
  104. package/backend/app/services/user_service.py +68 -0
  105. package/backend/app/worker.py +389 -0
  106. package/backend/entrypoint.sh +10 -0
  107. package/backend/pyproject.toml +68 -0
  108. package/backend/tests/test_analysis_api.py +154 -0
  109. package/backend/tests/test_atomgit_client.py +360 -0
  110. package/backend/tests/test_atomgit_models.py +257 -0
  111. package/backend/tests/test_attention.py +291 -0
  112. package/backend/tests/test_audit_middleware.py +135 -0
  113. package/backend/tests/test_classification.py +498 -0
  114. package/backend/tests/test_config.py +41 -0
  115. package/backend/tests/test_crypto.py +68 -0
  116. package/backend/tests/test_exceptions.py +62 -0
  117. package/backend/tests/test_health.py +63 -0
  118. package/backend/tests/test_llm_provider.py +320 -0
  119. package/backend/tests/test_meeting_domain.py +169 -0
  120. package/backend/tests/test_permissions.py +69 -0
  121. package/backend/tests/test_pull_query_wiring.py +66 -0
  122. package/backend/tests/test_pull_schemas.py +82 -0
  123. package/backend/tests/test_release_domain.py +82 -0
  124. package/backend/tests/test_review_parser.py +301 -0
  125. package/backend/tests/test_schemas_user.py +97 -0
  126. package/backend/tests/test_security.py +92 -0
  127. package/cli/index.js +338 -0
  128. package/compose.yaml +105 -0
  129. package/frontend/.dockerignore +4 -0
  130. package/frontend/Dockerfile +27 -0
  131. package/frontend/index.html +14 -0
  132. package/frontend/nginx.conf +47 -0
  133. package/frontend/package-lock.json +5020 -0
  134. package/frontend/package.json +35 -0
  135. package/frontend/src/api/ai.ts +124 -0
  136. package/frontend/src/api/analytics.ts +66 -0
  137. package/frontend/src/api/attention.ts +181 -0
  138. package/frontend/src/api/auth.ts +74 -0
  139. package/frontend/src/api/classification.ts +170 -0
  140. package/frontend/src/api/issues.ts +90 -0
  141. package/frontend/src/api/pulls.ts +233 -0
  142. package/frontend/src/api/repositories.ts +95 -0
  143. package/frontend/src/api/sig.ts +232 -0
  144. package/frontend/src/app/antd-theme.ts +94 -0
  145. package/frontend/src/app/providers.tsx +59 -0
  146. package/frontend/src/app/router.tsx +411 -0
  147. package/frontend/src/app/search.ts +30 -0
  148. package/frontend/src/components/ClassificationBadge.tsx +58 -0
  149. package/frontend/src/components/GateBadge.tsx +13 -0
  150. package/frontend/src/components/SeverityBadge.tsx +20 -0
  151. package/frontend/src/components/layout/AppShell.tsx +16 -0
  152. package/frontend/src/components/layout/AuthLayout.tsx +32 -0
  153. package/frontend/src/components/layout/Sidebar.tsx +223 -0
  154. package/frontend/src/components/layout/TopBar.tsx +47 -0
  155. package/frontend/src/components/pulls/DiscussionTimeline.tsx +145 -0
  156. package/frontend/src/components/pulls/FacetRail.tsx +199 -0
  157. package/frontend/src/components/pulls/LabelChips.tsx +87 -0
  158. package/frontend/src/components/ui/alert.tsx +30 -0
  159. package/frontend/src/components/ui/badge.tsx +53 -0
  160. package/frontend/src/components/ui/button.tsx +51 -0
  161. package/frontend/src/components/ui/card.tsx +64 -0
  162. package/frontend/src/components/ui/chart-theme.ts +65 -0
  163. package/frontend/src/components/ui/data-table.tsx +39 -0
  164. package/frontend/src/components/ui/echart.tsx +70 -0
  165. package/frontend/src/components/ui/empty-state.tsx +22 -0
  166. package/frontend/src/components/ui/input.tsx +39 -0
  167. package/frontend/src/components/ui/lazy-chart.tsx +21 -0
  168. package/frontend/src/components/ui/skeleton.tsx +19 -0
  169. package/frontend/src/hooks/use-current-repository.ts +44 -0
  170. package/frontend/src/hooks/use-current-user.ts +38 -0
  171. package/frontend/src/lib/api-client.ts +93 -0
  172. package/frontend/src/lib/css-color.ts +60 -0
  173. package/frontend/src/lib/utils.ts +45 -0
  174. package/frontend/src/main.tsx +22 -0
  175. package/frontend/src/pages/attention/AttentionQueuePage.tsx +316 -0
  176. package/frontend/src/pages/attention/RuleSettingsPanel.tsx +177 -0
  177. package/frontend/src/pages/branches/BranchDetailPage.tsx +632 -0
  178. package/frontend/src/pages/branches/BranchListPage.tsx +308 -0
  179. package/frontend/src/pages/dashboard/DashboardPage.tsx +657 -0
  180. package/frontend/src/pages/issues/IssueListPage.tsx +284 -0
  181. package/frontend/src/pages/login/LoginPage.tsx +95 -0
  182. package/frontend/src/pages/meetings/MeetingDetailPage.tsx +403 -0
  183. package/frontend/src/pages/meetings/MeetingListPage.tsx +264 -0
  184. package/frontend/src/pages/members/MembersPage.tsx +534 -0
  185. package/frontend/src/pages/pulls/PullDetailPage.tsx +833 -0
  186. package/frontend/src/pages/pulls/PullListPage.tsx +399 -0
  187. package/frontend/src/pages/settings/AiSettingsPage.tsx +449 -0
  188. package/frontend/src/pages/settings/SettingsPage.tsx +321 -0
  189. package/frontend/src/pages/setup/SetupPage.tsx +144 -0
  190. package/frontend/src/styles/globals.css +147 -0
  191. package/frontend/tsconfig.json +22 -0
  192. package/frontend/vite.config.ts +57 -0
  193. package/package.json +48 -0
  194. package/scripts/e2e-auth-flow.py +131 -0
  195. package/scripts/e2e-pr-detail.py +128 -0
  196. package/scripts/e2e-verify.py +773 -0
  197. package/scripts/verify-ai-pipeline.py +616 -0
  198. package/scripts/verify-analysis-pipeline.py +303 -0
@@ -0,0 +1,284 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { useState } from 'react'
3
+ import { Select, Tooltip } from 'antd'
4
+ import type { ColumnsType } from 'antd/es/table'
5
+
6
+ import {
7
+ ISSUE_STATE_TONE,
8
+ issuesApi,
9
+ type IssueFilters,
10
+ type IssueListItem,
11
+ } from '@/api/issues'
12
+ import { useCurrentRepository } from '@/hooks/use-current-repository'
13
+ import { ClassificationBadge } from '@/components/ClassificationBadge'
14
+ import { DataTable } from '@/components/ui/data-table'
15
+ import { Badge, type BadgeTone } from '@/components/ui/badge'
16
+ import { Input } from '@/components/ui/input'
17
+ import { EmptyState } from '@/components/ui/empty-state'
18
+ import { Skeleton } from '@/components/ui/skeleton'
19
+ import { cn, formatNumber, formatRelativeTime } from '@/lib/utils'
20
+
21
+ const PER_PAGE = 30
22
+
23
+ const STATE_OPTIONS = [
24
+ { value: 'open', label: '未关闭' },
25
+ { value: 'closed', label: '已关闭' },
26
+ { value: 'all', label: '全部' },
27
+ ]
28
+
29
+ const SORT_OPTIONS = [
30
+ { value: 'updated', label: '最近更新' },
31
+ { value: 'created', label: '最近创建' },
32
+ { value: 'comments', label: '讨论最多' },
33
+ ]
34
+
35
+ const COLUMNS: ColumnsType<IssueListItem> = [
36
+ {
37
+ title: '编号',
38
+ width: 70,
39
+ render: (_, issue) => (
40
+ <span className="font-mono text-xs text-[var(--color-text-muted)]">#{issue.number}</span>
41
+ ),
42
+ },
43
+ {
44
+ title: '标题',
45
+ render: (_, issue) => (
46
+ <>
47
+ {issue.html_url ? (
48
+ <Tooltip title={issue.title}>
49
+ <a
50
+ href={issue.html_url}
51
+ target="_blank"
52
+ rel="noreferrer noopener"
53
+ className="line-clamp-1 hover:text-[var(--color-accent)]"
54
+ >
55
+ {issue.title}
56
+ </a>
57
+ </Tooltip>
58
+ ) : (
59
+ <Tooltip title={issue.title}>
60
+ <span className="line-clamp-1">{issue.title}</span>
61
+ </Tooltip>
62
+ )}
63
+ {issue.comments_count > 0 && (
64
+ <span className="ml-2 text-2xs text-[var(--color-text-muted)]">
65
+ {issue.comments_count} 条讨论
66
+ </span>
67
+ )}
68
+ </>
69
+ ),
70
+ },
71
+ {
72
+ title: '类别',
73
+ width: 180,
74
+ // 两枚徽章并排:本平台判定的类别,与 AtomGit 自己的 issue_type。
75
+ // 前者是推断,后者是原始登记,混作一谈会让人以为推断也是权威。
76
+ render: (_, issue) => (
77
+ <div className="flex flex-wrap items-center gap-1.5">
78
+ {issue.kind ? (
79
+ <ClassificationBadge kind={issue.kind} />
80
+ ) : (
81
+ <span className="text-xs text-[var(--color-text-muted)]">未分类</span>
82
+ )}
83
+ {issue.issue_type && (
84
+ <Badge tone="neutral" className="font-normal">
85
+ {issue.issue_type}
86
+ </Badge>
87
+ )}
88
+ </div>
89
+ ),
90
+ },
91
+ {
92
+ title: '状态',
93
+ width: 90,
94
+ render: (_, issue) =>
95
+ issue.issue_state ? (
96
+ <Badge tone={(ISSUE_STATE_TONE[issue.issue_state] ?? 'neutral') as BadgeTone}>
97
+ {issue.issue_state}
98
+ </Badge>
99
+ ) : (
100
+ <span className="text-xs text-[var(--color-text-muted)]">—</span>
101
+ ),
102
+ },
103
+ {
104
+ title: '优先级',
105
+ width: 100,
106
+ render: (_, issue) => (
107
+ <span className="text-xs text-[var(--color-text-secondary)]">
108
+ {issue.priority_label ?? '—'}
109
+ </span>
110
+ ),
111
+ },
112
+ {
113
+ title: '负责人',
114
+ width: 120,
115
+ render: (_, issue) => (
116
+ <span className="text-xs text-[var(--color-text-secondary)]">
117
+ {issue.assignee_login ?? '未指派'}
118
+ </span>
119
+ ),
120
+ },
121
+ {
122
+ title: '更新',
123
+ width: 100,
124
+ render: (_, issue) => (
125
+ <span className="text-xs text-[var(--color-text-muted)]">
126
+ {formatRelativeTime(issue.atomgit_updated_at)}
127
+ </span>
128
+ ),
129
+ },
130
+ ]
131
+
132
+ export function IssueListPage() {
133
+ const [filters, setFilters] = useState<IssueFilters>({
134
+ state: 'open',
135
+ sort: 'updated',
136
+ order: 'desc',
137
+ page: 1,
138
+ per_page: PER_PAGE,
139
+ })
140
+ const [searchInput, setSearchInput] = useState('')
141
+
142
+ const { repository, isLoading: reposLoading } = useCurrentRepository()
143
+
144
+ const { data: facets } = useQuery({
145
+ queryKey: ['issue-facets', repository?.id],
146
+ queryFn: () => issuesApi.facets(repository!.id),
147
+ enabled: Boolean(repository),
148
+ })
149
+
150
+ const { data, isLoading, isFetching } = useQuery({
151
+ queryKey: ['issues', repository?.id, filters],
152
+ queryFn: () => issuesApi.list(repository!.id, filters),
153
+ enabled: Boolean(repository),
154
+ placeholderData: (previous) => previous,
155
+ })
156
+
157
+ function update(patch: Partial<IssueFilters>) {
158
+ setFilters((prev) => ({ ...prev, ...patch, page: patch.page ?? 1 }))
159
+ }
160
+
161
+ const totalPages = data
162
+ ? Math.max(1, Math.ceil(data.total / (filters.per_page ?? PER_PAGE)))
163
+ : 1
164
+
165
+ if (reposLoading) return <Skeleton className="h-64 w-full" />
166
+
167
+ if (!repository) {
168
+ return (
169
+ <EmptyState
170
+ title="尚未纳管任何仓库"
171
+ description="请先在「设置 → 仓库与同步」中完成配置。"
172
+ />
173
+ )
174
+ }
175
+
176
+ return (
177
+ <div className="space-y-4">
178
+ <header>
179
+ <h1 className="text-base font-semibold tracking-tight">Issue</h1>
180
+ <p className="mt-0.5 text-sm text-[var(--color-text-secondary)]">
181
+ {repository.display_name ?? `${repository.owner}/${repository.name}`}
182
+ {data ? ` · 共 ${formatNumber(data.total)} 个` : ''}
183
+ </p>
184
+ </header>
185
+
186
+ {/* 类型分布直接用 AtomGit 的原生字段,比从标签猜测可靠。
187
+ 这组 chip 保持自绘的 <button aria-pressed>:AntD 的 Tag.CheckableTag
188
+ 渲染成不可聚焦、没有 role 的 <span>,读屏器既念不出"已选中"也没法用
189
+ 键盘切换 —— 换它只是换个观感,代价是可访问性倒退。
190
+ 单选语义(再点一次取消)也正好是 aria-pressed 能表达的。 */}
191
+ {facets && facets.types.length > 0 && (
192
+ <div className="flex flex-wrap items-center gap-1.5">
193
+ {facets.types.map((facet) => {
194
+ const active = filters.issue_type === facet.value
195
+ return (
196
+ <button
197
+ key={facet.value}
198
+ type="button"
199
+ aria-pressed={active}
200
+ onClick={() => update({ issue_type: active ? undefined : facet.value })}
201
+ className={cn(
202
+ 'rounded-full border px-2.5 py-1 text-xs transition-colors duration-150',
203
+ active
204
+ ? 'border-[var(--color-accent)] bg-[color-mix(in_oklch,var(--color-accent)_18%,transparent)] text-[var(--color-accent)]'
205
+ : 'border-[var(--color-border-subtle)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-strong)]',
206
+ )}
207
+ >
208
+ {facet.value}
209
+ <span className="ml-1.5 font-mono tabular-nums text-[var(--color-text-muted)]">
210
+ {facet.count}
211
+ </span>
212
+ </button>
213
+ )
214
+ })}
215
+ </div>
216
+ )}
217
+
218
+ <div className="flex flex-wrap items-center gap-2">
219
+ <form
220
+ className="min-w-[220px] flex-1"
221
+ onSubmit={(event) => {
222
+ event.preventDefault()
223
+ update({ q: searchInput || undefined })
224
+ }}
225
+ >
226
+ <Input
227
+ placeholder="按标题或 Issue 编号搜索,回车确认"
228
+ value={searchInput}
229
+ onChange={(event) => setSearchInput(event.target.value)}
230
+ className="h-8 text-sm"
231
+ />
232
+ </form>
233
+
234
+ <Select
235
+ aria-label="状态"
236
+ value={filters.state ?? 'open'}
237
+ options={STATE_OPTIONS}
238
+ onChange={(value) => update({ state: value })}
239
+ />
240
+ {/* 优先级可清空:清空即"不限",用 allowClear 而不是往选项里
241
+ 塞一个空字符串的项 —— 后者在浮层里看起来像一个真选项。 */}
242
+ <Select
243
+ aria-label="优先级"
244
+ value={filters.priority ?? undefined}
245
+ placeholder="全部优先级"
246
+ allowClear
247
+ options={(facets?.priorities ?? []).map((f) => ({
248
+ value: f.value,
249
+ label: `${f.value} (${f.count})`,
250
+ }))}
251
+ onChange={(value) => update({ priority: value || undefined })}
252
+ />
253
+ <Select
254
+ aria-label="排序"
255
+ value={filters.sort ?? 'updated'}
256
+ options={SORT_OPTIONS}
257
+ onChange={(value) => update({ sort: value })}
258
+ />
259
+ </div>
260
+
261
+ <DataTable<IssueListItem>
262
+ rowKey="id"
263
+ columns={COLUMNS}
264
+ dataSource={data?.items ?? []}
265
+ loading={isLoading}
266
+ className={cn(isFetching && 'opacity-90')}
267
+ locale={{
268
+ emptyText: (
269
+ <EmptyState title="没有符合条件的 Issue" description="试试放宽筛选条件。" />
270
+ ),
271
+ }}
272
+ pagination={{
273
+ current: filters.page ?? 1,
274
+ pageSize: filters.per_page ?? PER_PAGE,
275
+ total: data?.total ?? 0,
276
+ showSizeChanger: false,
277
+ showQuickJumper: totalPages > 10,
278
+ showTotal: (count) => `共 ${formatNumber(count)} 条`,
279
+ onChange: (page) => update({ page }),
280
+ }}
281
+ />
282
+ </div>
283
+ )
284
+ }
@@ -0,0 +1,95 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import { useNavigate } from '@tanstack/react-router'
3
+ import { useState, type FormEvent } from 'react'
4
+
5
+ import { authApi } from '@/api/auth'
6
+ import { AuthLayout } from '@/components/layout/AuthLayout'
7
+ import { Alert } from '@/components/ui/alert'
8
+ import { Button } from '@/components/ui/button'
9
+ import { Input, Label } from '@/components/ui/input'
10
+ import { ApiError } from '@/lib/api-client'
11
+
12
+ export function LoginPage() {
13
+ const navigate = useNavigate()
14
+ const queryClient = useQueryClient()
15
+ const [username, setUsername] = useState('')
16
+ const [password, setPassword] = useState('')
17
+
18
+ const { data: setup } = useQuery({
19
+ queryKey: ['setup-status'],
20
+ queryFn: authApi.setupStatus,
21
+ staleTime: Infinity,
22
+ })
23
+
24
+ const login = useMutation({
25
+ mutationFn: () => authApi.login({ username, password }),
26
+ onSuccess: async () => {
27
+ // 跳转前先把会话写入查询缓存。RequireAuth 直接读缓存,
28
+ // 因此缓存就绪即代表鉴权判断可用,不存在时序空窗。
29
+ const me = await authApi.me()
30
+ queryClient.setQueryData(['me'], me)
31
+ await navigate({ to: '/dashboard' })
32
+ },
33
+ })
34
+
35
+ if (setup?.needs_setup) {
36
+ return (
37
+ <AuthLayout title="尚未初始化" description="系统还没有任何账号,请先创建管理员。">
38
+ <Button onClick={() => navigate({ to: '/setup' })}>前往初始化</Button>
39
+ </AuthLayout>
40
+ )
41
+ }
42
+
43
+ const errorMessage =
44
+ login.error instanceof ApiError
45
+ ? login.error.message
46
+ : login.error
47
+ ? '登录失败,请稍后重试'
48
+ : null
49
+
50
+ function handleSubmit(event: FormEvent) {
51
+ event.preventDefault()
52
+ login.mutate()
53
+ }
54
+
55
+ return (
56
+ <AuthLayout
57
+ title="登录"
58
+ description="使用平台账号登录,管理 openEuler Kernel SIG 的 PR 与 Issue。"
59
+ >
60
+ <form onSubmit={handleSubmit} className="space-y-4">
61
+ {errorMessage && <Alert tone="danger">{errorMessage}</Alert>}
62
+
63
+ <div>
64
+ <Label htmlFor="username">用户名</Label>
65
+ <Input
66
+ id="username"
67
+ name="username"
68
+ autoComplete="username"
69
+ autoFocus
70
+ required
71
+ value={username}
72
+ onChange={(e) => setUsername(e.target.value)}
73
+ />
74
+ </div>
75
+
76
+ <div>
77
+ <Label htmlFor="password">密码</Label>
78
+ <Input
79
+ id="password"
80
+ name="password"
81
+ type="password"
82
+ autoComplete="current-password"
83
+ required
84
+ value={password}
85
+ onChange={(e) => setPassword(e.target.value)}
86
+ />
87
+ </div>
88
+
89
+ <Button type="submit" className="w-full" loading={login.isPending}>
90
+ 登录
91
+ </Button>
92
+ </form>
93
+ </AuthLayout>
94
+ )
95
+ }