feat: implement command context and provider for command handling; add search button component; enhance network chart with packet loss calculation and display; update translations for new features

This commit is contained in:
hamster1963
2025-10-09 11:26:45 +08:00
parent 48704b1135
commit 1fda5ada9f
17 changed files with 403 additions and 123 deletions
+15 -3
View File
@@ -1,7 +1,7 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Route, BrowserRouter as Router, Routes } from "react-router-dom" import { Route, BrowserRouter as Router, Routes, useLocation } from "react-router-dom"
import { DashCommand } from "./components/DashCommand" import { DashCommand } from "./components/DashCommand"
import ErrorBoundary from "./components/ErrorBoundary" import ErrorBoundary from "./components/ErrorBoundary"
@@ -17,7 +17,12 @@ import NotFound from "./pages/NotFound"
import Server from "./pages/Server" import Server from "./pages/Server"
import ServerDetail from "./pages/ServerDetail" import ServerDetail from "./pages/ServerDetail"
const App: React.FC = () => { // Route checker component
const RouteChecker: React.FC = () => {
return <MainApp />
}
const MainApp: React.FC = () => {
const { data: settingData, error } = useQuery({ const { data: settingData, error } = useQuery({
queryKey: ["setting"], queryKey: ["setting"],
queryFn: () => fetchSetting(), queryFn: () => fetchSetting(),
@@ -66,7 +71,6 @@ const App: React.FC = () => {
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined
return ( return (
<Router basename={import.meta.env.BASE_URL}>
<ErrorBoundary> <ErrorBoundary>
{/* 固定定位的背景层 */} {/* 固定定位的背景层 */}
{customBackgroundImage && ( {customBackgroundImage && (
@@ -102,6 +106,14 @@ const App: React.FC = () => {
</main> </main>
</div> </div>
</ErrorBoundary> </ErrorBoundary>
)
}
// Main App wrapper with router
const App: React.FC = () => {
return (
<Router basename={import.meta.env.BASE_URL}>
<RouteChecker />
</Router> </Router>
) )
} }
+7 -6
View File
@@ -1,6 +1,7 @@
"use client" "use client"
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command" import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command"
import { useCommand } from "@/hooks/use-command"
import { useTheme } from "@/hooks/use-theme" import { useTheme } from "@/hooks/use-theme"
import { useWebSocketContext } from "@/hooks/use-websocket-context" import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { formatNezhaInfo } from "@/lib/utils" import { formatNezhaInfo } from "@/lib/utils"
@@ -11,7 +12,7 @@ import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
export function DashCommand() { export function DashCommand() {
const [open, setOpen] = useState(false) const { isOpen, closeCommand, toggleCommand } = useCommand()
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const navigate = useNavigate() const navigate = useNavigate()
const { t } = useTranslation() const { t } = useTranslation()
@@ -25,13 +26,13 @@ export function DashCommand() {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) { if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault() e.preventDefault()
setOpen((open) => !open) toggleCommand()
} }
} }
document.addEventListener("keydown", down) document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down) return () => document.removeEventListener("keydown", down)
}, []) }, [toggleCommand])
if (!connected || !nezhaWsData) return null if (!connected || !nezhaWsData) return null
@@ -67,7 +68,7 @@ export function DashCommand() {
return ( return (
<> <>
<CommandDialog open={open} onOpenChange={setOpen}> <CommandDialog open={isOpen} onOpenChange={closeCommand}>
<CommandInput placeholder={t("TypeCommand")} value={search} onValueChange={setSearch} /> <CommandInput placeholder={t("TypeCommand")} value={search} onValueChange={setSearch} />
<CommandList className="border-t"> <CommandList className="border-t">
<CommandEmpty>{t("NoResults")}</CommandEmpty> <CommandEmpty>{t("NoResults")}</CommandEmpty>
@@ -80,7 +81,7 @@ export function DashCommand() {
value={server.name} value={server.name}
onSelect={() => { onSelect={() => {
navigate(`/server/${server.id}`) navigate(`/server/${server.id}`)
setOpen(false) closeCommand()
}} }}
> >
{formatNezhaInfo(nezhaWsData.now, server).online ? ( {formatNezhaInfo(nezhaWsData.now, server).online ? (
@@ -103,7 +104,7 @@ export function DashCommand() {
value={item.value} value={item.value}
onSelect={() => { onSelect={() => {
item.action() item.action()
setOpen(false) closeCommand()
}} }}
> >
{item.icon} {item.icon}
+2
View File
@@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { LanguageSwitcher } from "./LanguageSwitcher" import { LanguageSwitcher } from "./LanguageSwitcher"
import { SearchButton } from "./SearchButton"
import { Loader, LoadingSpinner } from "./loading/Loader" import { Loader, LoadingSpinner } from "./loading/Loader"
import { Button } from "./ui/button" import { Button } from "./ui/button"
@@ -103,6 +104,7 @@ function Header() {
<Links /> <Links />
<DashboardLink /> <DashboardLink />
</div> </div>
<SearchButton />
<LanguageSwitcher /> <LanguageSwitcher />
<ModeToggle /> <ModeToggle />
{(customBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) && ( {(customBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) && (
+204 -25
View File
@@ -9,7 +9,7 @@ import { useQuery } from "@tanstack/react-query"
import * as React from "react" import * as React from "react"
import { useCallback, useMemo } from "react" import { useCallback, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts" import { Area, CartesianGrid, ComposedChart, Line, XAxis, YAxis } from "recharts"
import NetworkChartLoading from "./NetworkChartLoading" import NetworkChartLoading from "./NetworkChartLoading"
import { Label } from "./ui/label" import { Label } from "./ui/label"
@@ -20,6 +20,63 @@ interface ResultItem {
[key: string]: number [key: string]: number
} }
/**
* Helper method to calculate packet loss from delay data
*/
const calculatePacketLoss = (delays: number[]): number[] => {
if (!delays || delays.length === 0) return []
const packetLossRates: number[] = []
const windowSize = Math.min(10, Math.max(3, Math.floor(delays.length / 10)))
const timeoutThreshold = 3000
const extremeDelayThreshold = 10000
for (let i = 0; i < delays.length; i++) {
const currentDelay = delays[i]
let lossRate = 0
if (currentDelay === 0 || currentDelay === null || currentDelay === undefined) {
lossRate = 100
} else if (currentDelay >= extremeDelayThreshold) {
lossRate = Math.min(95, 60 + (currentDelay - extremeDelayThreshold) / 1000)
} else if (currentDelay >= timeoutThreshold) {
lossRate = Math.min(50, (currentDelay - timeoutThreshold) / 200)
} else {
const start = Math.max(0, i - Math.floor(windowSize / 2))
const end = Math.min(delays.length, i + Math.ceil(windowSize / 2))
const windowDelays = delays.slice(start, end).filter((d) => d > 0)
if (windowDelays.length > 2) {
const mean = windowDelays.reduce((sum, d) => sum + d, 0) / windowDelays.length
const variance = windowDelays.reduce((sum, d) => sum + (d - mean) ** 2, 0) / windowDelays.length
const standardDeviation = Math.sqrt(variance)
const coefficientOfVariation = standardDeviation / mean
if (coefficientOfVariation > 0.8) {
lossRate = Math.min(25, coefficientOfVariation * 15)
} else if (coefficientOfVariation > 0.5) {
lossRate = Math.min(10, coefficientOfVariation * 8)
} else if (coefficientOfVariation > 0.3) {
lossRate = Math.min(5, coefficientOfVariation * 5)
}
if (currentDelay > mean * 2.5) {
lossRate += Math.min(15, (currentDelay / mean - 2.5) * 10)
}
}
}
if (i > 0) {
const alpha = 0.3
lossRate = alpha * lossRate + (1 - alpha) * packetLossRates[i - 1]
}
packetLossRates.push(Math.max(0, Math.min(100, lossRate)))
}
return packetLossRates.map((rate) => Number(rate.toFixed(2)))
}
export function NetworkChart({ server_id, show }: { server_id: number; show: boolean }) { export function NetworkChart({ server_id, show }: { server_id: number; show: boolean }) {
const { t } = useTranslation() const { t } = useTranslation()
@@ -125,7 +182,15 @@ export const NetworkChartClient = React.memo(function NetworkChart({
const chartButtons = useMemo( const chartButtons = useMemo(
() => () =>
chartDataKey.map((key) => ( chartDataKey.map((key) => {
const monitorData = chartData[key]
const lastDelay = monitorData[monitorData.length - 1].avg_delay
// Calculate average packet loss if available
const packetLossData = monitorData.filter((item) => item.packet_loss !== undefined).map((item) => item.packet_loss!)
const avgPacketLoss = packetLossData.length > 0 ? packetLossData.reduce((sum, loss) => sum + loss, 0) / packetLossData.length : null
return (
<button <button
key={key} key={key}
data-active={activeCharts.includes(key)} data-active={activeCharts.includes(key)}
@@ -133,31 +198,66 @@ export const NetworkChartClient = React.memo(function NetworkChart({
onClick={() => handleButtonClick(key)} onClick={() => handleButtonClick(key)}
> >
<span className="whitespace-nowrap text-xs text-muted-foreground">{key}</span> <span className="whitespace-nowrap text-xs text-muted-foreground">{key}</span>
<span className="text-md font-bold leading-none sm:text-lg">{chartData[key][chartData[key].length - 1].avg_delay.toFixed(2)}ms</span> <div className="flex flex-col gap-0.5">
<span className="text-md font-bold leading-none sm:text-lg">{lastDelay.toFixed(2)}ms</span>
{avgPacketLoss !== null && <span className="text-xs text-muted-foreground">{avgPacketLoss.toFixed(2)}% avg loss</span>}
</div>
</button> </button>
)), )
}),
[chartDataKey, activeCharts, chartData, handleButtonClick], [chartDataKey, activeCharts, chartData, handleButtonClick],
) )
const chartLines = useMemo(() => { const chartElements = useMemo(() => {
// If we have active charts selected, render only those const elements = []
if (activeCharts.length > 0) {
return activeCharts.map((chart) => ( // If exactly one chart is selected, show delay line and packet loss area
if (activeCharts.length === 1) {
const chart = activeCharts[0]
elements.push(
<Area
key="packet-loss-area"
isAnimationActive={false}
dataKey="packet_loss"
stroke="none"
fill="hsl(45, 100%, 60%)"
fillOpacity={0.3}
yAxisId="packet-loss"
/>,
<Line
key="delay-line"
isAnimationActive={false}
strokeWidth={1}
type="linear"
dot={false}
dataKey="avg_delay"
stroke={getColorByIndex(chart)}
yAxisId="delay"
connectNulls={true}
/>,
)
} else if (activeCharts.length > 1) {
// Multiple charts selected - show only delay lines for selected monitors
elements.push(
...activeCharts.map((chart) => (
<Line <Line
key={chart} key={chart}
isAnimationActive={false} isAnimationActive={false}
strokeWidth={1} strokeWidth={1}
type="linear" type="linear"
dot={false} dot={false}
dataKey={chart} // Change from "avg_delay" to the actual chart key name dataKey={chart}
stroke={getColorByIndex(chart)} stroke={getColorByIndex(chart)}
name={chart} name={chart}
connectNulls={true} connectNulls={true}
yAxisId="delay"
/> />
)) )),
} )
// Otherwise show all charts (default view) } else {
return chartDataKey.map((key) => ( // No selection - show all charts (default view)
elements.push(
...chartDataKey.map((key) => (
<Line <Line
key={key} key={key}
isAnimationActive={false} isAnimationActive={false}
@@ -167,18 +267,33 @@ export const NetworkChartClient = React.memo(function NetworkChart({
dataKey={key} dataKey={key}
stroke={getColorByIndex(key)} stroke={getColorByIndex(key)}
connectNulls={true} connectNulls={true}
yAxisId="delay"
/> />
)) )),
)
}
return elements
}, [activeCharts, chartDataKey, getColorByIndex]) }, [activeCharts, chartDataKey, getColorByIndex])
const processedData = useMemo(() => { const processedData = useMemo(() => {
if (!isPeakEnabled) { // Special handling for single chart selection
// Always use formattedData when multiple charts are selected or none selected let baseData = formattedData
return formattedData if (activeCharts.length === 1) {
const selectedChart = activeCharts[0]
baseData = chartData[selectedChart].map((item) => ({
created_at: item.created_at,
avg_delay: item.avg_delay,
packet_loss: item.packet_loss ?? 0,
}))
} }
// For peak cutting, always use the formatted data which contains all series if (!isPeakEnabled) {
const data = formattedData return baseData
}
// For peak cutting, use the base data
const data = baseData
const windowSize = 11 // 增加窗口大小以获取更好的统计效果 const windowSize = 11 // 增加窗口大小以获取更好的统计效果
const alpha = 0.3 // EWMA平滑因子 const alpha = 0.3 // EWMA平滑因子
@@ -225,6 +340,23 @@ export const NetworkChartClient = React.memo(function NetworkChart({
const window = data.slice(index - windowSize + 1, index + 1) const window = data.slice(index - windowSize + 1, index + 1)
const smoothed = { ...point } as ResultItem const smoothed = { ...point } as ResultItem
// Special handling for single chart selection
if (activeCharts.length === 1) {
// Process avg_delay for single chart
const values = window.map((w) => w.avg_delay as number).filter((v) => v !== undefined && v !== null)
if (values.length > 0) {
const processed = processValues(values)
if (processed !== null) {
if (ewmaHistory.avg_delay === undefined) {
ewmaHistory.avg_delay = processed
} else {
ewmaHistory.avg_delay = alpha * processed + (1 - alpha) * ewmaHistory.avg_delay
}
smoothed.avg_delay = ewmaHistory.avg_delay
}
}
} else {
// Process all chart keys or just the selected ones // Process all chart keys or just the selected ones
const keysToProcess = activeCharts.length > 0 ? activeCharts : chartDataKey const keysToProcess = activeCharts.length > 0 ? activeCharts : chartDataKey
@@ -244,10 +376,11 @@ export const NetworkChartClient = React.memo(function NetworkChart({
} }
} }
}) })
}
return smoothed return smoothed
}) })
}, [isPeakEnabled, activeCharts, formattedData, chartDataKey]) }, [isPeakEnabled, activeCharts, formattedData, chartData, chartDataKey])
return ( return (
<Card <Card
@@ -281,7 +414,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
</button> </button>
)} )}
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full"> <ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
<LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}> <ComposedChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<XAxis <XAxis
dataKey="created_at" dataKey="created_at"
@@ -316,7 +449,18 @@ export const NetworkChartClient = React.memo(function NetworkChart({
return minutes === 0 ? `${date.getHours()}:00` : `${date.getHours()}:${minutes}` return minutes === 0 ? `${date.getHours()}:00` : `${date.getHours()}:${minutes}`
}} }}
/> />
<YAxis tickLine={false} axisLine={false} tickMargin={15} minTickGap={20} tickFormatter={(value) => `${value}ms`} /> <YAxis yAxisId="delay" tickLine={false} axisLine={false} tickMargin={15} minTickGap={20} tickFormatter={(value) => `${value}ms`} />
{activeCharts.length === 1 && (
<YAxis
yAxisId="packet-loss"
orientation="right"
tickLine={false}
axisLine={false}
tickMargin={15}
minTickGap={20}
tickFormatter={(value) => `${value}%`}
/>
)}
<ChartTooltip <ChartTooltip
isAnimationActive={false} isAnimationActive={false}
content={ content={
@@ -326,12 +470,35 @@ export const NetworkChartClient = React.memo(function NetworkChart({
labelFormatter={(_, payload) => { labelFormatter={(_, payload) => {
return formatTime(payload[0].payload.created_at) return formatTime(payload[0].payload.created_at)
}} }}
formatter={(value, name) => {
let formattedValue: string
let label: string
if (name === "packet_loss") {
formattedValue = `${Number(value).toFixed(2)}%`
label = t("monitor.packetLoss", "Packet Loss")
} else if (name === "avg_delay") {
formattedValue = `${Number(value).toFixed(2)}ms`
label = t("monitor.avgDelay", "Avg Delay")
} else {
// For monitor names (in multi-chart view) - delay data
formattedValue = `${Number(value).toFixed(2)}ms`
label = name as string
}
return (
<div className="flex flex-1 items-center justify-between leading-none">
<span className="text-muted-foreground">{label}</span>
<span className="ml-2 font-medium text-foreground tabular-nums">{formattedValue}</span>
</div>
)
}}
/> />
} }
/> />
<ChartLegend content={<ChartLegendContent />} /> {activeCharts.length !== 1 && <ChartLegend content={<ChartLegendContent />} />}
{chartLines} {chartElements}
</LineChart> </ComposedChart>
</ChartContainer> </ChartContainer>
</div> </div>
</CardContent> </CardContent>
@@ -349,10 +516,14 @@ const transformData = (data: NezhaMonitor[]) => {
monitorData[monitorName] = [] monitorData[monitorName] = []
} }
// Calculate packet loss from delay data if not provided
const packetLoss = item.packet_loss || calculatePacketLoss(item.avg_delay)
for (let i = 0; i < item.created_at.length; i++) { for (let i = 0; i < item.created_at.length; i++) {
monitorData[monitorName].push({ monitorData[monitorName].push({
created_at: item.created_at[i], created_at: item.created_at[i],
avg_delay: item.avg_delay[i], avg_delay: item.avg_delay[i],
packet_loss: packetLoss[i],
}) })
} }
}) })
@@ -373,6 +544,9 @@ const formatData = (rawData: NezhaMonitor[]) => {
rawData.forEach((item) => { rawData.forEach((item) => {
const { monitor_name, created_at, avg_delay } = item const { monitor_name, created_at, avg_delay } = item
// Calculate packet loss if not provided
const packetLoss = item.packet_loss || calculatePacketLoss(avg_delay)
allTimeArray.forEach((time) => { allTimeArray.forEach((time) => {
if (!result[time]) { if (!result[time]) {
result[time] = { created_at: time } result[time] = { created_at: time }
@@ -381,6 +555,11 @@ const formatData = (rawData: NezhaMonitor[]) => {
const timeIndex = created_at.indexOf(time) const timeIndex = created_at.indexOf(time)
// @ts-expect-error - avg_delay is an array // @ts-expect-error - avg_delay is an array
result[time][monitor_name] = timeIndex !== -1 ? avg_delay[timeIndex] : null result[time][monitor_name] = timeIndex !== -1 ? avg_delay[timeIndex] : null
// Add packet loss data if available
if (packetLoss) {
// @ts-expect-error - packet_loss is calculated
result[time][`${monitor_name}_packet_loss`] = timeIndex !== -1 ? packetLoss[timeIndex] : null
}
}) })
}) })
+23
View File
@@ -0,0 +1,23 @@
"use client"
import { useCommand } from "@/hooks/use-command"
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"
import { Button } from "./ui/button"
export function SearchButton() {
const { openCommand } = useCommand()
return (
<Button
variant="outline"
size="sm"
className="cursor-pointer rounded-full bg-white px-[9px] hover:bg-accent/50 dark:bg-black dark:hover:bg-accent/50"
onClick={openCommand}
title="Search"
>
<MagnifyingGlassIcon className="size-4" />
<span className="sr-only">Search</span>
</Button>
)
}
+10
View File
@@ -0,0 +1,10 @@
import { createContext } from "react"
export interface CommandContextType {
isOpen: boolean
openCommand: () => void
closeCommand: () => void
toggleCommand: () => void
}
export const CommandContext = createContext<CommandContextType | undefined>(undefined)
+24
View File
@@ -0,0 +1,24 @@
import { ReactNode, useCallback, useState } from "react"
import { CommandContext } from "./command-context"
export function CommandProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false)
const openCommand = useCallback(() => setIsOpen(true), [])
const closeCommand = useCallback(() => setIsOpen(false), [])
const toggleCommand = useCallback(() => setIsOpen((prev) => !prev), [])
return (
<CommandContext.Provider
value={{
isOpen,
openCommand,
closeCommand,
toggleCommand,
}}
>
{children}
</CommandContext.Provider>
)
}
+10
View File
@@ -0,0 +1,10 @@
import { CommandContext } from "@/context/command-context"
import { useContext } from "react"
export function useCommand() {
const context = useContext(CommandContext)
if (context === undefined) {
throw new Error("useCommand must be used within a CommandProvider")
}
return context
}
+3 -1
View File
@@ -45,7 +45,9 @@
"monitor": { "monitor": {
"monitorCount": "Services", "monitorCount": "Services",
"noData": "Kein Server Monitoring Daten, bitte fügen sie zuerst einen Monitor hinzu", "noData": "Kein Server Monitoring Daten, bitte fügen sie zuerst einen Monitor hinzu",
"avgDelay": "Latenz" "avgDelay": "Latenz",
"packetLoss": "Paketverlust",
"clearSelections": "Löschen"
}, },
"billingInfo": { "billingInfo": {
"error": "Fehler", "error": "Fehler",
+3 -1
View File
@@ -107,7 +107,9 @@
"monitor": { "monitor": {
"noData": "No server monitor data, please add a service monitor first", "noData": "No server monitor data, please add a service monitor first",
"avgDelay": "Latency", "avgDelay": "Latency",
"monitorCount": "Services" "monitorCount": "Services",
"packetLoss": "Packet Loss",
"clearSelections": "Clear"
}, },
"pwa": { "pwa": {
"offlineReady": "App ready to work offline", "offlineReady": "App ready to work offline",
+3 -1
View File
@@ -91,7 +91,9 @@
"monitor": { "monitor": {
"avgDelay": "Latencia", "avgDelay": "Latencia",
"noData": "No hay datos de servidores, primero agregue un monitor de servicio", "noData": "No hay datos de servidores, primero agregue un monitor de servicio",
"monitorCount": "Servicios" "monitorCount": "Servicios",
"packetLoss": "Pérdida de paquetes",
"clearSelections": "Limpiar"
}, },
"error": { "error": {
"pageNotFound": "Página no encontrada", "pageNotFound": "Página no encontrada",
+3 -1
View File
@@ -98,7 +98,9 @@
"monitor": { "monitor": {
"noData": "Нет данных мониторинга сервера, пожалуйста, сначала добавьте монитор службы", "noData": "Нет данных мониторинга сервера, пожалуйста, сначала добавьте монитор службы",
"avgDelay": "Задержка", "avgDelay": "Задержка",
"monitorCount": "Сервисы" "monitorCount": "Сервисы",
"packetLoss": "Потеря пакетов",
"clearSelections": "Очистить"
}, },
"pwa": { "pwa": {
"newContent": "Доступен новый контент", "newContent": "Доступен новый контент",
+3 -1
View File
@@ -93,7 +93,9 @@
"monitor": { "monitor": {
"noData": "சேவையக மானிட்டர் தரவு இல்லை, முதலில் ஒரு பணி மானிட்டரைச் சேர்க்கவும்", "noData": "சேவையக மானிட்டர் தரவு இல்லை, முதலில் ஒரு பணி மானிட்டரைச் சேர்க்கவும்",
"avgDelay": "சுணக்கம்", "avgDelay": "சுணக்கம்",
"monitorCount": "சேவைகள்" "monitorCount": "சேவைகள்",
"packetLoss": "தொகுப்பு இழப்பு",
"clearSelections": "அழி"
}, },
"pwa": { "pwa": {
"offlineReady": "ஆஃப்லைனில் வேலை செய்ய பயன்பாடு தயாராக உள்ளது", "offlineReady": "ஆஃப்லைனில் வேலை செய்ய பயன்பாடு தயாராக உள்ளது",
+3 -1
View File
@@ -108,7 +108,9 @@
"monitor": { "monitor": {
"noData": "没有服务监控数据,请在管理后台服务页添加监控任务", "noData": "没有服务监控数据,请在管理后台服务页添加监控任务",
"avgDelay": "延迟", "avgDelay": "延迟",
"monitorCount": "个监控服务" "monitorCount": "个监控服务",
"packetLoss": "丢包率",
"clearSelections": "清除"
}, },
"pwa": { "pwa": {
"offlineReady": "应用可以离线使用了", "offlineReady": "应用可以离线使用了",
+3 -1
View File
@@ -110,7 +110,9 @@
"noData": "沒有服務監控數據,請在管理後台服務新增監控任務", "noData": "沒有服務監控數據,請在管理後台服務新增監控任務",
"status": "狀態", "status": "狀態",
"avgDelay": "延遲", "avgDelay": "延遲",
"monitorCount": "個監控" "monitorCount": "個監控",
"packetLoss": "丟包率",
"clearSelections": "清除"
}, },
"billingInfo": { "billingInfo": {
"remaining": "剩餘天數", "remaining": "剩餘天數",
+3
View File
@@ -7,6 +7,7 @@ import App from "./App"
import { ThemeColorManager } from "./components/ThemeColorManager" import { ThemeColorManager } from "./components/ThemeColorManager"
import { ThemeProvider } from "./components/ThemeProvider" import { ThemeProvider } from "./components/ThemeProvider"
import { MotionProvider } from "./components/motion/motion-provider" import { MotionProvider } from "./components/motion/motion-provider"
import { CommandProvider } from "./context/command-provider"
import { SortProvider } from "./context/sort-provider" import { SortProvider } from "./context/sort-provider"
import { StatusProvider } from "./context/status-provider" import { StatusProvider } from "./context/status-provider"
import { TooltipProvider } from "./context/tooltip-provider" import { TooltipProvider } from "./context/tooltip-provider"
@@ -22,6 +23,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<ThemeColorManager /> <ThemeColorManager />
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<WebSocketProvider url="/api/v1/ws/server"> <WebSocketProvider url="/api/v1/ws/server">
<CommandProvider>
<StatusProvider> <StatusProvider>
<SortProvider> <SortProvider>
<TooltipProvider> <TooltipProvider>
@@ -40,6 +42,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
</TooltipProvider> </TooltipProvider>
</SortProvider> </SortProvider>
</StatusProvider> </StatusProvider>
</CommandProvider>
</WebSocketProvider> </WebSocketProvider>
</QueryClientProvider> </QueryClientProvider>
</ThemeProvider> </ThemeProvider>
+2
View File
@@ -86,6 +86,7 @@ export type ServerMonitorChart = {
[key: string]: { [key: string]: {
created_at: number created_at: number
avg_delay: number avg_delay: number
packet_loss?: number
}[] }[]
} }
@@ -96,6 +97,7 @@ export interface NezhaMonitor {
server_name: string server_name: string
created_at: number[] created_at: number[]
avg_delay: number[] avg_delay: number[]
packet_loss?: number[]
} }
export interface ServiceResponse { export interface ServiceResponse {