@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,632 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { Link } from '@tanstack/react-router'
|
|
3
|
+
import { Segmented } from 'antd'
|
|
4
|
+
import { ArrowLeft, ExternalLink } from 'lucide-react'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
|
|
7
|
+
import { KIND_META, type PRKind } from '@/api/classification'
|
|
8
|
+
import { useCurrentRepository } from '@/hooks/use-current-repository'
|
|
9
|
+
import {
|
|
10
|
+
BRANCH_KIND_META,
|
|
11
|
+
sigApi,
|
|
12
|
+
type PeriodSummary,
|
|
13
|
+
type ReleasePatch,
|
|
14
|
+
type ReleaseReport,
|
|
15
|
+
} from '@/api/sig'
|
|
16
|
+
import { Badge } from '@/components/ui/badge'
|
|
17
|
+
import { Card, CardBody, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
18
|
+
import { CHART_THEME, chartColor } from '@/components/ui/chart-theme'
|
|
19
|
+
import { LazyChart } from '@/components/ui/lazy-chart'
|
|
20
|
+
import type { EChartsOption } from '@/components/ui/echart'
|
|
21
|
+
import { EmptyState } from '@/components/ui/empty-state'
|
|
22
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
23
|
+
import { cn, formatNumber, formatRelativeTime } from '@/lib/utils'
|
|
24
|
+
|
|
25
|
+
type Granularity = 'month' | 'biweek'
|
|
26
|
+
|
|
27
|
+
const GRANULARITY_LABEL: Record<Granularity, string> = { month: '按月', biweek: '按双周' }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 单个版本的页面。
|
|
31
|
+
*
|
|
32
|
+
* 顺序按维护者提问的频率排,而不是按数据表的结构排:这个版本现在什么状态、
|
|
33
|
+
* 谁负责、最近几期在合什么、上一次发版说了什么。
|
|
34
|
+
*
|
|
35
|
+
* 分期总结给出**连续多期**而不是只给上一期:单期只能回答"上个月合了多少",
|
|
36
|
+
* 看不出"这个版本是在收敛还是在堆积"—— 后者才是要判断的事。
|
|
37
|
+
*/
|
|
38
|
+
export function BranchDetailPage({ branch }: { branch: string }) {
|
|
39
|
+
// 默认双周:例会就是双周节奏,维护者看的是"这一期例会要报什么"
|
|
40
|
+
const [granularity, setGranularity] = useState<Granularity>('biweek')
|
|
41
|
+
const [selected, setSelected] = useState(0)
|
|
42
|
+
|
|
43
|
+
const { repository, isLoading: reposLoading } = useCurrentRepository()
|
|
44
|
+
|
|
45
|
+
const { data: branches } = useQuery({
|
|
46
|
+
queryKey: ['branches', repository?.id],
|
|
47
|
+
queryFn: () => sigApi.branches(repository!.id),
|
|
48
|
+
enabled: Boolean(repository),
|
|
49
|
+
})
|
|
50
|
+
const meta = branches?.find((item) => item.name === branch)
|
|
51
|
+
|
|
52
|
+
const { data: timeline } = useQuery({
|
|
53
|
+
queryKey: ['branch-timeline', repository?.id, branch, granularity],
|
|
54
|
+
queryFn: () => sigApi.branchTimeline(repository!.id, branch, granularity, 12),
|
|
55
|
+
enabled: Boolean(repository),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const { data: periods } = useQuery({
|
|
59
|
+
queryKey: ['branch-periods', repository?.id, branch, granularity],
|
|
60
|
+
queryFn: () => sigApi.branchPeriods(repository!.id, branch, granularity, 8),
|
|
61
|
+
enabled: Boolean(repository),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const { data: notes } = useQuery({
|
|
65
|
+
queryKey: ['release-notes', repository?.id, branch],
|
|
66
|
+
queryFn: () => sigApi.releaseNotes(repository!.id, branch, 12),
|
|
67
|
+
enabled: Boolean(repository),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (reposLoading) return <Skeleton className="h-64 w-full" />
|
|
71
|
+
if (!repository) return <EmptyState title="尚未纳管仓库" />
|
|
72
|
+
|
|
73
|
+
const period = periods?.[Math.min(selected, (periods?.length ?? 1) - 1)]
|
|
74
|
+
const recentTotal = (periods ?? []).reduce((sum, item) => sum + item.total, 0)
|
|
75
|
+
const recentCves = (periods ?? []).reduce((sum, item) => sum + item.cve_count, 0)
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="space-y-5">
|
|
79
|
+
<Link
|
|
80
|
+
to="/branches"
|
|
81
|
+
className="inline-flex items-center gap-1.5 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]"
|
|
82
|
+
>
|
|
83
|
+
<ArrowLeft className="size-3.5" aria-hidden />
|
|
84
|
+
返回版本与分支
|
|
85
|
+
</Link>
|
|
86
|
+
|
|
87
|
+
<header className="space-y-2.5">
|
|
88
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
89
|
+
<h1 className="font-mono text-base font-semibold tracking-tight">{branch}</h1>
|
|
90
|
+
{meta && (
|
|
91
|
+
<Badge tone="neutral" className="font-normal">
|
|
92
|
+
{BRANCH_KIND_META[meta.kind]?.label ?? meta.kind}
|
|
93
|
+
</Badge>
|
|
94
|
+
)}
|
|
95
|
+
{meta?.upstream_base && (
|
|
96
|
+
<span className="text-xs text-[var(--color-text-muted)]">
|
|
97
|
+
上游基线 {meta.upstream_base}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
{meta?.latest_tag && (
|
|
101
|
+
<span className="font-mono text-xs text-[var(--color-text-secondary)]">
|
|
102
|
+
当前 tag {meta.latest_tag}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
{meta?.last_merged_at && (
|
|
106
|
+
<span className="text-xs text-[var(--color-text-muted)]">
|
|
107
|
+
最近合入 {formatRelativeTime(meta.last_merged_at)}
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{meta?.note && (
|
|
113
|
+
<p className="max-w-3xl text-sm leading-relaxed text-[var(--color-text-secondary)]">
|
|
114
|
+
{meta.note}
|
|
115
|
+
</p>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{(meta?.keepers.length ?? 0) > 0 && (
|
|
119
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
120
|
+
<span className="text-xs text-[var(--color-text-muted)]">负责人</span>
|
|
121
|
+
{meta!.keepers.map((login) => (
|
|
122
|
+
<Link
|
|
123
|
+
key={login}
|
|
124
|
+
to="/members"
|
|
125
|
+
search={{ member: login }}
|
|
126
|
+
title={`在成员页查看 @${login}`}
|
|
127
|
+
className="rounded-full border border-[var(--color-border-subtle)] px-2 py-0.5 font-mono text-xs hover:border-[var(--color-border-strong)] hover:text-[var(--color-accent)]"
|
|
128
|
+
>
|
|
129
|
+
@{login}
|
|
130
|
+
</Link>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{/* 列出归并的上游写法:同名分支被合并统计时,得让人能核对合的是哪些 */}
|
|
136
|
+
{meta && meta.raw_names.length > 1 && (
|
|
137
|
+
<p className="text-xs text-[var(--color-text-muted)]">
|
|
138
|
+
上游写法:{meta.raw_names.join('、')}
|
|
139
|
+
</p>
|
|
140
|
+
)}
|
|
141
|
+
</header>
|
|
142
|
+
|
|
143
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
144
|
+
<Metric
|
|
145
|
+
label="待处理"
|
|
146
|
+
value={meta?.open_count ?? 0}
|
|
147
|
+
to={{ search: { state: 'open', branch: [branch] } }}
|
|
148
|
+
/>
|
|
149
|
+
<Metric label="历史合入" value={meta?.merged_count ?? 0} />
|
|
150
|
+
<Metric label="近 30 天合入" value={meta?.merged_recent ?? 0} />
|
|
151
|
+
<Metric
|
|
152
|
+
label={`近 ${periods?.length ?? 0} 期合入`}
|
|
153
|
+
value={recentTotal}
|
|
154
|
+
hint={periods && periods.length > 0 ? `其中 CVE ${formatNumber(recentCves)} 个` : undefined}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<Card>
|
|
159
|
+
<CardHeader>
|
|
160
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
161
|
+
<CardTitle>合入趋势</CardTitle>
|
|
162
|
+
<GranularityToggle value={granularity} onChange={setGranularity} />
|
|
163
|
+
</div>
|
|
164
|
+
</CardHeader>
|
|
165
|
+
<CardContent>
|
|
166
|
+
{!timeline || timeline.length === 0 ? (
|
|
167
|
+
<p className="text-sm text-[var(--color-text-muted)]">该分支暂无合入记录。</p>
|
|
168
|
+
) : (
|
|
169
|
+
<LazyChart height={180} option={timelineOption(timeline, granularity)} />
|
|
170
|
+
)}
|
|
171
|
+
</CardContent>
|
|
172
|
+
</Card>
|
|
173
|
+
|
|
174
|
+
<div className="grid gap-4 lg:grid-cols-[3fr_2fr]">
|
|
175
|
+
<Card>
|
|
176
|
+
<CardHeader>
|
|
177
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
178
|
+
<CardTitle>分期合入总结</CardTitle>
|
|
179
|
+
<GranularityToggle value={granularity} onChange={setGranularity} />
|
|
180
|
+
</div>
|
|
181
|
+
</CardHeader>
|
|
182
|
+
<CardContent className="px-0 pb-2">
|
|
183
|
+
{!periods || periods.length === 0 ? (
|
|
184
|
+
<p className="px-4 text-sm text-[var(--color-text-muted)]">
|
|
185
|
+
该分支暂无已结束的统计周期。
|
|
186
|
+
</p>
|
|
187
|
+
) : (
|
|
188
|
+
<ul className="divide-y divide-[var(--color-border-subtle)]">
|
|
189
|
+
{periods.map((item, index) => (
|
|
190
|
+
<PeriodRow
|
|
191
|
+
key={item.label}
|
|
192
|
+
period={item}
|
|
193
|
+
active={index === selected}
|
|
194
|
+
dense={granularity === 'biweek'}
|
|
195
|
+
onSelect={() => setSelected(index)}
|
|
196
|
+
/>
|
|
197
|
+
))}
|
|
198
|
+
</ul>
|
|
199
|
+
)}
|
|
200
|
+
</CardContent>
|
|
201
|
+
</Card>
|
|
202
|
+
|
|
203
|
+
<Card>
|
|
204
|
+
<CardHeader>
|
|
205
|
+
<CardTitle className="text-sm">
|
|
206
|
+
{period ? `${period.label} 合入构成` : '合入构成'}
|
|
207
|
+
</CardTitle>
|
|
208
|
+
</CardHeader>
|
|
209
|
+
<CardContent className="space-y-4">
|
|
210
|
+
{!period || period.total === 0 ? (
|
|
211
|
+
<p className="text-sm text-[var(--color-text-muted)]">该期没有合入。</p>
|
|
212
|
+
) : (
|
|
213
|
+
<>
|
|
214
|
+
<LazyChart height={Math.max(160, kindRows(period).length * 26)} option={kindOption(period.by_kind)} />
|
|
215
|
+
{period.top_contributors.length > 0 && (
|
|
216
|
+
<div className="border-t border-[var(--color-border-subtle)] pt-3">
|
|
217
|
+
<p className="mb-2 text-xs text-[var(--color-text-muted)]">主要贡献者</p>
|
|
218
|
+
<div className="flex flex-wrap gap-1.5">
|
|
219
|
+
{period.top_contributors.slice(0, 8).map(([login, count]) => (
|
|
220
|
+
<Link
|
|
221
|
+
key={login}
|
|
222
|
+
to="/members"
|
|
223
|
+
search={{ member: login }}
|
|
224
|
+
className="inline-flex items-center gap-1 rounded-full border border-[var(--color-border-subtle)] px-2 py-0.5 text-2xs hover:border-[var(--color-border-strong)] hover:text-[var(--color-accent)]"
|
|
225
|
+
>
|
|
226
|
+
<span className="font-mono">{login}</span>
|
|
227
|
+
<span className="tabular-nums text-[var(--color-text-muted)]">{count}</span>
|
|
228
|
+
</Link>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</>
|
|
234
|
+
)}
|
|
235
|
+
</CardContent>
|
|
236
|
+
</Card>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<section className="space-y-3">
|
|
240
|
+
<h2 className="text-sm font-semibold">发版说明</h2>
|
|
241
|
+
{!notes || notes.length === 0 ? (
|
|
242
|
+
<EmptyState
|
|
243
|
+
title="暂无发版说明"
|
|
244
|
+
description="发版说明来自 SIG 例会纪要中的版本进展报告。若该分支近期未在例会上汇报,这里会是空的。"
|
|
245
|
+
/>
|
|
246
|
+
) : (
|
|
247
|
+
<div className="space-y-3">
|
|
248
|
+
{notes.map((note) => (
|
|
249
|
+
<ReleaseNoteCard key={note.id} note={note} />
|
|
250
|
+
))}
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
</section>
|
|
254
|
+
</div>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 粒度切换。两个互斥选项、一次选一个,正是分段控件的语义。 */
|
|
259
|
+
function GranularityToggle({
|
|
260
|
+
value,
|
|
261
|
+
onChange,
|
|
262
|
+
}: {
|
|
263
|
+
value: Granularity
|
|
264
|
+
onChange: (next: Granularity) => void
|
|
265
|
+
}) {
|
|
266
|
+
return (
|
|
267
|
+
<Segmented
|
|
268
|
+
size="small"
|
|
269
|
+
value={value}
|
|
270
|
+
onChange={(next) => onChange(next as Granularity)}
|
|
271
|
+
options={[
|
|
272
|
+
{ value: 'biweek', label: GRANULARITY_LABEL.biweek },
|
|
273
|
+
{ value: 'month', label: GRANULARITY_LABEL.month },
|
|
274
|
+
]}
|
|
275
|
+
/>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 一行一期。整行可点:点开右侧就换成这一期的构成。 */
|
|
280
|
+
function PeriodRow({
|
|
281
|
+
period,
|
|
282
|
+
active,
|
|
283
|
+
dense,
|
|
284
|
+
onSelect,
|
|
285
|
+
}: {
|
|
286
|
+
period: PeriodSummary
|
|
287
|
+
active: boolean
|
|
288
|
+
dense: boolean
|
|
289
|
+
onSelect: () => void
|
|
290
|
+
}) {
|
|
291
|
+
return (
|
|
292
|
+
<li>
|
|
293
|
+
{/* 这里**不**用分段控件:一行是一期,带合入数、CVE 数与贡献者,
|
|
294
|
+
是"从列表里挑一条",不是"在几个等价选项里选一个"。
|
|
295
|
+
用 aria-current 表达"当前查看的是这条"。
|
|
296
|
+
原生 button 也保留:换成 AntD 的按钮会为一个纯排版差异
|
|
297
|
+
多引入一层 DOM。 */}
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
onClick={onSelect}
|
|
301
|
+
// aria-current 的取值就是 true/false —— 写成 false 而不是省略,
|
|
302
|
+
// 这样"有多少期"和"哪一期被选中"两个问题都能从这一个属性回答。
|
|
303
|
+
aria-current={active ? 'true' : 'false'}
|
|
304
|
+
className={cn(
|
|
305
|
+
'flex w-full items-center gap-x-3 gap-y-1 px-4 py-2 text-left text-xs transition-colors duration-150',
|
|
306
|
+
active ? 'bg-[var(--color-surface-2)]' : 'hover:bg-[var(--color-surface-2)]',
|
|
307
|
+
)}
|
|
308
|
+
>
|
|
309
|
+
<span
|
|
310
|
+
className={cn(
|
|
311
|
+
'w-4 shrink-0 font-mono text-2xs',
|
|
312
|
+
active ? 'text-[var(--color-accent)]' : 'text-transparent',
|
|
313
|
+
)}
|
|
314
|
+
aria-hidden
|
|
315
|
+
>
|
|
316
|
+
▸
|
|
317
|
+
</span>
|
|
318
|
+
<span className={cn('shrink-0 font-mono tabular-nums', dense ? 'w-40' : 'w-20')}>
|
|
319
|
+
{period.label}
|
|
320
|
+
</span>
|
|
321
|
+
<span className="shrink-0 font-mono tabular-nums">
|
|
322
|
+
合入 <span className="text-[var(--color-text-primary)]">{period.total}</span>
|
|
323
|
+
</span>
|
|
324
|
+
<span className="shrink-0 font-mono tabular-nums text-[var(--color-kind-cve)]">
|
|
325
|
+
CVE {period.cve_count}
|
|
326
|
+
</span>
|
|
327
|
+
<span className="min-w-0 flex-1 truncate text-right text-2xs text-[var(--color-text-muted)]">
|
|
328
|
+
{period.top_contributors.slice(0, 3).map(([login]) => login).join('、')}
|
|
329
|
+
</span>
|
|
330
|
+
</button>
|
|
331
|
+
</li>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function Metric({
|
|
336
|
+
label,
|
|
337
|
+
value,
|
|
338
|
+
hint,
|
|
339
|
+
to,
|
|
340
|
+
}: {
|
|
341
|
+
label: string
|
|
342
|
+
value: number
|
|
343
|
+
hint?: string
|
|
344
|
+
/** 数字本身带上出处:看到"待处理 42"就要能点进那 42 个。 */
|
|
345
|
+
to?: { search: { state: string; branch: string[] } }
|
|
346
|
+
}) {
|
|
347
|
+
const content = (
|
|
348
|
+
<Card className={cn('h-full', to && 'transition-[border-color,box-shadow] duration-150 hover:border-[var(--color-border-strong)] hover:shadow-[var(--shadow-card-hover)]')}>
|
|
349
|
+
<CardBody>
|
|
350
|
+
<div className="text-xs text-[var(--color-text-muted)]">{label}</div>
|
|
351
|
+
<div className="mt-1 font-mono text-xl tabular-nums">{formatNumber(value)}</div>
|
|
352
|
+
{hint && <div className="mt-0.5 text-2xs text-[var(--color-text-muted)]">{hint}</div>}
|
|
353
|
+
</CardBody>
|
|
354
|
+
</Card>
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if (!to) return content
|
|
358
|
+
return (
|
|
359
|
+
<Link to="/pulls" search={to.search} className="block">
|
|
360
|
+
{content}
|
|
361
|
+
</Link>
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 一份发版说明。
|
|
367
|
+
*
|
|
368
|
+
* 补丁清单默认折叠:一期动辄几十条,全展开会把相邻的几期推到屏幕之外,
|
|
369
|
+
* 而维护者多数时候只想看"这一期改了多大、有没有 CVE"。
|
|
370
|
+
*/
|
|
371
|
+
function ReleaseNoteCard({ note }: { note: ReleaseReport }) {
|
|
372
|
+
const [expanded, setExpanded] = useState(false)
|
|
373
|
+
const { data: patches } = useQuery({
|
|
374
|
+
queryKey: ['report-patches', note.id],
|
|
375
|
+
queryFn: () => sigApi.reportPatches(note.id),
|
|
376
|
+
enabled: expanded,
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<Card>
|
|
381
|
+
<CardHeader className="pb-2">
|
|
382
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
383
|
+
<CardTitle className="font-mono text-sm">
|
|
384
|
+
{note.from_tag ?? '—'} → {note.to_tag ?? '—'}
|
|
385
|
+
</CardTitle>
|
|
386
|
+
{note.meeting_held_on && (
|
|
387
|
+
<span className="text-xs text-[var(--color-text-muted)]">
|
|
388
|
+
{note.meeting_held_on} 例会汇报
|
|
389
|
+
</span>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
</CardHeader>
|
|
393
|
+
<CardContent className="space-y-3">
|
|
394
|
+
<p className="text-sm text-[var(--color-text-secondary)]">
|
|
395
|
+
本期合入{' '}
|
|
396
|
+
<span className="font-mono tabular-nums text-[var(--color-text-primary)]">
|
|
397
|
+
{note.patch_count ?? note.listed_count}
|
|
398
|
+
</span>{' '}
|
|
399
|
+
个补丁
|
|
400
|
+
{note.patch_count != null && note.listed_count > 0 && (
|
|
401
|
+
<span className="text-[var(--color-text-muted)]">
|
|
402
|
+
(纪要中列出 {note.listed_count} 条)
|
|
403
|
+
</span>
|
|
404
|
+
)}
|
|
405
|
+
</p>
|
|
406
|
+
|
|
407
|
+
{note.groups && note.groups.length > 0 && (
|
|
408
|
+
<div className="flex flex-wrap gap-1.5">
|
|
409
|
+
{note.groups.map((group) => (
|
|
410
|
+
<Badge key={group.name} tone="neutral" className="font-normal">
|
|
411
|
+
{group.name}
|
|
412
|
+
{group.count != null && (
|
|
413
|
+
<span className="ml-1 font-mono tabular-nums text-[var(--color-text-muted)]">
|
|
414
|
+
{group.count}
|
|
415
|
+
</span>
|
|
416
|
+
)}
|
|
417
|
+
</Badge>
|
|
418
|
+
))}
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
|
|
422
|
+
{note.cve_ids && note.cve_ids.length > 0 && (
|
|
423
|
+
<div>
|
|
424
|
+
<p className="mb-1 text-xs text-[var(--color-text-muted)]">
|
|
425
|
+
本期修复 CVE {note.cve_ids.length} 个
|
|
426
|
+
</p>
|
|
427
|
+
<div className="flex flex-wrap gap-1">
|
|
428
|
+
{note.cve_ids.slice(expanded ? undefined : 8).map((cve) => (
|
|
429
|
+
<a
|
|
430
|
+
key={cve}
|
|
431
|
+
href={`https://www.cve.org/CVERecord?id=${cve}`}
|
|
432
|
+
target="_blank"
|
|
433
|
+
rel="noreferrer noopener"
|
|
434
|
+
className="font-mono text-2xs text-[var(--color-kind-cve)] hover:underline"
|
|
435
|
+
>
|
|
436
|
+
{cve}
|
|
437
|
+
</a>
|
|
438
|
+
))}
|
|
439
|
+
{!expanded && note.cve_ids.length > 8 && (
|
|
440
|
+
<span className="text-2xs text-[var(--color-text-muted)]">
|
|
441
|
+
等 {note.cve_ids.length} 个
|
|
442
|
+
</span>
|
|
443
|
+
)}
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
)}
|
|
447
|
+
|
|
448
|
+
{note.iso_urls && note.iso_urls.length > 0 && (
|
|
449
|
+
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
|
450
|
+
{note.iso_urls.map((url) => (
|
|
451
|
+
<a
|
|
452
|
+
key={url}
|
|
453
|
+
href={url}
|
|
454
|
+
target="_blank"
|
|
455
|
+
rel="noreferrer noopener"
|
|
456
|
+
className="inline-flex items-center gap-1 text-xs text-[var(--color-accent)] hover:underline"
|
|
457
|
+
>
|
|
458
|
+
{url.replace('https://repo.openeuler.org/', '')}
|
|
459
|
+
<ExternalLink className="size-3" aria-hidden />
|
|
460
|
+
</a>
|
|
461
|
+
))}
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
|
|
465
|
+
<button
|
|
466
|
+
type="button"
|
|
467
|
+
onClick={() => setExpanded((value) => !value)}
|
|
468
|
+
className="text-xs text-[var(--color-accent)] hover:underline"
|
|
469
|
+
>
|
|
470
|
+
{expanded ? '收起补丁清单' : `展开补丁清单(${note.listed_count} 条)`}
|
|
471
|
+
</button>
|
|
472
|
+
|
|
473
|
+
{expanded &&
|
|
474
|
+
(patches && patches.length > 0 ? (
|
|
475
|
+
<PatchList patches={patches} />
|
|
476
|
+
) : (
|
|
477
|
+
<p className="text-xs text-[var(--color-text-muted)]">纪要未列出具体补丁条目。</p>
|
|
478
|
+
))}
|
|
479
|
+
</CardContent>
|
|
480
|
+
</Card>
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function PatchList({ patches }: { patches: ReleasePatch[] }) {
|
|
485
|
+
const groups = new Map<string, ReleasePatch[]>()
|
|
486
|
+
for (const patch of patches) {
|
|
487
|
+
const key = patch.group_name ?? '未分组'
|
|
488
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
489
|
+
groups.get(key)!.push(patch)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<div className="space-y-3 border-t border-[var(--color-border-subtle)] pt-3">
|
|
494
|
+
{[...groups.entries()].map(([name, list]) => (
|
|
495
|
+
<div key={name}>
|
|
496
|
+
<p className="mb-1 text-xs text-[var(--color-text-muted)]">
|
|
497
|
+
{name} · {list.length} 条
|
|
498
|
+
</p>
|
|
499
|
+
<ul className="space-y-1">
|
|
500
|
+
{list.map((patch) => (
|
|
501
|
+
<li key={patch.sequence} className="flex items-start gap-2 text-xs">
|
|
502
|
+
{/* 有编号的链到本平台的 PR;没有的(纪要被截断)链回 AtomGit 原文 */}
|
|
503
|
+
{patch.pull_number != null ? (
|
|
504
|
+
<Link
|
|
505
|
+
to="/pulls"
|
|
506
|
+
search={{ q: String(patch.pull_number) }}
|
|
507
|
+
className="shrink-0 font-mono text-[var(--color-accent)] hover:underline"
|
|
508
|
+
>
|
|
509
|
+
#{patch.pull_number}
|
|
510
|
+
</Link>
|
|
511
|
+
) : (
|
|
512
|
+
<span className="shrink-0 text-[var(--color-text-muted)]">—</span>
|
|
513
|
+
)}
|
|
514
|
+
<span className="min-w-0 flex-1 text-[var(--color-text-secondary)]">
|
|
515
|
+
{patch.title}
|
|
516
|
+
</span>
|
|
517
|
+
{patch.url && (
|
|
518
|
+
<a
|
|
519
|
+
href={patch.url}
|
|
520
|
+
target="_blank"
|
|
521
|
+
rel="noreferrer noopener"
|
|
522
|
+
aria-label="在 AtomGit 打开"
|
|
523
|
+
className="shrink-0 text-[var(--color-text-muted)] hover:text-[var(--color-accent)]"
|
|
524
|
+
>
|
|
525
|
+
<ExternalLink className="size-3" aria-hidden />
|
|
526
|
+
</a>
|
|
527
|
+
)}
|
|
528
|
+
</li>
|
|
529
|
+
))}
|
|
530
|
+
</ul>
|
|
531
|
+
</div>
|
|
532
|
+
))}
|
|
533
|
+
</div>
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function kindRows(period: PeriodSummary): [string, number][] {
|
|
538
|
+
return Object.entries(period.by_kind).sort((left, right) => left[1] - right[1])
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* 合入趋势。
|
|
543
|
+
*
|
|
544
|
+
* 空周期**保留**并画一个极短的柱,而不是跳过:跳过会让"那一期没有合入"
|
|
545
|
+
* 与"那一期不存在"在图上长得一样,而这两者的含义完全不同。
|
|
546
|
+
*/
|
|
547
|
+
function timelineOption(
|
|
548
|
+
timeline: { label: string; total: number }[],
|
|
549
|
+
granularity: Granularity,
|
|
550
|
+
): EChartsOption {
|
|
551
|
+
return {
|
|
552
|
+
grid: { left: 8, right: 8, top: 16, bottom: 4, containLabel: true },
|
|
553
|
+
tooltip: {
|
|
554
|
+
trigger: 'axis',
|
|
555
|
+
backgroundColor: CHART_THEME.surface,
|
|
556
|
+
borderColor: CHART_THEME.border,
|
|
557
|
+
textStyle: { color: CHART_THEME.text, fontSize: 12 },
|
|
558
|
+
formatter: (params: { name: string; value: number }[]) => {
|
|
559
|
+
const first = params[0]
|
|
560
|
+
return `${first.name}<br/>合入 ${first.value} 个`
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
xAxis: {
|
|
564
|
+
type: 'category',
|
|
565
|
+
data: timeline.map((item) =>
|
|
566
|
+
granularity === 'month' ? item.label.slice(2) : item.label.slice(5, 10),
|
|
567
|
+
),
|
|
568
|
+
axisLine: { lineStyle: { color: CHART_THEME.border } },
|
|
569
|
+
axisLabel: { color: CHART_THEME.muted, fontSize: 10 },
|
|
570
|
+
axisTick: { show: false },
|
|
571
|
+
},
|
|
572
|
+
yAxis: {
|
|
573
|
+
type: 'value',
|
|
574
|
+
splitLine: { lineStyle: { color: CHART_THEME.border, type: 'dashed' } },
|
|
575
|
+
axisLabel: { color: CHART_THEME.muted, fontSize: 10 },
|
|
576
|
+
},
|
|
577
|
+
series: [
|
|
578
|
+
{
|
|
579
|
+
type: 'bar',
|
|
580
|
+
data: timeline.map((item) => item.total),
|
|
581
|
+
itemStyle: { color: CHART_THEME.accent, borderRadius: [3, 3, 0, 0] },
|
|
582
|
+
barMaxWidth: 28,
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** 类别分布。用横向条形:类别名是中文短语,横排才放得下且不必旋转标签。 */
|
|
589
|
+
function kindOption(byKind: Record<string, number>): EChartsOption {
|
|
590
|
+
const rows = Object.entries(byKind).sort((left, right) => left[1] - right[1])
|
|
591
|
+
return {
|
|
592
|
+
grid: { left: 8, right: 32, top: 8, bottom: 4, containLabel: true },
|
|
593
|
+
tooltip: {
|
|
594
|
+
trigger: 'item',
|
|
595
|
+
backgroundColor: CHART_THEME.surface,
|
|
596
|
+
borderColor: CHART_THEME.border,
|
|
597
|
+
textStyle: { color: CHART_THEME.text, fontSize: 12 },
|
|
598
|
+
},
|
|
599
|
+
xAxis: {
|
|
600
|
+
type: 'value',
|
|
601
|
+
splitLine: { lineStyle: { color: CHART_THEME.border, type: 'dashed' } },
|
|
602
|
+
axisLabel: { color: CHART_THEME.muted, fontSize: 10 },
|
|
603
|
+
},
|
|
604
|
+
yAxis: {
|
|
605
|
+
type: 'category',
|
|
606
|
+
data: rows.map(([kind]) => KIND_META[kind as PRKind]?.label ?? kind),
|
|
607
|
+
axisLine: { lineStyle: { color: CHART_THEME.border } },
|
|
608
|
+
axisLabel: { color: CHART_THEME.text, fontSize: 11 },
|
|
609
|
+
axisTick: { show: false },
|
|
610
|
+
},
|
|
611
|
+
series: [
|
|
612
|
+
{
|
|
613
|
+
type: 'bar',
|
|
614
|
+
data: rows.map(([kind, count]) => ({
|
|
615
|
+
value: count,
|
|
616
|
+
itemStyle: {
|
|
617
|
+
// 类别色在 KIND_META 里记为 CSS 变量名,canvas 里必须先解析
|
|
618
|
+
color: chartColor(KIND_META[kind as PRKind]?.color ?? CHART_THEME.accent),
|
|
619
|
+
borderRadius: [0, 3, 3, 0],
|
|
620
|
+
},
|
|
621
|
+
})),
|
|
622
|
+
label: {
|
|
623
|
+
show: true,
|
|
624
|
+
position: 'right',
|
|
625
|
+
color: CHART_THEME.muted,
|
|
626
|
+
fontSize: 10,
|
|
627
|
+
},
|
|
628
|
+
barMaxWidth: 14,
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
}
|
|
632
|
+
}
|