@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,165 @@
|
|
|
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 } from '@tanstack/react-router'
|
|
19
|
+
import { useState, useEffect } from 'react'
|
|
20
|
+
import { useTranslation } from 'react-i18next'
|
|
21
|
+
import { Activity, Filter, RefreshCw, Trash2 } from 'lucide-react'
|
|
22
|
+
import { PageHeader } from '@/components/common/page-header'
|
|
23
|
+
import { RequestLogTable } from '@/components/monitoring/request-log-table'
|
|
24
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
|
25
|
+
import { getRequestLogs } from '@/server/serverFns/monitoring/get-request-logs'
|
|
26
|
+
import { resetMonitoring } from '@/server/serverFns/monitoring/reset'
|
|
27
|
+
import { DataTablePagination } from '@/components/data-table/data-table-pagination'
|
|
28
|
+
import { toast } from 'sonner'
|
|
29
|
+
|
|
30
|
+
export const Route = createLazyFileRoute('/_nav/monitoring/logs')({
|
|
31
|
+
component: MonitoringLogs,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function MonitoringLogs() {
|
|
35
|
+
const { t } = useTranslation('common')
|
|
36
|
+
const [loading, setLoading] = useState(true)
|
|
37
|
+
const [logs, setLogs] = useState<any[]>([])
|
|
38
|
+
const [total, setTotal] = useState(0)
|
|
39
|
+
const [page, setPage] = useState(1)
|
|
40
|
+
const [method, setMethod] = useState('')
|
|
41
|
+
const [status, setStatus] = useState('')
|
|
42
|
+
const [path, setPath] = useState('')
|
|
43
|
+
const [filterMonitoring, setFilterMonitoring] = useState(false)
|
|
44
|
+
const limit = 50
|
|
45
|
+
|
|
46
|
+
const loadLogs = async () => {
|
|
47
|
+
setLoading(true)
|
|
48
|
+
try {
|
|
49
|
+
const result = await getRequestLogs({
|
|
50
|
+
data: {
|
|
51
|
+
limit,
|
|
52
|
+
offset: (page - 1) * limit,
|
|
53
|
+
method: method || undefined,
|
|
54
|
+
status: status ? Number(status) : undefined,
|
|
55
|
+
path: path || undefined,
|
|
56
|
+
excludeMonitoring: filterMonitoring,
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
setLogs(result.logs)
|
|
60
|
+
setTotal(result.total)
|
|
61
|
+
} catch {
|
|
62
|
+
// ignore
|
|
63
|
+
}
|
|
64
|
+
setLoading(false)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
loadLogs()
|
|
69
|
+
}, [page, method, status, path, filterMonitoring])
|
|
70
|
+
|
|
71
|
+
const handleClearOld = async () => {
|
|
72
|
+
try {
|
|
73
|
+
await resetMonitoring({ data: { type: 'request-logs', days: 7 } })
|
|
74
|
+
toast.success(t('monitoring.logs.clearSuccess'))
|
|
75
|
+
loadLogs()
|
|
76
|
+
} catch {
|
|
77
|
+
toast.error(t('monitoring.logs.clearFailed'))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex flex-col flex-1 h-full space-y-4 my-4 overflow-hidden">
|
|
83
|
+
<PageHeader title={t('monitoring.logs.title')} description={t('monitoring.logs.description')} icon={Activity} />
|
|
84
|
+
|
|
85
|
+
<div className="flex items-center gap-3 rounded-lg border bg-card p-3 shrink-0">
|
|
86
|
+
<Filter className="size-4 text-muted-foreground" />
|
|
87
|
+
<select
|
|
88
|
+
value={method}
|
|
89
|
+
onChange={(e) => { setMethod(e.target.value); setPage(1) }}
|
|
90
|
+
className="rounded-md border bg-background px-2 py-1 text-sm"
|
|
91
|
+
>
|
|
92
|
+
<option value="">{t('monitoring.logs.filter.allMethods')}</option>
|
|
93
|
+
<option value="GET">GET</option>
|
|
94
|
+
<option value="POST">POST</option>
|
|
95
|
+
<option value="PUT">PUT</option>
|
|
96
|
+
<option value="DELETE">DELETE</option>
|
|
97
|
+
</select>
|
|
98
|
+
<select
|
|
99
|
+
value={status}
|
|
100
|
+
onChange={(e) => { setStatus(e.target.value); setPage(1) }}
|
|
101
|
+
className="rounded-md border bg-background px-2 py-1 text-sm"
|
|
102
|
+
>
|
|
103
|
+
<option value="">{t('monitoring.logs.filter.allStatus')}</option>
|
|
104
|
+
<option value="200">200</option>
|
|
105
|
+
<option value="400">4xx</option>
|
|
106
|
+
<option value="500">5xx</option>
|
|
107
|
+
</select>
|
|
108
|
+
<input
|
|
109
|
+
type="text"
|
|
110
|
+
value={path}
|
|
111
|
+
onChange={(e) => { setPath(e.target.value); setPage(1) }}
|
|
112
|
+
placeholder={t('monitoring.logs.filter.searchPath')}
|
|
113
|
+
className="flex-1 rounded-md border bg-background px-2 py-1 text-sm outline-none focus:ring-2 focus:ring-ring/50"
|
|
114
|
+
/>
|
|
115
|
+
<button
|
|
116
|
+
onClick={loadLogs}
|
|
117
|
+
disabled={loading}
|
|
118
|
+
className="inline-flex items-center gap-1 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
|
|
119
|
+
>
|
|
120
|
+
<RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />
|
|
121
|
+
{t('monitoring.logs.query')}
|
|
122
|
+
</button>
|
|
123
|
+
<button
|
|
124
|
+
onClick={handleClearOld}
|
|
125
|
+
className="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-accent transition-colors"
|
|
126
|
+
>
|
|
127
|
+
<Trash2 className="size-4" />
|
|
128
|
+
{t('monitoring.logs.clear7Days')}
|
|
129
|
+
</button>
|
|
130
|
+
<label className="inline-flex items-center gap-1.5 text-sm shrink-0 cursor-pointer">
|
|
131
|
+
<Checkbox
|
|
132
|
+
checked={filterMonitoring}
|
|
133
|
+
onCheckedChange={(checked) => {
|
|
134
|
+
setFilterMonitoring(Boolean(checked))
|
|
135
|
+
setPage(1)
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
{t('monitoring.logs.filterMonitoring')}
|
|
139
|
+
</label>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{loading ? (
|
|
143
|
+
<div className="flex items-center justify-center h-32 shrink-0">
|
|
144
|
+
<RefreshCw className="size-5 animate-spin text-muted-foreground" />
|
|
145
|
+
</div>
|
|
146
|
+
) : logs.length > 0 ? (
|
|
147
|
+
<div className="flex-1 min-h-0 flex flex-col">
|
|
148
|
+
<RequestLogTable
|
|
149
|
+
logs={logs}
|
|
150
|
+
actions={
|
|
151
|
+
<DataTablePagination
|
|
152
|
+
page={page}
|
|
153
|
+
totalPages={Math.ceil(total / limit)}
|
|
154
|
+
total={total}
|
|
155
|
+
onPageChange={setPage}
|
|
156
|
+
/>
|
|
157
|
+
}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
) : (
|
|
161
|
+
<div className="flex items-center justify-center h-32 text-sm text-muted-foreground shrink-0">{t('monitoring.logs.empty')}</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
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 { describeMetric } from '@/lib/monitoring-descriptions'
|
|
19
|
+
import { getBenchmark, getBenchmarkSource, compareDuration } from '@/lib/monitoring-benchmarks'
|
|
20
|
+
import { formatTimestamp } from '@allbluecn/web-app/lib/datetime'
|
|
21
|
+
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
22
|
+
import { useState, useEffect } from 'react'
|
|
23
|
+
import { useTranslation } from 'react-i18next'
|
|
24
|
+
import { BarChart3, RefreshCw, Trash2, MemoryStick } from 'lucide-react'
|
|
25
|
+
import { ArrowDown, ArrowUp } from 'lucide-react'
|
|
26
|
+
import { PageHeader } from '@/components/common/page-header'
|
|
27
|
+
import { MetricsStats } from '@/components/monitoring/metrics-stats'
|
|
28
|
+
import { getMetrics } from '@/server/serverFns/monitoring/get-metrics'
|
|
29
|
+
import { resetMonitoring } from '@/server/serverFns/monitoring/reset'
|
|
30
|
+
import { DataTable } from '@/components/data-table/data-table'
|
|
31
|
+
import { toast } from 'sonner'
|
|
32
|
+
|
|
33
|
+
export const Route = createLazyFileRoute('/_nav/monitoring/metrics')({
|
|
34
|
+
component: MonitoringMetrics,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
function formatBytes(bytes: number): string {
|
|
38
|
+
if (bytes < 1024) return `${bytes}B`
|
|
39
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`
|
|
40
|
+
return `${(bytes / 1048576).toFixed(1)}MB`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function MonitoringMetrics() {
|
|
44
|
+
const { t } = useTranslation('common')
|
|
45
|
+
const [loading, setLoading] = useState(true)
|
|
46
|
+
const [data, setData] = useState<any>(null)
|
|
47
|
+
|
|
48
|
+
const loadData = async () => {
|
|
49
|
+
setLoading(true)
|
|
50
|
+
try {
|
|
51
|
+
const result = await getMetrics()
|
|
52
|
+
setData(result)
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore
|
|
55
|
+
}
|
|
56
|
+
setLoading(false)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
loadData()
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
const handleReset = async () => {
|
|
64
|
+
try {
|
|
65
|
+
await resetMonitoring({ data: { type: 'metrics' } })
|
|
66
|
+
toast.success(t('monitoring.metrics.clearSuccess'))
|
|
67
|
+
loadData()
|
|
68
|
+
} catch {
|
|
69
|
+
toast.error(t('monitoring.metrics.clearFailed'))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex flex-col flex-1 h-full space-y-4 my-4 overflow-hidden">
|
|
75
|
+
<div className="flex items-center justify-between">
|
|
76
|
+
<PageHeader title={t('monitoring.metrics.title')} description={t('monitoring.metrics.description')} icon={BarChart3} />
|
|
77
|
+
<div className="flex items-center gap-2">
|
|
78
|
+
<button
|
|
79
|
+
onClick={loadData}
|
|
80
|
+
disabled={loading}
|
|
81
|
+
className="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-accent transition-colors disabled:opacity-50"
|
|
82
|
+
>
|
|
83
|
+
<RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />
|
|
84
|
+
{t('common.refresh')}
|
|
85
|
+
</button>
|
|
86
|
+
<button
|
|
87
|
+
onClick={handleReset}
|
|
88
|
+
className="inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-accent transition-colors"
|
|
89
|
+
>
|
|
90
|
+
<Trash2 className="size-4" />
|
|
91
|
+
{t('monitoring.metrics.clearData')}
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="rounded-lg border bg-card p-4">
|
|
97
|
+
<div className="flex items-center gap-2 mb-3">
|
|
98
|
+
<MemoryStick className="size-4 text-muted-foreground" />
|
|
99
|
+
<h3 className="text-sm font-medium">{t('monitoring.metrics.memoryUsage')}</h3>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
102
|
+
<div>
|
|
103
|
+
<p className="text-xs text-muted-foreground">RSS</p>
|
|
104
|
+
<p className="text-sm font-medium">{data?.memory?.rss ? formatBytes(data.memory.rss) : '-'}</p>
|
|
105
|
+
</div>
|
|
106
|
+
<div>
|
|
107
|
+
<p className="text-xs text-muted-foreground">Heap Total</p>
|
|
108
|
+
<p className="text-sm font-medium">{data?.memory?.heapTotal ? formatBytes(data.memory.heapTotal) : '-'}</p>
|
|
109
|
+
</div>
|
|
110
|
+
<div>
|
|
111
|
+
<p className="text-xs text-muted-foreground">Heap Used</p>
|
|
112
|
+
<p className="text-sm font-medium">{data?.memory?.heapUsed ? formatBytes(data.memory.heapUsed) : '-'}</p>
|
|
113
|
+
</div>
|
|
114
|
+
<div>
|
|
115
|
+
<p className="text-xs text-muted-foreground">External</p>
|
|
116
|
+
<p className="text-sm font-medium">{data?.memory?.external ? formatBytes(data.memory.external) : '-'}</p>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{loading ? (
|
|
122
|
+
<div className="flex items-center justify-center h-32">
|
|
123
|
+
<RefreshCw className="size-5 animate-spin text-muted-foreground" />
|
|
124
|
+
</div>
|
|
125
|
+
) : data?.current && Object.keys(data.current).length > 0 ? (
|
|
126
|
+
<MetricsStats data={data.current} />
|
|
127
|
+
) : (
|
|
128
|
+
<div className="flex items-center justify-center h-32 text-sm text-muted-foreground">{t('monitoring.metrics.empty')}</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{data?.history?.length > 0 && (
|
|
132
|
+
<div className="rounded-lg border bg-card flex-1 min-h-0 flex flex-col">
|
|
133
|
+
<div className="border-b px-4 py-3 shrink-0">
|
|
134
|
+
<h3 className="text-sm font-medium">{t('monitoring.metrics.history')}</h3>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="p-4 flex-1 min-h-0">
|
|
137
|
+
<MetricsHistoryTable data={data.history} />
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface MetricsHistoryRow {
|
|
146
|
+
metricName: string
|
|
147
|
+
count: number
|
|
148
|
+
avg: number
|
|
149
|
+
p95: number
|
|
150
|
+
p99: number
|
|
151
|
+
recordedAt: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function MetricsBenchmarkCell({ value, metricName }: { value: number; metricName: string }) {
|
|
155
|
+
const path = metricName.replace(/^request\./, '')
|
|
156
|
+
const benchmark = getBenchmark(path)
|
|
157
|
+
const comp = compareDuration(value, benchmark)
|
|
158
|
+
const isGood = value <= benchmark
|
|
159
|
+
const pct = Math.round((Math.abs(value - benchmark) / benchmark) * 100)
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<span className="inline-flex items-center gap-1">
|
|
163
|
+
{value.toFixed(1)}ms
|
|
164
|
+
{isGood ? (
|
|
165
|
+
<span
|
|
166
|
+
className="inline-flex items-center gap-0.5 text-green-600 dark:text-green-400"
|
|
167
|
+
title={`${getBenchmarkSource(path)}: ${benchmark}ms P95`}
|
|
168
|
+
>
|
|
169
|
+
<ArrowUp className="size-3" />
|
|
170
|
+
<span className="text-[10px]">{pct}%</span>
|
|
171
|
+
</span>
|
|
172
|
+
) : (
|
|
173
|
+
<span
|
|
174
|
+
className="inline-flex items-center gap-0.5 text-red-600 dark:text-red-400"
|
|
175
|
+
title={`${getBenchmarkSource(path)}: ${benchmark}ms P95`}
|
|
176
|
+
>
|
|
177
|
+
<ArrowDown className="size-3" strokeWidth={3} />
|
|
178
|
+
<span className="text-[10px] font-bold">{pct}%</span>
|
|
179
|
+
</span>
|
|
180
|
+
)}
|
|
181
|
+
</span>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function MetricsHistoryTable({ data }: { data: MetricsHistoryRow[] }) {
|
|
186
|
+
const { t } = useTranslation('common')
|
|
187
|
+
const columns = [
|
|
188
|
+
{
|
|
189
|
+
header: t('monitoring.metrics.columns.time'),
|
|
190
|
+
accessorKey: 'recordedAt',
|
|
191
|
+
meta: { style: { width: 30 } },
|
|
192
|
+
cell: ({ row }: { row: { original: MetricsHistoryRow } }) => (
|
|
193
|
+
<span className="text-xs">
|
|
194
|
+
{formatTimestamp(row.original.recordedAt, 'yyyy-MM-dd HH:mm:ss')}
|
|
195
|
+
</span>
|
|
196
|
+
),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
header: t('monitoring.metrics.columns.metricName'),
|
|
200
|
+
accessorKey: 'metricName',
|
|
201
|
+
meta: { style: { width: 150 } },
|
|
202
|
+
cell: ({ row }: { row: { original: MetricsHistoryRow } }) => (
|
|
203
|
+
<span className="font-mono text-xs block truncate">
|
|
204
|
+
{row.original.metricName}
|
|
205
|
+
</span>
|
|
206
|
+
),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
header: t('monitoring.metrics.columns.count'),
|
|
210
|
+
accessorKey: 'count',
|
|
211
|
+
meta: { style: { width: 15, textAlign: 'right' } },
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
header: t('monitoring.metrics.columns.avg'),
|
|
215
|
+
accessorKey: 'avg',
|
|
216
|
+
meta: { style: { width: 30, textAlign: 'right' } },
|
|
217
|
+
cell: ({ row }: { row: { original: MetricsHistoryRow } }) => (
|
|
218
|
+
<MetricsBenchmarkCell value={row.original.avg} metricName={row.original.metricName} />
|
|
219
|
+
),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
header: 'P95',
|
|
223
|
+
accessorKey: 'p95',
|
|
224
|
+
meta: { style: { width: 30, textAlign: 'right' } },
|
|
225
|
+
cell: ({ row }: { row: { original: MetricsHistoryRow } }) => (
|
|
226
|
+
<MetricsBenchmarkCell value={row.original.p95} metricName={row.original.metricName} />
|
|
227
|
+
),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
header: 'P99',
|
|
231
|
+
accessorKey: 'p99',
|
|
232
|
+
meta: { style: { width: 30, textAlign: 'right' } },
|
|
233
|
+
cell: ({ row }: { row: { original: MetricsHistoryRow } }) => (
|
|
234
|
+
<MetricsBenchmarkCell value={row.original.p99} metricName={row.original.metricName} />
|
|
235
|
+
),
|
|
236
|
+
},
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
return <DataTable columns={columns as any} data={data} />
|
|
240
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
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 } from '@tanstack/react-router'
|
|
19
|
+
import { useState, useEffect } from 'react'
|
|
20
|
+
import { useTranslation } from 'react-i18next'
|
|
21
|
+
import { Activity, RefreshCw } from 'lucide-react'
|
|
22
|
+
import { PageHeader } from '@/components/common/page-header'
|
|
23
|
+
import { HealthCards } from '@/components/monitoring/health-cards'
|
|
24
|
+
import { ErrorList } from '@/components/monitoring/error-list'
|
|
25
|
+
import { MetricsStats } from '@/components/monitoring/metrics-stats'
|
|
26
|
+
import { getHealthStatus } from '@/server/serverFns/monitoring/get-health'
|
|
27
|
+
import { getErrors } from '@/server/serverFns/monitoring/get-errors'
|
|
28
|
+
import { getMetrics } from '@/server/serverFns/monitoring/get-metrics'
|
|
29
|
+
|
|
30
|
+
export const Route = createLazyFileRoute('/_nav/monitoring/overview')({
|
|
31
|
+
component: MonitoringOverview,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function MonitoringOverview() {
|
|
35
|
+
const { t } = useTranslation('common')
|
|
36
|
+
const [loading, setLoading] = useState(true)
|
|
37
|
+
const [health, setHealth] = useState<any>(null)
|
|
38
|
+
const [errors, setErrors] = useState<any>(null)
|
|
39
|
+
const [metrics, setMetrics] = useState<any>(null)
|
|
40
|
+
|
|
41
|
+
const loadData = async () => {
|
|
42
|
+
setLoading(true)
|
|
43
|
+
try {
|
|
44
|
+
const [h, e, m] = await Promise.all([
|
|
45
|
+
getHealthStatus(),
|
|
46
|
+
getErrors(),
|
|
47
|
+
getMetrics(),
|
|
48
|
+
])
|
|
49
|
+
setHealth(h)
|
|
50
|
+
setErrors(e)
|
|
51
|
+
setMetrics(m)
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
setLoading(false)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
loadData()
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
if (loading) {
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex items-center justify-center h-64">
|
|
65
|
+
<RefreshCw className="size-5 animate-spin text-muted-foreground" />
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="space-y-4 my-4">
|
|
72
|
+
<div className="flex items-center justify-between">
|
|
73
|
+
<PageHeader title={t('monitoring.overview.title')} description={t('monitoring.overview.description')} icon={Activity} />
|
|
74
|
+
<button
|
|
75
|
+
onClick={loadData}
|
|
76
|
+
className="inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium hover:bg-accent transition-colors"
|
|
77
|
+
>
|
|
78
|
+
<RefreshCw className="size-4" />
|
|
79
|
+
{t('common.refresh')}
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<HealthCards health={health?.current || {}} />
|
|
84
|
+
|
|
85
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
86
|
+
<div className="rounded-lg border bg-card">
|
|
87
|
+
<div className="border-b px-4 py-3">
|
|
88
|
+
<h3 className="text-sm font-medium">{t('monitoring.overview.requestStats.title')}</h3>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="p-4 grid grid-cols-2 gap-3">
|
|
91
|
+
<div>
|
|
92
|
+
<p className="text-xs text-muted-foreground">{t('monitoring.overview.requestStats.totalRequests')}</p>
|
|
93
|
+
<p className="text-lg font-semibold">{health?.requestStats?.total || 0}</p>
|
|
94
|
+
</div>
|
|
95
|
+
<div>
|
|
96
|
+
<p className="text-xs text-muted-foreground">{t('monitoring.overview.requestStats.avgDuration')}</p>
|
|
97
|
+
<p className="text-lg font-semibold">{health?.requestStats?.avgDuration?.toFixed(1) || 0}ms</p>
|
|
98
|
+
</div>
|
|
99
|
+
<div>
|
|
100
|
+
<p className="text-xs text-muted-foreground">{t('monitoring.overview.requestStats.maxDuration')}</p>
|
|
101
|
+
<p className="text-lg font-semibold">{health?.requestStats?.maxDuration || 0}ms</p>
|
|
102
|
+
</div>
|
|
103
|
+
<div>
|
|
104
|
+
<p className="text-xs text-muted-foreground">{t('monitoring.overview.requestStats.errorTypes')}</p>
|
|
105
|
+
<p className="text-lg font-semibold">{health?.errorCount || 0}</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div className="rounded-lg border bg-card">
|
|
111
|
+
<div className="border-b px-4 py-3">
|
|
112
|
+
<h3 className="text-sm font-medium">{t('monitoring.overview.recentErrors.title')}</h3>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="p-4">
|
|
115
|
+
{errors?.errors?.slice(0, 5).length > 0 ? (
|
|
116
|
+
<ErrorList errors={errors.errors.slice(0, 5)} />
|
|
117
|
+
) : (
|
|
118
|
+
<p className="text-sm text-muted-foreground text-center py-4">{t('monitoring.overview.recentErrors.empty')}</p>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div className="rounded-lg border bg-card">
|
|
125
|
+
<div className="border-b px-4 py-3">
|
|
126
|
+
<h3 className="text-sm font-medium">{t('monitoring.overview.metricsSnapshot.title')}</h3>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="p-4">
|
|
129
|
+
{metrics?.current && Object.keys(metrics.current).length > 0 ? (
|
|
130
|
+
<MetricsStats data={metrics.current} />
|
|
131
|
+
) : (
|
|
132
|
+
<p className="text-sm text-muted-foreground text-center py-4">{t('monitoring.overview.metricsSnapshot.empty')}</p>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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, Outlet, useLocation } from '@tanstack/react-router'
|
|
19
|
+
import { useTranslation } from 'react-i18next'
|
|
20
|
+
import { SidebarTrigger } from '@/components/ui/sidebar'
|
|
21
|
+
import { Separator } from '@/components/ui/separator'
|
|
22
|
+
|
|
23
|
+
export const Route = createLazyFileRoute('/_nav/monitoring')({
|
|
24
|
+
component: MonitoringLayout,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
function MonitoringLayout() {
|
|
28
|
+
const { t } = useTranslation('common')
|
|
29
|
+
const location = useLocation()
|
|
30
|
+
|
|
31
|
+
const subNavItems = [
|
|
32
|
+
{ id: 'overview', label: t('monitoring.nav.overview'), path: '/monitoring/overview' },
|
|
33
|
+
{ id: 'logs', label: t('monitoring.nav.logs'), path: '/monitoring/logs' },
|
|
34
|
+
{ id: 'errors', label: t('monitoring.nav.errors'), path: '/monitoring/errors' },
|
|
35
|
+
{ id: 'metrics', label: t('monitoring.nav.metrics'), path: '/monitoring/metrics' },
|
|
36
|
+
{ id: 'health', label: t('monitoring.nav.health'), path: '/monitoring/health' },
|
|
37
|
+
{ id: 'sentry', label: t('monitoring.nav.sentry'), path: '/monitoring/sentry' },
|
|
38
|
+
{ id: 'ai-usage', label: t('monitoring.nav.aiUsage'), path: '/monitoring/ai-usage' },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const currentId = subNavItems.find((item) => location.pathname.startsWith(item.path))?.id || 'overview'
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex flex-col min-h-0 h-full overflow-hidden">
|
|
45
|
+
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
|
|
46
|
+
<SidebarTrigger className="-ml-1" />
|
|
47
|
+
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
48
|
+
<nav className="flex gap-1">
|
|
49
|
+
{subNavItems.map((item) => (
|
|
50
|
+
<a
|
|
51
|
+
key={item.id}
|
|
52
|
+
href={item.path}
|
|
53
|
+
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${currentId === item.id
|
|
54
|
+
? 'bg-accent text-accent-foreground'
|
|
55
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
|
|
56
|
+
}`}
|
|
57
|
+
>
|
|
58
|
+
{item.label}
|
|
59
|
+
</a>
|
|
60
|
+
))}
|
|
61
|
+
</nav>
|
|
62
|
+
</header>
|
|
63
|
+
<div className="flex-1 min-h-0 p-4 pt-0 flex flex-col">
|
|
64
|
+
<Outlet />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -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')({})
|