forked from hamster1963/nezha-dash-v1
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5f2e9fe
commit d7f0410
Showing
13 changed files
with
467 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,290 @@ | ||
"use client"; | ||
|
||
import { | ||
Card, | ||
CardContent, | ||
CardDescription, | ||
CardHeader, | ||
CardTitle, | ||
} from "@/components/ui/card"; | ||
import { | ||
ChartConfig, | ||
ChartContainer, | ||
ChartLegend, | ||
ChartLegendContent, | ||
ChartTooltip, | ||
ChartTooltipContent, | ||
} from "@/components/ui/chart"; | ||
import { fetchMonitor } from "@/lib/nezha-api"; | ||
import { formatTime } from "@/lib/utils"; | ||
import { formatRelativeTime } from "@/lib/utils"; | ||
import { useQuery } from "@tanstack/react-query"; | ||
import * as React from "react"; | ||
import { useCallback, useMemo } from "react"; | ||
import { useTranslation } from "react-i18next"; | ||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; | ||
import NetworkChartLoading from "./NetworkChartLoading"; | ||
import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api"; | ||
|
||
|
||
interface ResultItem { | ||
created_at: number; | ||
[key: string]: number | null; | ||
} | ||
|
||
export function NetworkChart({ | ||
server_id, | ||
show, | ||
}: { | ||
server_id: number; | ||
show: boolean; | ||
}) { | ||
const { t } = useTranslation(); | ||
|
||
const { data: monitorData} = useQuery( | ||
{ | ||
queryKey: ["monitor", server_id], | ||
queryFn: () => fetchMonitor(server_id), | ||
enabled: show, | ||
refetchOnMount: true, | ||
refetchOnWindowFocus: true, | ||
refetchInterval: 10000, | ||
} | ||
) | ||
|
||
if (!monitorData) return <NetworkChartLoading />; | ||
|
||
if (monitorData?.success && monitorData.data.length === 0) { | ||
return ( | ||
<> | ||
<div className="flex flex-col items-center justify-center"> | ||
<p className="text-sm font-medium opacity-40"></p> | ||
<p className="text-sm font-medium opacity-40"> | ||
{t("monitor.noData")} | ||
</p> | ||
</div> | ||
<NetworkChartLoading /> | ||
</> | ||
); | ||
} | ||
|
||
|
||
|
||
const transformedData = transformData(monitorData.data); | ||
|
||
const formattedData = formatData(monitorData.data); | ||
|
||
const initChartConfig = { | ||
avg_delay: { | ||
label: t("monitor.avgDelay"), | ||
}, | ||
} satisfies ChartConfig; | ||
|
||
const chartDataKey = Object.keys(transformedData); | ||
|
||
return ( | ||
<NetworkChartClient | ||
chartDataKey={chartDataKey} | ||
chartConfig={initChartConfig} | ||
chartData={transformedData} | ||
serverName={monitorData.data[0].server_name} | ||
formattedData={formattedData} | ||
/> | ||
); | ||
} | ||
|
||
export const NetworkChartClient = React.memo(function NetworkChart({ | ||
chartDataKey, | ||
chartConfig, | ||
chartData, | ||
serverName, | ||
formattedData, | ||
}: { | ||
chartDataKey: string[]; | ||
chartConfig: ChartConfig; | ||
chartData: ServerMonitorChart; | ||
serverName: string; | ||
formattedData: ResultItem[]; | ||
}) { | ||
const { t } = useTranslation(); | ||
|
||
const defaultChart = "All"; | ||
|
||
const [activeChart, setActiveChart] = React.useState(defaultChart); | ||
|
||
const handleButtonClick = useCallback( | ||
(chart: string) => { | ||
setActiveChart((prev) => (prev === chart ? defaultChart : chart)); | ||
}, | ||
[defaultChart], | ||
); | ||
|
||
const getColorByIndex = useCallback( | ||
(chart: string) => { | ||
const index = chartDataKey.indexOf(chart); | ||
return `hsl(var(--chart-${(index % 10) + 1}))`; | ||
}, | ||
[chartDataKey], | ||
); | ||
|
||
const chartButtons = useMemo( | ||
() => | ||
chartDataKey.map((key) => ( | ||
<button | ||
key={key} | ||
data-active={activeChart === key} | ||
className={`relative z-30 flex cursor-pointer flex-1 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)} | ||
> | ||
<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> | ||
</button> | ||
)), | ||
[chartDataKey, activeChart, chartData, handleButtonClick], | ||
); | ||
|
||
const chartLines = useMemo(() => { | ||
if (activeChart !== defaultChart) { | ||
return ( | ||
<Line | ||
isAnimationActive={false} | ||
strokeWidth={1} | ||
type="linear" | ||
dot={false} | ||
dataKey="avg_delay" | ||
stroke={getColorByIndex(activeChart)} | ||
/> | ||
); | ||
} | ||
return chartDataKey.map((key) => ( | ||
<Line | ||
key={key} | ||
isAnimationActive={false} | ||
strokeWidth={1} | ||
type="linear" | ||
dot={false} | ||
dataKey={key} | ||
stroke={getColorByIndex(key)} | ||
connectNulls={true} | ||
/> | ||
)); | ||
}, [activeChart, defaultChart, chartDataKey, getColorByIndex]); | ||
|
||
return ( | ||
<Card> | ||
<CardHeader className="flex flex-col items-stretch space-y-0 p-0 sm:flex-row"> | ||
<div className="flex flex-none flex-col justify-center gap-1 border-b px-6 py-4"> | ||
<CardTitle className="flex flex-none items-center gap-0.5 text-md"> | ||
{serverName} | ||
</CardTitle> | ||
<CardDescription className="text-xs"> | ||
{chartDataKey.length} {t("monitor.monitorCount")} | ||
</CardDescription> | ||
</div> | ||
<div className="flex flex-wrap">{chartButtons}</div> | ||
</CardHeader> | ||
<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" | ||
> | ||
<LineChart | ||
accessibilityLayer | ||
data={ | ||
activeChart === defaultChart | ||
? formattedData | ||
: chartData[activeChart] | ||
} | ||
margin={{ left: 12, right: 12 }} | ||
> | ||
<CartesianGrid vertical={false} /> | ||
<XAxis | ||
dataKey="created_at" | ||
tickLine={false} | ||
axisLine={false} | ||
tickMargin={8} | ||
minTickGap={32} | ||
interval={"preserveStartEnd"} | ||
tickFormatter={(value) => formatRelativeTime(value)} | ||
/> | ||
<YAxis | ||
tickLine={false} | ||
axisLine={false} | ||
tickMargin={15} | ||
minTickGap={20} | ||
tickFormatter={(value) => `${value}ms`} | ||
/> | ||
<ChartTooltip | ||
isAnimationActive={false} | ||
content={ | ||
<ChartTooltipContent | ||
indicator={"line"} | ||
labelKey="created_at" | ||
labelFormatter={(_, payload) => { | ||
return formatTime(payload[0].payload.created_at); | ||
}} | ||
/> | ||
} | ||
/> | ||
{activeChart === defaultChart && ( | ||
<ChartLegend content={<ChartLegendContent />} /> | ||
)} | ||
{chartLines} | ||
</LineChart> | ||
</ChartContainer> | ||
</CardContent> | ||
</Card> | ||
); | ||
}); | ||
|
||
const transformData = (data: NezhaMonitor[]) => { | ||
const monitorData: ServerMonitorChart = {}; | ||
|
||
data.forEach((item) => { | ||
const monitorName = item.monitor_name; | ||
|
||
if (!monitorData[monitorName]) { | ||
monitorData[monitorName] = []; | ||
} | ||
|
||
for (let i = 0; i < item.created_at.length; i++) { | ||
monitorData[monitorName].push({ | ||
created_at: item.created_at[i], | ||
avg_delay: item.avg_delay[i], | ||
}); | ||
} | ||
}); | ||
|
||
return monitorData; | ||
}; | ||
|
||
const formatData = (rawData: NezhaMonitor[]) => { | ||
const result: { [time: number]: ResultItem } = {}; | ||
|
||
const allTimes = new Set<number>(); | ||
rawData.forEach((item) => { | ||
item.created_at.forEach((time) => allTimes.add(time)); | ||
}); | ||
|
||
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b); | ||
|
||
rawData.forEach((item) => { | ||
const { monitor_name, created_at, avg_delay } = item; | ||
|
||
allTimeArray.forEach((time) => { | ||
if (!result[time]) { | ||
result[time] = { created_at: time }; | ||
} | ||
|
||
const timeIndex = created_at.indexOf(time); | ||
result[time][monitor_name] = | ||
timeIndex !== -1 ? avg_delay[timeIndex] : null; | ||
}); | ||
}); | ||
|
||
return Object.values(result).sort((a, b) => a.created_at - b.created_at); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Loader } from "@/components/loading/Loader"; | ||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | ||
|
||
export default function NetworkChartLoading() { | ||
return ( | ||
<Card> | ||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row"> | ||
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5"> | ||
<CardTitle className="flex items-center gap-0.5 text-xl"> | ||
<div className="aspect-auto h-[20px] w-24 bg-muted"></div> | ||
</CardTitle> | ||
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div> | ||
</div> | ||
<div className="hidden pr-4 pt-4 sm:block"> | ||
<Loader visible={true} /> | ||
</div> | ||
</CardHeader> | ||
<CardContent className="px-2 sm:p-6"> | ||
<div className="aspect-auto h-[250px] w-full"></div> | ||
</CardContent> | ||
</Card> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.