@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.
- package/.env.example +50 -0
- package/LICENSE +127 -0
- package/Makefile +66 -0
- package/README.md +279 -0
- package/backend/.dockerignore +12 -0
- package/backend/Dockerfile +43 -0
- package/backend/alembic/env.py +75 -0
- package/backend/alembic/script.py.mako +25 -0
- package/backend/alembic/versions/0001_initial.py +88 -0
- package/backend/alembic/versions/350e2d9b6553_add_pr_comment_table.py +48 -0
- package/backend/alembic/versions/4f8342e727fe_sig_info_roster_fields_and_branch_.py +39 -0
- package/backend/alembic/versions/5a31ade90136_add_repository_credential_pull_request_.py +293 -0
- package/backend/alembic/versions/87a008142f17_add_classification_attention_and_ai_.py +436 -0
- package/backend/alembic/versions/a1c7d90e4b52_branch_belongs_to_a_repository.py +72 -0
- package/backend/alembic/versions/b41c9d7e5f28_extend_pr_kind_categories.py +43 -0
- package/backend/alembic/versions/c8e4f2a71b93_add_release_kind.py +40 -0
- package/backend/alembic/versions/d1275091dfdc_add_needs_detail_flag_to_pull_request.py +44 -0
- package/backend/alembic/versions/e5b27c9d3a41_downgrade_cve_without_ids.py +54 -0
- package/backend/alembic/versions/ee3159a4eff0_add_sig_meeting_member_and_release_.py +164 -0
- package/backend/alembic/versions/fcf3c186d63b_attention_rule_subscriptions.py +42 -0
- package/backend/alembic.ini +40 -0
- package/backend/app/__init__.py +0 -0
- package/backend/app/api/__init__.py +0 -0
- package/backend/app/api/deps.py +74 -0
- package/backend/app/api/v1/__init__.py +0 -0
- package/backend/app/api/v1/ai.py +386 -0
- package/backend/app/api/v1/analytics.py +48 -0
- package/backend/app/api/v1/attention.py +142 -0
- package/backend/app/api/v1/auth.py +106 -0
- package/backend/app/api/v1/classification.py +192 -0
- package/backend/app/api/v1/health.py +29 -0
- package/backend/app/api/v1/issues.py +143 -0
- package/backend/app/api/v1/pulls.py +388 -0
- package/backend/app/api/v1/repositories.py +273 -0
- package/backend/app/api/v1/router.py +32 -0
- package/backend/app/api/v1/sig.py +565 -0
- package/backend/app/api/v1/users.py +87 -0
- package/backend/app/api/v1/webhooks.py +135 -0
- package/backend/app/core/__init__.py +0 -0
- package/backend/app/core/config.py +72 -0
- package/backend/app/core/crypto.py +74 -0
- package/backend/app/core/db.py +79 -0
- package/backend/app/core/exceptions.py +60 -0
- package/backend/app/core/logging.py +69 -0
- package/backend/app/core/permissions.py +87 -0
- package/backend/app/core/queue.py +57 -0
- package/backend/app/core/security.py +69 -0
- package/backend/app/domain/__init__.py +0 -0
- package/backend/app/domain/attention.py +618 -0
- package/backend/app/domain/classification.py +689 -0
- package/backend/app/domain/meeting.py +408 -0
- package/backend/app/domain/release.py +163 -0
- package/backend/app/domain/review.py +416 -0
- package/backend/app/domain/sig.py +178 -0
- package/backend/app/domain/sig_info.py +201 -0
- package/backend/app/integrations/__init__.py +0 -0
- package/backend/app/integrations/atomgit/__init__.py +0 -0
- package/backend/app/integrations/atomgit/client.py +505 -0
- package/backend/app/integrations/atomgit/models.py +311 -0
- package/backend/app/integrations/llm/__init__.py +0 -0
- package/backend/app/integrations/llm/prompts.py +222 -0
- package/backend/app/integrations/llm/provider.py +326 -0
- package/backend/app/main.py +242 -0
- package/backend/app/middleware/__init__.py +0 -0
- package/backend/app/middleware/audit.py +129 -0
- package/backend/app/middleware/request_context.py +42 -0
- package/backend/app/models/__init__.py +112 -0
- package/backend/app/models/ai.py +214 -0
- package/backend/app/models/attention.py +131 -0
- package/backend/app/models/attention_settings.py +61 -0
- package/backend/app/models/audit.py +53 -0
- package/backend/app/models/base.py +35 -0
- package/backend/app/models/classification.py +144 -0
- package/backend/app/models/credential.py +56 -0
- package/backend/app/models/issue.py +132 -0
- package/backend/app/models/meeting.py +189 -0
- package/backend/app/models/pull_request.py +288 -0
- package/backend/app/models/repository.py +196 -0
- package/backend/app/models/sig.py +194 -0
- package/backend/app/models/user.py +44 -0
- package/backend/app/schemas/__init__.py +0 -0
- package/backend/app/schemas/ai.py +120 -0
- package/backend/app/schemas/attention.py +43 -0
- package/backend/app/schemas/auth.py +26 -0
- package/backend/app/schemas/classification.py +58 -0
- package/backend/app/schemas/common.py +43 -0
- package/backend/app/schemas/pull_request.py +214 -0
- package/backend/app/schemas/repository.py +94 -0
- package/backend/app/schemas/sig.py +246 -0
- package/backend/app/schemas/user.py +79 -0
- package/backend/app/services/__init__.py +0 -0
- package/backend/app/services/ai_service.py +569 -0
- package/backend/app/services/analytics_service.py +390 -0
- package/backend/app/services/attention_queue.py +451 -0
- package/backend/app/services/attention_service.py +432 -0
- package/backend/app/services/auth_service.py +74 -0
- package/backend/app/services/classification_service.py +658 -0
- package/backend/app/services/credential_service.py +105 -0
- package/backend/app/services/pull_query.py +273 -0
- package/backend/app/services/release_service.py +446 -0
- package/backend/app/services/repository_service.py +132 -0
- package/backend/app/services/sig_service.py +385 -0
- package/backend/app/services/sync_service.py +752 -0
- package/backend/app/services/user_service.py +68 -0
- package/backend/app/worker.py +389 -0
- package/backend/entrypoint.sh +10 -0
- package/backend/pyproject.toml +68 -0
- package/backend/tests/test_analysis_api.py +154 -0
- package/backend/tests/test_atomgit_client.py +360 -0
- package/backend/tests/test_atomgit_models.py +257 -0
- package/backend/tests/test_attention.py +291 -0
- package/backend/tests/test_audit_middleware.py +135 -0
- package/backend/tests/test_classification.py +498 -0
- package/backend/tests/test_config.py +41 -0
- package/backend/tests/test_crypto.py +68 -0
- package/backend/tests/test_exceptions.py +62 -0
- package/backend/tests/test_health.py +63 -0
- package/backend/tests/test_llm_provider.py +320 -0
- package/backend/tests/test_meeting_domain.py +169 -0
- package/backend/tests/test_permissions.py +69 -0
- package/backend/tests/test_pull_query_wiring.py +66 -0
- package/backend/tests/test_pull_schemas.py +82 -0
- package/backend/tests/test_release_domain.py +82 -0
- package/backend/tests/test_review_parser.py +301 -0
- package/backend/tests/test_schemas_user.py +97 -0
- package/backend/tests/test_security.py +92 -0
- package/cli/index.js +338 -0
- package/compose.yaml +105 -0
- package/frontend/.dockerignore +4 -0
- package/frontend/Dockerfile +27 -0
- package/frontend/index.html +14 -0
- package/frontend/nginx.conf +47 -0
- package/frontend/package-lock.json +5020 -0
- package/frontend/package.json +35 -0
- package/frontend/src/api/ai.ts +124 -0
- package/frontend/src/api/analytics.ts +66 -0
- package/frontend/src/api/attention.ts +181 -0
- package/frontend/src/api/auth.ts +74 -0
- package/frontend/src/api/classification.ts +170 -0
- package/frontend/src/api/issues.ts +90 -0
- package/frontend/src/api/pulls.ts +233 -0
- package/frontend/src/api/repositories.ts +95 -0
- package/frontend/src/api/sig.ts +232 -0
- package/frontend/src/app/antd-theme.ts +94 -0
- package/frontend/src/app/providers.tsx +59 -0
- package/frontend/src/app/router.tsx +411 -0
- package/frontend/src/app/search.ts +30 -0
- package/frontend/src/components/ClassificationBadge.tsx +58 -0
- package/frontend/src/components/GateBadge.tsx +13 -0
- package/frontend/src/components/SeverityBadge.tsx +20 -0
- package/frontend/src/components/layout/AppShell.tsx +16 -0
- package/frontend/src/components/layout/AuthLayout.tsx +32 -0
- package/frontend/src/components/layout/Sidebar.tsx +223 -0
- package/frontend/src/components/layout/TopBar.tsx +47 -0
- package/frontend/src/components/pulls/DiscussionTimeline.tsx +145 -0
- package/frontend/src/components/pulls/FacetRail.tsx +199 -0
- package/frontend/src/components/pulls/LabelChips.tsx +87 -0
- package/frontend/src/components/ui/alert.tsx +30 -0
- package/frontend/src/components/ui/badge.tsx +53 -0
- package/frontend/src/components/ui/button.tsx +51 -0
- package/frontend/src/components/ui/card.tsx +64 -0
- package/frontend/src/components/ui/chart-theme.ts +65 -0
- package/frontend/src/components/ui/data-table.tsx +39 -0
- package/frontend/src/components/ui/echart.tsx +70 -0
- package/frontend/src/components/ui/empty-state.tsx +22 -0
- package/frontend/src/components/ui/input.tsx +39 -0
- package/frontend/src/components/ui/lazy-chart.tsx +21 -0
- package/frontend/src/components/ui/skeleton.tsx +19 -0
- package/frontend/src/hooks/use-current-repository.ts +44 -0
- package/frontend/src/hooks/use-current-user.ts +38 -0
- package/frontend/src/lib/api-client.ts +93 -0
- package/frontend/src/lib/css-color.ts +60 -0
- package/frontend/src/lib/utils.ts +45 -0
- package/frontend/src/main.tsx +22 -0
- package/frontend/src/pages/attention/AttentionQueuePage.tsx +316 -0
- package/frontend/src/pages/attention/RuleSettingsPanel.tsx +177 -0
- package/frontend/src/pages/branches/BranchDetailPage.tsx +632 -0
- package/frontend/src/pages/branches/BranchListPage.tsx +308 -0
- package/frontend/src/pages/dashboard/DashboardPage.tsx +657 -0
- package/frontend/src/pages/issues/IssueListPage.tsx +284 -0
- package/frontend/src/pages/login/LoginPage.tsx +95 -0
- package/frontend/src/pages/meetings/MeetingDetailPage.tsx +403 -0
- package/frontend/src/pages/meetings/MeetingListPage.tsx +264 -0
- package/frontend/src/pages/members/MembersPage.tsx +534 -0
- package/frontend/src/pages/pulls/PullDetailPage.tsx +833 -0
- package/frontend/src/pages/pulls/PullListPage.tsx +399 -0
- package/frontend/src/pages/settings/AiSettingsPage.tsx +449 -0
- package/frontend/src/pages/settings/SettingsPage.tsx +321 -0
- package/frontend/src/pages/setup/SetupPage.tsx +144 -0
- package/frontend/src/styles/globals.css +147 -0
- package/frontend/tsconfig.json +22 -0
- package/frontend/vite.config.ts +57 -0
- package/package.json +48 -0
- package/scripts/e2e-auth-flow.py +131 -0
- package/scripts/e2e-pr-detail.py +128 -0
- package/scripts/e2e-verify.py +773 -0
- package/scripts/verify-ai-pipeline.py +616 -0
- package/scripts/verify-analysis-pipeline.py +303 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { Link } from '@tanstack/react-router'
|
|
3
|
+
import { Tooltip } from 'antd'
|
|
4
|
+
|
|
5
|
+
import { analyticsApi } from '@/api/analytics'
|
|
6
|
+
import { attentionQueueApi, SEVERITY_META, SEVERITY_ORDER } from '@/api/attention'
|
|
7
|
+
import { GATE_STAGE_META } from '@/api/pulls'
|
|
8
|
+
import { sigApi } from '@/api/sig'
|
|
9
|
+
import { useCurrentRepository } from '@/hooks/use-current-repository'
|
|
10
|
+
import { Badge, TONE_COLOR } from '@/components/ui/badge'
|
|
11
|
+
import { GateBadge } from '@/components/GateBadge'
|
|
12
|
+
import { Card, CardBody, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
13
|
+
import { EmptyState } from '@/components/ui/empty-state'
|
|
14
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
15
|
+
import { useCurrentUser } from '@/hooks/use-current-user'
|
|
16
|
+
import { cn, formatNumber } from '@/lib/utils'
|
|
17
|
+
|
|
18
|
+
export function DashboardPage() {
|
|
19
|
+
const { user } = useCurrentUser()
|
|
20
|
+
|
|
21
|
+
const { repository, isLoading: reposLoading } = useCurrentRepository()
|
|
22
|
+
|
|
23
|
+
const { data, isLoading } = useQuery({
|
|
24
|
+
queryKey: ['analytics', repository?.id],
|
|
25
|
+
queryFn: () => analyticsApi.summary(repository!.id),
|
|
26
|
+
enabled: Boolean(repository),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
if (reposLoading) return <Skeleton className="h-64 w-full" />
|
|
30
|
+
|
|
31
|
+
if (!repository) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="mx-auto max-w-3xl">
|
|
34
|
+
<EmptyState
|
|
35
|
+
title="尚未纳管仓库"
|
|
36
|
+
description="请先在「设置 → 仓库与同步」中配置 AtomGit 凭据并纳管仓库,随后此处会展示协作指标。"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (isLoading || !data) {
|
|
43
|
+
return (
|
|
44
|
+
<div className="space-y-4">
|
|
45
|
+
<Skeleton className="h-24 w-full" />
|
|
46
|
+
<Skeleton className="h-56 w-full" />
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { overview, waiting, intervals, quality } = data
|
|
52
|
+
const openPulls = overview.pulls.open || 1
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="space-y-5">
|
|
56
|
+
<header className="flex flex-wrap items-end justify-between gap-3">
|
|
57
|
+
<div>
|
|
58
|
+
<h1 className="text-base font-semibold tracking-tight">总览</h1>
|
|
59
|
+
<p className="mt-0.5 text-sm text-[var(--color-text-secondary)]">
|
|
60
|
+
{user ? `${user.display_name},` : ''}
|
|
61
|
+
{repository.display_name ?? `${repository.owner}/${repository.name}`} 的协作现状
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
<Link
|
|
65
|
+
to="/pulls"
|
|
66
|
+
search={{ state: 'open' }}
|
|
67
|
+
className="text-sm text-[var(--color-accent)] hover:underline"
|
|
68
|
+
>
|
|
69
|
+
查看全部 PR →
|
|
70
|
+
</Link>
|
|
71
|
+
</header>
|
|
72
|
+
|
|
73
|
+
{/* 最需要被看见的一组数字:可执行的待办 + 积压风险 */}
|
|
74
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
75
|
+
<MetricCard
|
|
76
|
+
label="可合入"
|
|
77
|
+
value={overview.pulls.ready_to_merge}
|
|
78
|
+
hint="CLA / CI / LGTM 均已就绪"
|
|
79
|
+
tone="success"
|
|
80
|
+
to="/pulls"
|
|
81
|
+
search={{ state: 'open', stage: ['ready_to_merge'] }}
|
|
82
|
+
/>
|
|
83
|
+
<MetricCard
|
|
84
|
+
label="阻塞中"
|
|
85
|
+
value={overview.pulls.blocked}
|
|
86
|
+
hint="CI 失败 / CLA 未通过 / 被否决"
|
|
87
|
+
tone="danger"
|
|
88
|
+
to="/pulls"
|
|
89
|
+
search={{ state: 'open', stage: ['ci_failed', 'cla_denied', 'rejected'] }}
|
|
90
|
+
/>
|
|
91
|
+
<MetricCard
|
|
92
|
+
label="待评审"
|
|
93
|
+
value={waiting.pending_review_count}
|
|
94
|
+
hint={`中位等待 ${waiting.median_waiting_days} 天`}
|
|
95
|
+
tone="warning"
|
|
96
|
+
to="/pulls"
|
|
97
|
+
search={{ state: 'open', stage: ['review_pending'] }}
|
|
98
|
+
/>
|
|
99
|
+
<MetricCard
|
|
100
|
+
label="超 30 天未评审"
|
|
101
|
+
value={waiting.over_30_days}
|
|
102
|
+
hint={`最长已等 ${Math.round(waiting.max_waiting_days)} 天`}
|
|
103
|
+
tone="danger"
|
|
104
|
+
to="/pulls"
|
|
105
|
+
// 与后端 waiting_time_stats 同口径:待评审且创建超过 30 天
|
|
106
|
+
search={{ state: 'open', stage: ['review_pending'], waiting_days_gte: 30 }}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
111
|
+
<div className="space-y-4">
|
|
112
|
+
<Card>
|
|
113
|
+
<CardHeader>
|
|
114
|
+
<CardTitle>门禁阶段分布</CardTitle>
|
|
115
|
+
<CardDescription>定位瓶颈环节:PR 卡在哪一步</CardDescription>
|
|
116
|
+
</CardHeader>
|
|
117
|
+
<CardContent className="space-y-2.5">
|
|
118
|
+
{data.stages.map((item) => (
|
|
119
|
+
<StageBar key={item.stage} stage={item.stage} count={item.count} total={openPulls} />
|
|
120
|
+
))}
|
|
121
|
+
</CardContent>
|
|
122
|
+
</Card>
|
|
123
|
+
|
|
124
|
+
<AttentionCard repositoryId={repository.id} />
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="space-y-4">
|
|
128
|
+
<Card>
|
|
129
|
+
<CardHeader>
|
|
130
|
+
<CardTitle>流转效率</CardTitle>
|
|
131
|
+
</CardHeader>
|
|
132
|
+
<CardContent className="space-y-3">
|
|
133
|
+
<StatRow label="已合并 PR" value={formatNumber(intervals.merged_count)} />
|
|
134
|
+
<StatRow
|
|
135
|
+
label="合并周期中位数"
|
|
136
|
+
value={`${intervals.median_merge_days} 天`}
|
|
137
|
+
hint="从创建到合入"
|
|
138
|
+
/>
|
|
139
|
+
<StatRow label="合并周期平均" value={`${intervals.avg_merge_days} 天`} />
|
|
140
|
+
<StatRow
|
|
141
|
+
label="最近 7 天合并"
|
|
142
|
+
value={formatNumber(overview.pulls.merged_last_7d)}
|
|
143
|
+
/>
|
|
144
|
+
<StatRow
|
|
145
|
+
label="最近 7 天新增"
|
|
146
|
+
value={formatNumber(overview.pulls.created_last_7d)}
|
|
147
|
+
/>
|
|
148
|
+
</CardContent>
|
|
149
|
+
</Card>
|
|
150
|
+
|
|
151
|
+
{/* 与「关注队列」划清界限:那边是**由规则判定出来的待办**,
|
|
152
|
+
这边是**仓库的当前存量**。原先这张卡叫「需要关注」并重复列出
|
|
153
|
+
合并冲突,与队列里的同一条规则撞名又撞数据,读的人自然要问
|
|
154
|
+
"这两个到底有什么不一样"。 */}
|
|
155
|
+
<Card>
|
|
156
|
+
<CardHeader>
|
|
157
|
+
<CardTitle>仓库概况</CardTitle>
|
|
158
|
+
<CardDescription>未关闭对象的存量与库内检查项</CardDescription>
|
|
159
|
+
</CardHeader>
|
|
160
|
+
<CardContent className="space-y-3">
|
|
161
|
+
<StatRow
|
|
162
|
+
label="未关闭 PR"
|
|
163
|
+
value={formatNumber(overview.pulls.open)}
|
|
164
|
+
hint="含全部门禁阶段"
|
|
165
|
+
/>
|
|
166
|
+
<StatRow label="未关闭 Issue" value={formatNumber(overview.issues.open)} />
|
|
167
|
+
<StatRow label="草稿 PR" value={formatNumber(quality.drafts)} />
|
|
168
|
+
<StatRow
|
|
169
|
+
label="二进制文件"
|
|
170
|
+
value={formatNumber(quality.binary_files)}
|
|
171
|
+
hint="内核仓库禁止提交"
|
|
172
|
+
tone={quality.binary_files > 0 ? 'danger' : undefined}
|
|
173
|
+
/>
|
|
174
|
+
</CardContent>
|
|
175
|
+
</Card>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<VersionAndMeeting />
|
|
180
|
+
|
|
181
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
182
|
+
<Card>
|
|
183
|
+
<CardHeader>
|
|
184
|
+
<CardTitle>评审人工作量</CardTitle>
|
|
185
|
+
<CardDescription>
|
|
186
|
+
取自评论中的 /lgtm 指令 —— openEuler 的评审人身份记录在评论里,不在标签里
|
|
187
|
+
</CardDescription>
|
|
188
|
+
</CardHeader>
|
|
189
|
+
<CardContent>
|
|
190
|
+
{data.reviewers.length === 0 ? (
|
|
191
|
+
<p className="text-sm text-[var(--color-text-muted)]">
|
|
192
|
+
暂无评审事件。需先同步 PR 详情以拉取评论。
|
|
193
|
+
</p>
|
|
194
|
+
) : (
|
|
195
|
+
<ul className="space-y-2">
|
|
196
|
+
{data.reviewers.slice(0, 8).map((reviewer) => (
|
|
197
|
+
<li key={reviewer.actor_login} className="flex items-center gap-3">
|
|
198
|
+
<span className="w-36 truncate text-sm">{reviewer.actor_login}</span>
|
|
199
|
+
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--color-surface-2)]">
|
|
200
|
+
<div
|
|
201
|
+
className="h-full rounded-full bg-[var(--color-accent)]"
|
|
202
|
+
style={{
|
|
203
|
+
width: `${Math.max(6, (reviewer.total / data.reviewers[0].total) * 100)}%`,
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
<span className="w-10 text-right font-mono text-xs tabular-nums text-[var(--color-text-secondary)]">
|
|
208
|
+
{reviewer.total}
|
|
209
|
+
</span>
|
|
210
|
+
</li>
|
|
211
|
+
))}
|
|
212
|
+
</ul>
|
|
213
|
+
)}
|
|
214
|
+
</CardContent>
|
|
215
|
+
</Card>
|
|
216
|
+
|
|
217
|
+
<div className="space-y-4">
|
|
218
|
+
<Card>
|
|
219
|
+
<CardHeader>
|
|
220
|
+
<CardTitle>目标分支分布</CardTitle>
|
|
221
|
+
<CardDescription>未关闭 PR 按目标分支</CardDescription>
|
|
222
|
+
</CardHeader>
|
|
223
|
+
<CardContent className="space-y-2">
|
|
224
|
+
{data.branches.slice(0, 6).map((item) => (
|
|
225
|
+
<div key={item.branch} className="flex items-center gap-3 text-sm">
|
|
226
|
+
<code className="w-44 truncate text-xs text-[var(--color-text-secondary)]">
|
|
227
|
+
{item.branch}
|
|
228
|
+
</code>
|
|
229
|
+
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--color-surface-2)]">
|
|
230
|
+
<div
|
|
231
|
+
className="h-full rounded-full bg-[var(--color-info)]"
|
|
232
|
+
style={{
|
|
233
|
+
width: `${Math.max(4, (item.count / data.branches[0].count) * 100)}%`,
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
<span className="w-10 text-right font-mono text-xs tabular-nums text-[var(--color-text-secondary)]">
|
|
238
|
+
{item.count}
|
|
239
|
+
</span>
|
|
240
|
+
</div>
|
|
241
|
+
))}
|
|
242
|
+
</CardContent>
|
|
243
|
+
</Card>
|
|
244
|
+
|
|
245
|
+
<Card>
|
|
246
|
+
<CardHeader>
|
|
247
|
+
<CardTitle>活跃作者</CardTitle>
|
|
248
|
+
<CardDescription>已排除自动化账号</CardDescription>
|
|
249
|
+
</CardHeader>
|
|
250
|
+
<CardContent className="space-y-2">
|
|
251
|
+
{data.authors.slice(0, 6).map((item) => (
|
|
252
|
+
<div key={item.author} className="flex items-center justify-between gap-3 text-sm">
|
|
253
|
+
<span
|
|
254
|
+
className={cn(
|
|
255
|
+
'truncate',
|
|
256
|
+
item.is_bot && 'text-[var(--color-text-muted)]',
|
|
257
|
+
)}
|
|
258
|
+
>
|
|
259
|
+
{item.author}
|
|
260
|
+
</span>
|
|
261
|
+
<span className="font-mono text-xs tabular-nums text-[var(--color-text-secondary)]">
|
|
262
|
+
{item.count}
|
|
263
|
+
</span>
|
|
264
|
+
</div>
|
|
265
|
+
))}
|
|
266
|
+
</CardContent>
|
|
267
|
+
</Card>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 关注队列摘要。
|
|
276
|
+
*
|
|
277
|
+
* 仪表盘上最该被看见的不是总量,而是"有多少组必须动手"。
|
|
278
|
+
* 用的口径与队列页同源(/attention/summary)—— 各自算一遍的话,
|
|
279
|
+
* 关掉一条规则后两个页面会显示两个不同的数字,而且从界面上看不出谁对。
|
|
280
|
+
*/
|
|
281
|
+
function AttentionCard({ repositoryId }: { repositoryId: string }) {
|
|
282
|
+
const { data } = useQuery({
|
|
283
|
+
queryKey: ['attention-summary', repositoryId],
|
|
284
|
+
queryFn: () => attentionQueueApi.summary(repositoryId),
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
if (!data || data.total === 0) {
|
|
288
|
+
return (
|
|
289
|
+
<Card>
|
|
290
|
+
<CardHeader>
|
|
291
|
+
<CardTitle>关注队列</CardTitle>
|
|
292
|
+
<CardDescription>由规则自动检出的待办</CardDescription>
|
|
293
|
+
</CardHeader>
|
|
294
|
+
<CardContent>
|
|
295
|
+
<p className="text-sm text-[var(--color-text-muted)]">
|
|
296
|
+
当前没有待办。关注项由定时任务重算,仓库刚纳管时请先完成一次同步。
|
|
297
|
+
</p>
|
|
298
|
+
</CardContent>
|
|
299
|
+
</Card>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<Card>
|
|
305
|
+
<CardHeader>
|
|
306
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
307
|
+
<div>
|
|
308
|
+
<CardTitle>关注队列</CardTitle>
|
|
309
|
+
<CardDescription>{formatNumber(data.total)} 个对象收敛为 {data.group_count} 组</CardDescription>
|
|
310
|
+
</div>
|
|
311
|
+
<Link
|
|
312
|
+
to="/attention"
|
|
313
|
+
className="shrink-0 text-xs text-[var(--color-accent)] hover:underline"
|
|
314
|
+
>
|
|
315
|
+
逐组处理 →
|
|
316
|
+
</Link>
|
|
317
|
+
</div>
|
|
318
|
+
</CardHeader>
|
|
319
|
+
<CardContent className="space-y-2.5">
|
|
320
|
+
<div className="flex flex-wrap gap-1.5">
|
|
321
|
+
{SEVERITY_ORDER.map((severity) => {
|
|
322
|
+
const count = data.by_severity[severity] ?? 0
|
|
323
|
+
if (count === 0) return null
|
|
324
|
+
return (
|
|
325
|
+
<span
|
|
326
|
+
key={severity}
|
|
327
|
+
className="inline-flex items-center gap-1.5 rounded-full border border-[var(--color-border-subtle)] px-2 py-0.5 text-2xs"
|
|
328
|
+
>
|
|
329
|
+
{SEVERITY_META[severity].label}
|
|
330
|
+
<span className="font-mono tabular-nums">{formatNumber(count)}</span>
|
|
331
|
+
</span>
|
|
332
|
+
)
|
|
333
|
+
})}
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<ul className="space-y-1">
|
|
337
|
+
{data.top.map((group) => (
|
|
338
|
+
<li key={group.rule}>
|
|
339
|
+
<Link
|
|
340
|
+
to="/attention"
|
|
341
|
+
className="-mx-1.5 flex items-center gap-3 rounded-[var(--radius-control)] px-1.5 py-1 text-sm hover:bg-[var(--color-surface-2)]"
|
|
342
|
+
>
|
|
343
|
+
<Badge tone={(SEVERITY_META[group.severity]?.tone ?? 'neutral') as never}>
|
|
344
|
+
{SEVERITY_META[group.severity]?.label}
|
|
345
|
+
</Badge>
|
|
346
|
+
<span className="min-w-0 flex-1 truncate">{group.label}</span>
|
|
347
|
+
{group.oldest_days !== null && (
|
|
348
|
+
<span className="shrink-0 font-mono text-2xs tabular-nums text-[var(--color-text-muted)]">
|
|
349
|
+
最久 {group.oldest_days} 天
|
|
350
|
+
</span>
|
|
351
|
+
)}
|
|
352
|
+
<span className="shrink-0 font-mono text-sm tabular-nums">
|
|
353
|
+
{formatNumber(group.count)}
|
|
354
|
+
</span>
|
|
355
|
+
</Link>
|
|
356
|
+
</li>
|
|
357
|
+
))}
|
|
358
|
+
</ul>
|
|
359
|
+
</CardContent>
|
|
360
|
+
</Card>
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function MetricCard({
|
|
365
|
+
label,
|
|
366
|
+
value,
|
|
367
|
+
hint,
|
|
368
|
+
tone,
|
|
369
|
+
to,
|
|
370
|
+
search,
|
|
371
|
+
}: {
|
|
372
|
+
label: string
|
|
373
|
+
value: number
|
|
374
|
+
hint?: string
|
|
375
|
+
tone?: 'success' | 'warning' | 'danger'
|
|
376
|
+
to?: '/pulls' | '/issues'
|
|
377
|
+
/** 目标是列表页时带上筛选条件:点进去看到的就该是这张卡片数的那批。 */
|
|
378
|
+
search?: { state?: string; stage?: string[]; kind?: string[]; waiting_days_gte?: number }
|
|
379
|
+
}) {
|
|
380
|
+
const toneClass = {
|
|
381
|
+
success: 'text-[var(--color-success)]',
|
|
382
|
+
warning: 'text-[var(--color-warning)]',
|
|
383
|
+
danger: 'text-[var(--color-danger)]',
|
|
384
|
+
}[tone ?? 'success']
|
|
385
|
+
|
|
386
|
+
const content = (
|
|
387
|
+
<Card className="h-full transition-[border-color,box-shadow] duration-150 hover:border-[var(--color-border-strong)] hover:shadow-[var(--shadow-card-hover)]">
|
|
388
|
+
<CardBody>
|
|
389
|
+
<div className="text-xs text-[var(--color-text-muted)]">{label}</div>
|
|
390
|
+
<div className={cn('mt-1 font-mono text-2xl tabular-nums', toneClass)}>
|
|
391
|
+
{formatNumber(value)}
|
|
392
|
+
</div>
|
|
393
|
+
{hint && (
|
|
394
|
+
<div className="mt-1 text-2xs text-[var(--color-text-muted)]">{hint}</div>
|
|
395
|
+
)}
|
|
396
|
+
</CardBody>
|
|
397
|
+
</Card>
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if (!to) return content
|
|
401
|
+
if (to === '/pulls') {
|
|
402
|
+
return (
|
|
403
|
+
<Link to="/pulls" search={search ?? {}} className="block">
|
|
404
|
+
{content}
|
|
405
|
+
</Link>
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
return (
|
|
409
|
+
<Link to="/issues" className="block">
|
|
410
|
+
{content}
|
|
411
|
+
</Link>
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 门禁阶段一行。
|
|
417
|
+
*
|
|
418
|
+
* 整行是链接:看到"CI 失败 83"却点不进去,就得自己去列表页重新选一遍筛选 ——
|
|
419
|
+
* 那正是这个平台要消掉的那一步。
|
|
420
|
+
*
|
|
421
|
+
* 条形用该阶段的语义色而不是统一的灰色:这张卡存在的意义是"一眼看出卡在哪",
|
|
422
|
+
* 十行灰条要看数字才能分辨,等于把图表退化成了表格。
|
|
423
|
+
*/
|
|
424
|
+
function StageBar({
|
|
425
|
+
stage,
|
|
426
|
+
count,
|
|
427
|
+
total,
|
|
428
|
+
}: {
|
|
429
|
+
stage: keyof typeof GATE_STAGE_META
|
|
430
|
+
count: number
|
|
431
|
+
total: number
|
|
432
|
+
}) {
|
|
433
|
+
const meta = GATE_STAGE_META[stage]
|
|
434
|
+
const percent = Math.max(1.5, (count / total) * 100)
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<Link
|
|
438
|
+
to="/pulls"
|
|
439
|
+
search={{ state: 'open', stage: [stage] }}
|
|
440
|
+
title={`查看处于「${meta.label}」的 PR`}
|
|
441
|
+
className="-mx-1.5 flex items-center gap-3 rounded-[var(--radius-control)] px-1.5 py-1 text-sm transition-colors duration-150 hover:bg-[var(--color-surface-2)]"
|
|
442
|
+
>
|
|
443
|
+
<span className="w-24 shrink-0">
|
|
444
|
+
<GateBadge stage={stage} />
|
|
445
|
+
</span>
|
|
446
|
+
<div className="h-2 flex-1 overflow-hidden rounded-full bg-[var(--color-surface-2)]">
|
|
447
|
+
<div
|
|
448
|
+
className="h-full rounded-full"
|
|
449
|
+
style={{ width: `${percent}%`, backgroundColor: TONE_COLOR[meta.tone] }}
|
|
450
|
+
/>
|
|
451
|
+
</div>
|
|
452
|
+
<span className="w-12 text-right font-mono text-xs tabular-nums text-[var(--color-text-secondary)]">
|
|
453
|
+
{count}
|
|
454
|
+
</span>
|
|
455
|
+
<span className="w-10 text-right font-mono text-2xs tabular-nums text-[var(--color-text-muted)]">
|
|
456
|
+
{Math.round((count / total) * 100)}%
|
|
457
|
+
</span>
|
|
458
|
+
</Link>
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function StatRow({
|
|
463
|
+
label,
|
|
464
|
+
value,
|
|
465
|
+
hint,
|
|
466
|
+
tone,
|
|
467
|
+
}: {
|
|
468
|
+
label: string
|
|
469
|
+
value: string
|
|
470
|
+
hint?: string
|
|
471
|
+
tone?: 'warning' | 'danger'
|
|
472
|
+
}) {
|
|
473
|
+
return (
|
|
474
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
475
|
+
<div>
|
|
476
|
+
<div className="text-sm text-[var(--color-text-secondary)]">{label}</div>
|
|
477
|
+
{hint && <div className="text-2xs text-[var(--color-text-muted)]">{hint}</div>}
|
|
478
|
+
</div>
|
|
479
|
+
<div
|
|
480
|
+
className={cn(
|
|
481
|
+
'font-mono text-base tabular-nums',
|
|
482
|
+
tone === 'danger' && 'text-[var(--color-danger)]',
|
|
483
|
+
tone === 'warning' && 'text-[var(--color-warning)]',
|
|
484
|
+
)}
|
|
485
|
+
>
|
|
486
|
+
{value}
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* 版本动态与下次例会。
|
|
494
|
+
*
|
|
495
|
+
* 放在仪表盘上而不是只留在各自的页面里:维护者每天打开的是总览,
|
|
496
|
+
* 而"哪个版本刚合入过、下一次例会在什么时候、该谁主持"属于开屏就该看到的
|
|
497
|
+
* 东西 —— 要再点两次才能看到的信息,实际使用中不会被看到。
|
|
498
|
+
*/
|
|
499
|
+
function VersionAndMeeting() {
|
|
500
|
+
const { repository } = useCurrentRepository()
|
|
501
|
+
|
|
502
|
+
const { data: branches } = useQuery({
|
|
503
|
+
queryKey: ['branches', repository?.id],
|
|
504
|
+
queryFn: () => sigApi.branches(repository!.id),
|
|
505
|
+
enabled: Boolean(repository),
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
const { data: meetings } = useQuery({
|
|
509
|
+
queryKey: ['meetings'],
|
|
510
|
+
queryFn: () => sigApi.meetings(undefined, 10),
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
const upcoming = meetings?.find((item) => item.status === 'scheduled')
|
|
514
|
+
const recent = meetings?.find((item) => item.status === 'done')
|
|
515
|
+
const active = (branches ?? []).filter((item) => item.kind === 'olk' || item.kind === 'lts')
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<div className="grid gap-4 lg:grid-cols-[2fr_1fr]">
|
|
519
|
+
<Card>
|
|
520
|
+
<CardHeader>
|
|
521
|
+
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
522
|
+
<CardTitle>版本动态</CardTitle>
|
|
523
|
+
<Link
|
|
524
|
+
to="/branches"
|
|
525
|
+
className="text-xs text-[var(--color-accent)] hover:underline"
|
|
526
|
+
>
|
|
527
|
+
全部版本与分支 →
|
|
528
|
+
</Link>
|
|
529
|
+
</div>
|
|
530
|
+
<CardDescription>各版本当前 tag、合入量与近 30 天进度</CardDescription>
|
|
531
|
+
</CardHeader>
|
|
532
|
+
<CardContent className="space-y-2.5">
|
|
533
|
+
{active.length === 0 ? (
|
|
534
|
+
<p className="text-sm text-[var(--color-text-muted)]">暂无分支数据。</p>
|
|
535
|
+
) : (
|
|
536
|
+
active.slice(0, 6).map((item) => (
|
|
537
|
+
<div key={item.name} className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
|
|
538
|
+
<Tooltip title={item.name}>
|
|
539
|
+
<Link
|
|
540
|
+
to="/branches/$branch"
|
|
541
|
+
params={{ branch: item.name }}
|
|
542
|
+
className="w-52 shrink-0 truncate font-mono text-xs hover:text-[var(--color-accent)]"
|
|
543
|
+
>
|
|
544
|
+
{item.name}
|
|
545
|
+
</Link>
|
|
546
|
+
</Tooltip>
|
|
547
|
+
<span className="w-32 shrink-0 font-mono text-2xs text-[var(--color-text-muted)]">
|
|
548
|
+
{item.latest_tag ?? '—'}
|
|
549
|
+
</span>
|
|
550
|
+
<Link
|
|
551
|
+
to="/pulls"
|
|
552
|
+
search={{ branch: [item.name] }}
|
|
553
|
+
className="shrink-0 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-accent)]"
|
|
554
|
+
title="查看该分支的待处理 PR"
|
|
555
|
+
>
|
|
556
|
+
待处理 {item.open_count}
|
|
557
|
+
</Link>
|
|
558
|
+
<span className="shrink-0 text-xs text-[var(--color-text-secondary)]">
|
|
559
|
+
已合入 {formatNumber(item.merged_count)}
|
|
560
|
+
</span>
|
|
561
|
+
<span className="ml-auto shrink-0 font-mono text-xs tabular-nums text-[var(--color-success)]">
|
|
562
|
+
近 30 天 +{item.merged_recent}
|
|
563
|
+
</span>
|
|
564
|
+
</div>
|
|
565
|
+
))
|
|
566
|
+
)}
|
|
567
|
+
</CardContent>
|
|
568
|
+
</Card>
|
|
569
|
+
|
|
570
|
+
<Card>
|
|
571
|
+
<CardHeader>
|
|
572
|
+
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
573
|
+
<CardTitle>SIG 例会</CardTitle>
|
|
574
|
+
<Link
|
|
575
|
+
to="/meetings"
|
|
576
|
+
className="text-xs text-[var(--color-accent)] hover:underline"
|
|
577
|
+
>
|
|
578
|
+
全部例会 →
|
|
579
|
+
</Link>
|
|
580
|
+
</div>
|
|
581
|
+
<CardDescription>双周例会,数据来自 Etherpad 纪要</CardDescription>
|
|
582
|
+
</CardHeader>
|
|
583
|
+
<CardContent className="space-y-3">
|
|
584
|
+
{upcoming ? (
|
|
585
|
+
<div className="rounded-[var(--radius-control)] border border-[var(--color-border-subtle)] p-3">
|
|
586
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
587
|
+
<Badge tone="info">下次</Badge>
|
|
588
|
+
<span className="font-mono text-sm tabular-nums">{upcoming.held_on}</span>
|
|
589
|
+
{upcoming.start_time && (
|
|
590
|
+
<span className="text-xs text-[var(--color-text-muted)]">
|
|
591
|
+
{upcoming.start_time}
|
|
592
|
+
</span>
|
|
593
|
+
)}
|
|
594
|
+
</div>
|
|
595
|
+
<p className="mt-1 text-xs text-[var(--color-text-secondary)]">
|
|
596
|
+
主持 {upcoming.host ?? '—'}
|
|
597
|
+
{upcoming.next_host && ` · 下次轮值 ${upcoming.next_host}`}
|
|
598
|
+
</p>
|
|
599
|
+
<p className="mt-0.5 text-xs text-[var(--color-text-muted)]">
|
|
600
|
+
已列入 {upcoming.agenda_count} 个议题
|
|
601
|
+
</p>
|
|
602
|
+
<div className="mt-2 flex flex-wrap gap-3">
|
|
603
|
+
<Link
|
|
604
|
+
to="/meetings/$meetingId"
|
|
605
|
+
params={{ meetingId: upcoming.id }}
|
|
606
|
+
className="text-xs text-[var(--color-accent)] hover:underline"
|
|
607
|
+
>
|
|
608
|
+
议题详情 →
|
|
609
|
+
</Link>
|
|
610
|
+
{upcoming.meeting_url && (
|
|
611
|
+
<a
|
|
612
|
+
href={upcoming.meeting_url}
|
|
613
|
+
target="_blank"
|
|
614
|
+
rel="noreferrer noopener"
|
|
615
|
+
className="text-xs text-[var(--color-accent)] hover:underline"
|
|
616
|
+
>
|
|
617
|
+
会议链接 ↗
|
|
618
|
+
</a>
|
|
619
|
+
)}
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
) : (
|
|
623
|
+
<p className="text-sm text-[var(--color-text-muted)]">暂无待召开的例会。</p>
|
|
624
|
+
)}
|
|
625
|
+
|
|
626
|
+
{recent && (
|
|
627
|
+
<div>
|
|
628
|
+
<p className="mb-1 text-xs text-[var(--color-text-muted)]">
|
|
629
|
+
上次例会 · {recent.held_on}
|
|
630
|
+
</p>
|
|
631
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
|
632
|
+
<span className="text-[var(--color-text-secondary)]">
|
|
633
|
+
主持 {recent.host ?? '—'}
|
|
634
|
+
</span>
|
|
635
|
+
<span className="text-[var(--color-text-secondary)]">
|
|
636
|
+
议题 {recent.agenda_count}
|
|
637
|
+
</span>
|
|
638
|
+
{recent.report_count > 0 && (
|
|
639
|
+
<span className="text-[var(--color-text-secondary)]">
|
|
640
|
+
版本报告 {recent.report_count}
|
|
641
|
+
</span>
|
|
642
|
+
)}
|
|
643
|
+
<Link
|
|
644
|
+
to="/meetings/$meetingId"
|
|
645
|
+
params={{ meetingId: recent.id }}
|
|
646
|
+
className="text-[var(--color-accent)] hover:underline"
|
|
647
|
+
>
|
|
648
|
+
查看 →
|
|
649
|
+
</Link>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
)}
|
|
653
|
+
</CardContent>
|
|
654
|
+
</Card>
|
|
655
|
+
</div>
|
|
656
|
+
)
|
|
657
|
+
}
|