mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 17:41:56 +08:00
feat: monitor chart
This commit is contained in:
parent
5f2e9fe38a
commit
d7f0410dcd
290
src/components/NetworkChart.tsx
Normal file
290
src/components/NetworkChart.tsx
Normal file
@ -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);
|
||||||
|
};
|
23
src/components/NetworkChartLoading.tsx
Normal file
23
src/components/NetworkChartLoading.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -3,7 +3,6 @@ import { ChartConfig, ChartContainer } from "@/components/ui/chart";
|
|||||||
import { formatNezhaInfo, formatRelativeTime } from "@/lib/utils";
|
import { formatNezhaInfo, formatRelativeTime } from "@/lib/utils";
|
||||||
import { NezhaServer, NezhaWebsocketResponse } from "@/types/nezha-api";
|
import { NezhaServer, NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
AreaChart,
|
AreaChart,
|
||||||
@ -51,8 +50,7 @@ type connectChartData = {
|
|||||||
udp: number;
|
udp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ServerDetailChart() {
|
export default function ServerDetailChart({server_id}: {server_id: string}) {
|
||||||
const { id } = useParams();
|
|
||||||
const { lastMessage, readyState } = useWebSocketContext();
|
const { lastMessage, readyState } = useWebSocketContext();
|
||||||
|
|
||||||
if (readyState !== 1) {
|
if (readyState !== 1) {
|
||||||
@ -67,7 +65,7 @@ export default function ServerDetailChart() {
|
|||||||
return <ServerDetailChartLoading />;
|
return <ServerDetailChartLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = nezhaWsData.servers.find((s) => s.id === Number(id));
|
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
return <ServerDetailChartLoading />;
|
return <ServerDetailChartLoading />;
|
||||||
|
@ -6,13 +6,13 @@ import { Card, CardContent } from "@/components/ui/card";
|
|||||||
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||||
import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils";
|
import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils";
|
||||||
import { NezhaWebsocketResponse } from "@/types/nezha-api";
|
import { NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function ServerDetailOverview() {
|
export default function ServerDetailOverview({server_id}: {server_id: string}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams();
|
|
||||||
const { lastMessage, readyState } = useWebSocketContext();
|
const { lastMessage, readyState } = useWebSocketContext();
|
||||||
|
|
||||||
if (readyState !== 1) {
|
if (readyState !== 1) {
|
||||||
@ -27,7 +27,7 @@ export default function ServerDetailOverview() {
|
|||||||
return <ServerDetailLoading />;
|
return <ServerDetailLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = nezhaWsData.servers.find((s) => s.id === Number(id));
|
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
return <ServerDetailLoading />;
|
return <ServerDetailLoading />;
|
||||||
|
48
src/components/TabSwitch.tsx
Normal file
48
src/components/TabSwitch.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { m } from "framer-motion";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
|
||||||
|
export default function TabSwitch({
|
||||||
|
tabs,
|
||||||
|
currentTab,
|
||||||
|
setCurrentTab,
|
||||||
|
}: {
|
||||||
|
tabs: string[];
|
||||||
|
currentTab: string;
|
||||||
|
setCurrentTab: (tab: string) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="z-50 flex flex-col items-start rounded-[50px]">
|
||||||
|
<div className="flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800">
|
||||||
|
{tabs.map((tab: string) => (
|
||||||
|
<div
|
||||||
|
key={tab}
|
||||||
|
onClick={() => setCurrentTab(tab)}
|
||||||
|
className={cn(
|
||||||
|
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
|
||||||
|
currentTab === tab
|
||||||
|
? "text-black dark:text-white"
|
||||||
|
: "text-stone-400 dark:text-stone-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{currentTab === tab && (
|
||||||
|
<m.div
|
||||||
|
layoutId="tab-switch-active"
|
||||||
|
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
||||||
|
style={{
|
||||||
|
originY: "0px",
|
||||||
|
borderRadius: 46,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="relative z-20 flex items-center gap-1">
|
||||||
|
<p className="whitespace-nowrap">{t("tabSwitch."+tab)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -67,7 +67,7 @@ ChartContainer.displayName = "Chart";
|
|||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
const colorConfig = Object.entries(config).filter(
|
const colorConfig = Object.entries(config).filter(
|
||||||
([_, config]) => config.theme || config.color,
|
([, config]) => config.theme || config.color,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
if (!colorConfig.length) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LoginUserResponse, ServerGroupResponse } from "@/types/nezha-api";
|
import { LoginUserResponse, MonitorResponse, ServerGroupResponse } from "@/types/nezha-api";
|
||||||
|
|
||||||
export const fetchServerGroup = async (): Promise<ServerGroupResponse> => {
|
export const fetchServerGroup = async (): Promise<ServerGroupResponse> => {
|
||||||
const response = await fetch("/api/v1/server-group");
|
const response = await fetch("/api/v1/server-group");
|
||||||
@ -17,3 +17,13 @@ export const fetchLoginUser = async (): Promise<LoginUserResponse> => {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const fetchMonitor = async (server_id: number): Promise<MonitorResponse> => {
|
||||||
|
const response = await fetch(`/api/v1/service/${server_id}`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
@ -57,5 +57,14 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"pageNotFound": "Page not found",
|
"pageNotFound": "Page not found",
|
||||||
"backToHome": "Back to home"
|
"backToHome": "Back to home"
|
||||||
|
},
|
||||||
|
"tabSwitch":{
|
||||||
|
"Detail": "Detail",
|
||||||
|
"Network": "Network"
|
||||||
|
},
|
||||||
|
"monitor":{
|
||||||
|
"noData": "No server monitor data",
|
||||||
|
"avgDelay": "Latency",
|
||||||
|
"monitorCount": "Services"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,5 +57,14 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"pageNotFound": "页面不存在",
|
"pageNotFound": "页面不存在",
|
||||||
"backToHome": "回到主页"
|
"backToHome": "回到主页"
|
||||||
|
},
|
||||||
|
"tabSwitch": {
|
||||||
|
"Detail": "详情",
|
||||||
|
"Network": "网络"
|
||||||
|
},
|
||||||
|
"monitor": {
|
||||||
|
"noData": "没有服务器监控数据",
|
||||||
|
"avgDelay": "延迟",
|
||||||
|
"monitorCount": "个监控服务"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,5 +57,14 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"pageNotFound": "頁面不存在",
|
"pageNotFound": "頁面不存在",
|
||||||
"backToHome": "回到主頁"
|
"backToHome": "回到主頁"
|
||||||
|
},
|
||||||
|
"tabSwitch": {
|
||||||
|
"detail": "詳細資訊",
|
||||||
|
"network": "網路"
|
||||||
|
},
|
||||||
|
"monitor": {
|
||||||
|
"noData": "沒有服務器監控數據",
|
||||||
|
"avgDelay": "延遲",
|
||||||
|
"monitorCount": "個監控"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,47 @@
|
|||||||
|
import { NetworkChart } from "@/components/NetworkChart";
|
||||||
import ServerDetailChart from "@/components/ServerDetailChart";
|
import ServerDetailChart from "@/components/ServerDetailChart";
|
||||||
import ServerDetailOverview from "@/components/ServerDetailOverview";
|
import ServerDetailOverview from "@/components/ServerDetailOverview";
|
||||||
|
import TabSwitch from "@/components/TabSwitch";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
export default function ServerDetail() {
|
export default function ServerDetail() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const tabs = ["Detail", "Network"];
|
||||||
|
const [currentTab, setCurrentTab] = useState(tabs[0]);
|
||||||
|
|
||||||
|
const { id: server_id } = useParams();
|
||||||
|
|
||||||
|
if (!server_id) {
|
||||||
|
navigate('/404');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl px-0 flex flex-col gap-4">
|
<div className="mx-auto w-full max-w-5xl px-0 flex flex-col gap-4">
|
||||||
<ServerDetailOverview />
|
<ServerDetailOverview server_id={server_id} />
|
||||||
<ServerDetailChart />
|
<section className="flex items-center my-2 w-full">
|
||||||
|
<Separator className="flex-1" />
|
||||||
|
<div className="flex justify-center w-full max-w-[200px]">
|
||||||
|
<TabSwitch
|
||||||
|
tabs={tabs}
|
||||||
|
currentTab={currentTab}
|
||||||
|
setCurrentTab={setCurrentTab}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Separator className="flex-1" />
|
||||||
|
</section>
|
||||||
|
<div style={{ display: currentTab === tabs[0] ? "block" : "none" }}>
|
||||||
|
<ServerDetailChart server_id={server_id} />
|
||||||
|
</div>
|
||||||
|
<div style={{ display: currentTab === tabs[1] ? "block" : "none" }}>
|
||||||
|
<NetworkChart
|
||||||
|
server_id={Number(server_id)}
|
||||||
|
show={currentTab === tabs[1]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -70,3 +70,25 @@ export interface LoginUserResponse {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface MonitorResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: NezhaMonitor[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerMonitorChart = {
|
||||||
|
[key: string]: {
|
||||||
|
created_at: number;
|
||||||
|
avg_delay: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NezhaMonitor {
|
||||||
|
monitor_id: number;
|
||||||
|
monitor_name: string;
|
||||||
|
server_id: number;
|
||||||
|
server_name: string;
|
||||||
|
created_at: number[];
|
||||||
|
avg_delay: number[];
|
||||||
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/footer.tsx","./src/components/groupswitch.tsx","./src/components/header.tsx","./src/components/icon.tsx","./src/components/languageswitcher.tsx","./src/components/servercard.tsx","./src/components/serverdetailchart.tsx","./src/components/serverdetailoverview.tsx","./src/components/serverflag.tsx","./src/components/serveroverview.tsx","./src/components/serverusagebar.tsx","./src/components/themeprovider.tsx","./src/components/themeswitcher.tsx","./src/components/loading/loader.tsx","./src/components/loading/serverdetailloading.tsx","./src/components/motion/framer-lazy-feature.ts","./src/components/motion/motion-provider.tsx","./src/components/ui/animated-circular-progress-bar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/progress.tsx","./src/components/ui/separator.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/table.tsx","./src/context/websocket-context.ts","./src/context/websocket-provider.tsx","./src/hooks/use-theme.ts","./src/hooks/use-websocket-context.ts","./src/lib/logo-class.tsx","./src/lib/nav-router.ts","./src/lib/nezha-api.ts","./src/lib/utils.ts","./src/pages/notfound.tsx","./src/pages/server.tsx","./src/pages/serverdetail.tsx","./src/types/nezha-api.ts"],"version":"5.6.3"}
|
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/footer.tsx","./src/components/groupswitch.tsx","./src/components/header.tsx","./src/components/icon.tsx","./src/components/languageswitcher.tsx","./src/components/networkchart.tsx","./src/components/networkchartloading.tsx","./src/components/servercard.tsx","./src/components/serverdetailchart.tsx","./src/components/serverdetailoverview.tsx","./src/components/serverflag.tsx","./src/components/serveroverview.tsx","./src/components/serverusagebar.tsx","./src/components/tabswitch.tsx","./src/components/themeprovider.tsx","./src/components/themeswitcher.tsx","./src/components/loading/loader.tsx","./src/components/loading/serverdetailloading.tsx","./src/components/motion/framer-lazy-feature.ts","./src/components/motion/motion-provider.tsx","./src/components/ui/animated-circular-progress-bar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/progress.tsx","./src/components/ui/separator.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/table.tsx","./src/context/websocket-context.ts","./src/context/websocket-provider.tsx","./src/hooks/use-theme.ts","./src/hooks/use-websocket-context.ts","./src/lib/logo-class.tsx","./src/lib/nav-router.ts","./src/lib/nezha-api.ts","./src/lib/utils.ts","./src/pages/notfound.tsx","./src/pages/server.tsx","./src/pages/serverdetail.tsx","./src/types/nezha-api.ts"],"version":"5.6.3"}
|
Loading…
x
Reference in New Issue
Block a user