@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,237 @@
|
|
|
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
|
+
|
|
19
|
+
import { useEffect, useState } from "react"
|
|
20
|
+
import { useTranslation } from 'react-i18next'
|
|
21
|
+
import { useTheme } from "next-themes"
|
|
22
|
+
import { MarketAppFormData } from "@/lib/anyhub/types"
|
|
23
|
+
import { CAPABILITY_GROUPS } from "@/lib/anyhub/capabilities"
|
|
24
|
+
import {
|
|
25
|
+
Dialog,
|
|
26
|
+
DialogContent,
|
|
27
|
+
DialogFooter,
|
|
28
|
+
DialogHeader,
|
|
29
|
+
DialogTitle,
|
|
30
|
+
} from "@/components/ui/dialog"
|
|
31
|
+
import { Button } from "@/components/ui/button"
|
|
32
|
+
import { Input } from "@/components/ui/input"
|
|
33
|
+
import { Label } from "@/components/ui/label"
|
|
34
|
+
import {
|
|
35
|
+
Select,
|
|
36
|
+
SelectContent,
|
|
37
|
+
SelectItem,
|
|
38
|
+
SelectTrigger,
|
|
39
|
+
SelectValue,
|
|
40
|
+
} from "@/components/ui/select"
|
|
41
|
+
import { toast } from '@/components/ui/sonner'
|
|
42
|
+
|
|
43
|
+
interface MarketAppFormProps {
|
|
44
|
+
open: boolean
|
|
45
|
+
onOpenChange: (open: boolean) => void
|
|
46
|
+
editData?: { id: string; data: MarketAppFormData } | null
|
|
47
|
+
onSuccess: () => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const CATEGORY_KEYS = ["productivity", "ai-assistant", "project-mgmt", "docs-collab", "data-analysis", "other"] as const
|
|
51
|
+
|
|
52
|
+
const emptyForm: MarketAppFormData = {
|
|
53
|
+
name: "",
|
|
54
|
+
displayName: "",
|
|
55
|
+
description: "",
|
|
56
|
+
entryUrl: "",
|
|
57
|
+
icon: "",
|
|
58
|
+
category: "",
|
|
59
|
+
tags: "",
|
|
60
|
+
capabilities: {},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function MarketAppForm({ open, onOpenChange, editData, onSuccess }: MarketAppFormProps) {
|
|
64
|
+
const [form, setForm] = useState<MarketAppFormData>(emptyForm)
|
|
65
|
+
const [loading, setLoading] = useState(false)
|
|
66
|
+
const { t } = useTranslation('common')
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (editData) {
|
|
70
|
+
setForm(editData.data)
|
|
71
|
+
} else {
|
|
72
|
+
setForm(emptyForm)
|
|
73
|
+
}
|
|
74
|
+
}, [editData, open])
|
|
75
|
+
|
|
76
|
+
const updateField = (field: keyof MarketAppFormData, value: string) => {
|
|
77
|
+
setForm((prev) => ({ ...prev, [field]: value }))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const toggleCapability = (capId: string) => {
|
|
81
|
+
setForm((prev) => ({
|
|
82
|
+
...prev,
|
|
83
|
+
capabilities: { ...prev.capabilities, [capId]: !prev.capabilities[capId] },
|
|
84
|
+
}))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handleSubmit = async () => {
|
|
88
|
+
if (!form.name.trim() || !form.displayName.trim() || !form.entryUrl.trim()) {
|
|
89
|
+
toast.error(t('market.form.fillRequired'))
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setLoading(true)
|
|
94
|
+
try {
|
|
95
|
+
const url = editData ? `/api/anyhub/market/${editData.id}` : "/api/anyhub/market"
|
|
96
|
+
const method = editData ? "PATCH" : "POST"
|
|
97
|
+
const res = await fetch(url, {
|
|
98
|
+
method,
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
body: JSON.stringify(form),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
const err = await res.json()
|
|
105
|
+
throw new Error(err.error || t('market.form.operationFailed'))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
toast.success(editData ? t('market.form.updated') : t('market.form.created'))
|
|
109
|
+
onOpenChange(false)
|
|
110
|
+
onSuccess()
|
|
111
|
+
} catch (e) {
|
|
112
|
+
toast.error(e instanceof Error ? e.message : t('market.form.operationFailed'))
|
|
113
|
+
} finally {
|
|
114
|
+
setLoading(false)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
120
|
+
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-lg">
|
|
121
|
+
<DialogHeader>
|
|
122
|
+
<DialogTitle>{editData ? t('market.form.edit') : t('market.form.create')}</DialogTitle>
|
|
123
|
+
</DialogHeader>
|
|
124
|
+
|
|
125
|
+
<div className="grid gap-4 py-4">
|
|
126
|
+
<div className="grid gap-2">
|
|
127
|
+
<Label>{t('market.form.name')}</Label>
|
|
128
|
+
<Input
|
|
129
|
+
value={form.name}
|
|
130
|
+
onChange={(e) => updateField("name", e.target.value)}
|
|
131
|
+
placeholder={t('market.form.namePlaceholder')}
|
|
132
|
+
disabled={!!editData}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="grid gap-2">
|
|
137
|
+
<Label>{t('market.form.displayName')}</Label>
|
|
138
|
+
<Input
|
|
139
|
+
value={form.displayName}
|
|
140
|
+
onChange={(e) => updateField("displayName", e.target.value)}
|
|
141
|
+
placeholder={t('market.form.displayNamePlaceholder')}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="grid gap-2">
|
|
146
|
+
<Label>{t('market.form.entryUrl')}</Label>
|
|
147
|
+
<Input
|
|
148
|
+
value={form.entryUrl}
|
|
149
|
+
onChange={(e) => updateField("entryUrl", e.target.value)}
|
|
150
|
+
placeholder="https://..."
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div className="grid gap-2">
|
|
155
|
+
<Label>{t('market.form.description')}</Label>
|
|
156
|
+
<Input
|
|
157
|
+
value={form.description || ""}
|
|
158
|
+
onChange={(e) => updateField("description", e.target.value)}
|
|
159
|
+
placeholder={t('market.form.descriptionPlaceholder')}
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div className="grid gap-2">
|
|
164
|
+
<Label>{t('market.form.icon')}</Label>
|
|
165
|
+
<Input
|
|
166
|
+
value={form.icon || ""}
|
|
167
|
+
onChange={(e) => updateField("icon", e.target.value)}
|
|
168
|
+
placeholder={t('market.form.iconPlaceholder')}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div className="grid gap-2">
|
|
173
|
+
<Label>{t('market.form.category')}</Label>
|
|
174
|
+
<Select value={form.category || ""} onValueChange={(v) => updateField("category", v === "__none__" ? "" : v)}>
|
|
175
|
+
<SelectTrigger>
|
|
176
|
+
<SelectValue placeholder={t('market.form.categoryPlaceholder')} />
|
|
177
|
+
</SelectTrigger>
|
|
178
|
+
<SelectContent>
|
|
179
|
+
<SelectItem value="__none__">{t('common.none')}</SelectItem>
|
|
180
|
+
{CATEGORY_KEYS.map((k) => (
|
|
181
|
+
<SelectItem key={k} value={t(`market.categories.${k}`)}>
|
|
182
|
+
{t(`market.categories.${k}`)}
|
|
183
|
+
</SelectItem>
|
|
184
|
+
))}
|
|
185
|
+
</SelectContent>
|
|
186
|
+
</Select>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div className="grid gap-2">
|
|
190
|
+
<Label>{t('market.form.tags')}</Label>
|
|
191
|
+
<Input
|
|
192
|
+
value={form.tags || ""}
|
|
193
|
+
onChange={(e) => updateField("tags", e.target.value)}
|
|
194
|
+
placeholder={t('market.form.tagsPlaceholder')}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div className="grid gap-2">
|
|
199
|
+
<Label>{t('market.form.capabilities')}</Label>
|
|
200
|
+
<div className="space-y-3 rounded-md border p-3">
|
|
201
|
+
{CAPABILITY_GROUPS.map((group) => (
|
|
202
|
+
<div key={group.id}>
|
|
203
|
+
<p className="mb-1.5 text-sm font-medium">{group.label}</p>
|
|
204
|
+
<div className="flex flex-wrap gap-3">
|
|
205
|
+
{group.capabilities.map((cap) => (
|
|
206
|
+
<label
|
|
207
|
+
key={cap.id}
|
|
208
|
+
className="flex cursor-pointer items-center gap-1.5 text-sm"
|
|
209
|
+
>
|
|
210
|
+
<input
|
|
211
|
+
type="checkbox"
|
|
212
|
+
checked={!!form.capabilities[cap.id]}
|
|
213
|
+
onChange={() => toggleCapability(cap.id)}
|
|
214
|
+
className="rounded border-input"
|
|
215
|
+
/>
|
|
216
|
+
{cap.label}
|
|
217
|
+
</label>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<DialogFooter>
|
|
227
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
228
|
+
{t('common.cancel')}
|
|
229
|
+
</Button>
|
|
230
|
+
<Button onClick={handleSubmit} disabled={loading}>
|
|
231
|
+
{loading ? t('status.loading') : editData ? t('common.save') : t('common.create')}
|
|
232
|
+
</Button>
|
|
233
|
+
</DialogFooter>
|
|
234
|
+
</DialogContent>
|
|
235
|
+
</Dialog>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
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 { useCallback, useEffect, useState } from "react"
|
|
19
|
+
import { useTranslation } from 'react-i18next'
|
|
20
|
+
import { DataTable } from "@/components/data-table/data-table"
|
|
21
|
+
import { getMarketColumns, MarketAppRow } from "./columns"
|
|
22
|
+
import { MarketAppForm } from "./market-app-form"
|
|
23
|
+
import { MarketAppFormData } from "@/lib/anyhub/types"
|
|
24
|
+
import { Button } from "@/components/ui/button"
|
|
25
|
+
import { PlusIcon } from "lucide-react"
|
|
26
|
+
import { toast } from '@/components/ui/sonner'
|
|
27
|
+
import { listMarketAppsFn, updateMarketAppFn, deleteMarketAppFn } from "@/server/serverFns/anyhub/market"
|
|
28
|
+
import {
|
|
29
|
+
AlertDialog,
|
|
30
|
+
AlertDialogAction,
|
|
31
|
+
AlertDialogCancel,
|
|
32
|
+
AlertDialogContent,
|
|
33
|
+
AlertDialogDescription,
|
|
34
|
+
AlertDialogFooter,
|
|
35
|
+
AlertDialogHeader,
|
|
36
|
+
AlertDialogTitle,
|
|
37
|
+
} from "@/components/ui/alert-dialog"
|
|
38
|
+
|
|
39
|
+
export function MarketAppList() {
|
|
40
|
+
const { t } = useTranslation('common')
|
|
41
|
+
const [apps, setApps] = useState<MarketAppRow[]>([])
|
|
42
|
+
const [loading, setLoading] = useState(true)
|
|
43
|
+
const [formOpen, setFormOpen] = useState(false)
|
|
44
|
+
const [editData, setEditData] = useState<{ id: string; data: MarketAppFormData } | null>(null)
|
|
45
|
+
const [deleteTarget, setDeleteTarget] = useState<MarketAppRow | null>(null)
|
|
46
|
+
|
|
47
|
+
const fetchApps = useCallback(async () => {
|
|
48
|
+
try {
|
|
49
|
+
const data = await listMarketAppsFn({ data: {} })
|
|
50
|
+
setApps(data.items.map((app) => ({
|
|
51
|
+
...app,
|
|
52
|
+
capabilities: (app.capabilities || {}) as Record<string, boolean>,
|
|
53
|
+
createdAt: app.createdAt.toISOString(),
|
|
54
|
+
updatedAt: app.updatedAt?.toISOString() ?? '',
|
|
55
|
+
publishedAt: app.publishedAt?.toISOString() ?? null,
|
|
56
|
+
})))
|
|
57
|
+
} catch {
|
|
58
|
+
toast.error(t('market.fetchFailed'))
|
|
59
|
+
} finally {
|
|
60
|
+
setLoading(false)
|
|
61
|
+
}
|
|
62
|
+
}, [t])
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
fetchApps()
|
|
66
|
+
}, [fetchApps])
|
|
67
|
+
|
|
68
|
+
const handleEdit = (app: MarketAppRow) => {
|
|
69
|
+
setEditData({
|
|
70
|
+
id: app.id,
|
|
71
|
+
data: {
|
|
72
|
+
name: app.name,
|
|
73
|
+
displayName: app.displayName,
|
|
74
|
+
description: app.description || "",
|
|
75
|
+
entryUrl: app.entryUrl,
|
|
76
|
+
icon: app.icon || "",
|
|
77
|
+
category: app.category || "",
|
|
78
|
+
tags: app.tags || "",
|
|
79
|
+
capabilities: app.capabilities || {},
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
setFormOpen(true)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const handleToggleStatus = async (app: MarketAppRow) => {
|
|
86
|
+
const newStatus = app.status === "PUBLISHED" ? "UNPUBLISHED" : "PUBLISHED"
|
|
87
|
+
try {
|
|
88
|
+
await updateMarketAppFn({ data: { id: app.id, isPublished: newStatus === "PUBLISHED" } })
|
|
89
|
+
toast.success(t('market.statusUpdated', { status: newStatus === "PUBLISHED" ? 'published' : 'unpublished' }))
|
|
90
|
+
fetchApps()
|
|
91
|
+
} catch {
|
|
92
|
+
toast.error(t('market.operationFailed'))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const handleDelete = async () => {
|
|
97
|
+
if (!deleteTarget) return
|
|
98
|
+
try {
|
|
99
|
+
await deleteMarketAppFn({ data: { id: deleteTarget.id } })
|
|
100
|
+
toast.success(t('market.deleted'))
|
|
101
|
+
setDeleteTarget(null)
|
|
102
|
+
fetchApps()
|
|
103
|
+
} catch {
|
|
104
|
+
toast.error(t('market.deleteFailed'))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const columns = getMarketColumns(handleEdit, handleToggleStatus, (app) => setDeleteTarget(app))
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="space-y-4">
|
|
112
|
+
<div className="flex items-center justify-between">
|
|
113
|
+
<h1 className="text-2xl font-bold">{t('market.title')}</h1>
|
|
114
|
+
<Button
|
|
115
|
+
onClick={() => {
|
|
116
|
+
setEditData(null)
|
|
117
|
+
setFormOpen(true)
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<PlusIcon className="mr-2 size-4" />
|
|
121
|
+
{t('market.addApp')}
|
|
122
|
+
</Button>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{loading ? (
|
|
126
|
+
<p className="text-muted-foreground">{t('common.loading')}</p>
|
|
127
|
+
) : (
|
|
128
|
+
<DataTable columns={columns} data={apps} />
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
<MarketAppForm
|
|
132
|
+
open={formOpen}
|
|
133
|
+
onOpenChange={setFormOpen}
|
|
134
|
+
editData={editData}
|
|
135
|
+
onSuccess={fetchApps}
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
|
139
|
+
<AlertDialogContent>
|
|
140
|
+
<AlertDialogHeader>
|
|
141
|
+
<AlertDialogTitle>{t('market.deleteConfirmTitle')}</AlertDialogTitle>
|
|
142
|
+
<AlertDialogDescription>
|
|
143
|
+
{t('market.deleteConfirmDesc', { name: deleteTarget?.displayName })}
|
|
144
|
+
</AlertDialogDescription>
|
|
145
|
+
</AlertDialogHeader>
|
|
146
|
+
<AlertDialogFooter>
|
|
147
|
+
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
|
148
|
+
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-white hover:bg-destructive/90">
|
|
149
|
+
{t('common.delete')}
|
|
150
|
+
</AlertDialogAction>
|
|
151
|
+
</AlertDialogFooter>
|
|
152
|
+
</AlertDialogContent>
|
|
153
|
+
</AlertDialog>
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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 { useState } from 'react'
|
|
19
|
+
import { useTranslation } from 'react-i18next'
|
|
20
|
+
import { DataTable, type AdminColumnDef } from '@/components/data-table/data-table'
|
|
21
|
+
|
|
22
|
+
function formatRelativeTime(dateStr: string, t: (key: string, params?: Record<string, number | string>) => string): string {
|
|
23
|
+
const now = new Date()
|
|
24
|
+
const target = new Date(dateStr)
|
|
25
|
+
const diff = now.getTime() - target.getTime()
|
|
26
|
+
const seconds = Math.floor(diff / 1000)
|
|
27
|
+
if (seconds < 60) return t('common.timeAgo.seconds', { count: seconds })
|
|
28
|
+
const minutes = Math.floor(seconds / 60)
|
|
29
|
+
if (minutes < 60) return t('common.timeAgo.minutes', { count: minutes })
|
|
30
|
+
const hours = Math.floor(minutes / 60)
|
|
31
|
+
if (hours < 24) return t('common.timeAgo.hours', { count: hours })
|
|
32
|
+
const days = Math.floor(hours / 24)
|
|
33
|
+
return t('common.timeAgo.days', { count: days })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ErrorItem {
|
|
37
|
+
id: string
|
|
38
|
+
errorName: string
|
|
39
|
+
errorMessage: string
|
|
40
|
+
stackTrace?: string | null
|
|
41
|
+
count: number
|
|
42
|
+
firstSeen: string
|
|
43
|
+
lastSeen: string
|
|
44
|
+
context?: Record<string, unknown>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ErrorListProps {
|
|
48
|
+
errors: ErrorItem[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ErrorList({ errors }: ErrorListProps) {
|
|
52
|
+
const { t } = useTranslation('common')
|
|
53
|
+
const [expandedId, setExpandedId] = useState<string | null>(null)
|
|
54
|
+
|
|
55
|
+
const columns: AdminColumnDef<ErrorItem>[] = [
|
|
56
|
+
{
|
|
57
|
+
header: t('errorList.columns.lastSeen'),
|
|
58
|
+
accessorKey: 'lastSeen',
|
|
59
|
+
meta: { style: { width: 60 } },
|
|
60
|
+
cell: ({ row }: { row: { original: ErrorItem } }) => (
|
|
61
|
+
<span className="text-xs text-muted-foreground">
|
|
62
|
+
{formatRelativeTime(row.original.lastSeen, t)}
|
|
63
|
+
</span>
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
header: t('errorList.columns.errorName'),
|
|
68
|
+
accessorKey: 'errorName',
|
|
69
|
+
meta: { style: { width: 120 } },
|
|
70
|
+
cell: ({ row }: { row: { original: ErrorItem } }) => (
|
|
71
|
+
<span className="font-medium text-xs block truncate">
|
|
72
|
+
{row.original.errorName}
|
|
73
|
+
</span>
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
header: t('errorList.columns.errorMessage'),
|
|
78
|
+
accessorKey: 'errorMessage',
|
|
79
|
+
meta: { style: { width: 280 } },
|
|
80
|
+
cell: ({ row }: { row: { original: ErrorItem } }) => {
|
|
81
|
+
const msg = row.original.errorMessage
|
|
82
|
+
return <span className="text-xs block truncate">{msg}</span>
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
header: t('errorList.columns.count'),
|
|
87
|
+
accessorKey: 'count',
|
|
88
|
+
meta: { style: { width: 60, textAlign: 'right' } },
|
|
89
|
+
cell: ({ row }: { row: { original: ErrorItem } }) => (
|
|
90
|
+
<span className="font-medium">{row.original.count}</span>
|
|
91
|
+
),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
header: t('errorList.columns.firstSeen'),
|
|
95
|
+
accessorKey: 'firstSeen',
|
|
96
|
+
meta: { style: { width: 60 } },
|
|
97
|
+
cell: ({ row }: { row: { original: ErrorItem } }) => (
|
|
98
|
+
<span className="text-xs text-muted-foreground">
|
|
99
|
+
{formatRelativeTime(row.original.firstSeen, t)}
|
|
100
|
+
</span>
|
|
101
|
+
),
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="space-y-3">
|
|
107
|
+
<DataTable
|
|
108
|
+
columns={columns}
|
|
109
|
+
data={errors}
|
|
110
|
+
onRowClick={(item) => {
|
|
111
|
+
setExpandedId(expandedId === item.id ? null : item.id)
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
{expandedId && (
|
|
115
|
+
<ErrorDetail errors={errors} expandedId={expandedId} t={t} />
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ErrorDetail({ errors, expandedId, t }: { errors: ErrorItem[]; expandedId: string; t: (key: string) => string }) {
|
|
122
|
+
const error = errors.find((e) => e.id === expandedId)
|
|
123
|
+
if (!error) return null
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
|
127
|
+
<div className="font-medium text-sm">{t('errorList.detail.title')}</div>
|
|
128
|
+
{error.stackTrace && (
|
|
129
|
+
<pre className="text-xs bg-background rounded-md p-3 overflow-auto max-h-40 border">
|
|
130
|
+
{error.stackTrace}
|
|
131
|
+
</pre>
|
|
132
|
+
)}
|
|
133
|
+
{error.context && JSON.stringify(error.context, null, 2) && (
|
|
134
|
+
<div>
|
|
135
|
+
<div className="text-xs font-medium text-muted-foreground mb-1">{t('errorList.detail.context')}</div>
|
|
136
|
+
<pre className="text-xs bg-background rounded-md p-3 overflow-auto border">
|
|
137
|
+
{JSON.stringify(error.context, null, 2)}
|
|
138
|
+
</pre>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
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 { useTranslation } from 'react-i18next'
|
|
19
|
+
|
|
20
|
+
interface HealthCardData {
|
|
21
|
+
status?: string
|
|
22
|
+
checks?: {
|
|
23
|
+
uptime?: number
|
|
24
|
+
database?: {
|
|
25
|
+
status?: string
|
|
26
|
+
latency?: number
|
|
27
|
+
error?: string
|
|
28
|
+
}
|
|
29
|
+
memory?: {
|
|
30
|
+
rss?: number
|
|
31
|
+
heapUsed?: number
|
|
32
|
+
heapTotal?: number
|
|
33
|
+
}
|
|
34
|
+
cpu?: {
|
|
35
|
+
usage?: number
|
|
36
|
+
cores?: number
|
|
37
|
+
model?: string
|
|
38
|
+
}
|
|
39
|
+
load?: {
|
|
40
|
+
one?: number
|
|
41
|
+
five?: number
|
|
42
|
+
fifteen?: number
|
|
43
|
+
}
|
|
44
|
+
disk?: {
|
|
45
|
+
total?: number
|
|
46
|
+
free?: number
|
|
47
|
+
usedPct?: number
|
|
48
|
+
}
|
|
49
|
+
system?: {
|
|
50
|
+
platform?: string
|
|
51
|
+
type?: string
|
|
52
|
+
release?: string
|
|
53
|
+
nodeVersion?: string
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
requestStats?: {
|
|
57
|
+
total?: number
|
|
58
|
+
avgDuration?: number
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatBytes(bytes: number): string {
|
|
63
|
+
if (bytes < 1024) return `${bytes}B`
|
|
64
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`
|
|
65
|
+
return `${(bytes / 1048576).toFixed(1)}MB`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface HealthCardsProps {
|
|
69
|
+
health: HealthCardData
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function HealthCards({ health }: HealthCardsProps) {
|
|
73
|
+
const { t } = useTranslation('common')
|
|
74
|
+
const db = health.checks?.database
|
|
75
|
+
const mem = health.checks?.memory
|
|
76
|
+
const cpu = health.checks?.cpu
|
|
77
|
+
const load = health.checks?.load
|
|
78
|
+
const disk = health.checks?.disk
|
|
79
|
+
const system = health.checks?.system
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="grid gap-3 md:grid-cols-3 lg:grid-cols-5">
|
|
83
|
+
<div className="rounded-lg border p-3">
|
|
84
|
+
<p className="text-xs text-muted-foreground">{t('monitoring.healthCards.database')}</p>
|
|
85
|
+
<p className={`text-sm font-semibold mt-1 ${db?.status === 'connected' ? 'text-green-600' : 'text-red-600'}`}>
|
|
86
|
+
{db?.status === 'connected' ? t('monitoring.healthCards.connected') : t('monitoring.healthCards.disconnected')}
|
|
87
|
+
</p>
|
|
88
|
+
{db?.status === 'connected' && db.latency != null && (
|
|
89
|
+
<p className="text-xs text-muted-foreground mt-0.5">{t('monitoring.healthCards.latency', { latency: db.latency })}ms</p>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="rounded-lg border p-3">
|
|
94
|
+
<p className="text-xs text-muted-foreground">{t('monitoring.healthCards.memoryRSS')}</p>
|
|
95
|
+
<p className="text-sm font-semibold mt-1">
|
|
96
|
+
{mem?.rss ? formatBytes(mem.rss) : '-'}
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="rounded-lg border p-3">
|
|
101
|
+
<p className="text-xs text-muted-foreground">{t('monitoring.healthCards.heapUsage')}</p>
|
|
102
|
+
<p className="text-sm font-semibold mt-1">
|
|
103
|
+
{mem?.heapUsed && mem?.heapTotal ? `${((mem.heapUsed / mem.heapTotal) * 100).toFixed(1)}%` : '-'}
|
|
104
|
+
</p>
|
|
105
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
106
|
+
{mem?.heapUsed ? formatBytes(mem.heapUsed) : '-'} / {mem?.heapTotal ? formatBytes(mem.heapTotal) : '-'}
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div className="rounded-lg border p-3">
|
|
111
|
+
<p className="text-xs text-muted-foreground">{t('monitoring.healthCards.cpuCores', { cores: cpu?.cores })}</p>
|
|
112
|
+
<p className="text-sm font-semibold mt-1">
|
|
113
|
+
{cpu?.usage != null ? `${cpu.usage.toFixed(1)}%` : '-'}
|
|
114
|
+
</p>
|
|
115
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
116
|
+
{cpu?.model ? cpu.model.split(' ').slice(0, 3).join(' ') : '-'}
|
|
117
|
+
</p>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div className="rounded-lg border p-3">
|
|
121
|
+
<p className="text-xs text-muted-foreground">{t('monitoring.healthCards.systemLoad')}</p>
|
|
122
|
+
<p className="text-sm font-semibold mt-1">
|
|
123
|
+
{load?.one != null ? `${load.one.toFixed(2)} / ${load.five?.toFixed(2) ?? '-'} / ${load.fifteen?.toFixed(2) ?? '-'}` : '-'}
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{disk && (
|
|
128
|
+
<div className="rounded-lg border p-3">
|
|
129
|
+
<p className="text-xs text-muted-foreground">{t('monitoring.healthCards.diskUsage')}</p>
|
|
130
|
+
<p className="text-sm font-semibold mt-1">
|
|
131
|
+
{disk.usedPct != null ? `${disk.usedPct.toFixed(1)}%` : '-'}
|
|
132
|
+
</p>
|
|
133
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
134
|
+
{t('monitoring.healthCards.available', { free: disk.free != null ? formatBytes(disk.free) : '-', total: disk.total != null ? formatBytes(disk.total) : '-' })}
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{system && (
|
|
140
|
+
<div className="rounded-lg border p-3">
|
|
141
|
+
<p className="text-xs text-muted-foreground">{t('monitoring.healthCards.runtime')}</p>
|
|
142
|
+
<p className="text-sm font-semibold mt-1">
|
|
143
|
+
{system.platform} {system.release}
|
|
144
|
+
</p>
|
|
145
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
146
|
+
{system.nodeVersion}
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|