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 (
+
+
+
+
+ {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();
}
- }
- }
- }
- }
+ },
+ },
+ },
+ },
});