feat:performance

This commit is contained in:
马登山
2026-02-03 19:26:32 +08:00
parent dcedc1d79f
commit 04d792aebc
5 changed files with 232 additions and 15 deletions
+2 -2
View File
@@ -38,7 +38,7 @@ function LoginPageContent() {
useEffect(() => {
if (isAuthenticated) {
router.replace("/")
router.replace(buildRoute("/browser"))
}
}, [isAuthenticated, router])
@@ -60,7 +60,7 @@ function LoginPageContent() {
await login(credentials, currentConfig)
message.success(t("Login Success"))
window.location.href = buildRoute("/")
window.location.href = buildRoute("/browser")
} catch {
message.error(t("Login Failed"))
}
+1 -1
View File
@@ -17,7 +17,7 @@ export default function ForbiddenPage() {
const router = useRouter()
const handleBack = () => {
router.replace("/")
router.replace("/browser")
}
return (
+217
View File
@@ -0,0 +1,217 @@
"use client"
import { Page } from "@/components/page"
import { PageHeader } from "@/components/page-header"
import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"
import { usePerformanceData } from "@/hooks/use-performance-data"
import { niceBytes } from "@/lib/functions"
import {
RiArchiveDrawerFill,
RiArchiveLine,
RiHardDrive2Line,
RiListSettingsFill,
RiRefreshLine,
RiSecurePaymentFill,
RiStackLine,
} from "@remixicon/react"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { PerformanceSummaryCards } from "../_components/performance-summary-cards"
import { PerformanceUsageCard } from "../_components/performance-usage-card"
import { PerformanceInfrastructureCard } from "../_components/performance-infrastructure-card"
import { PerformanceBackendCard } from "../_components/performance-backend-card"
import { PerformanceServerList } from "../_components/performance-server-list"
dayjs.extend(relativeTime)
export default function PerformancePage() {
const { t } = useTranslation()
const { systemInfo, metricsInfo, datausageinfo, storageinfo, loading, error, refetch } = usePerformanceData()
const numberFormatter = useMemo(() => new Intl.NumberFormat(), [])
const storageBackend = useMemo(
() => storageinfo?.backend ?? (storageinfo as { Backend?: typeof storageinfo.backend })?.Backend,
[storageinfo],
)
const usedPercent = useMemo(() => {
const total = Number(datausageinfo.total_capacity || 0)
if (!total) return 0
const used = Number(datausageinfo.total_used_capacity || 0)
return Math.min(100, Math.max(0, (used / total) * 100))
}, [datausageinfo])
const lastUpdatedLabel = useMemo(() => {
const last = metricsInfo?.aggregated?.scanner?.current_started
const time = dayjs(last)
return time.isValid() ? time.fromNow() : "--"
}, [metricsInfo])
const summaryMetrics = useMemo(
() => [
{
label: t("Buckets"),
display: numberFormatter.format(systemInfo?.buckets?.count ?? 0),
icon: RiArchiveLine,
caption: null as string | null,
href: "/browser",
},
{
label: t("Objects"),
display: numberFormatter.format(systemInfo?.objects?.count ?? 0),
icon: RiStackLine,
caption: null as string | null,
href: "/browser",
},
{
label: t("Total Capacity"),
display: datausageinfo.total_capacity ? niceBytes(String(datausageinfo.total_capacity)) : "--",
icon: RiHardDrive2Line,
caption: datausageinfo.total_used_capacity
? `${t("Used")}: ${niceBytes(String(datausageinfo.total_used_capacity))}`
: null,
href: undefined as string | undefined,
},
],
[systemInfo, datausageinfo, numberFormatter, t],
)
const fromLastStartTime = useMemo(() => {
const times = metricsInfo?.aggregated?.scanner?.cycle_complete_times || []
if (!times.length) return "--"
const start = dayjs(times[times.length - 1])
return dayjs().from(start)
}, [metricsInfo])
const fromLastScanTime = useMemo(() => {
const start = dayjs(metricsInfo?.aggregated?.scanner?.current_started)
if (!start.isValid()) return "--"
return dayjs().from(start)
}, [metricsInfo])
const lastScanTime = useMemo(() => {
const currentStart = dayjs(metricsInfo?.aggregated?.scanner?.current_started)
const cycleTimes = metricsInfo?.aggregated?.scanner?.cycle_complete_times || []
if (!currentStart.isValid()) return "--"
const lastComplete = dayjs(cycleTimes[cycleTimes.length - 1])
return lastComplete.isValid() && currentStart.isBefore(lastComplete)
? lastComplete.from(currentStart)
: dayjs().from(currentStart)
}, [metricsInfo])
const usageStats = useMemo(
() => [
{ label: t("Last Normal Operation"), value: fromLastStartTime },
{ label: t("Last Scan Activity"), value: fromLastScanTime },
{ label: t("Uptime"), value: lastScanTime },
],
[t, fromLastStartTime, fromLastScanTime, lastScanTime],
)
const onlineServers = useMemo(
() => (systemInfo?.servers || []).filter((s) => s.state === "online").length,
[systemInfo],
)
const offlineServers = useMemo(
() => (systemInfo?.servers || []).filter((s) => s.state === "offline").length,
[systemInfo],
)
const backendInfo = useMemo(
() => [
{
icon: RiArchiveDrawerFill,
title: t("Backend Type"),
value: systemInfo?.backend?.backendType,
},
{
icon: RiSecurePaymentFill,
title: t("Standard Storage Parity"),
value: storageBackend?.StandardSCParity,
},
{
icon: RiListSettingsFill,
title: t("Reduced Redundancy Parity"),
value: storageBackend?.RRSCParity,
},
],
[systemInfo, storageBackend, t],
)
if (loading && !Object.keys(systemInfo).length && !Object.keys(datausageinfo).length) {
return (
<Page>
<PageHeader>
<h1 className="text-2xl font-bold">{t("Server Information")}</h1>
</PageHeader>
<div className="flex items-center justify-center py-24">
<Spinner className="size-8 text-muted-foreground" />
</div>
</Page>
)
}
if (error) {
return (
<Page>
<PageHeader
actions={
<Button variant="outline" onClick={refetch}>
<RiRefreshLine className="mr-2 size-4" aria-hidden />
{t("Sync")}
</Button>
}
>
<h1 className="text-2xl font-bold">{t("Server Information")}</h1>
</PageHeader>
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-6 text-center">
<p className="text-sm text-destructive">{error}</p>
</div>
</Page>
)
}
return (
<Page>
<PageHeader
actions={
<Button variant="outline" onClick={refetch} disabled={loading}>
<RiRefreshLine className="mr-2 size-4" aria-hidden />
{t("Sync")}
</Button>
}
>
<h1 className="text-2xl font-bold">{t("Server Information")}</h1>
</PageHeader>
<div className="space-y-8">
<PerformanceSummaryCards metrics={summaryMetrics} />
<PerformanceUsageCard
lastUpdatedLabel={lastUpdatedLabel}
totalUsedCapacity={datausageinfo.total_used_capacity ?? 0}
usedPercent={usedPercent}
usageStats={usageStats}
t={t}
/>
<PerformanceInfrastructureCard
onlineServers={onlineServers}
offlineServers={offlineServers}
onlineDisks={systemInfo?.backend?.onlineDisks ?? 0}
offlineDisks={systemInfo?.backend?.offlineDisks ?? 0}
t={t}
/>
<PerformanceBackendCard items={backendInfo} t={t} />
<PerformanceServerList servers={systemInfo?.servers ?? []} t={t} />
</div>
</Page>
)
}
+1 -1
View File
@@ -127,7 +127,7 @@ export function AppSidebar() {
return (
<Sidebar collapsible="icon">
<SidebarHeader>
<Link href="/" className="flex items-center gap-3">
<Link href="/browser" className="flex items-center gap-3">
{isCollapsed ? (
<div className="flex size-8 items-center justify-center rounded-lg bg-primary text-md font-semibold text-primary-foreground">
<span>{brandInitial}</span>
+11 -11
View File
@@ -2,11 +2,11 @@ import type { NavItem } from "@/types/app-config"
// icons: https://icones.js.org/collection/ri
export default [
{
label: "Home",
to: "/",
icon: "ri:home-line",
},
// {
// label: "Home",
// to: "/",
// icon: "ri:home-line",
// },
{
label: "Browser",
to: "/browser",
@@ -41,12 +41,12 @@ export default [
icon: "ri:download-2-line",
isAdminOnly: true,
},
// {
// label: "Performance",
// to: "/performance",
// icon: "ri:bar-chart-box-line",
// isAdminOnly: true,
// },
{
label: "Performance",
to: "/performance",
icon: "ri:bar-chart-box-line",
isAdminOnly: true,
},
{
label: "divider",
key: "divider-1",