From 2462dfc21b3c3ff8419679ce8cb55a08d2f84676 Mon Sep 17 00:00:00 2001 From: hamster1963 <1410514192@qq.com> Date: Fri, 29 Nov 2024 09:00:04 +0800 Subject: [PATCH] feat: service tracker --- src/components/NetworkChart.tsx | 23 ++++----- src/components/ServerCard.tsx | 6 ++- src/components/ServerDetailChart.tsx | 6 ++- src/components/ServerDetailOverview.tsx | 8 +++- src/components/ServiceTracker.tsx | 56 ++++++++++++++++++++++ src/components/ServiceTrackerClient.tsx | 62 +++++++++++++++++++++++++ src/components/TabSwitch.tsx | 3 +- src/lib/nav-router.ts | 30 ------------ src/lib/nezha-api.ts | 23 +++++++-- src/locales/en/translation.json | 4 +- src/pages/Server.tsx | 16 ++++++- src/pages/ServerDetail.tsx | 8 ++-- src/types/nezha-api.ts | 37 ++++++++++++++- vite.config.ts | 16 ++++--- 14 files changed, 229 insertions(+), 69 deletions(-) create mode 100644 src/components/ServiceTracker.tsx create mode 100644 src/components/ServiceTrackerClient.tsx delete mode 100644 src/lib/nav-router.ts diff --git a/src/components/NetworkChart.tsx b/src/components/NetworkChart.tsx index e22f640..adcc36f 100644 --- a/src/components/NetworkChart.tsx +++ b/src/components/NetworkChart.tsx @@ -26,7 +26,6 @@ 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; @@ -41,17 +40,15 @@ export function NetworkChart({ }) { const { t } = useTranslation(); - const { data: monitorData} = useQuery( - { - queryKey: ["monitor", server_id], - queryFn: () => fetchMonitor(server_id), - enabled: show, - refetchOnMount: true, - refetchOnWindowFocus: true, - refetchInterval: 10000, - } - ) - + const { data: monitorData } = useQuery({ + queryKey: ["monitor", server_id], + queryFn: () => fetchMonitor(server_id), + enabled: show, + refetchOnMount: true, + refetchOnWindowFocus: true, + refetchInterval: 10000, + }); + if (!monitorData) return ; if (monitorData?.success && monitorData.data.length === 0) { @@ -68,8 +65,6 @@ export function NetworkChart({ ); } - - const transformedData = transformData(monitorData.data); const formattedData = formatData(monitorData.data); diff --git a/src/components/ServerCard.tsx b/src/components/ServerCard.tsx index dabc0c6..d077ee1 100644 --- a/src/components/ServerCard.tsx +++ b/src/components/ServerCard.tsx @@ -7,7 +7,11 @@ import { Card } from "./ui/card"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -export default function ServerCard({ serverInfo }: { serverInfo: NezhaServer }) { +export default function ServerCard({ + serverInfo, +}: { + serverInfo: NezhaServer; +}) { const { t } = useTranslation(); const navigate = useNavigate(); const { name, country_code, online, cpu, up, down, mem, stg } = diff --git a/src/components/ServerDetailChart.tsx b/src/components/ServerDetailChart.tsx index 4cae648..7bb9a81 100644 --- a/src/components/ServerDetailChart.tsx +++ b/src/components/ServerDetailChart.tsx @@ -50,7 +50,11 @@ type connectChartData = { udp: number; }; -export default function ServerDetailChart({server_id}: {server_id: string}) { +export default function ServerDetailChart({ + server_id, +}: { + server_id: string; +}) { const { lastMessage, readyState } = useWebSocketContext(); if (readyState !== 1) { diff --git a/src/components/ServerDetailOverview.tsx b/src/components/ServerDetailOverview.tsx index 02330e8..22e1b01 100644 --- a/src/components/ServerDetailOverview.tsx +++ b/src/components/ServerDetailOverview.tsx @@ -9,10 +9,14 @@ import { NezhaWebsocketResponse } from "@/types/nezha-api"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -export default function ServerDetailOverview({server_id}: {server_id: string}) { +export default function ServerDetailOverview({ + server_id, +}: { + server_id: string; +}) { const { t } = useTranslation(); const navigate = useNavigate(); - + const { lastMessage, readyState } = useWebSocketContext(); if (readyState !== 1) { diff --git a/src/components/ServiceTracker.tsx b/src/components/ServiceTracker.tsx new file mode 100644 index 0000000..da65b66 --- /dev/null +++ b/src/components/ServiceTracker.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import ServiceTrackerClient from "./ServiceTrackerClient"; +import { useQuery } from "@tanstack/react-query"; +import { fetchService } from "@/lib/nezha-api"; +import { ServiceData } from "@/types/nezha-api"; + +export const ServiceTracker: React.FC = () => { + const { data: serviceData, isLoading } = useQuery({ + queryKey: ["service"], + queryFn: () => fetchService(), + refetchOnMount: true, + refetchOnWindowFocus: true, + refetchInterval: 10000, + }); + + const processServiceData = (serviceData: ServiceData) => { + const days = serviceData.up.map((up, index) => ({ + completed: up > serviceData.down[index], + date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000), + })); + + const totalUp = serviceData.up.reduce((a, b) => a + b, 0); + const totalChecks = + serviceData.up.reduce((a, b) => a + b, 0) + + serviceData.down.reduce((a, b) => a + b, 0); + const uptime = (totalUp / totalChecks) * 100; + + return { days, uptime }; + }; + + if (isLoading) { + return
Loading...
; + } + + if (!serviceData?.data?.services) { + return
No service data available
; + } + + return ( +
+ {Object.entries(serviceData.data.services).map(([name, data]) => { + const { days, uptime } = processServiceData(data); + return ( + + ); + })} +
+ ); +}; + +export default ServiceTracker; diff --git a/src/components/ServiceTrackerClient.tsx b/src/components/ServiceTrackerClient.tsx new file mode 100644 index 0000000..eb1d5a7 --- /dev/null +++ b/src/components/ServiceTrackerClient.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { cn } from "@/lib/utils"; + +interface ServiceTrackerProps { + days: Array<{ + completed: boolean; + date?: Date; + }>; + className?: string; + title?: string; + uptime?: number; +} + +export const ServiceTrackerClient: React.FC = ({ + days, + className, + title, + uptime = 100, +}) => { + return ( +
+
+
+
+
+
+ {title} +
+ + {uptime.toFixed(1)}% uptime + +
+ +
+ {days.map((day, index) => ( +
+ ))} +
+ +
+ 30 DAYS AGO + TODAY +
+
+ ); +}; + +export default ServiceTrackerClient; diff --git a/src/components/TabSwitch.tsx b/src/components/TabSwitch.tsx index 6baa93f..dc890ee 100644 --- a/src/components/TabSwitch.tsx +++ b/src/components/TabSwitch.tsx @@ -2,7 +2,6 @@ import { cn } from "@/lib/utils"; import { m } from "framer-motion"; import { useTranslation } from "react-i18next"; - export default function TabSwitch({ tabs, currentTab, @@ -38,7 +37,7 @@ export default function TabSwitch({ /> )}
-

{t("tabSwitch."+tab)}

+

{t("tabSwitch." + tab)}

))} diff --git a/src/lib/nav-router.ts b/src/lib/nav-router.ts deleted file mode 100644 index c486b69..0000000 --- a/src/lib/nav-router.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const navRouter = [ - { - name: "服务器", - path: "/", - }, - { - name: "服务(Dev)", - path: "/service", - }, - { - name: "任务(Dev)", - path: "/task", - }, - { - name: "告警(Dev)", - path: "/alarm", - }, - { - name: "内网穿透(Dev)", - path: "/intranet", - }, - { - name: "用户", - path: "/user", - }, - { - name: "设置(Dev)", - path: "/setting", - }, -]; diff --git a/src/lib/nezha-api.ts b/src/lib/nezha-api.ts index c44e51e..d82b298 100644 --- a/src/lib/nezha-api.ts +++ b/src/lib/nezha-api.ts @@ -1,4 +1,9 @@ -import { LoginUserResponse, MonitorResponse, ServerGroupResponse } from "@/types/nezha-api"; +import { + LoginUserResponse, + MonitorResponse, + ServerGroupResponse, + ServiceResponse, +} from "@/types/nezha-api"; export const fetchServerGroup = async (): Promise => { const response = await fetch("/api/v1/server-group"); @@ -18,12 +23,22 @@ export const fetchLoginUser = async (): Promise => { return data; }; - -export const fetchMonitor = async (server_id: number): Promise => { +export const fetchMonitor = async ( + server_id: number, +): Promise => { const response = await fetch(`/api/v1/service/${server_id}`); const data = await response.json(); if (data.error) { throw new Error(data.error); } return data; -}; \ No newline at end of file +}; + +export const fetchService = async (): Promise => { + const response = await fetch("/api/v1/service"); + const data = await response.json(); + if (data.error) { + throw new Error(data.error); + } + return data; +}; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 85cc6fd..2a120e3 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -58,11 +58,11 @@ "pageNotFound": "Page not found", "backToHome": "Back to home" }, - "tabSwitch":{ + "tabSwitch": { "Detail": "Detail", "Network": "Network" }, - "monitor":{ + "monitor": { "noData": "No server monitor data", "avgDelay": "Latency", "monitorCount": "Services" diff --git a/src/pages/Server.tsx b/src/pages/Server.tsx index fb1dc14..9ad833a 100644 --- a/src/pages/Server.tsx +++ b/src/pages/Server.tsx @@ -10,6 +10,8 @@ import GroupSwitch from "@/components/GroupSwitch"; import { ServerGroup } from "@/types/nezha-api"; import { useWebSocketContext } from "@/hooks/use-websocket-context"; import { useTranslation } from "react-i18next"; +import { ChartBarSquareIcon } from "@heroicons/react/20/solid"; +import { ServiceTracker } from "@/components/ServiceTracker"; export default function Servers() { const { t } = useTranslation(); @@ -19,6 +21,7 @@ export default function Servers() { }); const { lastMessage, readyState } = useWebSocketContext(); + const [showServices, setShowServices] = useState(false); const [currentGroup, setCurrentGroup] = useState("All"); const groupTabs = [ @@ -91,13 +94,22 @@ export default function Servers() { up={up} down={down} /> -
+
+ -
+ + {showServices && }
{filteredServers.map((serverInfo) => ( diff --git a/src/pages/ServerDetail.tsx b/src/pages/ServerDetail.tsx index fc9ed9b..a9bce74 100644 --- a/src/pages/ServerDetail.tsx +++ b/src/pages/ServerDetail.tsx @@ -15,10 +15,10 @@ export default function ServerDetail() { const { id: server_id } = useParams(); if (!server_id) { - navigate('/404'); + navigate("/404"); return null; } - + return (
@@ -34,10 +34,10 @@ export default function ServerDetail() {
- +
- diff --git a/src/types/nezha-api.ts b/src/types/nezha-api.ts index 877cd37..11b63d9 100644 --- a/src/types/nezha-api.ts +++ b/src/types/nezha-api.ts @@ -71,7 +71,6 @@ export interface LoginUserResponse { }; } - export interface MonitorResponse { success: boolean; data: NezhaMonitor[]; @@ -92,3 +91,39 @@ export interface NezhaMonitor { created_at: number[]; avg_delay: number[]; } + +export interface ServiceResponse { + success: boolean; + data: { + services: { + [key: string]: ServiceData; + }; + }; +} + +export interface Service { + // created_at: string; + // updated_at: string; + name: string; + // type: number; + // target: string; + // duration: number; + // notification_group_id: number; + // cover: number; + // fail_trigger_tasks: null | any[]; + // recover_trigger_tasks: null | any[]; + // min_latency: number; + // max_latency: number; + // skip_servers: null | any[]; +} + +export interface ServiceData { + service: Service; + current_up: number; + current_down: number; + total_up: number; + total_down: number; + delay: number[]; + up: number[]; + down: number[]; +} diff --git a/vite.config.ts b/vite.config.ts index 63f1315..9a2c4e9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -28,11 +28,15 @@ export default defineConfig({ rollupOptions: { output: { manualChunks(id) { - if (id.includes('node_modules')) { - return id.toString().split('node_modules/')[1].split('/')[0].toString(); + if (id.includes("node_modules")) { + return id + .toString() + .split("node_modules/")[1] + .split("/")[0] + .toString(); } - } - } - } - } + }, + }, + }, + }, });