feat: service tracker

This commit is contained in:
hamster1963 2024-11-29 09:00:04 +08:00
parent d7f0410dcd
commit 2462dfc21b
14 changed files with 229 additions and 69 deletions

View File

@ -26,7 +26,6 @@ import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import NetworkChartLoading from "./NetworkChartLoading"; import NetworkChartLoading from "./NetworkChartLoading";
import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api"; import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api";
interface ResultItem { interface ResultItem {
created_at: number; created_at: number;
[key: string]: number | null; [key: string]: number | null;
@ -41,16 +40,14 @@ export function NetworkChart({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { data: monitorData} = useQuery( const { data: monitorData } = useQuery({
{
queryKey: ["monitor", server_id], queryKey: ["monitor", server_id],
queryFn: () => fetchMonitor(server_id), queryFn: () => fetchMonitor(server_id),
enabled: show, enabled: show,
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchInterval: 10000, refetchInterval: 10000,
} });
)
if (!monitorData) return <NetworkChartLoading />; if (!monitorData) return <NetworkChartLoading />;
@ -68,8 +65,6 @@ export function NetworkChart({
); );
} }
const transformedData = transformData(monitorData.data); const transformedData = transformData(monitorData.data);
const formattedData = formatData(monitorData.data); const formattedData = formatData(monitorData.data);

View File

@ -7,7 +7,11 @@ import { Card } from "./ui/card";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function ServerCard({ serverInfo }: { serverInfo: NezhaServer }) { export default function ServerCard({
serverInfo,
}: {
serverInfo: NezhaServer;
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { name, country_code, online, cpu, up, down, mem, stg } = const { name, country_code, online, cpu, up, down, mem, stg } =

View File

@ -50,7 +50,11 @@ type connectChartData = {
udp: number; udp: number;
}; };
export default function ServerDetailChart({server_id}: {server_id: string}) { export default function ServerDetailChart({
server_id,
}: {
server_id: string;
}) {
const { lastMessage, readyState } = useWebSocketContext(); const { lastMessage, readyState } = useWebSocketContext();
if (readyState !== 1) { if (readyState !== 1) {

View File

@ -9,7 +9,11 @@ import { NezhaWebsocketResponse } from "@/types/nezha-api";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; 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 { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -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 <div className="mt-4">Loading...</div>;
}
if (!serviceData?.data?.services) {
return <div className="mt-4">No service data available</div>;
}
return (
<div className="mt-4 w-full mx-auto grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-4">
{Object.entries(serviceData.data.services).map(([name, data]) => {
const { days, uptime } = processServiceData(data);
return (
<ServiceTrackerClient
key={name}
days={days}
title={data.service.name}
uptime={uptime}
/>
);
})}
</div>
);
};
export default ServiceTracker;

View File

@ -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<ServiceTrackerProps> = ({
days,
className,
title,
uptime = 100,
}) => {
return (
<div
className={cn(
"w-full space-y-3 bg-white px-4 py-4 dark:bg-black rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className,
)}
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div className="w-5 h-5 rounded-full bg-green-600 flex items-center justify-center">
<div className="w-3 h-3 rounded-full bg-white dark:bg-black" />
</div>
<span className="font-medium text-sm">{title}</span>
</div>
<span className="text-green-600 font-medium text-sm">
{uptime.toFixed(1)}% uptime
</span>
</div>
<div className="flex gap-[2px]">
{days.map((day, index) => (
<div
key={index}
className={cn(
"flex-1 h-6 rounded-[5px] transition-colors",
day.completed ? "bg-green-600" : "bg-red-500",
)}
title={
day.date ? day.date.toLocaleDateString() : `Day ${index + 1}`
}
/>
))}
</div>
<div className="flex justify-between text-xs text-stone-500 dark:text-stone-400">
<span>30 DAYS AGO</span>
<span>TODAY</span>
</div>
</div>
);
};
export default ServiceTrackerClient;

View File

@ -2,7 +2,6 @@ import { cn } from "@/lib/utils";
import { m } from "framer-motion"; import { m } from "framer-motion";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function TabSwitch({ export default function TabSwitch({
tabs, tabs,
currentTab, currentTab,
@ -38,7 +37,7 @@ export default function TabSwitch({
/> />
)} )}
<div className="relative z-20 flex items-center gap-1"> <div className="relative z-20 flex items-center gap-1">
<p className="whitespace-nowrap">{t("tabSwitch."+tab)}</p> <p className="whitespace-nowrap">{t("tabSwitch." + tab)}</p>
</div> </div>
</div> </div>
))} ))}

View File

@ -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",
},
];

View File

@ -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<ServerGroupResponse> => { export const fetchServerGroup = async (): Promise<ServerGroupResponse> => {
const response = await fetch("/api/v1/server-group"); const response = await fetch("/api/v1/server-group");
@ -18,8 +23,9 @@ export const fetchLoginUser = async (): Promise<LoginUserResponse> => {
return data; return data;
}; };
export const fetchMonitor = async (
export const fetchMonitor = async (server_id: number): Promise<MonitorResponse> => { server_id: number,
): Promise<MonitorResponse> => {
const response = await fetch(`/api/v1/service/${server_id}`); const response = await fetch(`/api/v1/service/${server_id}`);
const data = await response.json(); const data = await response.json();
if (data.error) { if (data.error) {
@ -27,3 +33,12 @@ export const fetchMonitor = async (server_id: number): Promise<MonitorResponse>
} }
return data; return data;
}; };
export const fetchService = async (): Promise<ServiceResponse> => {
const response = await fetch("/api/v1/service");
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
return data;
};

View File

@ -58,11 +58,11 @@
"pageNotFound": "Page not found", "pageNotFound": "Page not found",
"backToHome": "Back to home" "backToHome": "Back to home"
}, },
"tabSwitch":{ "tabSwitch": {
"Detail": "Detail", "Detail": "Detail",
"Network": "Network" "Network": "Network"
}, },
"monitor":{ "monitor": {
"noData": "No server monitor data", "noData": "No server monitor data",
"avgDelay": "Latency", "avgDelay": "Latency",
"monitorCount": "Services" "monitorCount": "Services"

View File

@ -10,6 +10,8 @@ import GroupSwitch from "@/components/GroupSwitch";
import { ServerGroup } from "@/types/nezha-api"; import { ServerGroup } from "@/types/nezha-api";
import { useWebSocketContext } from "@/hooks/use-websocket-context"; import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ChartBarSquareIcon } from "@heroicons/react/20/solid";
import { ServiceTracker } from "@/components/ServiceTracker";
export default function Servers() { export default function Servers() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -19,6 +21,7 @@ export default function Servers() {
}); });
const { lastMessage, readyState } = useWebSocketContext(); const { lastMessage, readyState } = useWebSocketContext();
const [showServices, setShowServices] = useState(false);
const [currentGroup, setCurrentGroup] = useState<string>("All"); const [currentGroup, setCurrentGroup] = useState<string>("All");
const groupTabs = [ const groupTabs = [
@ -91,13 +94,22 @@ export default function Servers() {
up={up} up={up}
down={down} down={down}
/> />
<div className="mt-6"> <section className="flex mt-6 items-center gap-2 w-full overflow-hidden">
<button
onClick={() => {
setShowServices(!showServices);
}}
className="rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 hover:bg-blue-500 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] hover:shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] "
>
<ChartBarSquareIcon className="size-[13px]" />
</button>
<GroupSwitch <GroupSwitch
tabs={groupTabs} tabs={groupTabs}
currentTab={currentGroup} currentTab={currentGroup}
setCurrentTab={setCurrentGroup} setCurrentTab={setCurrentGroup}
/> />
</div> </section>
{showServices && <ServiceTracker />}
<section className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6"> <section className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6">
{filteredServers.map((serverInfo) => ( {filteredServers.map((serverInfo) => (
<ServerCard key={serverInfo.id} serverInfo={serverInfo} /> <ServerCard key={serverInfo.id} serverInfo={serverInfo} />

View File

@ -15,7 +15,7 @@ export default function ServerDetail() {
const { id: server_id } = useParams(); const { id: server_id } = useParams();
if (!server_id) { if (!server_id) {
navigate('/404'); navigate("/404");
return null; return null;
} }

View File

@ -71,7 +71,6 @@ export interface LoginUserResponse {
}; };
} }
export interface MonitorResponse { export interface MonitorResponse {
success: boolean; success: boolean;
data: NezhaMonitor[]; data: NezhaMonitor[];
@ -92,3 +91,39 @@ export interface NezhaMonitor {
created_at: number[]; created_at: number[];
avg_delay: 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[];
}

View File

@ -28,11 +28,15 @@ export default defineConfig({
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks(id) { manualChunks(id) {
if (id.includes('node_modules')) { if (id.includes("node_modules")) {
return id.toString().split('node_modules/')[1].split('/')[0].toString(); return id
} .toString()
} .split("node_modules/")[1]
} .split("/")[0]
} .toString();
} }
},
},
},
},
}); });