mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 16:46:37 +08:00
wavelet init
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {useCallback, useEffect, useRef, useState} from "react"
|
||||
import {toast} from "sonner"
|
||||
import {Activity, Cpu, Database, HardDrive, Layers, RefreshCw} from "lucide-react"
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Switch} from "@/components/ui/switch"
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select"
|
||||
import {Badge} from "@/components/ui/badge"
|
||||
import {Progress} from "@/components/ui/progress"
|
||||
import {Skeleton} from "@/components/ui/skeleton"
|
||||
import type {SystemStatus} from "@/lib/services/admin"
|
||||
import services from "@/lib/services"
|
||||
|
||||
/**
|
||||
* 格式化数字,每3位加逗号
|
||||
*/
|
||||
const formatNumber = (num: number | string) => {
|
||||
if (num === undefined || num === null) return "0"
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ",")
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时系统状态展示与管理组件
|
||||
*/
|
||||
export function SystemStatusManager() {
|
||||
const [status, setStatus] = useState<SystemStatus | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [wavelet, setWavelet] = useState(false)
|
||||
const [autoRefresh, setAutoRefresh] = useState(true)
|
||||
const [intervalTime, setIntervalTime] = useState("5000") // 默认5秒
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const prevStatusRef = useRef<SystemStatus | null>(null)
|
||||
const [changedFields, setChangedFields] = useState<Record<string, boolean>>({})
|
||||
|
||||
// 获取状态数据
|
||||
const fetchStatus = useCallback(async (isSilent = false) => {
|
||||
if (!isSilent) setWavelet(true)
|
||||
try {
|
||||
const data = await services.adminStatus.getSystemStatus()
|
||||
|
||||
// 检测变化的字段用于微动画效果
|
||||
if (prevStatusRef.current) {
|
||||
const changes: Record<string, boolean> = {}
|
||||
Object.keys(data).forEach((key) => {
|
||||
const k = key as keyof SystemStatus
|
||||
if (prevStatusRef.current && prevStatusRef.current[k] !== data[k]) {
|
||||
changes[k] = true
|
||||
}
|
||||
})
|
||||
setChangedFields(changes)
|
||||
// 1秒后清除变化状态
|
||||
setTimeout(() => {
|
||||
setChangedFields({})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
setStatus(data)
|
||||
prevStatusRef.current = data
|
||||
} catch (err) {
|
||||
toast.error("获取系统状态失败", {
|
||||
description: err instanceof Error ? err.message : "请求时发生未知错误"
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setWavelet(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 轮询逻辑
|
||||
useEffect(() => {
|
||||
// 首次加载
|
||||
fetchStatus()
|
||||
}, [fetchStatus])
|
||||
|
||||
useEffect(() => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
|
||||
if (autoRefresh) {
|
||||
const time = parseInt(intervalTime, 10)
|
||||
intervalRef.current = setInterval(() => {
|
||||
fetchStatus(true)
|
||||
}, time)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
}
|
||||
}, [autoRefresh, intervalTime, fetchStatus])
|
||||
|
||||
// 计算内存分配百分比 (Alloc / Sys)
|
||||
const getMemoryUsagePercent = () => {
|
||||
if (!status) return 0
|
||||
|
||||
const parseSizeToBytes = (sizeStr: string): number => {
|
||||
const parts = sizeStr.split(" ")
|
||||
if (parts.length < 2) return 0
|
||||
const value = parseFloat(parts[0])
|
||||
const unit = parts[1].toLowerCase()
|
||||
|
||||
if (unit.startsWith("gib")) return value * 1024 * 1024 * 1024
|
||||
if (unit.startsWith("mib")) return value * 1024 * 1024
|
||||
if (unit.startsWith("kib")) return value * 1024
|
||||
return value
|
||||
}
|
||||
|
||||
const alloc = parseSizeToBytes(status.alloc)
|
||||
const sys = parseSizeToBytes(status.sys)
|
||||
if (sys === 0) return 0
|
||||
return Math.min(Math.round((alloc / sys) * 100), 100)
|
||||
}
|
||||
|
||||
// 闪烁动画类
|
||||
const getPulseClass = (field: string) => {
|
||||
return changedFields[field] ? "animate-pulse text-primary font-bold transition-all duration-300 scale-105 inline-block" : "transition-all duration-300"
|
||||
}
|
||||
|
||||
const RenderMetricRow = ({ label, value, field }: { label: string; value: string | number; field?: string }) => (
|
||||
<div className="flex items-center justify-between py-2 border-b border-dashed border-border/40 last:border-b-0 hover:bg-muted/30 px-2 rounded-sm transition-colors duration-150">
|
||||
<span className="text-xs font-medium text-muted-foreground">{label}</span>
|
||||
<span className={`text-xs font-mono font-medium ${field ? getPulseClass(field) : ""}`}>
|
||||
{typeof value === "number" ? formatNumber(value) : value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6 p-1">
|
||||
<div className="flex items-center justify-between border-b border-border/40 pb-4">
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-8 w-24" />
|
||||
<Skeleton className="h-8 w-16" />
|
||||
<Skeleton className="h-8 w-8" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<Skeleton className="h-48 w-full" />
|
||||
<Skeleton className="h-48 w-full" />
|
||||
<Skeleton className="h-48 w-full" />
|
||||
<Skeleton className="h-48 w-full" />
|
||||
<Skeleton className="h-48 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6 px-1 space-y-6">
|
||||
{/* 顶部控制栏 */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between pb-4 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="size-5 text-primary" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">系统状态</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center flex-wrap gap-4 bg-muted/30 p-1.5 rounded-lg border border-border/40 backdrop-blur-sm">
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
<Switch
|
||||
id="auto-refresh"
|
||||
checked={autoRefresh}
|
||||
onCheckedChange={setAutoRefresh}
|
||||
/>
|
||||
<label htmlFor="auto-refresh" className="text-xs font-medium cursor-pointer select-none">
|
||||
自动刷新
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{autoRefresh && (
|
||||
<Select value={intervalTime} onValueChange={setIntervalTime}>
|
||||
<SelectTrigger className="h-7 w-20 text-[11px] bg-background border-border/40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1000">1s</SelectItem>
|
||||
<SelectItem value="2000">2s</SelectItem>
|
||||
<SelectItem value="5000">5s</SelectItem>
|
||||
<SelectItem value="10000">10s</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="h-7 text-[11px] gap-1.5"
|
||||
onClick={() => fetchStatus()}
|
||||
disabled={wavelet}
|
||||
>
|
||||
<RefreshCw className={`size-3 ${wavelet ? "animate-spin" : ""}`} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status && (
|
||||
<>
|
||||
{/* 主网格排版 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
{/* 1. 服务概览 */}
|
||||
<Card className="shadow-sm border-border/40 bg-card/50 backdrop-blur-md hover:border-primary/20 transition-all duration-300">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<div className="space-y-0.5">
|
||||
<CardTitle className="text-sm font-semibold">服务概览</CardTitle>
|
||||
<CardDescription className="text-[10px]">服务的基础生命指标</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary" className="p-1 rounded-full bg-primary/10 text-primary border-none">
|
||||
<Activity className="size-4" />
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1">
|
||||
<RenderMetricRow label="服务运行时间" value={status.uptime} field="uptime" />
|
||||
<RenderMetricRow label="当前 Goroutines 数量" value={status.num_goroutine} field="num_goroutine" />
|
||||
<RenderMetricRow label="Heap 对象数量" value={status.heap_objects} field="heap_objects" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 2. 内存统计 */}
|
||||
<Card className="shadow-sm border-border/40 bg-card/50 backdrop-blur-md hover:border-primary/20 transition-all duration-300">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<div className="space-y-0.5">
|
||||
<CardTitle className="text-sm font-semibold">内存统计</CardTitle>
|
||||
<CardDescription className="text-[10px]">主内存消耗概览</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary" className="p-1 rounded-full bg-primary/10 text-primary border-none">
|
||||
<Cpu className="size-4" />
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<RenderMetricRow label="当前内存使用量" value={status.alloc} field="alloc" />
|
||||
<RenderMetricRow label="所有已分配的内存" value={status.total_alloc} field="total_alloc" />
|
||||
<RenderMetricRow label="内存占用量" value={status.sys} field="sys" />
|
||||
<RenderMetricRow label="下次 GC 内存回收量" value={status.next_gc} field="next_gc" />
|
||||
</div>
|
||||
{/* 物理内存水位比例 */}
|
||||
<div className="space-y-1 px-1 pt-1">
|
||||
<div className="flex justify-between text-[10px] text-muted-foreground">
|
||||
<span>当前使用率 (Alloc / Sys)</span>
|
||||
<span className="font-mono">{getMemoryUsagePercent()}%</span>
|
||||
</div>
|
||||
<Progress value={getMemoryUsagePercent()} className="h-1.5" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 3. 堆/栈详情 */}
|
||||
<Card className="shadow-sm border-border/40 bg-card/50 backdrop-blur-md hover:border-primary/20 transition-all duration-300">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<div className="space-y-0.5">
|
||||
<CardTitle className="text-sm font-semibold">堆/栈详情</CardTitle>
|
||||
<CardDescription className="text-[10px]">运行时堆栈内存空间细节</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary" className="p-1 rounded-full bg-primary/10 text-primary border-none">
|
||||
<Database className="size-4" />
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1">
|
||||
<RenderMetricRow label="当前 Heap 内存使用量" value={status.heap_alloc} field="heap_alloc" />
|
||||
<RenderMetricRow label="Heap 内存占用量" value={status.heap_sys} field="heap_sys" />
|
||||
<RenderMetricRow label="Heap 内存空闲量" value={status.heap_idle} field="heap_idle" />
|
||||
<RenderMetricRow label="正在使用的 Heap 内存" value={status.heap_inuse} field="heap_inuse" />
|
||||
<RenderMetricRow label="已释放的 Heap 内存" value={status.heap_released} field="heap_released" />
|
||||
<RenderMetricRow label="启动 Stack 使用量" value={status.stack_inuse} field="stack_inuse" />
|
||||
<RenderMetricRow label="已分配的 Stack 内存" value={status.stack_sys} field="stack_sys" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 4. 底层组件与结构体 */}
|
||||
<Card className="shadow-sm border-border/40 bg-card/50 backdrop-blur-md hover:border-primary/20 transition-all duration-300">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<div className="space-y-0.5">
|
||||
<CardTitle className="text-sm font-semibold">底层及结构体内存</CardTitle>
|
||||
<CardDescription className="text-[10px]">运行时底层管理结构体开销</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary" className="p-1 rounded-full bg-primary/10 text-primary border-none">
|
||||
<HardDrive className="size-4" />
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1">
|
||||
<RenderMetricRow label="MSpan 结构内存使用量" value={status.mspan_inuse} field="mspan_inuse" />
|
||||
<RenderMetricRow label="已分配的 MSpan 结构内存" value={status.mspan_sys} field="mspan_sys" />
|
||||
<RenderMetricRow label="MCache 结构内存使用量" value={status.mcache_inuse} field="mcache_inuse" />
|
||||
<RenderMetricRow label="已分配的 MCache 结构内存" value={status.mcache_sys} field="mcache_sys" />
|
||||
<RenderMetricRow label="已分配的剖析哈希表内存" value={status.buck_hash_sys} field="buck_hash_sys" />
|
||||
<RenderMetricRow label="已分配的 GC 元数据内存" value={status.gc_sys} field="gc_sys" />
|
||||
<RenderMetricRow label="其它已分配的系统内存" value={status.other_sys} field="other_sys" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 5. 垃圾回收与分配计数 */}
|
||||
<Card className="shadow-sm border-border/40 bg-card/50 backdrop-blur-md hover:border-primary/20 transition-all duration-300 md:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<div className="space-y-0.5">
|
||||
<CardTitle className="text-sm font-semibold">垃圾回收与分配计数</CardTitle>
|
||||
<CardDescription className="text-[10px]">GC 历史数据与分配频次</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary" className="p-1 rounded-full bg-primary/10 text-primary border-none">
|
||||
<Layers className="size-4" />
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6">
|
||||
<div>
|
||||
<RenderMetricRow label="GC 执行次数" value={status.num_gc} field="num_gc" />
|
||||
<RenderMetricRow label="距离上次 GC 时间" value={status.last_gc_time} field="last_gc_time" />
|
||||
<RenderMetricRow label="GC 暂停时间总量" value={status.pause_total_ns} field="pause_total_ns" />
|
||||
<RenderMetricRow label="上次 GC 暂停时间" value={status.last_pause} field="last_pause" />
|
||||
</div>
|
||||
<div>
|
||||
<RenderMetricRow label="内存分配次数" value={status.mallocs} field="mallocs" />
|
||||
<RenderMetricRow label="内存释放次数" value={status.frees} field="frees" />
|
||||
<RenderMetricRow label="指针查找次数" value={status.lookups} field="lookups" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"use client"
|
||||
|
||||
import dynamic from "next/dynamic"
|
||||
import {useEffect, useMemo} from "react"
|
||||
import {useQuery} from "@tanstack/react-query"
|
||||
import {Loader2, Settings} from "lucide-react"
|
||||
import {useRouter} from "next/navigation"
|
||||
import {motion} from "motion/react"
|
||||
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
|
||||
import {useAuth} from "@/components/providers/auth-provider"
|
||||
import services from "@/lib/services"
|
||||
import type {SystemConfig} from "@/lib/services/admin"
|
||||
|
||||
const tabFallback = (
|
||||
<div className="h-64 animate-pulse rounded-lg border border-border/40 bg-muted/20" />
|
||||
)
|
||||
|
||||
const SecurityTab = dynamic(
|
||||
() => import("@/components/common/settings/security-tab").then((mod) => mod.SecurityTab),
|
||||
{ loading: () => tabFallback },
|
||||
)
|
||||
|
||||
const OperationTab = dynamic(
|
||||
() => import("@/components/common/settings/operation-tab").then((mod) => mod.OperationTab),
|
||||
{ loading: () => tabFallback },
|
||||
)
|
||||
|
||||
const SystemTab = dynamic(
|
||||
() => import("@/components/common/settings/system-tab").then((mod) => mod.SystemTab),
|
||||
{ loading: () => tabFallback },
|
||||
)
|
||||
|
||||
const OtherTab = dynamic(
|
||||
() => import("@/components/common/settings/other-tab").then((mod) => mod.OtherTab),
|
||||
{ loading: () => tabFallback },
|
||||
)
|
||||
|
||||
const InfoTab = dynamic(
|
||||
() => import("@/components/common/settings/info-tab").then((mod) => mod.InfoTab),
|
||||
{ loading: () => tabFallback },
|
||||
)
|
||||
|
||||
const SystemStatusManager = dynamic(
|
||||
() => import("./components/system-status").then((mod) => mod.SystemStatusManager),
|
||||
{ loading: () => tabFallback },
|
||||
)
|
||||
|
||||
function systemConfigMap(configs: SystemConfig[]) {
|
||||
return configs.reduce<Record<string, SystemConfig>>((accumulator, config) => {
|
||||
accumulator[config.key] = config
|
||||
return accumulator
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function AdminSettingsPageClient() {
|
||||
const { user, loading } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
const systemConfigsQuery = useQuery({
|
||||
queryKey: ["admin", "system-configs"],
|
||||
queryFn: () => services.adminSystemConfig.listSystemConfigs("system"),
|
||||
enabled: !!user?.is_admin,
|
||||
})
|
||||
|
||||
const configs = useMemo(
|
||||
() => systemConfigMap(systemConfigsQuery.data ?? []),
|
||||
[systemConfigsQuery.data],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && (!user || !user.is_admin)) {
|
||||
router.replace("/settings/profile")
|
||||
}
|
||||
}, [user, loading, router])
|
||||
|
||||
if (loading || !user || !user.is_admin) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<Loader2 className="size-6 animate-spin text-indigo-500" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.35, ease: "easeOut" }}
|
||||
className="py-6 space-y-6 w-full"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="size-5 text-primary" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">系统设置</h1>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs defaultValue="security" className="w-full">
|
||||
<TabsList variant="line" className="w-fit inline-flex gap-8 mb-6">
|
||||
<TabsTrigger value="security" className="px-0 pb-2 text-xs font-semibold">
|
||||
安全设置
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="operation" className="px-0 pb-2 text-xs font-semibold">
|
||||
业务设置
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="system" className="px-0 pb-2 text-xs font-semibold">
|
||||
系统设置
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="other" className="px-0 pb-2 text-xs font-semibold">
|
||||
其他设置
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="status" className="px-0 pb-2 text-xs font-semibold">
|
||||
系统状态
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="info" className="px-0 pb-2 text-xs font-semibold">
|
||||
系统信息
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="security" className="focus-visible:outline-none">
|
||||
<SecurityTab configs={configs} systemConfigsQuery={systemConfigsQuery} />
|
||||
</TabsContent>
|
||||
<TabsContent value="operation" className="focus-visible:outline-none">
|
||||
<OperationTab configs={configs} systemConfigsQuery={systemConfigsQuery} />
|
||||
</TabsContent>
|
||||
<TabsContent value="system" className="focus-visible:outline-none">
|
||||
<SystemTab configs={configs} systemConfigsQuery={systemConfigsQuery} />
|
||||
</TabsContent>
|
||||
<TabsContent value="status" className="focus-visible:outline-none">
|
||||
<SystemStatusManager />
|
||||
</TabsContent>
|
||||
<TabsContent value="other" className="focus-visible:outline-none">
|
||||
<OtherTab configs={configs} />
|
||||
</TabsContent>
|
||||
<TabsContent value="info" className="focus-visible:outline-none">
|
||||
<InfoTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {AdminSettingsPageClient} from "./page-client"
|
||||
|
||||
export default function AdminSettingsPage() {
|
||||
return <AdminSettingsPageClient />
|
||||
}
|
||||
Reference in New Issue
Block a user