@allbluecn/admin-app 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 (276) hide show
  1. package/package.json +86 -0
  2. package/src/client.tsx +31 -0
  3. package/src/components/admin/admin-user-menu.tsx +90 -0
  4. package/src/components/admin/developer-log.tsx +356 -0
  5. package/src/components/ai-provider/model-list-editor.tsx +147 -0
  6. package/src/components/ai-provider/provider-form.tsx +434 -0
  7. package/src/components/ai-provider/provider-icon.tsx +43 -0
  8. package/src/components/ai-provider/provider-logo.tsx +87 -0
  9. package/src/components/ai-provider/provider-table.tsx +258 -0
  10. package/src/components/auth/admin-login-form.tsx +214 -0
  11. package/src/components/auth/auth-brand-panel.tsx +60 -0
  12. package/src/components/auth/auth-header.tsx +38 -0
  13. package/src/components/auth/auth-layout.tsx +66 -0
  14. package/src/components/auth/auth-providers.tsx +34 -0
  15. package/src/components/auth/icon-input.tsx +46 -0
  16. package/src/components/auth/oauth-section.tsx +51 -0
  17. package/src/components/auth/password-input.tsx +67 -0
  18. package/src/components/common/page-header.tsx +42 -0
  19. package/src/components/common/placeholder-page.tsx +42 -0
  20. package/src/components/component/component-form-dialog.tsx +348 -0
  21. package/src/components/component/component-import-dialog.tsx +235 -0
  22. package/src/components/component/component-table.tsx +153 -0
  23. package/src/components/data-table/data-table-pagination.tsx +114 -0
  24. package/src/components/data-table/data-table.tsx +218 -0
  25. package/src/components/data-table/table-detail-layout.tsx +99 -0
  26. package/src/components/dependency/changelog-dialog.tsx +108 -0
  27. package/src/components/dependency/columns.tsx +232 -0
  28. package/src/components/dependency/dependency-table.tsx +368 -0
  29. package/src/components/dependency/edit-dialog.tsx +200 -0
  30. package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
  31. package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
  32. package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
  33. package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
  34. package/src/components/layout/sidebar-02/index.tsx +33 -0
  35. package/src/components/layout/sidebar-02/logo.tsx +40 -0
  36. package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
  37. package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
  38. package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
  39. package/src/components/magicui/animated-theme-toggler.tsx +272 -0
  40. package/src/components/magicui/rainbow-button.tsx +78 -0
  41. package/src/components/market/columns.tsx +151 -0
  42. package/src/components/market/market-app-form.tsx +237 -0
  43. package/src/components/market/market-app-list.tsx +156 -0
  44. package/src/components/monitoring/error-list.tsx +143 -0
  45. package/src/components/monitoring/health-cards.tsx +152 -0
  46. package/src/components/monitoring/metrics-stats.tsx +184 -0
  47. package/src/components/monitoring/request-log-table.tsx +179 -0
  48. package/src/components/providers/theme-toggle.tsx +59 -0
  49. package/src/components/settings/avatar/avatar-config.ts +46 -0
  50. package/src/components/ui/alert-dialog.tsx +216 -0
  51. package/src/components/ui/alert.tsx +76 -0
  52. package/src/components/ui/avatar.tsx +129 -0
  53. package/src/components/ui/badge.tsx +66 -0
  54. package/src/components/ui/button.tsx +84 -0
  55. package/src/components/ui/card.tsx +120 -0
  56. package/src/components/ui/checkbox.tsx +50 -0
  57. package/src/components/ui/collapsible.tsx +50 -0
  58. package/src/components/ui/dialog.tsx +185 -0
  59. package/src/components/ui/dropdown-menu.tsx +286 -0
  60. package/src/components/ui/field.tsx +255 -0
  61. package/src/components/ui/input-group.tsx +171 -0
  62. package/src/components/ui/input.tsx +36 -0
  63. package/src/components/ui/label.tsx +41 -0
  64. package/src/components/ui/pagination.tsx +146 -0
  65. package/src/components/ui/select.tsx +209 -0
  66. package/src/components/ui/separator.tsx +45 -0
  67. package/src/components/ui/sheet.tsx +164 -0
  68. package/src/components/ui/sidebar.tsx +719 -0
  69. package/src/components/ui/skeleton.tsx +30 -0
  70. package/src/components/ui/sonner.tsx +37 -0
  71. package/src/components/ui/switch.tsx +48 -0
  72. package/src/components/ui/table.tsx +133 -0
  73. package/src/components/ui/textarea.tsx +35 -0
  74. package/src/components/ui/tooltip.tsx +74 -0
  75. package/src/components/user/columns.tsx +153 -0
  76. package/src/components/user/settings-activity-log.tsx +358 -0
  77. package/src/components/user/settings-profile/constants.ts +42 -0
  78. package/src/components/user/settings-profile/types.ts +47 -0
  79. package/src/components/user/settings-profile.tsx +664 -0
  80. package/src/components/user/user-create-dialog.tsx +137 -0
  81. package/src/components/user/user-filters.tsx +117 -0
  82. package/src/hooks/use-mobile.ts +36 -0
  83. package/src/instrument.client.ts +34 -0
  84. package/src/lib/anyhub/capabilities.ts +59 -0
  85. package/src/lib/anyhub/types.ts +43 -0
  86. package/src/lib/auth-client.ts +83 -0
  87. package/src/lib/auth-utils.ts +24 -0
  88. package/src/lib/auth.config.ts +264 -0
  89. package/src/lib/avatar.ts +41 -0
  90. package/src/lib/i18n/client.ts +32 -0
  91. package/src/lib/i18n/config.ts +40 -0
  92. package/src/lib/i18n/detect.ts +43 -0
  93. package/src/lib/i18n/locales/en/admin-ai.json +128 -0
  94. package/src/lib/i18n/locales/en/admin-billing.json +56 -0
  95. package/src/lib/i18n/locales/en/admin-users.json +88 -0
  96. package/src/lib/i18n/locales/en/common.json +877 -0
  97. package/src/lib/i18n/locales/en/enterprise.json +208 -0
  98. package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
  99. package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
  100. package/src/lib/i18n/locales/zh/admin-users.json +88 -0
  101. package/src/lib/i18n/locales/zh/common.json +877 -0
  102. package/src/lib/i18n/locales/zh/enterprise.json +208 -0
  103. package/src/lib/i18n/server.ts +18 -0
  104. package/src/lib/i18n/types.ts +5 -0
  105. package/src/lib/monitoring-benchmarks.ts +85 -0
  106. package/src/lib/monitoring-descriptions.ts +78 -0
  107. package/src/lib/prisma.ts +18 -0
  108. package/src/lib/router-context.ts +38 -0
  109. package/src/lib/session.ts +38 -0
  110. package/src/lib/utils.ts +23 -0
  111. package/src/providers/index.tsx +45 -0
  112. package/src/routeTree.gen.ts +1199 -0
  113. package/src/router.tsx +51 -0
  114. package/src/routes/__root.tsx +170 -0
  115. package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
  116. package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
  117. package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
  118. package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
  119. package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
  120. package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
  121. package/src/routes/_nav/ai-providers/route.tsx +31 -0
  122. package/src/routes/_nav/apps/route.lazy.tsx +30 -0
  123. package/src/routes/_nav/apps/route.tsx +20 -0
  124. package/src/routes/_nav/audit/route.lazy.tsx +100 -0
  125. package/src/routes/_nav/audit/route.tsx +25 -0
  126. package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
  127. package/src/routes/_nav/billing/invoices/route.tsx +25 -0
  128. package/src/routes/_nav/billing/route.lazy.tsx +48 -0
  129. package/src/routes/_nav/billing/route.tsx +27 -0
  130. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
  131. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
  132. package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
  133. package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
  134. package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
  135. package/src/routes/_nav/compliance/route.tsx +25 -0
  136. package/src/routes/_nav/components/route.lazy.tsx +184 -0
  137. package/src/routes/_nav/components/route.tsx +40 -0
  138. package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
  139. package/src/routes/_nav/dashboard/route.tsx +20 -0
  140. package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
  141. package/src/routes/_nav/dependencies/route.tsx +32 -0
  142. package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
  143. package/src/routes/_nav/developer-logs/route.tsx +14 -0
  144. package/src/routes/_nav/docs/route.lazy.tsx +30 -0
  145. package/src/routes/_nav/docs/route.tsx +20 -0
  146. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
  147. package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
  148. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
  149. package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
  150. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
  151. package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
  152. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
  153. package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
  154. package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
  155. package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
  156. package/src/routes/_nav/licenses/route.tsx +25 -0
  157. package/src/routes/_nav/market/route.lazy.tsx +31 -0
  158. package/src/routes/_nav/market/route.tsx +20 -0
  159. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
  160. package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
  161. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
  162. package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
  163. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
  164. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
  165. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
  166. package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
  167. package/src/routes/_nav/monitoring/route.tsx +20 -0
  168. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
  169. package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
  170. package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
  171. package/src/routes/_nav/permissions/route.tsx +26 -0
  172. package/src/routes/_nav/route.lazy.tsx +31 -0
  173. package/src/routes/_nav/route.tsx +34 -0
  174. package/src/routes/_nav/settings/route.lazy.tsx +30 -0
  175. package/src/routes/_nav/settings/route.tsx +20 -0
  176. package/src/routes/_nav/sso/route.lazy.tsx +145 -0
  177. package/src/routes/_nav/sso/route.tsx +25 -0
  178. package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
  179. package/src/routes/_nav/users/$userId/route.tsx +27 -0
  180. package/src/routes/_nav/users/-components/users-table.tsx +38 -0
  181. package/src/routes/_nav/users/route.lazy.tsx +74 -0
  182. package/src/routes/_nav/users/route.tsx +43 -0
  183. package/src/routes/api/compliance-download/$jobId.ts +35 -0
  184. package/src/routes/api/observability/errors.ts +41 -0
  185. package/src/routes/api/observability/health.ts +30 -0
  186. package/src/routes/api.auth.$.ts +31 -0
  187. package/src/routes/api.sentry-test.ts +33 -0
  188. package/src/routes/index/route.lazy.tsx +22 -0
  189. package/src/routes/index/route.tsx +24 -0
  190. package/src/routes/login/route.lazy.tsx +46 -0
  191. package/src/routes/login/route.tsx +28 -0
  192. package/src/routes/sentry-tunnel.ts +61 -0
  193. package/src/routes/setup/route.lazy.tsx +94 -0
  194. package/src/routes/setup/route.tsx +28 -0
  195. package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
  196. package/src/server/__tests__/licenses-issue.test.ts +98 -0
  197. package/src/server/admin-ai-alerts.ts +177 -0
  198. package/src/server/admin-ai-usage.ts +134 -0
  199. package/src/server/auth-helpers.ts +51 -0
  200. package/src/server/entitlements.ts +24 -0
  201. package/src/server/knowledge-public/canonical-id.ts +55 -0
  202. package/src/server/knowledge-public/github-sync.ts +86 -0
  203. package/src/server/license-issue.ts +78 -0
  204. package/src/server/middlewares/request.ts +26 -0
  205. package/src/server/observability/debug-middleware.ts +39 -0
  206. package/src/server/observability/error-reporter.ts +99 -0
  207. package/src/server/observability/health-check.ts +140 -0
  208. package/src/server/observability/index.ts +33 -0
  209. package/src/server/observability/logger.ts +49 -0
  210. package/src/server/observability/metrics.ts +105 -0
  211. package/src/server/observability/request-middleware.ts +125 -0
  212. package/src/server/observability/timing.ts +42 -0
  213. package/src/server/pro-services.ts +32 -0
  214. package/src/server/serverFns/admin-billing.ts +94 -0
  215. package/src/server/serverFns/ai-provider/create.ts +92 -0
  216. package/src/server/serverFns/ai-provider/delete.ts +42 -0
  217. package/src/server/serverFns/ai-provider/get.ts +35 -0
  218. package/src/server/serverFns/ai-provider/list.ts +92 -0
  219. package/src/server/serverFns/ai-provider/update.ts +73 -0
  220. package/src/server/serverFns/anyhub/market.ts +64 -0
  221. package/src/server/serverFns/audit.ts +57 -0
  222. package/src/server/serverFns/auth/session.ts +26 -0
  223. package/src/server/serverFns/auth/setup.ts +73 -0
  224. package/src/server/serverFns/compliance.ts +85 -0
  225. package/src/server/serverFns/components/import.ts +197 -0
  226. package/src/server/serverFns/components/list.ts +51 -0
  227. package/src/server/serverFns/components/mutations.ts +84 -0
  228. package/src/server/serverFns/dependency/helpers.ts +245 -0
  229. package/src/server/serverFns/dependency/list.ts +127 -0
  230. package/src/server/serverFns/dependency/sync.ts +123 -0
  231. package/src/server/serverFns/dependency/update.ts +51 -0
  232. package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
  233. package/src/server/serverFns/developer-logs.ts +132 -0
  234. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
  235. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
  236. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
  237. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
  238. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
  239. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
  240. package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
  241. package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
  242. package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
  243. package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
  244. package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
  245. package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
  246. package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
  247. package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
  248. package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
  249. package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
  250. package/src/server/serverFns/licenses.ts +92 -0
  251. package/src/server/serverFns/locale.ts +10 -0
  252. package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
  253. package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
  254. package/src/server/serverFns/monitoring/get-errors.ts +43 -0
  255. package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
  256. package/src/server/serverFns/monitoring/get-health.ts +55 -0
  257. package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
  258. package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
  259. package/src/server/serverFns/monitoring/report-error.ts +55 -0
  260. package/src/server/serverFns/monitoring/reset.ts +70 -0
  261. package/src/server/serverFns/permissions.ts +214 -0
  262. package/src/server/serverFns/sentry/config.ts +88 -0
  263. package/src/server/serverFns/sso-config.ts +83 -0
  264. package/src/server/serverFns/user/create.ts +40 -0
  265. package/src/server/serverFns/user/delete.ts +29 -0
  266. package/src/server/serverFns/user/detail.ts +45 -0
  267. package/src/server/serverFns/user/list.ts +56 -0
  268. package/src/server/serverFns/user/reset-password.ts +28 -0
  269. package/src/server/serverFns/user/update-role.ts +26 -0
  270. package/src/server/serverFns/user/update-status.ts +26 -0
  271. package/src/server.ts +27 -0
  272. package/src/start.ts +85 -0
  273. package/src/styles/_keyframe-animations.scss +91 -0
  274. package/src/styles/_variables.scss +296 -0
  275. package/src/styles/globals.css +200 -0
  276. package/src/types/start-authjs.d.ts +27 -0
