Merge pull request #16 from hamster1963/main

sync
This commit is contained in:
wood chen 2025-04-29 02:43:36 +08:00 committed by GitHub
commit 3c6e8c1730
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 118 additions and 97 deletions

View File

@ -90,21 +90,30 @@ export const NetworkChartClient = React.memo(function NetworkChart({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const defaultChart = "All"
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const forcePeakCutEnabled = (window.ForcePeakCutEnabled as boolean) ?? false const forcePeakCutEnabled = (window.ForcePeakCutEnabled as boolean) ?? false
const [activeChart, setActiveChart] = React.useState(defaultChart) // Change from string to string array for multi-selection
const [activeCharts, setActiveCharts] = React.useState<string[]>([])
const [isPeakEnabled, setIsPeakEnabled] = React.useState(forcePeakCutEnabled) const [isPeakEnabled, setIsPeakEnabled] = React.useState(forcePeakCutEnabled)
const handleButtonClick = useCallback( // Function to clear all selected charts
(chart: string) => { const clearAllSelections = useCallback(() => {
setActiveChart((prev) => (prev === chart ? defaultChart : chart)) setActiveCharts([])
}, }, [])
[defaultChart],
) // Updated to handle multiple selections
const handleButtonClick = useCallback((chart: string) => {
setActiveCharts((prev) => {
// If chart is already selected, remove it
if (prev.includes(chart)) {
return prev.filter((c) => c !== chart)
}
// Otherwise, add it to selected charts
return [...prev, chart]
})
}, [])
const getColorByIndex = useCallback( const getColorByIndex = useCallback(
(chart: string) => { (chart: string) => {
@ -119,7 +128,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
chartDataKey.map((key) => ( chartDataKey.map((key) => (
<button <button
key={key} key={key}
data-active={activeChart === key} data-active={activeCharts.includes(key)}
className={`relative z-30 flex cursor-pointer grow basis-0 flex-col justify-center gap-1 border-b border-neutral-200 dark:border-neutral-800 px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6`} className={`relative z-30 flex cursor-pointer grow basis-0 flex-col justify-center gap-1 border-b border-neutral-200 dark:border-neutral-800 px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6`}
onClick={() => handleButtonClick(key)} onClick={() => handleButtonClick(key)}
> >
@ -127,13 +136,27 @@ export const NetworkChartClient = React.memo(function NetworkChart({
<span className="text-md font-bold leading-none sm:text-lg">{chartData[key][chartData[key].length - 1].avg_delay.toFixed(2)}ms</span> <span className="text-md font-bold leading-none sm:text-lg">{chartData[key][chartData[key].length - 1].avg_delay.toFixed(2)}ms</span>
</button> </button>
)), )),
[chartDataKey, activeChart, chartData, handleButtonClick], [chartDataKey, activeCharts, chartData, handleButtonClick],
) )
const chartLines = useMemo(() => { const chartLines = useMemo(() => {
if (activeChart !== defaultChart) { // If we have active charts selected, render only those
return <Line isAnimationActive={false} strokeWidth={1} type="linear" dot={false} dataKey="avg_delay" stroke={getColorByIndex(activeChart)} /> if (activeCharts.length > 0) {
return activeCharts.map((chart) => (
<Line
key={chart}
isAnimationActive={false}
strokeWidth={1}
type="linear"
dot={false}
dataKey={chart} // Change from "avg_delay" to the actual chart key name
stroke={getColorByIndex(chart)}
name={chart}
connectNulls={true}
/>
))
} }
// Otherwise show all charts (default view)
return chartDataKey.map((key) => ( return chartDataKey.map((key) => (
<Line <Line
key={key} key={key}
@ -146,14 +169,16 @@ export const NetworkChartClient = React.memo(function NetworkChart({
connectNulls={true} connectNulls={true}
/> />
)) ))
}, [activeChart, defaultChart, chartDataKey, getColorByIndex]) }, [activeCharts, chartDataKey, getColorByIndex])
const processedData = useMemo(() => { const processedData = useMemo(() => {
if (!isPeakEnabled) { if (!isPeakEnabled) {
return activeChart === defaultChart ? formattedData : chartData[activeChart] // Always use formattedData when multiple charts are selected or none selected
return formattedData
} }
const data = (activeChart === defaultChart ? formattedData : chartData[activeChart]) as ResultItem[] // For peak cutting, always use the formatted data which contains all series
const data = formattedData
const windowSize = 11 // 增加窗口大小以获取更好的统计效果 const windowSize = 11 // 增加窗口大小以获取更好的统计效果
const alpha = 0.3 // EWMA平滑因子 const alpha = 0.3 // EWMA平滑因子
@ -200,43 +225,29 @@ 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
if (activeChart === defaultChart) { // Process all chart keys or just the selected ones
chartDataKey.forEach((key) => { const keysToProcess = activeCharts.length > 0 ? activeCharts : chartDataKey
const values = window.map((w) => w[key]).filter((v) => v !== undefined && v !== null) as number[]
if (values.length > 0) { keysToProcess.forEach((key) => {
const processed = processValues(values) const values = window.map((w) => w[key]).filter((v) => v !== undefined && v !== null) as number[]
if (processed !== null) {
// 应用EWMA平滑
if (ewmaHistory[key] === undefined) {
ewmaHistory[key] = processed
} else {
ewmaHistory[key] = alpha * processed + (1 - alpha) * ewmaHistory[key]
}
smoothed[key] = ewmaHistory[key]
}
}
})
} else {
const values = window.map((w) => w.avg_delay).filter((v) => v !== undefined && v !== null) as number[]
if (values.length > 0) { if (values.length > 0) {
const processed = processValues(values) const processed = processValues(values)
if (processed !== null) { if (processed !== null) {
// 应用EWMA平滑 // Apply EWMA smoothing
if (ewmaHistory["current"] === undefined) { if (ewmaHistory[key] === undefined) {
ewmaHistory["current"] = processed ewmaHistory[key] = processed
} else { } else {
ewmaHistory["current"] = alpha * processed + (1 - alpha) * ewmaHistory["current"] ewmaHistory[key] = alpha * processed + (1 - alpha) * ewmaHistory[key]
} }
smoothed.avg_delay = ewmaHistory["current"] smoothed[key] = ewmaHistory[key]
} }
} }
} })
return smoothed return smoothed
}) })
}, [isPeakEnabled, activeChart, formattedData, chartData, chartDataKey, defaultChart]) }, [isPeakEnabled, activeCharts, formattedData, chartDataKey])
return ( return (
<Card <Card
@ -260,59 +271,69 @@ export const NetworkChartClient = React.memo(function NetworkChart({
<div className="flex flex-wrap w-full">{chartButtons}</div> <div className="flex flex-wrap w-full">{chartButtons}</div>
</CardHeader> </CardHeader>
<CardContent className="pr-2 pl-0 py-4 sm:pt-6 sm:pb-6 sm:pr-6 sm:pl-2"> <CardContent className="pr-2 pl-0 py-4 sm:pt-6 sm:pb-6 sm:pr-6 sm:pl-2">
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full"> <div className="relative">
<LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}> {activeCharts.length > 0 && (
<CartesianGrid vertical={false} /> <button
<XAxis className="absolute -top-2 right-1 z-10 text-xs px-2 py-1 bg-stone-100/80 dark:bg-stone-800/80 backdrop-blur-sm rounded-[5px] text-muted-foreground hover:text-foreground transition-colors"
dataKey="created_at" onClick={clearAllSelections}
tickLine={true} >
tickSize={3} {t("monitor.clearSelections", "Clear")} ({activeCharts.length})
axisLine={false} </button>
tickMargin={8} )}
minTickGap={80} <ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
ticks={processedData <LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}>
.filter((item, index, array) => { <CartesianGrid vertical={false} />
if (array.length < 6) { <XAxis
return index === 0 || index === array.length - 1 dataKey="created_at"
} tickLine={true}
tickSize={3}
axisLine={false}
tickMargin={8}
minTickGap={80}
ticks={processedData
.filter((item, index, array) => {
if (array.length < 6) {
return index === 0 || index === array.length - 1
}
// 计算数据的总时间跨度(毫秒) // 计算数据的总时间跨度(毫秒)
const timeSpan = array[array.length - 1].created_at - array[0].created_at const timeSpan = array[array.length - 1].created_at - array[0].created_at
const hours = timeSpan / (1000 * 60 * 60) const hours = timeSpan / (1000 * 60 * 60)
// 根据时间跨度调整显示间隔 // 根据时间跨度调整显示间隔
if (hours <= 12) { if (hours <= 12) {
// 12小时内每60分钟显示一个刻度 // 12小时内每60分钟显示一个刻度
return index === 0 || index === array.length - 1 || new Date(item.created_at).getMinutes() % 60 === 0 return index === 0 || index === array.length - 1 || new Date(item.created_at).getMinutes() % 60 === 0
} }
// 超过12小时每2小时显示一个刻度 // 超过12小时每2小时显示一个刻度
const date = new Date(item.created_at) const date = new Date(item.created_at)
return date.getMinutes() === 0 && date.getHours() % 2 === 0 return date.getMinutes() === 0 && date.getHours() % 2 === 0
}) })
.map((item) => item.created_at)} .map((item) => item.created_at)}
tickFormatter={(value) => { tickFormatter={(value) => {
const date = new Date(value) const date = new Date(value)
const minutes = date.getMinutes() const minutes = date.getMinutes()
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 tickLine={false} axisLine={false} tickMargin={15} minTickGap={20} tickFormatter={(value) => `${value}ms`} />
<ChartTooltip <ChartTooltip
isAnimationActive={false} isAnimationActive={false}
content={ content={
<ChartTooltipContent <ChartTooltipContent
indicator={"line"} indicator={"line"}
labelKey="created_at" labelKey="created_at"
labelFormatter={(_, payload) => { labelFormatter={(_, payload) => {
return formatTime(payload[0].payload.created_at) return formatTime(payload[0].payload.created_at)
}} }}
/> />
} }
/> />
{activeChart === defaultChart && <ChartLegend content={<ChartLegendContent />} />} <ChartLegend content={<ChartLegendContent />} />
{chartLines} {chartLines}
</LineChart> </LineChart>
</ChartContainer> </ChartContainer>
</div>
</CardContent> </CardContent>
</Card> </Card>
) )

View File

@ -1,8 +1,8 @@
import { createContext } from "react" import { createContext } from "react"
export type SortType = "default" | "name" | "uptime" | "system" | "cpu" | "mem" | "stg" | "up" | "down" | "up total" | "down total" export type SortType = "default" | "name" | "uptime" | "system" | "cpu" | "mem" | "disk" | "up" | "down" | "up total" | "down total"
export const SORT_TYPES: SortType[] = ["default", "name", "uptime", "system", "cpu", "mem", "stg", "up", "down", "up total", "down total"] export const SORT_TYPES: SortType[] = ["default", "name", "uptime", "system", "cpu", "mem", "disk", "up", "down", "up total", "down total"]
export type SortOrder = "asc" | "desc" export type SortOrder = "asc" | "desc"

View File

@ -190,10 +190,10 @@ export default function Servers() {
comparison = (a.state?.cpu ?? 0) - (b.state?.cpu ?? 0) comparison = (a.state?.cpu ?? 0) - (b.state?.cpu ?? 0)
break break
case "mem": case "mem":
comparison = (a.state?.mem_used ?? 0) - (b.state?.mem_used ?? 0) comparison = (formatNezhaInfo(nezhaWsData.now, a).mem ?? 0) - (formatNezhaInfo(nezhaWsData.now, b).mem ?? 0)
break break
case "stg": case "disk":
comparison = (a.state?.disk_used ?? 0) - (b.state?.disk_used ?? 0) comparison = (formatNezhaInfo(nezhaWsData.now, a).disk ?? 0) - (formatNezhaInfo(nezhaWsData.now, b).disk ?? 0)
break break
case "up": case "up":
comparison = (a.state?.net_out_speed ?? 0) - (b.state?.net_out_speed ?? 0) comparison = (a.state?.net_out_speed ?? 0) - (b.state?.net_out_speed ?? 0)