@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.
- package/package.json +86 -0
- package/src/client.tsx +31 -0
- package/src/components/admin/admin-user-menu.tsx +90 -0
- package/src/components/admin/developer-log.tsx +356 -0
- package/src/components/ai-provider/model-list-editor.tsx +147 -0
- package/src/components/ai-provider/provider-form.tsx +434 -0
- package/src/components/ai-provider/provider-icon.tsx +43 -0
- package/src/components/ai-provider/provider-logo.tsx +87 -0
- package/src/components/ai-provider/provider-table.tsx +258 -0
- package/src/components/auth/admin-login-form.tsx +214 -0
- package/src/components/auth/auth-brand-panel.tsx +60 -0
- package/src/components/auth/auth-header.tsx +38 -0
- package/src/components/auth/auth-layout.tsx +66 -0
- package/src/components/auth/auth-providers.tsx +34 -0
- package/src/components/auth/icon-input.tsx +46 -0
- package/src/components/auth/oauth-section.tsx +51 -0
- package/src/components/auth/password-input.tsx +67 -0
- package/src/components/common/page-header.tsx +42 -0
- package/src/components/common/placeholder-page.tsx +42 -0
- package/src/components/component/component-form-dialog.tsx +348 -0
- package/src/components/component/component-import-dialog.tsx +235 -0
- package/src/components/component/component-table.tsx +153 -0
- package/src/components/data-table/data-table-pagination.tsx +114 -0
- package/src/components/data-table/data-table.tsx +218 -0
- package/src/components/data-table/table-detail-layout.tsx +99 -0
- package/src/components/dependency/changelog-dialog.tsx +108 -0
- package/src/components/dependency/columns.tsx +232 -0
- package/src/components/dependency/dependency-table.tsx +368 -0
- package/src/components/dependency/edit-dialog.tsx +200 -0
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
- package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
- package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
- package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
- package/src/components/layout/sidebar-02/index.tsx +33 -0
- package/src/components/layout/sidebar-02/logo.tsx +40 -0
- package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
- package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
- package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
- package/src/components/magicui/animated-theme-toggler.tsx +272 -0
- package/src/components/magicui/rainbow-button.tsx +78 -0
- package/src/components/market/columns.tsx +151 -0
- package/src/components/market/market-app-form.tsx +237 -0
- package/src/components/market/market-app-list.tsx +156 -0
- package/src/components/monitoring/error-list.tsx +143 -0
- package/src/components/monitoring/health-cards.tsx +152 -0
- package/src/components/monitoring/metrics-stats.tsx +184 -0
- package/src/components/monitoring/request-log-table.tsx +179 -0
- package/src/components/providers/theme-toggle.tsx +59 -0
- package/src/components/settings/avatar/avatar-config.ts +46 -0
- package/src/components/ui/alert-dialog.tsx +216 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/avatar.tsx +129 -0
- package/src/components/ui/badge.tsx +66 -0
- package/src/components/ui/button.tsx +84 -0
- package/src/components/ui/card.tsx +120 -0
- package/src/components/ui/checkbox.tsx +50 -0
- package/src/components/ui/collapsible.tsx +50 -0
- package/src/components/ui/dialog.tsx +185 -0
- package/src/components/ui/dropdown-menu.tsx +286 -0
- package/src/components/ui/field.tsx +255 -0
- package/src/components/ui/input-group.tsx +171 -0
- package/src/components/ui/input.tsx +36 -0
- package/src/components/ui/label.tsx +41 -0
- package/src/components/ui/pagination.tsx +146 -0
- package/src/components/ui/select.tsx +209 -0
- package/src/components/ui/separator.tsx +45 -0
- package/src/components/ui/sheet.tsx +164 -0
- package/src/components/ui/sidebar.tsx +719 -0
- package/src/components/ui/skeleton.tsx +30 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +48 -0
- package/src/components/ui/table.tsx +133 -0
- package/src/components/ui/textarea.tsx +35 -0
- package/src/components/ui/tooltip.tsx +74 -0
- package/src/components/user/columns.tsx +153 -0
- package/src/components/user/settings-activity-log.tsx +358 -0
- package/src/components/user/settings-profile/constants.ts +42 -0
- package/src/components/user/settings-profile/types.ts +47 -0
- package/src/components/user/settings-profile.tsx +664 -0
- package/src/components/user/user-create-dialog.tsx +137 -0
- package/src/components/user/user-filters.tsx +117 -0
- package/src/hooks/use-mobile.ts +36 -0
- package/src/instrument.client.ts +34 -0
- package/src/lib/anyhub/capabilities.ts +59 -0
- package/src/lib/anyhub/types.ts +43 -0
- package/src/lib/auth-client.ts +83 -0
- package/src/lib/auth-utils.ts +24 -0
- package/src/lib/auth.config.ts +264 -0
- package/src/lib/avatar.ts +41 -0
- package/src/lib/i18n/client.ts +32 -0
- package/src/lib/i18n/config.ts +40 -0
- package/src/lib/i18n/detect.ts +43 -0
- package/src/lib/i18n/locales/en/admin-ai.json +128 -0
- package/src/lib/i18n/locales/en/admin-billing.json +56 -0
- package/src/lib/i18n/locales/en/admin-users.json +88 -0
- package/src/lib/i18n/locales/en/common.json +877 -0
- package/src/lib/i18n/locales/en/enterprise.json +208 -0
- package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
- package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
- package/src/lib/i18n/locales/zh/admin-users.json +88 -0
- package/src/lib/i18n/locales/zh/common.json +877 -0
- package/src/lib/i18n/locales/zh/enterprise.json +208 -0
- package/src/lib/i18n/server.ts +18 -0
- package/src/lib/i18n/types.ts +5 -0
- package/src/lib/monitoring-benchmarks.ts +85 -0
- package/src/lib/monitoring-descriptions.ts +78 -0
- package/src/lib/prisma.ts +18 -0
- package/src/lib/router-context.ts +38 -0
- package/src/lib/session.ts +38 -0
- package/src/lib/utils.ts +23 -0
- package/src/providers/index.tsx +45 -0
- package/src/routeTree.gen.ts +1199 -0
- package/src/router.tsx +51 -0
- package/src/routes/__root.tsx +170 -0
- package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
- package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
- package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
- package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
- package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
- package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
- package/src/routes/_nav/ai-providers/route.tsx +31 -0
- package/src/routes/_nav/apps/route.lazy.tsx +30 -0
- package/src/routes/_nav/apps/route.tsx +20 -0
- package/src/routes/_nav/audit/route.lazy.tsx +100 -0
- package/src/routes/_nav/audit/route.tsx +25 -0
- package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
- package/src/routes/_nav/billing/invoices/route.tsx +25 -0
- package/src/routes/_nav/billing/route.lazy.tsx +48 -0
- package/src/routes/_nav/billing/route.tsx +27 -0
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
- package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
- package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
- package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
- package/src/routes/_nav/compliance/route.tsx +25 -0
- package/src/routes/_nav/components/route.lazy.tsx +184 -0
- package/src/routes/_nav/components/route.tsx +40 -0
- package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
- package/src/routes/_nav/dashboard/route.tsx +20 -0
- package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
- package/src/routes/_nav/dependencies/route.tsx +32 -0
- package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
- package/src/routes/_nav/developer-logs/route.tsx +14 -0
- package/src/routes/_nav/docs/route.lazy.tsx +30 -0
- package/src/routes/_nav/docs/route.tsx +20 -0
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
- package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
- package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
- package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
- package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
- package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
- package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
- package/src/routes/_nav/licenses/route.tsx +25 -0
- package/src/routes/_nav/market/route.lazy.tsx +31 -0
- package/src/routes/_nav/market/route.tsx +20 -0
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
- package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
- package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
- package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
- package/src/routes/_nav/monitoring/route.tsx +20 -0
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
- package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
- package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
- package/src/routes/_nav/permissions/route.tsx +26 -0
- package/src/routes/_nav/route.lazy.tsx +31 -0
- package/src/routes/_nav/route.tsx +34 -0
- package/src/routes/_nav/settings/route.lazy.tsx +30 -0
- package/src/routes/_nav/settings/route.tsx +20 -0
- package/src/routes/_nav/sso/route.lazy.tsx +145 -0
- package/src/routes/_nav/sso/route.tsx +25 -0
- package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
- package/src/routes/_nav/users/$userId/route.tsx +27 -0
- package/src/routes/_nav/users/-components/users-table.tsx +38 -0
- package/src/routes/_nav/users/route.lazy.tsx +74 -0
- package/src/routes/_nav/users/route.tsx +43 -0
- package/src/routes/api/compliance-download/$jobId.ts +35 -0
- package/src/routes/api/observability/errors.ts +41 -0
- package/src/routes/api/observability/health.ts +30 -0
- package/src/routes/api.auth.$.ts +31 -0
- package/src/routes/api.sentry-test.ts +33 -0
- package/src/routes/index/route.lazy.tsx +22 -0
- package/src/routes/index/route.tsx +24 -0
- package/src/routes/login/route.lazy.tsx +46 -0
- package/src/routes/login/route.tsx +28 -0
- package/src/routes/sentry-tunnel.ts +61 -0
- package/src/routes/setup/route.lazy.tsx +94 -0
- package/src/routes/setup/route.tsx +28 -0
- package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
- package/src/server/__tests__/licenses-issue.test.ts +98 -0
- package/src/server/admin-ai-alerts.ts +177 -0
- package/src/server/admin-ai-usage.ts +134 -0
- package/src/server/auth-helpers.ts +51 -0
- package/src/server/entitlements.ts +24 -0
- package/src/server/knowledge-public/canonical-id.ts +55 -0
- package/src/server/knowledge-public/github-sync.ts +86 -0
- package/src/server/license-issue.ts +78 -0
- package/src/server/middlewares/request.ts +26 -0
- package/src/server/observability/debug-middleware.ts +39 -0
- package/src/server/observability/error-reporter.ts +99 -0
- package/src/server/observability/health-check.ts +140 -0
- package/src/server/observability/index.ts +33 -0
- package/src/server/observability/logger.ts +49 -0
- package/src/server/observability/metrics.ts +105 -0
- package/src/server/observability/request-middleware.ts +125 -0
- package/src/server/observability/timing.ts +42 -0
- package/src/server/pro-services.ts +32 -0
- package/src/server/serverFns/admin-billing.ts +94 -0
- package/src/server/serverFns/ai-provider/create.ts +92 -0
- package/src/server/serverFns/ai-provider/delete.ts +42 -0
- package/src/server/serverFns/ai-provider/get.ts +35 -0
- package/src/server/serverFns/ai-provider/list.ts +92 -0
- package/src/server/serverFns/ai-provider/update.ts +73 -0
- package/src/server/serverFns/anyhub/market.ts +64 -0
- package/src/server/serverFns/audit.ts +57 -0
- package/src/server/serverFns/auth/session.ts +26 -0
- package/src/server/serverFns/auth/setup.ts +73 -0
- package/src/server/serverFns/compliance.ts +85 -0
- package/src/server/serverFns/components/import.ts +197 -0
- package/src/server/serverFns/components/list.ts +51 -0
- package/src/server/serverFns/components/mutations.ts +84 -0
- package/src/server/serverFns/dependency/helpers.ts +245 -0
- package/src/server/serverFns/dependency/list.ts +127 -0
- package/src/server/serverFns/dependency/sync.ts +123 -0
- package/src/server/serverFns/dependency/update.ts +51 -0
- package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
- package/src/server/serverFns/developer-logs.ts +132 -0
- package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
- package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
- package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
- package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
- package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
- package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
- package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
- package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
- package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
- package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
- package/src/server/serverFns/licenses.ts +92 -0
- package/src/server/serverFns/locale.ts +10 -0
- package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
- package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
- package/src/server/serverFns/monitoring/get-errors.ts +43 -0
- package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
- package/src/server/serverFns/monitoring/get-health.ts +55 -0
- package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
- package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
- package/src/server/serverFns/monitoring/report-error.ts +55 -0
- package/src/server/serverFns/monitoring/reset.ts +70 -0
- package/src/server/serverFns/permissions.ts +214 -0
- package/src/server/serverFns/sentry/config.ts +88 -0
- package/src/server/serverFns/sso-config.ts +83 -0
- package/src/server/serverFns/user/create.ts +40 -0
- package/src/server/serverFns/user/delete.ts +29 -0
- package/src/server/serverFns/user/detail.ts +45 -0
- package/src/server/serverFns/user/list.ts +56 -0
- package/src/server/serverFns/user/reset-password.ts +28 -0
- package/src/server/serverFns/user/update-role.ts +26 -0
- package/src/server/serverFns/user/update-status.ts +26 -0
- package/src/server.ts +27 -0
- package/src/start.ts +85 -0
- package/src/styles/_keyframe-animations.scss +91 -0
- package/src/styles/_variables.scss +296 -0
- package/src/styles/globals.css +200 -0
- package/src/types/start-authjs.d.ts +27 -0
|
@@ -0,0 +1,399 @@
|
|
|
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 { createLazyFileRoute, useRouter } from '@tanstack/react-router'
|
|
19
|
+
import { useTranslation } from 'react-i18next'
|
|
20
|
+
import * as Sentry from '@sentry/tanstackstart-react'
|
|
21
|
+
import { BugPlay, ExternalLink, Bug, Activity, AlertTriangle, Eye, EyeOff, Save, RotateCcw, CheckCircle2, XCircle, RefreshCw, ShieldCheck } from 'lucide-react'
|
|
22
|
+
import { IconBrandSentry } from '@tabler/icons-react';
|
|
23
|
+
import { useEffect, useState } from 'react'
|
|
24
|
+
import { toast } from 'sonner'
|
|
25
|
+
import { getSentryConfig, saveSentryConfig, reconfigureSentryServer } from '@/server/serverFns/sentry/config'
|
|
26
|
+
import type { SentryConfig } from '@/server/serverFns/sentry/config'
|
|
27
|
+
|
|
28
|
+
export const Route = createLazyFileRoute('/_nav/monitoring/sentry')({
|
|
29
|
+
component: SentryPage,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
type ConfigField = {
|
|
33
|
+
key: keyof SentryConfig
|
|
34
|
+
label: string
|
|
35
|
+
placeholder: string
|
|
36
|
+
secret: boolean
|
|
37
|
+
instruction: string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function SentryPage() {
|
|
41
|
+
const { t } = useTranslation('common')
|
|
42
|
+
const router = useRouter()
|
|
43
|
+
const [config, setConfig] = useState<SentryConfig>({ dsn: '', org: '', project: '', authToken: '' })
|
|
44
|
+
const [loading, setLoading] = useState(true)
|
|
45
|
+
const [saving, setSaving] = useState(false)
|
|
46
|
+
const [changed, setChanged] = useState(false)
|
|
47
|
+
const [visibleFields, setVisibleFields] = useState<Set<string>>(new Set())
|
|
48
|
+
const [verifyResult, setVerifyResult] = useState<{ ok: boolean; detail: string } | null>(null)
|
|
49
|
+
const [verifying, setVerifying] = useState(false)
|
|
50
|
+
|
|
51
|
+
const CONFIG_FIELDS: ConfigField[] = [
|
|
52
|
+
{
|
|
53
|
+
key: 'dsn',
|
|
54
|
+
label: 'DSN',
|
|
55
|
+
placeholder: 'https://<key>@o<orgId>.ingest.sentry.io/<projectId>',
|
|
56
|
+
secret: false,
|
|
57
|
+
instruction: [
|
|
58
|
+
t('monitoring.sentry.fields.dsn.instruction.step1'),
|
|
59
|
+
t('monitoring.sentry.fields.dsn.instruction.step2'),
|
|
60
|
+
t('monitoring.sentry.fields.dsn.instruction.step3'),
|
|
61
|
+
t('monitoring.sentry.fields.dsn.instruction.step4'),
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'org',
|
|
66
|
+
label: t('monitoring.sentry.fields.org.label'),
|
|
67
|
+
placeholder: 'my-organization',
|
|
68
|
+
secret: false,
|
|
69
|
+
instruction: [
|
|
70
|
+
t('monitoring.sentry.fields.org.instruction.step1'),
|
|
71
|
+
t('monitoring.sentry.fields.org.instruction.step2'),
|
|
72
|
+
t('monitoring.sentry.fields.org.instruction.step3'),
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: 'project',
|
|
77
|
+
label: t('monitoring.sentry.fields.project.label'),
|
|
78
|
+
placeholder: 'my-project',
|
|
79
|
+
secret: false,
|
|
80
|
+
instruction: [
|
|
81
|
+
t('monitoring.sentry.fields.project.instruction.step1'),
|
|
82
|
+
t('monitoring.sentry.fields.project.instruction.step2'),
|
|
83
|
+
t('monitoring.sentry.fields.project.instruction.step3'),
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: 'authToken',
|
|
88
|
+
label: 'Auth Token',
|
|
89
|
+
placeholder: 'sntrys_********************',
|
|
90
|
+
secret: true,
|
|
91
|
+
instruction: [
|
|
92
|
+
t('monitoring.sentry.fields.authToken.instruction.step1'),
|
|
93
|
+
t('monitoring.sentry.fields.authToken.instruction.step2'),
|
|
94
|
+
t('monitoring.sentry.fields.authToken.instruction.step3'),
|
|
95
|
+
t('monitoring.sentry.fields.authToken.instruction.step4'),
|
|
96
|
+
t('monitoring.sentry.fields.authToken.instruction.step5'),
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
const loadConfig = async () => {
|
|
102
|
+
setLoading(true)
|
|
103
|
+
try {
|
|
104
|
+
const cfg = await getSentryConfig()
|
|
105
|
+
setConfig(cfg)
|
|
106
|
+
} catch {
|
|
107
|
+
toast.error(t('monitoring.sentry.loadFailed'))
|
|
108
|
+
}
|
|
109
|
+
setLoading(false)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
loadConfig()
|
|
114
|
+
}, [])
|
|
115
|
+
|
|
116
|
+
const dsnConfigured = !!config.dsn || !!import.meta.env.VITE_SENTRY_DSN
|
|
117
|
+
|
|
118
|
+
const updateField = (key: keyof SentryConfig, value: string) => {
|
|
119
|
+
setConfig((prev) => ({ ...prev, [key]: value }))
|
|
120
|
+
setChanged(true)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const toggleVisible = (key: string) => {
|
|
124
|
+
setVisibleFields((prev) => {
|
|
125
|
+
const next = new Set(prev)
|
|
126
|
+
if (next.has(key)) next.delete(key)
|
|
127
|
+
else next.add(key)
|
|
128
|
+
return next
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const reconfigure = (dsn: string) => {
|
|
133
|
+
Sentry.init({
|
|
134
|
+
dsn,
|
|
135
|
+
environment: import.meta.env.VITE_SENTRY_ENVIRONMENT || import.meta.env.MODE,
|
|
136
|
+
dataCollection: {},
|
|
137
|
+
tracesSampleRate: 1.0,
|
|
138
|
+
replaysSessionSampleRate: 0.1,
|
|
139
|
+
replaysOnErrorSampleRate: 1.0,
|
|
140
|
+
enableLogs: true,
|
|
141
|
+
tunnel: '/sentry-tunnel',
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const handleSave = async () => {
|
|
146
|
+
setSaving(true)
|
|
147
|
+
try {
|
|
148
|
+
await saveSentryConfig({ data: config })
|
|
149
|
+
reconfigure(config.dsn)
|
|
150
|
+
await reconfigureSentryServer({ data: { dsn: config.dsn } })
|
|
151
|
+
toast.success(t('monitoring.sentry.saveSuccess'))
|
|
152
|
+
setChanged(false)
|
|
153
|
+
router.invalidate()
|
|
154
|
+
} catch {
|
|
155
|
+
toast.error(t('monitoring.sentry.saveFailed'))
|
|
156
|
+
}
|
|
157
|
+
setSaving(false)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const validateDsn = (dsn: string) => {
|
|
161
|
+
if (!dsn) return { ok: false, detail: t('monitoring.sentry.validate.empty') }
|
|
162
|
+
if (!dsn.startsWith('https://')) return { ok: false, detail: t('monitoring.sentry.validate.httpsPrefix') }
|
|
163
|
+
if (!dsn.includes('.sentry.io')) return { ok: false, detail: t('monitoring.sentry.validate.notSentryIo') }
|
|
164
|
+
if (!dsn.includes('@')) return { ok: false, detail: t('monitoring.sentry.validate.missingAt') }
|
|
165
|
+
const projectId = dsn.split('/').pop()
|
|
166
|
+
if (!projectId || !/^\d+$/.test(projectId)) return { ok: false, detail: t('monitoring.sentry.validate.missingProjectId') }
|
|
167
|
+
return { ok: true, detail: t('monitoring.sentry.validate.valid') }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const verifyConfig = async () => {
|
|
171
|
+
const activeDsn = config.dsn || import.meta.env.VITE_SENTRY_DSN
|
|
172
|
+
const fmt = validateDsn(activeDsn || '')
|
|
173
|
+
if (!fmt.ok) {
|
|
174
|
+
setVerifyResult(fmt)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
setVerifying(true)
|
|
178
|
+
setVerifyResult(null)
|
|
179
|
+
try {
|
|
180
|
+
const client = Sentry.getClient()
|
|
181
|
+
const clientDsn = client?.getOptions().dsn
|
|
182
|
+
if (clientDsn !== activeDsn) {
|
|
183
|
+
await reconfigure(activeDsn)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const eventId = Sentry.captureException(new Error('Sentry config validation - test error'))
|
|
187
|
+
if (!eventId) {
|
|
188
|
+
setVerifyResult({ ok: false, detail: t('monitoring.sentry.verify.notInitialized') })
|
|
189
|
+
setVerifying(false)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
const flushed = await Sentry.flush(5000)
|
|
193
|
+
if (flushed) {
|
|
194
|
+
setVerifyResult({ ok: true, detail: t('monitoring.sentry.verify.success', { eventId }) })
|
|
195
|
+
} else {
|
|
196
|
+
setVerifyResult({ ok: false, detail: t('monitoring.sentry.verify.timeout') })
|
|
197
|
+
}
|
|
198
|
+
} catch (e) {
|
|
199
|
+
setVerifyResult({ ok: false, detail: t('monitoring.sentry.verify.error', { message: (e as Error).message }) })
|
|
200
|
+
}
|
|
201
|
+
setVerifying(false)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className="p-6 space-y-6">
|
|
206
|
+
<div className="flex items-center justify-between">
|
|
207
|
+
<div>
|
|
208
|
+
<h2 className="text-2xl font-bold flex items-center gap-2">
|
|
209
|
+
<IconBrandSentry stroke={2} />
|
|
210
|
+
{t('monitoring.sentry.title')}
|
|
211
|
+
</h2>
|
|
212
|
+
<p className="mt-1 text-muted-foreground">{t('monitoring.sentry.subtitle')}</p>
|
|
213
|
+
</div>
|
|
214
|
+
<a
|
|
215
|
+
href="https://sentry.io"
|
|
216
|
+
target="_blank"
|
|
217
|
+
rel="noopener noreferrer"
|
|
218
|
+
className="inline-flex items-center gap-1.5 rounded-lg border bg-card px-4 py-2 text-sm font-medium hover:bg-accent transition-colors"
|
|
219
|
+
>
|
|
220
|
+
<ExternalLink className="size-4" />
|
|
221
|
+
{t('monitoring.sentry.openSentry')}
|
|
222
|
+
</a>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div className="grid gap-4 md:grid-cols-4">
|
|
226
|
+
<StatusCard
|
|
227
|
+
title={t('monitoring.sentry.statusCards.dsn.title')}
|
|
228
|
+
status={dsnConfigured ? 'active' : 'inactive'}
|
|
229
|
+
icon={dsnConfigured ? CheckCircle2 : XCircle}
|
|
230
|
+
description={dsnConfigured ? t('monitoring.sentry.statusCards.dsn.configured') : t('monitoring.sentry.statusCards.dsn.notConfigured')}
|
|
231
|
+
/>
|
|
232
|
+
<StatusCard
|
|
233
|
+
title={t('monitoring.sentry.statusCards.errors.title')}
|
|
234
|
+
status={dsnConfigured ? 'active' : 'inactive'}
|
|
235
|
+
icon={Bug}
|
|
236
|
+
description={dsnConfigured ? t('monitoring.sentry.statusCards.errors.enabled') : t('monitoring.sentry.statusCards.errors.disabled')}
|
|
237
|
+
/>
|
|
238
|
+
<StatusCard
|
|
239
|
+
title={t('monitoring.sentry.statusCards.performance.title')}
|
|
240
|
+
status={dsnConfigured ? 'active' : 'inactive'}
|
|
241
|
+
icon={Activity}
|
|
242
|
+
description={dsnConfigured ? t('monitoring.sentry.statusCards.performance.enabled') : t('monitoring.sentry.statusCards.performance.disabled')}
|
|
243
|
+
/>
|
|
244
|
+
<StatusCard
|
|
245
|
+
title="Session Replay"
|
|
246
|
+
status={dsnConfigured ? 'active' : 'inactive'}
|
|
247
|
+
icon={BugPlay}
|
|
248
|
+
description={dsnConfigured ? t('monitoring.sentry.statusCards.replay.enabled') : t('monitoring.sentry.statusCards.replay.disabled')}
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div className="rounded-lg border bg-card">
|
|
253
|
+
<div className="border-b px-6 py-4">
|
|
254
|
+
<h3 className="text-lg font-semibold">{t('monitoring.sentry.config.title')}</h3>
|
|
255
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
256
|
+
{t('monitoring.sentry.config.description')}
|
|
257
|
+
</p>
|
|
258
|
+
</div>
|
|
259
|
+
<div className="divide-y">
|
|
260
|
+
{CONFIG_FIELDS.map((field) => (
|
|
261
|
+
<ConfigFieldCard
|
|
262
|
+
key={field.key}
|
|
263
|
+
field={field}
|
|
264
|
+
value={config[field.key]}
|
|
265
|
+
visible={visibleFields.has(field.key)}
|
|
266
|
+
onChange={(v) => updateField(field.key, v)}
|
|
267
|
+
onToggleVisible={() => toggleVisible(field.key)}
|
|
268
|
+
/>
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
<div className="border-t px-6 py-4 flex items-center gap-3">
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
onClick={handleSave}
|
|
275
|
+
disabled={!changed || saving}
|
|
276
|
+
className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
|
|
277
|
+
>
|
|
278
|
+
{saving ? <RefreshCw className="size-4 animate-spin" /> : <Save className="size-4" />}
|
|
279
|
+
{t('monitoring.sentry.saveConfig')}
|
|
280
|
+
</button>
|
|
281
|
+
{changed && (
|
|
282
|
+
<span className="text-xs text-muted-foreground">{t('monitoring.sentry.unsaved')}</span>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{verifyResult && (
|
|
288
|
+
<div
|
|
289
|
+
className={`rounded-lg border p-4 text-sm flex items-start gap-3 ${verifyResult.ok
|
|
290
|
+
? 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950 text-green-700 dark:text-green-300'
|
|
291
|
+
: 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950 text-red-700 dark:text-red-300'
|
|
292
|
+
}`}
|
|
293
|
+
>
|
|
294
|
+
{verifyResult.ok ? <CheckCircle2 className="size-4 mt-0.5 shrink-0" /> : <XCircle className="size-4 mt-0.5 shrink-0" />}
|
|
295
|
+
<span>{verifyResult.detail}</span>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
<div className="rounded-lg border bg-card p-6">
|
|
300
|
+
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
301
|
+
<ShieldCheck className="size-5" />
|
|
302
|
+
{t('monitoring.sentry.verify.title')}
|
|
303
|
+
</h3>
|
|
304
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
305
|
+
{t('monitoring.sentry.verify.description')}
|
|
306
|
+
<h4 className="text-xs text-muted-foreground mb-4">
|
|
307
|
+
{t('monitoring.sentry.verify.note')}
|
|
308
|
+
</h4>
|
|
309
|
+
</p>
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
onClick={verifyConfig}
|
|
313
|
+
disabled={verifying || (!config.dsn && !import.meta.env.VITE_SENTRY_DSN)}
|
|
314
|
+
className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-40"
|
|
315
|
+
>
|
|
316
|
+
{verifying ? <RefreshCw className="size-4 animate-spin" /> : <ShieldCheck className="size-4" />}
|
|
317
|
+
{t('monitoring.sentry.verify.button')}
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function ConfigFieldCard({
|
|
325
|
+
field,
|
|
326
|
+
value,
|
|
327
|
+
visible,
|
|
328
|
+
onChange,
|
|
329
|
+
onToggleVisible,
|
|
330
|
+
}: {
|
|
331
|
+
field: ConfigField
|
|
332
|
+
value: string
|
|
333
|
+
visible: boolean
|
|
334
|
+
onChange: (v: string) => void
|
|
335
|
+
onToggleVisible: () => void
|
|
336
|
+
}) {
|
|
337
|
+
const { t } = useTranslation('common')
|
|
338
|
+
return (
|
|
339
|
+
<div className="px-6 py-5 space-y-3">
|
|
340
|
+
<div className="flex items-center justify-between">
|
|
341
|
+
<label className="text-sm font-medium">{field.label}</label>
|
|
342
|
+
{field.secret && (
|
|
343
|
+
<button
|
|
344
|
+
type="button"
|
|
345
|
+
onClick={onToggleVisible}
|
|
346
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
347
|
+
>
|
|
348
|
+
{visible ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
|
349
|
+
</button>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<div className="relative">
|
|
354
|
+
<input
|
|
355
|
+
type={field.secret && !visible ? 'password' : 'text'}
|
|
356
|
+
value={value}
|
|
357
|
+
onChange={(e) => onChange(e.target.value)}
|
|
358
|
+
placeholder={field.placeholder}
|
|
359
|
+
className="w-full rounded-lg border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-ring/50"
|
|
360
|
+
/>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<div className="rounded-lg bg-muted/50 p-3">
|
|
364
|
+
<p className="text-xs font-medium text-muted-foreground mb-1.5">{t('monitoring.sentry.fields.obtainMethod')}</p>
|
|
365
|
+
<ol className="list-decimal list-inside space-y-0.5">
|
|
366
|
+
{field.instruction.map((step, i) => (
|
|
367
|
+
<li key={i} className="text-xs text-muted-foreground">{step}</li>
|
|
368
|
+
))}
|
|
369
|
+
</ol>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function StatusCard({
|
|
376
|
+
title,
|
|
377
|
+
status,
|
|
378
|
+
icon: Icon,
|
|
379
|
+
description,
|
|
380
|
+
}: {
|
|
381
|
+
title: string
|
|
382
|
+
status: 'active' | 'inactive'
|
|
383
|
+
icon: React.ComponentType<{ className?: string }>
|
|
384
|
+
description: string
|
|
385
|
+
}) {
|
|
386
|
+
return (
|
|
387
|
+
<div className={`rounded-lg border p-4 ${status === 'active' ? 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950' : ''}`}>
|
|
388
|
+
<div className="flex items-center gap-3">
|
|
389
|
+
<div className="flex size-10 items-center justify-center rounded-full bg-background shadow-sm">
|
|
390
|
+
<Icon className={`size-5 ${status === 'active' ? 'text-green-500' : 'text-muted-foreground'}`} />
|
|
391
|
+
</div>
|
|
392
|
+
<div>
|
|
393
|
+
<p className="text-sm font-medium">{title}</p>
|
|
394
|
+
<p className="text-xs text-muted-foreground mt-0.5">{description}</p>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
)
|
|
399
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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 { createFileRoute } from '@tanstack/react-router'
|
|
19
|
+
|
|
20
|
+
export const Route = createFileRoute('/_nav/monitoring/sentry')({})
|