@@ -0,0 +1,232 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import {
19
+ columnVisibilityFeature,
20
+ createPaginatedRowModel,
21
+ rowPaginationFeature,
22
+ tableFeatures,
23
+ type ColumnDef,
24
+ } from "@tanstack/react-table"
25
+ import { CheckCircle2, ExternalLink, Loader2, RefreshCw } from "lucide-react"
26
+ import {
27
+ DEPENDENCY_CATEGORIES,
28
+ type ThirdPartyDependencyRow,
29
+ type VersionStatus,
30
+ } from "@allbluecn/kernel"
31
+ import { Badge } from "@/components/ui/badge"
32
+ import { Button } from "@/components/ui/button"
33
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
34
+ import { cn } from "@/lib/utils"
35
+ import { EditDependencyDialog } from "./edit-dialog"
36
+ import { ChangelogDialog } from "./changelog-dialog"
37
+
38
+ export const dependencyTableFeatures = tableFeatures({
39
+ columnVisibilityFeature,
40
+ rowPaginationFeature,
41
+ paginatedRowModel: createPaginatedRowModel(),
42
+ });
43
+
44
+ export const STATUS_META: Record<VersionStatus, { className: string }> = {
45
+ latest: { className: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border-emerald-500/30" },
46
+ patch: { className: "bg-sky-500/15 text-sky-700 dark:text-sky-400 border-sky-500/30" },
47
+ minor: { className: "bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-500/30" },
48
+ major: { className: "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30" },
49
+ unknown: { className: "bg-muted text-muted-foreground border-border" },
50
+ }
51
+
52
+ export function getDependencyStatusLabel(t: (k: string) => string, status: VersionStatus) {
53
+ return t(`dependency.statusLabel.${status}`)
54
+ }
55
+
56
+ export function getDependencyTimeAgo(t: (k: string, params?: Record<string, unknown>) => string, iso: string | null): string {
57
+ if (!iso) return "—"
58
+ const d = new Date(iso)
59
+ if (Number.isNaN(d.getTime())) return "—"
60
+ const diff = Date.now() - d.getTime()
61
+ const min = Math.floor(diff / 60000)
62
+ if (min < 1) return t('dependency.timeAgo.justNow')
63
+ if (min < 60) return t('dependency.timeAgo.minutes', { count: min })
64
+ const hr = Math.floor(min / 60)
65
+ if (hr < 24) return t('dependency.timeAgo.hours', { count: hr })
66
+ const day = Math.floor(hr / 24)
67
+ return t('dependency.timeAgo.days', { count: day })
68
+ }
69
+
70
+ interface ColumnOpts {
71
+ syncing: Set<string>
72
+ onSync: (name: string) => void
73
+ onSaved: () => void
74
+ t: (k: string, params?: Record<string, unknown>) => string
75
+ }
76
+
77
+ export function getDependencyColumns(opts: ColumnOpts): ColumnDef<typeof dependencyTableFeatures, ThirdPartyDependencyRow>[] {
78
+ const { syncing, onSync, onSaved, t } = opts
79
+ return [
80
+ {
81
+ accessorKey: "displayName",
82
+ header: t('dependency.table.columns.dependency'),
83
+ cell: ({ row }) => {
84
+ const r = row.original
85
+ return (
86
+ <div className="flex flex-col gap-0.5">
87
+ <div className="flex items-center gap-1.5">
88
+ <span className="font-medium">{r.displayName}</span>
89
+ {!r.isWatched && (
90
+ <Badge variant="outline" className="px-1 py-0 text-[10px] text-muted-foreground">
91
+ {t('dependency.table.cell.untracked')}
92
+ </Badge>
93
+ )}
94
+ {r.priority === "high" && r.isWatched && (
95
+ <Badge variant="outline" className={cn("px-1 py-0 text-[10px]", PRIORITY_BADGE.high)}>
96
+ {t('dependency.table.cell.highPriority')}
97
+ </Badge>
98
+ )}
99
+ </div>
100
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
101
+ <span>{r.name}</span>
102
+ {r.homepage && (
103
+ <a href={r.homepage} target="_blank" rel="noreferrer" className="hover:text-foreground">
104
+ <ExternalLink className="size-3" />
105
+ </a>
106
+ )}
107
+ <a href={r.npmUrl} target="_blank" rel="noreferrer" className="hover:text-foreground">
108
+ <span className="font-mono text-[10px]">npm</span>
109
+ </a>
110
+ </div>
111
+ </div>
112
+ )
113
+ },
114
+ },
115
+ {
116
+ accessorKey: "description",
117
+ header: t('dependency.table.columns.description'),
118
+ cell: ({ row }) => {
119
+ const desc = row.original.description
120
+ if (!desc) return <span className="text-xs text-muted-foreground">—</span>
121
+ return (
122
+ <Tooltip>
123
+ <TooltipTrigger asChild>
124
+ <span className="line-clamp-2 max-w-[280px] text-xs text-muted-foreground">
125
+ {desc}
126
+ </span>
127
+ </TooltipTrigger>
128
+ <TooltipContent className="max-w-sm">{desc}</TooltipContent>
129
+ </Tooltip>
130
+ )
131
+ },
132
+ },
133
+ {
134
+ accessorKey: "category",
135
+ header: t('dependency.table.columns.category'),
136
+ cell: ({ row }) => (
137
+ <Badge variant="outline" className="font-normal">
138
+ {DEPENDENCY_CATEGORIES.find((c) => c.value === row.original.category)?.label ?? row.original.category}
139
+ </Badge>
140
+ ),
141
+ },
142
+ {
143
+ accessorKey: "currentVersion",
144
+ header: t('dependency.table.columns.currentVersion'),
145
+ cell: ({ row }) => {
146
+ const r = row.original
147
+ if (!r.currentVersion) return <span className="text-xs text-muted-foreground">{t('dependency.table.cell.notInstalled')}</span>
148
+ if (r.usages.length > 1) {
149
+ return (
150
+ <Tooltip>
151
+ <TooltipTrigger asChild>
152
+ <span className="cursor-help border-b border-dashed border-muted-foreground/50">
153
+ {r.currentVersion}
154
+ </span>
155
+ </TooltipTrigger>
156
+ <TooltipContent>
157
+ <div className="space-y-0.5 text-xs">
158
+ {r.usages.map((u) => (
159
+ <div key={u.app}>
160
+ <span className="font-mono">{u.app}</span>: {u.declaredRange}
161
+ </div>
162
+ ))}
163
+ </div>
164
+ </TooltipContent>
165
+ </Tooltip>
166
+ )
167
+ }
168
+ return <span className="font-mono text-xs">{r.currentVersion}</span>
169
+ },
170
+ },
171
+ {
172
+ accessorKey: "latestVersion",
173
+ header: t('dependency.table.columns.latestVersion'),
174
+ cell: ({ row }) =>
175
+ row.original.latestVersion ?? <span className="text-muted-foreground">—</span>,
176
+ },
177
+ {
178
+ id: "changelog",
179
+ header: t('dependency.table.columns.changelog'),
180
+ cell: ({ row }) => <ChangelogDialog row={row.original} />,
181
+ },
182
+ {
183
+ accessorKey: "versionStatus",
184
+ header: t('dependency.table.columns.status'),
185
+ cell: ({ row }) => {
186
+ const status = row.original.versionStatus
187
+ return (
188
+ <Badge variant="outline" className={cn("font-normal", STATUS_META[status].className)}>
189
+ {status === "latest" && <CheckCircle2 className="mr-1 size-3" />}
190
+ {getDependencyStatusLabel(t, status)}
191
+ </Badge>
192
+ )
193
+ },
194
+ },
195
+ {
196
+ accessorKey: "lastSyncAt",
197
+ header: t('dependency.table.columns.lastSync'),
198
+ cell: ({ row }) => (
199
+ <span className="text-xs text-muted-foreground">{getDependencyTimeAgo(t, row.original.lastSyncAt)}</span>
200
+ ),
201
+ },
202
+ {
203
+ id: "actions",
204
+ header: () => <div className="text-right">{t('dependency.table.columns.actions')}</div>,
205
+ cell: ({ row }) => {
206
+ const r = row.original
207
+ const isSyncing = syncing.has(r.name)
208
+ return (
209
+ <div className="flex items-center justify-end gap-1">
210
+ <Button
211
+ size="sm"
212
+ variant="ghost"
213
+ className="size-7 p-0"
214
+ onClick={() => onSync(r.name)}
215
+ disabled={isSyncing}
216
+ title={t('dependency.table.cell.syncTooltip')}
217
+ >
218
+ {isSyncing ? <Loader2 className="size-3.5 animate-spin" /> : <RefreshCw className="size-3.5" />}
219
+ </Button>
220
+ <EditDependencyDialog row={r} onSaved={onSaved} />
221
+ </div>
222
+ )
223
+ },
224
+ },
225
+ ]
226
+ }
227
+
228
+ const PRIORITY_BADGE: Record<string, string> = {
229
+ high: "border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-400",
230
+ normal: "border-border bg-muted text-muted-foreground",
231
+ low: "border-border bg-transparent text-muted-foreground",
232
+ }
@@ -0,0 +1,368 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { useMemo, useState } from "react"
19
+ import { useTranslation } from 'react-i18next'
20
+ import {
21
+ flexRender,
22
+ useTable,
23
+ type PaginationState,
24
+ } from "@tanstack/react-table"
25
+ import { toast } from '@/components/ui/sonner'
26
+ import { RefreshCw, Search, TriangleAlert, Loader2 } from "lucide-react"
27
+ import {
28
+ DEPENDENCY_CATEGORIES,
29
+ type DependencyCategory,
30
+ type ThirdPartyDependencyRow,
31
+ type VersionStatus,
32
+ } from "@allbluecn/kernel"
33
+ import { syncAllDependenciesFn, syncDependencyFn } from "@/server/serverFns/dependency/sync"
34
+ import { Badge } from "@/components/ui/badge"
35
+ import { Button } from "@/components/ui/button"
36
+ import { Input } from "@/components/ui/input"
37
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
38
+ import {
39
+ Table,
40
+ TableBody,
41
+ TableCell,
42
+ TableHead,
43
+ TableHeader,
44
+ TableRow,
45
+ } from "@/components/ui/table"
46
+ import {
47
+ Pagination,
48
+ PaginationContent,
49
+ PaginationEllipsis,
50
+ PaginationItem,
51
+ PaginationLink,
52
+ PaginationNext,
53
+ PaginationPrevious,
54
+ } from "@/components/ui/pagination"
55
+ import { cn } from "@/lib/utils"
56
+ import { dependencyTableFeatures, getDependencyColumns } from "./columns"
57
+
58
+ const PAGE_SIZE = 15
59
+
60
+ const STATUS_FILTERS: Array<{ value: "all" | VersionStatus }> = [
61
+ { value: "all" },
62
+ { value: "latest" },
63
+ { value: "patch" },
64
+ { value: "minor" },
65
+ { value: "major" },
66
+ { value: "unknown" },
67
+ ]
68
+
69
+ export function DependencyTable({
70
+ data,
71
+ onMutated,
72
+ }: {
73
+ data: ThirdPartyDependencyRow[]
74
+ onMutated?: () => void
75
+ }) {
76
+ const { t } = useTranslation('common')
77
+ const [search, setSearch] = useState("")
78
+ const [categoryFilter, setCategoryFilter] = useState<"all" | DependencyCategory>("all")
79
+ const [statusFilter, setStatusFilter] = useState<"all" | VersionStatus>("all")
80
+ const [viewFilter, setViewFilter] = useState<"watched" | "all">("watched")
81
+ const [syncing, setSyncing] = useState<Set<string>>(new Set())
82
+ const [syncingAll, setSyncingAll] = useState(false)
83
+ const [{ pageIndex }, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: PAGE_SIZE })
84
+
85
+ // 外部筛选(搜索 + 分类 + 状态 + 视图),得到 filtered 后交给 TanStack Table 分页
86
+ const filtered = useMemo(() => {
87
+ const lower = search.trim().toLowerCase()
88
+ return data.filter((r) => {
89
+ if (viewFilter === "watched" && !r.isWatched) return false
90
+ if (categoryFilter !== "all" && r.category !== categoryFilter) return false
91
+ if (statusFilter !== "all" && r.versionStatus !== statusFilter) return false
92
+ if (
93
+ lower &&
94
+ !r.name.toLowerCase().includes(lower) &&
95
+ !r.displayName.toLowerCase().includes(lower)
96
+ )
97
+ return false
98
+ return true
99
+ })
100
+ }, [data, search, categoryFilter, statusFilter, viewFilter])
101
+
102
+ const columns = useMemo(
103
+ () => getDependencyColumns({ syncing, onSync: handleSync, onSaved: () => onMutated?.(), t }),
104
+ [syncing, onMutated, t],
105
+ )
106
+
107
+ const table = useTable({
108
+ features: dependencyTableFeatures,
109
+ data: filtered,
110
+ columns,
111
+ state: { pagination: { pageIndex, pageSize: PAGE_SIZE } },
112
+ onPaginationChange: setPagination,
113
+ })
114
+
115
+ // 筛选条件变化后,若当前页超出范围,回到第一页
116
+ const pageCount = table.getPageCount()
117
+ if (pageIndex >= pageCount && pageIndex > 0) {
118
+ setPagination({ pageIndex: 0, pageSize: PAGE_SIZE })
119
+ }
120
+
121
+ const watchedCount = data.filter((r) => r.isWatched).length
122
+ const outdatedCount = data.filter(
123
+ (r) => r.versionStatus === "major" || r.versionStatus === "minor",
124
+ ).length
125
+
126
+ async function handleSync(name: string) {
127
+ setSyncing((s) => new Set(s).add(name))
128
+ try {
129
+ const res = await syncDependencyFn({ data: { name } })
130
+ if (res.success) {
131
+ toast.success(t('dependency.table.toast.updated', { name, version: res.latestVersion ?? "?" }))
132
+ onMutated?.()
133
+ } else {
134
+ toast.error(t('dependency.table.toast.syncFailed', { name, error: res.error ?? t('dependency.table.toast.unknown') }))
135
+ }
136
+ } finally {
137
+ setSyncing((s) => {
138
+ const next = new Set(s)
139
+ next.delete(name)
140
+ return next
141
+ })
142
+ }
143
+ }
144
+
145
+ async function handleSyncAll() {
146
+ const names = filtered.map((r) => r.name)
147
+ if (names.length === 0) {
148
+ toast.info(t('dependency.table.toast.noSyncable'))
149
+ return
150
+ }
151
+ setSyncingAll(true)
152
+ try {
153
+ const results = await syncAllDependenciesFn({ data: { names } })
154
+ const ok = results.filter((r) => r.success)
155
+ const fail = results.filter((r) => !r.success)
156
+ if (ok.length) toast.success(t('dependency.table.toast.syncSuccess', { count: ok.length }))
157
+ if (fail.length)
158
+ toast.error(t('dependency.table.toast.syncFailedCount', { count: fail.length }), {
159
+ description: fail.map((f) => `${f.name}: ${f.error}`).slice(0, 3).join("\n"),
160
+ })
161
+ onMutated?.()
162
+ } finally {
163
+ setSyncingAll(false)
164
+ }
165
+ }
166
+
167
+ const currentPage = pageIndex + 1
168
+ const totalPages = pageCount
169
+
170
+ return (
171
+ <div className="space-y-4">
172
+ <div className="flex flex-wrap items-center gap-3">
173
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
174
+ <Badge variant="outline">{t('dependency.table.stats.watched', { watchedCount })}</Badge>
175
+ <Badge variant="outline" className="border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400">
176
+ <TriangleAlert className="mr-1 size-3" />
177
+ {t('dependency.table.stats.outdated', { outdatedCount })}
178
+ </Badge>
179
+ </div>
180
+ <div className="ml-auto flex items-center gap-2">
181
+ <Button onClick={handleSyncAll} disabled={syncingAll} variant="outline" size="sm">
182
+ {syncingAll ? <Loader2 className="mr-1 size-3.5 animate-spin" /> : <RefreshCw className="mr-1 size-3.5" />}
183
+ {t('dependency.table.syncAll', { count: filtered.length })}
184
+ </Button>
185
+ </div>
186
+ </div>
187
+
188
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1.5">
189
+ <div className="flex flex-wrap items-center gap-1.5">
190
+ <CategoryButton active={categoryFilter === "all"} onClick={() => setCategoryFilter("all")}>
191
+ {t('dependency.table.allCategory')}
192
+ </CategoryButton>
193
+ {DEPENDENCY_CATEGORIES.map((c) => (
194
+ <CategoryButton
195
+ key={c.value}
196
+ active={categoryFilter === c.value}
197
+ onClick={() => setCategoryFilter(c.value)}
198
+ >
199
+ {c.label}
200
+ </CategoryButton>
201
+ ))}
202
+ </div>
203
+ <div className="ml-auto flex flex-wrap items-center gap-1.5">
204
+ {STATUS_FILTERS.map((s) => (
205
+ <CategoryButton
206
+ key={s.value}
207
+ active={statusFilter === s.value}
208
+ onClick={() => setStatusFilter(s.value)}
209
+ >
210
+ {t(`dependency.table.statusFilter.${s.value}`)}
211
+ </CategoryButton>
212
+ ))}
213
+ </div>
214
+ </div>
215
+
216
+ <div className="flex flex-wrap items-center gap-2">
217
+ <div className="relative">
218
+ <Search className="absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
219
+ <Input
220
+ value={search}
221
+ onChange={(e) => setSearch(e.target.value)}
222
+ placeholder={t('dependency.table.searchPlaceholder')}
223
+ className="h-9 w-56 pl-8"
224
+ />
225
+ </div>
226
+ <Select value={viewFilter} onValueChange={(v) => setViewFilter(v as "watched" | "all")}>
227
+ <SelectTrigger className="h-9 w-32" size="sm">
228
+ <SelectValue />
229
+ </SelectTrigger>
230
+ <SelectContent>
231
+ <SelectItem value="watched">{t('dependency.table.viewFilter.watched')}</SelectItem>
232
+ <SelectItem value="all">{t('dependency.table.viewFilter.all')}</SelectItem>
233
+ </SelectContent>
234
+ </Select>
235
+ <span className="ml-auto text-xs text-muted-foreground">
236
+ {t('pagination.total', { total: filtered.length, page: currentPage, totalPages })}
237
+ </span>
238
+ </div>
239
+
240
+ <div className="rounded-lg border">
241
+ <Table>
242
+ <TableHeader>
243
+ {table.getHeaderGroups().map((headerGroup) => (
244
+ <TableRow key={headerGroup.id}>
245
+ {headerGroup.headers.map((header) => (
246
+ <TableHead key={header.id} className={header.id === "actions" ? "text-right" : undefined}>
247
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
248
+ </TableHead>
249
+ ))}
250
+ </TableRow>
251
+ ))}
252
+ </TableHeader>
253
+ <TableBody>
254
+ {table.getRowModel().rows.length ? (
255
+ table.getRowModel().rows.map((row) => (
256
+ <TableRow key={row.id}>
257
+ {row.getVisibleCells().map((cell) => (
258
+ <TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
259
+ ))}
260
+ </TableRow>
261
+ ))
262
+ ) : (
263
+ <TableRow>
264
+ <TableCell colSpan={columns.length} className="py-10 text-center text-muted-foreground">
265
+ {t('dependency.table.empty')}
266
+ </TableCell>
267
+ </TableRow>
268
+ )}
269
+ </TableBody>
270
+ </Table>
271
+ </div>
272
+
273
+ {totalPages > 1 && (
274
+ <Pagination className="mx-0 justify-end">
275
+ <PaginationContent>
276
+ <PaginationItem>
277
+ <PaginationPrevious
278
+ text={t('pagination.prev')}
279
+ href="#"
280
+ onClick={(e) => {
281
+ e.preventDefault()
282
+ table.previousPage()
283
+ }}
284
+ aria-disabled={!table.getCanPreviousPage()}
285
+ className={cn(!table.getCanPreviousPage() && "pointer-events-none opacity-50")}
286
+ />
287
+ </PaginationItem>
288
+ {renderPageItems(currentPage, totalPages, (p) => table.setPageIndex(p - 1))}
289
+ <PaginationItem>
290
+ <PaginationNext
291
+ text={t('pagination.next')}
292
+ href="#"
293
+ onClick={(e) => {
294
+ e.preventDefault()
295
+ table.nextPage()
296
+ }}
297
+ aria-disabled={!table.getCanNextPage()}
298
+ className={cn(!table.getCanNextPage() && "pointer-events-none opacity-50")}
299
+ />
300
+ </PaginationItem>
301
+ </PaginationContent>
302
+ </Pagination>
303
+ )}
304
+ </div>
305
+ )
306
+ }
307
+
308
+ function CategoryButton({
309
+ active,
310
+ onClick,
311
+ children,
312
+ }: {
313
+ active: boolean
314
+ onClick: () => void
315
+ children: React.ReactNode
316
+ }) {
317
+ return (
318
+ <button
319
+ type="button"
320
+ onClick={onClick}
321
+ className={cn(
322
+ "rounded-full border px-3 py-1 text-xs font-medium transition-colors",
323
+ active
324
+ ? "border-primary bg-primary text-primary-foreground"
325
+ : "border-border bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground",
326
+ )}
327
+ >
328
+ {children}
329
+ </button>
330
+ )
331
+ }
332
+
333
+ function renderPageItems(
334
+ current: number,
335
+ total: number,
336
+ setPageIndex: (zeroBased: number) => void,
337
+ ): React.ReactNode[] {
338
+ const items: React.ReactNode[] = []
339
+ const push = (p: number) =>
340
+ items.push(
341
+ <PaginationItem key={p}>
342
+ <PaginationLink
343
+ href="#"
344
+ isActive={p === current}
345
+ onClick={(e) => {
346
+ e.preventDefault()
347
+ setPageIndex(p - 1)
348
+ }}
349
+ >
350
+ {p}
351
+ </PaginationLink>
352
+ </PaginationItem>,
353
+ )
354
+
355
+ if (total <= 7) {
356
+ for (let i = 1; i <= total; i++) push(i)
357
+ return items
358
+ }
359
+
360
+ push(1)
361
+ if (current > 3) items.push(<PaginationEllipsis key="start-ellipsis" />)
362
+ const start = Math.max(2, current - 1)
363
+ const end = Math.min(total - 1, current + 1)
364
+ for (let i = start; i <= end; i++) push(i)
365
+ if (current < total - 2) items.push(<PaginationEllipsis key="end-ellipsis" />)
366
+ push(total)
367
+ return items
368
+ }