fix: prettier config

This commit is contained in:
hamster1963
2024-12-13 17:26:28 +08:00
parent 1483ce56fa
commit 9a2f3ea8e6
81 changed files with 1666 additions and 2286 deletions

View File

@@ -1,10 +1,11 @@
import React from "react";
import { CycleTransferStats } from "@/types/nezha-api";
import { CycleTransferStatsClient } from "./CycleTransferStatsClient";
import { CycleTransferStats } from "@/types/nezha-api"
import React from "react"
import { CycleTransferStatsClient } from "./CycleTransferStatsClient"
interface CycleTransferStatsProps {
cycleStats: CycleTransferStats;
className?: string;
cycleStats: CycleTransferStats
className?: string
}
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({
@@ -15,41 +16,39 @@ export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({
<section className="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-4">
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
if (!cycleData.server_name) {
return null;
return null
}
return Object.entries(cycleData.server_name).map(
([serverId, serverName]) => {
const transfer = cycleData.transfer?.[serverId] || 0;
const nextUpdate = cycleData.next_update?.[serverId];
return Object.entries(cycleData.server_name).map(([serverId, serverName]) => {
const transfer = cycleData.transfer?.[serverId] || 0
const nextUpdate = cycleData.next_update?.[serverId]
if (!transfer && !nextUpdate) {
return null;
}
if (!transfer && !nextUpdate) {
return null
}
return (
<CycleTransferStatsClient
key={`${cycleId}-${serverId}`}
name={cycleData.name}
from={cycleData.from}
to={cycleData.to}
max={cycleData.max}
serverStats={[
{
serverId,
serverName,
transfer,
nextUpdate: nextUpdate || "",
},
]}
className={className}
/>
);
},
);
return (
<CycleTransferStatsClient
key={`${cycleId}-${serverId}`}
name={cycleData.name}
from={cycleData.from}
to={cycleData.to}
max={cycleData.max}
serverStats={[
{
serverId,
serverName,
transfer,
nextUpdate: nextUpdate || "",
},
]}
className={className}
/>
)
})
})}
</section>
);
};
)
}
export default CycleTransferStatsCard;
export default CycleTransferStatsCard

View File

@@ -1,28 +1,34 @@
import React from "react";
import { cn } from "@/lib/utils";
import { formatBytes } from "@/lib/format";
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar";
import { CircleStackIcon } from "@heroicons/react/24/outline";
import { useTranslation } from "react-i18next";
import { formatBytes } from "@/lib/format"
import { cn } from "@/lib/utils"
import { CircleStackIcon } from "@heroicons/react/24/outline"
import React from "react"
import { useTranslation } from "react-i18next"
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar"
interface CycleTransferStatsClientProps {
name: string;
from: string;
to: string;
max: number;
name: string
from: string
to: string
max: number
serverStats: Array<{
serverId: string;
serverName: string;
transfer: number;
nextUpdate: string;
}>;
className?: string;
serverId: string
serverName: string
transfer: number
nextUpdate: string
}>
className?: string
}
export const CycleTransferStatsClient: React.FC<
CycleTransferStatsClientProps
> = ({ name, from, to, max, serverStats, className }) => {
const { t } = useTranslation();
export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> = ({
name,
from,
to,
max,
serverStats,
className,
}) => {
const { t } = useTranslation()
return (
<div
className={cn(
@@ -31,7 +37,7 @@ export const CycleTransferStatsClient: React.FC<
)}
>
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => {
const progress = (transfer / max) * 100;
const progress = (transfer / max) * 100
return (
<div key={serverId}>
@@ -40,8 +46,7 @@ export const CycleTransferStatsClient: React.FC<
{name}
</div>
<span className="text-stone-600 dark:text-stone-400 text-xs">
{new Date(from).toLocaleDateString()} -{" "}
{new Date(to).toLocaleDateString()}
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
</span>
</section>
@@ -51,9 +56,7 @@ export const CycleTransferStatsClient: React.FC<
<span className="text-sm font-semibold">{serverName}</span>
</div>
<div className="flex items-center gap-1">
<p className="text-xs text-end w-10 font-medium">
{progress.toFixed(0)}%
</p>
<p className="text-xs text-end w-10 font-medium">{progress.toFixed(0)}%</p>
<AnimatedCircularProgressBar
className="size-4 text-[0px]"
max={100}
@@ -82,15 +85,14 @@ export const CycleTransferStatsClient: React.FC<
<section className="flex justify-between items-center mt-2">
<div className="text-xs text-stone-500 dark:text-stone-400">
{t("cycleTransfer.nextUpdate")}:{" "}
{new Date(nextUpdate).toLocaleString()}
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()}
</div>
</section>
</div>
);
)
})}
</div>
);
};
)
}
export default CycleTransferStatsClient;
export default CycleTransferStatsClient

View File

@@ -1,17 +1,17 @@
import { fetchSetting } from "@/lib/nezha-api";
import { useQuery } from "@tanstack/react-query";
import React from "react";
import { useTranslation } from "react-i18next";
import { fetchSetting } from "@/lib/nezha-api"
import { useQuery } from "@tanstack/react-query"
import React from "react"
import { useTranslation } from "react-i18next"
const Footer: React.FC = () => {
const { t } = useTranslation();
const { t } = useTranslation()
const { data: settingData } = useQuery({
queryKey: ["setting"],
queryFn: () => fetchSetting(),
refetchOnMount: true,
refetchOnWindowFocus: true,
});
})
return (
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4">
@@ -26,10 +26,7 @@ const Footer: React.FC = () => {
</div>
<p>
{t("footer.themeBy")}
<a
href={"https://github.com/hamster1963/nezha-dash"}
target="_blank"
>
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank">
nezha-dash
</a>
{import.meta.env.VITE_GIT_HASH && (
@@ -39,7 +36,7 @@ const Footer: React.FC = () => {
</section>
</section>
</footer>
);
};
)
}
export default Footer;
export default Footer

View File

@@ -1,41 +1,35 @@
import { geoJsonString } from "@/lib/geo-json-string";
import { NezhaServer } from "@/types/nezha-api";
import { useTranslation } from "react-i18next";
import { geoEquirectangular, geoPath } from "d3-geo";
import { countryCoordinates } from "@/lib/geo-limit";
import MapTooltip from "./MapTooltip";
import useTooltip from "@/hooks/use-tooltip";
import { formatNezhaInfo } from "@/lib/utils";
import useTooltip from "@/hooks/use-tooltip"
import { geoJsonString } from "@/lib/geo-json-string"
import { countryCoordinates } from "@/lib/geo-limit"
import { formatNezhaInfo } from "@/lib/utils"
import { NezhaServer } from "@/types/nezha-api"
import { geoEquirectangular, geoPath } from "d3-geo"
import { useTranslation } from "react-i18next"
export default function GlobalMap({
serverList,
now,
}: {
serverList: NezhaServer[];
now: number;
}) {
const { t } = useTranslation();
const countryList: string[] = [];
const serverCounts: { [key: string]: number } = {};
import MapTooltip from "./MapTooltip"
export default function GlobalMap({ serverList, now }: { serverList: NezhaServer[]; now: number }) {
const { t } = useTranslation()
const countryList: string[] = []
const serverCounts: { [key: string]: number } = {}
serverList.forEach((server) => {
if (server.country_code) {
const countryCode = server.country_code.toUpperCase();
const countryCode = server.country_code.toUpperCase()
if (!countryList.includes(countryCode)) {
countryList.push(countryCode);
countryList.push(countryCode)
}
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1;
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1
}
});
})
const width = 900;
const height = 500;
const width = 900
const height = 500
const geoJson = JSON.parse(geoJsonString);
const geoJson = JSON.parse(geoJsonString)
const filteredFeatures = geoJson.features.filter(
(feature: { properties: { iso_a3_eh: string } }) =>
feature.properties.iso_a3_eh !== "",
);
(feature: { properties: { iso_a3_eh: string } }) => feature.properties.iso_a3_eh !== "",
)
return (
<section className="flex flex-col gap-4 mt-8">
@@ -54,24 +48,24 @@ export default function GlobalMap({
/>
</div>
</section>
);
)
}
interface InteractiveMapProps {
countries: string[];
serverCounts: { [key: string]: number };
width: number;
height: number;
countries: string[]
serverCounts: { [key: string]: number }
width: number
height: number
filteredFeatures: {
type: "Feature";
type: "Feature"
properties: {
iso_a2_eh: string;
[key: string]: string;
};
geometry: never;
}[];
nezhaServerList: NezhaServer[];
now: number;
iso_a2_eh: string
[key: string]: string
}
geometry: never
}[]
nezhaServerList: NezhaServer[]
now: number
}
export function InteractiveMap({
@@ -83,20 +77,17 @@ export function InteractiveMap({
nezhaServerList,
now,
}: InteractiveMapProps) {
const { setTooltipData } = useTooltip();
const { setTooltipData } = useTooltip()
const projection = geoEquirectangular()
.scale(140)
.translate([width / 2, height / 2])
.rotate([-12, 0, 0]);
.rotate([-12, 0, 0])
const path = geoPath().projection(projection);
const path = geoPath().projection(projection)
return (
<div
className="relative w-full aspect-[2/1]"
onMouseLeave={() => setTooltipData(null)}
>
<div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}>
<svg
width={width}
height={height}
@@ -120,11 +111,9 @@ export function InteractiveMap({
onMouseEnter={() => setTooltipData(null)}
/>
{filteredFeatures.map((feature, index) => {
const isHighlighted = countries.includes(
feature.properties.iso_a2_eh,
);
const isHighlighted = countries.includes(feature.properties.iso_a2_eh)
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0;
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0
return (
<path
@@ -137,30 +126,29 @@ export function InteractiveMap({
}
onMouseEnter={() => {
if (!isHighlighted) {
setTooltipData(null);
return;
setTooltipData(null)
return
}
if (path.centroid(feature)) {
const countryCode = feature.properties.iso_a2_eh;
const countryCode = feature.properties.iso_a2_eh
const countryServers = nezhaServerList
.filter(
(server: NezhaServer) =>
server.country_code?.toUpperCase() === countryCode,
(server: NezhaServer) => server.country_code?.toUpperCase() === countryCode,
)
.map((server: NezhaServer) => ({
name: server.name,
status: formatNezhaInfo(now, server).online,
}));
}))
setTooltipData({
centroid: path.centroid(feature),
country: feature.properties.name,
count: serverCount,
servers: countryServers,
});
})
}
}}
/>
);
)
})}
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
@@ -168,18 +156,18 @@ export function InteractiveMap({
// 检查该国家是否已经在 filteredFeatures 中
const isInFilteredFeatures = filteredFeatures.some(
(feature) => feature.properties.iso_a2_eh === countryCode,
);
)
// 如果已经在 filteredFeatures 中,跳过
if (isInFilteredFeatures) return null;
if (isInFilteredFeatures) return null
// 获取国家的经纬度
const coords = countryCoordinates[countryCode];
if (!coords) return null;
const coords = countryCoordinates[countryCode]
if (!coords) return null
// 使用投影函数将经纬度转换为 SVG 坐标
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0];
const serverCount = serverCounts[countryCode] || 0;
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0]
const serverCount = serverCounts[countryCode] || 0
return (
<g
@@ -188,19 +176,18 @@ export function InteractiveMap({
const countryServers = nezhaServerList
.filter(
(server: NezhaServer) =>
server.country_code?.toUpperCase() ===
countryCode.toUpperCase(),
server.country_code?.toUpperCase() === countryCode.toUpperCase(),
)
.map((server: NezhaServer) => ({
name: server.name,
status: formatNezhaInfo(now, server).online,
}));
}))
setTooltipData({
centroid: [x, y],
country: coords.name,
count: serverCount,
servers: countryServers,
});
})
}}
className="cursor-pointer"
>
@@ -211,11 +198,11 @@ export function InteractiveMap({
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
/>
</g>
);
)
})}
</g>
</svg>
<MapTooltip />
</div>
);
)
}

View File

@@ -1,14 +1,14 @@
import { cn } from "@/lib/utils";
import { m } from "framer-motion";
import { cn } from "@/lib/utils"
import { m } from "framer-motion"
export default function GroupSwitch({
tabs,
currentTab,
setCurrentTab,
}: {
tabs: string[];
currentTab: string;
setCurrentTab: (tab: string) => void;
tabs: string[]
currentTab: string
setCurrentTab: (tab: string) => void
}) {
return (
<div className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]">
@@ -41,5 +41,5 @@ export default function GroupSwitch({
))}
</div>
</div>
);
)
}

View File

@@ -1,72 +1,71 @@
import { ModeToggle } from "@/components/ThemeSwitcher";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api";
import { useQuery } from "@tanstack/react-query";
import { DateTime } from "luxon";
import { useEffect, useRef, useState, useCallback } from "react";
import { LanguageSwitcher } from "./LanguageSwitcher";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { ModeToggle } from "@/components/ThemeSwitcher"
import { Separator } from "@/components/ui/separator"
import { Skeleton } from "@/components/ui/skeleton"
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"
import { useQuery } from "@tanstack/react-query"
import { DateTime } from "luxon"
import { useCallback, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { LanguageSwitcher } from "./LanguageSwitcher"
function Header() {
const { t } = useTranslation();
const navigate = useNavigate();
const { t } = useTranslation()
const navigate = useNavigate()
const { data: settingData, isLoading } = useQuery({
queryKey: ["setting"],
queryFn: () => fetchSetting(),
refetchOnMount: true,
refetchOnWindowFocus: true,
});
})
const siteName = settingData?.data?.site_name;
const siteName = settingData?.data?.site_name
const InjectContext = useCallback((content: string) => {
const tempDiv = document.createElement("div");
tempDiv.innerHTML = content;
const tempDiv = document.createElement("div")
tempDiv.innerHTML = content
const handlers: { [key: string]: (element: HTMLElement) => void } = {
SCRIPT: (element) => {
const script = document.createElement("script");
const script = document.createElement("script")
if ((element as HTMLScriptElement).src) {
script.src = (element as HTMLScriptElement).src;
script.src = (element as HTMLScriptElement).src
} else {
script.textContent = element.textContent;
script.textContent = element.textContent
}
document.body.appendChild(script);
document.body.appendChild(script)
},
STYLE: (element) => {
const style = document.createElement("style");
style.textContent = element.textContent;
document.head.appendChild(style);
const style = document.createElement("style")
style.textContent = element.textContent
document.head.appendChild(style)
},
DEFAULT: (element) => {
document.body.appendChild(element);
document.body.appendChild(element)
},
};
}
Array.from(tempDiv.childNodes).forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement;
(handlers[element.tagName] || handlers.DEFAULT)(element);
const element = node as HTMLElement
;(handlers[element.tagName] || handlers.DEFAULT)(element)
} else if (node.nodeType === Node.TEXT_NODE) {
document.body.appendChild(
document.createTextNode(node.textContent || ""),
);
document.body.appendChild(document.createTextNode(node.textContent || ""))
}
});
}, []);
})
}, [])
useEffect(() => {
document.title = siteName || "NEZHA";
}, [siteName]);
document.title = siteName || "NEZHA"
}, [siteName])
useEffect(() => {
if (settingData?.data?.custom_code) {
InjectContext(settingData?.data?.custom_code);
InjectContext(settingData?.data?.custom_code)
}
}, [settingData?.data?.custom_code]);
}, [settingData?.data?.custom_code])
return (
<div className="mx-auto w-full max-w-5xl">
@@ -89,13 +88,8 @@ function Header() {
) : (
siteName || "NEZHA"
)}
<Separator
orientation="vertical"
className="mx-2 hidden h-4 w-[1px] md:block"
/>
<p className="hidden text-sm font-medium opacity-40 md:block">
{t("nezha")}
</p>
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
<p className="hidden text-sm font-medium opacity-40 md:block">{t("nezha")}</p>
</section>
<section className="flex items-center gap-2">
<DashboardLink />
@@ -105,17 +99,17 @@ function Header() {
</section>
<Overview />
</div>
);
)
}
function DashboardLink() {
const { t } = useTranslation();
const { t } = useTranslation()
const { data: userData } = useQuery({
queryKey: ["login-user"],
queryFn: () => fetchLoginUser(),
refetchOnMount: true,
refetchOnWindowFocus: true,
});
})
return (
<div className="flex items-center gap-2">
@@ -129,37 +123,37 @@ function DashboardLink() {
{userData?.data?.id && t("dashboard")}
</a>
</div>
);
)
}
// https://github.com/streamich/react-use/blob/master/src/useInterval.ts
const useInterval = (callback: () => void, delay: number | null) => {
const savedCallback = useRef<() => void>(() => {});
const savedCallback = useRef<() => void>(() => {})
useEffect(() => {
savedCallback.current = callback;
});
savedCallback.current = callback
})
useEffect(() => {
if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0);
return () => clearInterval(interval);
const interval = setInterval(() => savedCallback.current(), delay || 0)
return () => clearInterval(interval)
}
return undefined;
}, [delay]);
};
return undefined
}, [delay])
}
function Overview() {
const { t } = useTranslation();
const [mouted, setMounted] = useState(false);
const { t } = useTranslation()
const [mouted, setMounted] = useState(false)
useEffect(() => {
setMounted(true);
}, []);
const timeOption = DateTime.TIME_SIMPLE;
timeOption.hour12 = true;
setMounted(true)
}, [])
const timeOption = DateTime.TIME_SIMPLE
timeOption.hour12 = true
const [timeString, setTimeString] = useState(
DateTime.now().setLocale("en-US").toLocaleString(timeOption),
);
)
useInterval(() => {
setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption));
}, 1000);
setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption))
}, 1000)
return (
<section className={"mt-10 flex flex-col md:mt-16"}>
<p className="text-base font-semibold">👋 {t("overview")}</p>
@@ -172,6 +166,6 @@ function Overview() {
)}
</div>
</section>
);
)
}
export default Header;
export default Header

View File

@@ -3,7 +3,7 @@ export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
<svg viewBox="0 0 496 512" fill="white" {...props}>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
</svg>
);
)
}
export function BackIcon() {
@@ -28,5 +28,5 @@ export function BackIcon() {
height="20"
/>
</>
);
)
}

View File

@@ -1,31 +1,30 @@
"use client";
"use client"
import { Button } from "@/components/ui/button";
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { useTranslation } from "react-i18next";
} from "@/components/ui/dropdown-menu"
import { CheckCircleIcon } from "@heroicons/react/20/solid"
import { useTranslation } from "react-i18next"
export function LanguageSwitcher() {
const { t, i18n } = useTranslation();
const { t, i18n } = useTranslation()
const locale = i18n.language;
const locale = i18n.language
const handleSelect = (e: Event, newLocale: string) => {
e.preventDefault(); // 阻止默认的关闭行为
i18n.changeLanguage(newLocale);
};
e.preventDefault() // 阻止默认的关闭行为
i18n.changeLanguage(newLocale)
}
const localeItems = [
{ name: t("language.zh-CN"), code: "zh-CN" },
{ name: t("language.zh-TW"), code: "zh-TW" },
{ name: t("language.en"), code: "en" },
];
]
return (
<DropdownMenu>
@@ -46,11 +45,10 @@ export function LanguageSwitcher() {
onSelect={(e) => handleSelect(e, item.code)}
className={locale === item.code ? "bg-muted gap-3" : ""}
>
{item.name}{" "}
{locale === item.code && <CheckCircleIcon className="size-4" />}
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
)
}

View File

@@ -1,13 +1,13 @@
import useTooltip from "@/hooks/use-tooltip";
import { AnimatePresence, m } from "framer-motion";
import { memo } from "react";
import { useTranslation } from "react-i18next";
import useTooltip from "@/hooks/use-tooltip"
import { AnimatePresence, m } from "framer-motion"
import { memo } from "react"
import { useTranslation } from "react-i18next"
const MapTooltip = memo(function MapTooltip() {
const { t } = useTranslation();
const { tooltipData } = useTooltip();
const { t } = useTranslation()
const { tooltipData } = useTooltip()
if (!tooltipData) return null;
if (!tooltipData) return null
return (
<AnimatePresence mode="wait">
@@ -23,14 +23,12 @@ const MapTooltip = memo(function MapTooltip() {
transform: "translate(20%, -50%)",
}}
onMouseEnter={(e) => {
e.stopPropagation();
e.stopPropagation()
}}
>
<div>
<p className="font-medium">
{tooltipData.country === "China"
? "Mainland China"
: tooltipData.country}
{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}
</p>
<p className="text-neutral-600 dark:text-neutral-400 mb-1">
{tooltipData.count} {t("map.Servers")}
@@ -56,7 +54,7 @@ const MapTooltip = memo(function MapTooltip() {
</div>
</m.div>
</AnimatePresence>
);
});
)
})
export default MapTooltip;
export default MapTooltip

View File

@@ -1,12 +1,6 @@
"use client";
"use client"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import {
ChartConfig,
ChartContainer,
@@ -14,33 +8,28 @@ import {
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";
import { Switch } from "./ui/switch";
import { Label } from "./ui/label";
} from "@/components/ui/chart"
import { fetchMonitor } from "@/lib/nezha-api"
import { formatTime } from "@/lib/utils"
import { formatRelativeTime } from "@/lib/utils"
import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api"
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 { Label } from "./ui/label"
import { Switch } from "./ui/switch"
interface ResultItem {
created_at: number;
[key: string]: number;
created_at: number
[key: string]: number
}
export function NetworkChart({
server_id,
show,
}: {
server_id: number;
show: boolean;
}) {
const { t } = useTranslation();
export function NetworkChart({ server_id, show }: { server_id: number; show: boolean }) {
const { t } = useTranslation()
const { data: monitorData } = useQuery({
queryKey: ["monitor", server_id],
@@ -49,29 +38,27 @@ export function NetworkChart({
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchInterval: 10000,
});
})
if (!monitorData) return <NetworkChartLoading />;
if (!monitorData) return <NetworkChartLoading />
if (monitorData?.success && !monitorData.data) {
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 mb-4">
{t("monitor.noData")}
</p>
<p className="text-sm font-medium opacity-40 mb-4">{t("monitor.noData")}</p>
</div>
<NetworkChartLoading />
</>
);
)
}
const transformedData = transformData(monitorData.data);
const transformedData = transformData(monitorData.data)
const formattedData = formatData(monitorData.data);
const formattedData = formatData(monitorData.data)
const chartDataKey = Object.keys(transformedData);
const chartDataKey = Object.keys(transformedData)
const initChartConfig = {
avg_delay: {
@@ -80,10 +67,10 @@ export function NetworkChart({
...chartDataKey.reduce((acc, key) => {
acc[key] = {
label: key,
};
return acc;
}
return acc
}, {} as ChartConfig),
} satisfies ChartConfig;
} satisfies ChartConfig
return (
<NetworkChartClient
@@ -93,7 +80,7 @@ export function NetworkChart({
serverName={monitorData.data[0].server_name}
formattedData={formattedData}
/>
);
)
}
export const NetworkChartClient = React.memo(function NetworkChart({
@@ -103,33 +90,33 @@ export const NetworkChartClient = React.memo(function NetworkChart({
serverName,
formattedData,
}: {
chartDataKey: string[];
chartConfig: ChartConfig;
chartData: ServerMonitorChart;
serverName: string;
formattedData: ResultItem[];
chartDataKey: string[]
chartConfig: ChartConfig
chartData: ServerMonitorChart
serverName: string
formattedData: ResultItem[]
}) {
const { t } = useTranslation();
const { t } = useTranslation()
const defaultChart = "All";
const defaultChart = "All"
const [activeChart, setActiveChart] = React.useState(defaultChart);
const [isPeakEnabled, setIsPeakEnabled] = React.useState(false);
const [activeChart, setActiveChart] = React.useState(defaultChart)
const [isPeakEnabled, setIsPeakEnabled] = React.useState(false)
const handleButtonClick = useCallback(
(chart: string) => {
setActiveChart((prev) => (prev === chart ? defaultChart : chart));
setActiveChart((prev) => (prev === chart ? defaultChart : chart))
},
[defaultChart],
);
)
const getColorByIndex = useCallback(
(chart: string) => {
const index = chartDataKey.indexOf(chart);
return `hsl(var(--chart-${(index % 10) + 1}))`;
const index = chartDataKey.indexOf(chart)
return `hsl(var(--chart-${(index % 10) + 1}))`
},
[chartDataKey],
);
)
const chartButtons = useMemo(
() =>
@@ -140,16 +127,14 @@ export const NetworkChartClient = React.memo(function NetworkChart({
className={`relative z-30 flex cursor-pointer grow basis-0 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="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) {
@@ -162,7 +147,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
dataKey="avg_delay"
stroke={getColorByIndex(activeChart)}
/>
);
)
}
return chartDataKey.map((key) => (
<Line
@@ -175,65 +160,50 @@ export const NetworkChartClient = React.memo(function NetworkChart({
stroke={getColorByIndex(key)}
connectNulls={true}
/>
));
}, [activeChart, defaultChart, chartDataKey, getColorByIndex]);
))
}, [activeChart, defaultChart, chartDataKey, getColorByIndex])
const processedData = useMemo(() => {
if (!isPeakEnabled) {
return activeChart === defaultChart
? formattedData
: chartData[activeChart];
return activeChart === defaultChart ? formattedData : chartData[activeChart]
}
// 如果开启了削峰,对数据进行处理
const data = (
activeChart === defaultChart ? formattedData : chartData[activeChart]
) as ResultItem[];
const windowSize = 7; // 增加到7个点的移动平均
const weights = [0.1, 0.1, 0.15, 0.3, 0.15, 0.1, 0.1]; // 加权平均的权重
) as ResultItem[]
const windowSize = 7 // 增加到7个点的移动平均
const weights = [0.1, 0.1, 0.15, 0.3, 0.15, 0.1, 0.1] // 加权平均的权重
return data.map((point, index) => {
if (index < windowSize - 1) return point;
if (index < windowSize - 1) return point
const window = data.slice(index - windowSize + 1, index + 1);
const smoothed = { ...point } as ResultItem;
const window = data.slice(index - windowSize + 1, index + 1)
const smoothed = { ...point } as ResultItem
if (activeChart === defaultChart) {
// 处理所有线路的数据
chartDataKey.forEach((key) => {
const values = window
.map((w) => w[key])
.filter((v) => v !== undefined && v !== null) as number[];
.filter((v) => v !== undefined && v !== null) as number[]
if (values.length === windowSize) {
smoothed[key] = values.reduce(
(acc, val, idx) => acc + val * weights[idx],
0,
);
smoothed[key] = values.reduce((acc, val, idx) => acc + val * weights[idx], 0)
}
});
})
} else {
// 处理单条线路的数据
const values = window
.map((w) => w.avg_delay)
.filter((v) => v !== undefined && v !== null) as number[];
.filter((v) => v !== undefined && v !== null) as number[]
if (values.length === windowSize) {
smoothed.avg_delay = values.reduce(
(acc, val, idx) => acc + val * weights[idx],
0,
);
smoothed.avg_delay = values.reduce((acc, val, idx) => acc + val * weights[idx], 0)
}
}
return smoothed;
});
}, [
isPeakEnabled,
activeChart,
formattedData,
chartData,
chartDataKey,
defaultChart,
]);
return smoothed
})
}, [isPeakEnabled, activeChart, formattedData, chartData, chartDataKey, defaultChart])
return (
<Card>
@@ -246,11 +216,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
{chartDataKey.length} {t("monitor.monitorCount")}
</CardDescription>
<div className="flex items-center mt-0.5 space-x-2">
<Switch
id="Peak"
checked={isPeakEnabled}
onCheckedChange={setIsPeakEnabled}
/>
<Switch id="Peak" checked={isPeakEnabled} onCheckedChange={setIsPeakEnabled} />
<Label className="text-xs" htmlFor="Peak">
Peak cut
</Label>
@@ -259,15 +225,8 @@ export const NetworkChartClient = React.memo(function NetworkChart({
<div className="flex flex-wrap w-full">{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={processedData}
margin={{ left: 12, right: 12 }}
>
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
<LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="created_at"
@@ -292,67 +251,64 @@ export const NetworkChartClient = React.memo(function NetworkChart({
indicator={"line"}
labelKey="created_at"
labelFormatter={(_, payload) => {
return formatTime(payload[0].payload.created_at);
return formatTime(payload[0].payload.created_at)
}}
/>
}
/>
{activeChart === defaultChart && (
<ChartLegend content={<ChartLegendContent />} />
)}
{activeChart === defaultChart && <ChartLegend content={<ChartLegendContent />} />}
{chartLines}
</LineChart>
</ChartContainer>
</CardContent>
</Card>
);
});
)
})
const transformData = (data: NezhaMonitor[]) => {
const monitorData: ServerMonitorChart = {};
const monitorData: ServerMonitorChart = {}
data.forEach((item) => {
const monitorName = item.monitor_name;
const monitorName = item.monitor_name
if (!monitorData[monitorName]) {
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;
};
return monitorData
}
const formatData = (rawData: NezhaMonitor[]) => {
const result: { [time: number]: ResultItem } = {};
const result: { [time: number]: ResultItem } = {}
const allTimes = new Set<number>();
const allTimes = new Set<number>()
rawData.forEach((item) => {
item.created_at.forEach((time) => allTimes.add(time));
});
item.created_at.forEach((time) => allTimes.add(time))
})
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b);
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b)
rawData.forEach((item) => {
const { monitor_name, created_at, avg_delay } = item;
const { monitor_name, created_at, avg_delay } = item
allTimeArray.forEach((time) => {
if (!result[time]) {
result[time] = { created_at: time };
result[time] = { created_at: time }
}
const timeIndex = created_at.indexOf(time);
const timeIndex = created_at.indexOf(time)
// @ts-expect-error - avg_delay is an array
result[time][monitor_name] =
timeIndex !== -1 ? avg_delay[timeIndex] : null;
});
});
result[time][monitor_name] = timeIndex !== -1 ? avg_delay[timeIndex] : null
})
})
return Object.values(result).sort((a, b) => a.created_at - b.created_at);
};
return Object.values(result).sort((a, b) => a.created_at - b.created_at)
}

View File

@@ -1,5 +1,5 @@
import { Loader } from "@/components/loading/Loader";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Loader } from "@/components/loading/Loader"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export default function NetworkChartLoading() {
return (
@@ -19,5 +19,5 @@ export default function NetworkChartLoading() {
<div className="aspect-auto h-[250px] w-full"></div>
</CardContent>
</Card>
);
)
}

View File

@@ -1,28 +1,17 @@
import ServerFlag from "@/components/ServerFlag";
import ServerUsageBar from "@/components/ServerUsageBar";
import ServerFlag from "@/components/ServerFlag"
import ServerUsageBar from "@/components/ServerUsageBar"
import { formatBytes } from "@/lib/format"
import { cn, formatNezhaInfo, getDaysBetweenDates, parsePublicNote } from "@/lib/utils"
import { NezhaServer } from "@/types/nezha-api"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import {
cn,
formatNezhaInfo,
parsePublicNote,
getDaysBetweenDates,
} from "@/lib/utils";
import { NezhaServer } from "@/types/nezha-api";
import { Card } from "./ui/card";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Badge } from "./ui/badge";
import { formatBytes } from "@/lib/format";
import { Badge } from "./ui/badge"
import { Card } from "./ui/card"
export default function ServerCard({
now,
serverInfo,
}: {
now: number;
serverInfo: NezhaServer;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
export default function ServerCard({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
const { t } = useTranslation()
const navigate = useNavigate()
const {
name,
country_code,
@@ -35,23 +24,20 @@ export default function ServerCard({
net_in_transfer,
net_out_transfer,
public_note,
} = formatNezhaInfo(now, serverInfo);
} = formatNezhaInfo(now, serverInfo)
const showFlag = true;
const showFlag = true
const parsedData = parsePublicNote(public_note);
const parsedData = parsePublicNote(public_note)
let daysLeft = 0;
let isNeverExpire = false;
let daysLeft = 0
let isNeverExpire = false
if (parsedData?.billingDataMod?.endDate) {
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
isNeverExpire = true;
isNeverExpire = true
} else {
daysLeft = getDaysBetweenDates(
parsedData.billingDataMod.endDate,
new Date(now).toISOString(),
);
daysLeft = getDaysBetweenDates(parsedData.billingDataMod.endDate, new Date(now).toISOString())
}
}
@@ -91,11 +77,7 @@ export default function ServerCard({
: {isNeverExpire ? "永久" : daysLeft + "天"}
</p>
) : (
<p
className={cn(
"text-[10px] text-muted-foreground text-red-600",
)}
>
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
: {daysLeft * -1}
</p>
))}
@@ -105,47 +87,29 @@ export default function ServerCard({
<section className={cn("grid grid-cols-5 items-center gap-3")}>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="flex items-center text-xs font-semibold">
{cpu.toFixed(2)}%
</div>
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
<ServerUsageBar value={cpu} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.mem")}
</p>
<div className="flex items-center text-xs font-semibold">
{mem.toFixed(2)}%
</div>
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p>
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
<ServerUsageBar value={mem} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.stg")}
</p>
<div className="flex items-center text-xs font-semibold">
{stg.toFixed(2)}%
</div>
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
<ServerUsageBar value={stg} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.upload")}
</p>
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
<div className="flex items-center text-xs font-semibold">
{up >= 1024
? `${(up / 1024).toFixed(2)}G/s`
: `${up.toFixed(2)}M/s`}
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
</div>
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.download")}
</p>
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
<div className="flex items-center text-xs font-semibold">
{down >= 1024
? `${(down / 1024).toFixed(2)}G/s`
: `${down.toFixed(2)}M/s`}
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
</div>
</div>
</section>
@@ -179,24 +143,16 @@ export default function ServerCard({
>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative">
<p
className={cn(
"break-all font-bold tracking-tight",
showFlag ? "text-xs" : "text-sm",
)}
>
<p className={cn("break-all font-bold tracking-tight", showFlag ? "text-xs" : "text-sm")}>
{name}
</p>
</div>
</section>
</Card>
);
)
}

View File

@@ -1,33 +1,24 @@
import ServerFlag from "@/components/ServerFlag";
import ServerUsageBar from "@/components/ServerUsageBar";
import ServerFlag from "@/components/ServerFlag"
import ServerUsageBar from "@/components/ServerUsageBar"
import { formatBytes } from "@/lib/format"
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
import { cn, formatNezhaInfo, getDaysBetweenDates, parsePublicNote } from "@/lib/utils"
import { NezhaServer } from "@/types/nezha-api"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import {
cn,
formatNezhaInfo,
getDaysBetweenDates,
parsePublicNote,
} from "@/lib/utils";
import { NezhaServer } from "@/types/nezha-api";
import { Card } from "./ui/card";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
GetFontLogoClass,
GetOsName,
MageMicrosoftWindows,
} from "@/lib/logo-class";
import { formatBytes } from "@/lib/format";
import { Separator } from "./ui/separator";
import { Card } from "./ui/card"
import { Separator } from "./ui/separator"
export default function ServerCardInline({
now,
serverInfo,
}: {
now: number;
serverInfo: NezhaServer;
now: number
serverInfo: NezhaServer
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const { t } = useTranslation()
const navigate = useNavigate()
const {
name,
country_code,
@@ -42,23 +33,20 @@ export default function ServerCardInline({
net_in_transfer,
net_out_transfer,
public_note,
} = formatNezhaInfo(now, serverInfo);
} = formatNezhaInfo(now, serverInfo)
const showFlag = true;
const showFlag = true
const parsedData = parsePublicNote(public_note);
const parsedData = parsePublicNote(public_note)
let daysLeft = 0;
let isNeverExpire = false;
let daysLeft = 0
let isNeverExpire = false
if (parsedData?.billingDataMod?.endDate) {
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
isNeverExpire = true;
isNeverExpire = true
} else {
daysLeft = getDaysBetweenDates(
parsedData.billingDataMod.endDate,
new Date(now).toISOString(),
);
daysLeft = getDaysBetweenDates(parsedData.billingDataMod.endDate, new Date(now).toISOString())
}
}
@@ -102,11 +90,7 @@ export default function ServerCardInline({
: {isNeverExpire ? "永久" : daysLeft + "天"}
</p>
) : (
<p
className={cn(
"text-[10px] text-muted-foreground text-red-600",
)}
>
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
: {daysLeft * -1}
</p>
))}
@@ -115,9 +99,7 @@ export default function ServerCardInline({
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
<div className="flex flex-col gap-2">
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
<div
className={"items-center flex flex-row gap-2 whitespace-nowrap"}
>
<div className={"items-center flex flex-row gap-2 whitespace-nowrap"}>
<div className="text-xs font-semibold">
{platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" />
@@ -126,81 +108,53 @@ export default function ServerCardInline({
)}
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.system")}
</p>
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p>
<div className="flex items-center text-[10.5px] font-semibold">
{platform.includes("Windows")
? "Windows"
: GetOsName(platform)}
{platform.includes("Windows") ? "Windows" : GetOsName(platform)}
</div>
</div>
</div>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.uptime")}
</p>
<p className="text-xs text-muted-foreground">{t("serverCard.uptime")}</p>
<div className="flex items-center text-xs font-semibold">
{(uptime / 86400).toFixed(0)} {t("serverCard.days")}
</div>
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="flex items-center text-xs font-semibold">
{cpu.toFixed(2)}%
</div>
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
<ServerUsageBar value={cpu} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.mem")}
</p>
<div className="flex items-center text-xs font-semibold">
{mem.toFixed(2)}%
</div>
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p>
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
<ServerUsageBar value={mem} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.stg")}
</p>
<div className="flex items-center text-xs font-semibold">
{stg.toFixed(2)}%
</div>
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
<ServerUsageBar value={stg} />
</div>
<div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.upload")}
</p>
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
<div className="flex items-center text-xs font-semibold">
{up >= 1024
? `${(up / 1024).toFixed(2)}G/s`
: `${up.toFixed(2)}M/s`}
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
</div>
</div>
<div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.download")}
</p>
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
<div className="flex items-center text-xs font-semibold">
{down >= 1024
? `${(down / 1024).toFixed(2)}G/s`
: `${down.toFixed(2)}M/s`}
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
</div>
</div>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.totalUpload")}
</p>
<p className="text-xs text-muted-foreground">{t("serverCard.totalUpload")}</p>
<div className="flex items-center text-xs font-semibold">
{formatBytes(net_out_transfer)}
</div>
</div>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.totalDownload")}
</p>
<p className="text-xs text-muted-foreground">{t("serverCard.totalDownload")}</p>
<div className="flex items-center text-xs font-semibold">
{formatBytes(net_in_transfer)}
</div>
@@ -222,10 +176,7 @@ export default function ServerCardInline({
>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
@@ -241,5 +192,5 @@ export default function ServerCardInline({
</div>
</section>
</Card>
);
)
}

View File

@@ -1,115 +1,92 @@
import { Card, CardContent } from "@/components/ui/card";
import { ChartConfig, ChartContainer } from "@/components/ui/chart";
import { formatNezhaInfo, formatRelativeTime } from "@/lib/utils";
import { NezhaServer, NezhaWebsocketResponse } from "@/types/nezha-api";
import { useEffect, useState } from "react";
import {
Area,
AreaChart,
CartesianGrid,
Line,
LineChart,
XAxis,
YAxis,
} from "recharts";
import { ServerDetailChartLoading } from "./loading/ServerDetailLoading";
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { useTranslation } from "react-i18next";
import { formatBytes } from "@/lib/format";
import { Card, CardContent } from "@/components/ui/card"
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { formatBytes } from "@/lib/format"
import { formatNezhaInfo, formatRelativeTime } from "@/lib/utils"
import { NezhaServer, NezhaWebsocketResponse } from "@/types/nezha-api"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { Area, AreaChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
import { ServerDetailChartLoading } from "./loading/ServerDetailLoading"
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar"
type gpuChartData = {
timeStamp: string;
gpu: number;
};
timeStamp: string
gpu: number
}
type cpuChartData = {
timeStamp: string;
cpu: number;
};
timeStamp: string
cpu: number
}
type processChartData = {
timeStamp: string;
process: number;
};
timeStamp: string
process: number
}
type diskChartData = {
timeStamp: string;
disk: number;
};
timeStamp: string
disk: number
}
type memChartData = {
timeStamp: string;
mem: number;
swap: number;
};
timeStamp: string
mem: number
swap: number
}
type networkChartData = {
timeStamp: string;
upload: number;
download: number;
};
timeStamp: string
upload: number
download: number
}
type connectChartData = {
timeStamp: string;
tcp: number;
udp: number;
};
timeStamp: string
tcp: number
udp: number
}
export default function ServerDetailChart({
server_id,
}: {
server_id: string;
}) {
const { lastMessage, connected } = useWebSocketContext();
export default function ServerDetailChart({ server_id }: { server_id: string }) {
const { lastMessage, connected } = useWebSocketContext()
if (!connected) {
return <ServerDetailChartLoading />;
return <ServerDetailChartLoading />
}
const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
if (!nezhaWsData) {
return <ServerDetailChartLoading />;
return <ServerDetailChartLoading />
}
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id))
if (!server) {
return <ServerDetailChartLoading />;
return <ServerDetailChartLoading />
}
const { online } = formatNezhaInfo(nezhaWsData.now, server);
const { online } = formatNezhaInfo(nezhaWsData.now, server)
if (!online) {
return <ServerDetailChartLoading />;
return <ServerDetailChartLoading />
}
const gpuStats = server.state.gpu || [];
const gpuList = server.host.gpu || [];
const gpuStats = server.state.gpu || []
const gpuList = server.host.gpu || []
return (
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
<CpuChart now={nezhaWsData.now} data={server} />
{gpuStats.length >= 1 && gpuList.length === gpuStats.length ? (
gpuList.map((gpu, index) => (
<GpuChart
now={nezhaWsData.now}
gpuStat={gpuStats[index]}
gpuName={gpu}
key={index}
/>
<GpuChart now={nezhaWsData.now} gpuStat={gpuStats[index]} gpuName={gpu} key={index} />
))
) : gpuStats.length > 0 ? (
gpuStats.map((gpu, index) => (
<GpuChart
now={nezhaWsData.now}
gpuStat={gpu}
gpuName={`#${index + 1}`}
key={index}
/>
<GpuChart now={nezhaWsData.now} gpuStat={gpu} gpuName={`#${index + 1}`} key={index} />
))
) : (
<></>
@@ -120,44 +97,36 @@ export default function ServerDetailChart({
<NetworkChart now={nezhaWsData.now} data={server} />
<ConnectChart now={nezhaWsData.now} data={server} />
</section>
);
)
}
function GpuChart({
now,
gpuStat,
gpuName,
}: {
now: number;
gpuStat: number;
gpuName?: string;
}) {
const [gpuChartData, setGpuChartData] = useState([] as gpuChartData[]);
function GpuChart({ now, gpuStat, gpuName }: { now: number; gpuStat: number; gpuName?: string }) {
const [gpuChartData, setGpuChartData] = useState([] as gpuChartData[])
useEffect(() => {
if (gpuStat) {
const timestamp = Date.now().toString();
let newData = [] as gpuChartData[];
const timestamp = Date.now().toString()
let newData = [] as gpuChartData[]
if (gpuChartData.length === 0) {
newData = [
{ timeStamp: timestamp, gpu: gpuStat },
{ timeStamp: timestamp, gpu: gpuStat },
];
]
} else {
newData = [...gpuChartData, { timeStamp: timestamp, gpu: gpuStat }];
newData = [...gpuChartData, { timeStamp: timestamp, gpu: gpuStat }]
}
if (newData.length > 30) {
newData.shift();
newData.shift()
}
setGpuChartData(newData);
setGpuChartData(newData)
}
}, [now, gpuStat]);
}, [now, gpuStat])
const chartConfig = {
gpu: {
label: "GPU",
},
} satisfies ChartConfig;
} satisfies ChartConfig
return (
<Card>
@@ -169,9 +138,7 @@ function GpuChart({
{gpuName && <p className="text-xs mt-1 mb-1.5">GPU: {gpuName}</p>}
</section>
<section className="flex items-center gap-2">
<p className="text-xs text-end w-10 font-medium">
{gpuStat.toFixed(0)}%
</p>
<p className="text-xs text-end w-10 font-medium">{gpuStat.toFixed(0)}%</p>
<AnimatedCircularProgressBar
className="size-3 text-[0px]"
max={100}
@@ -181,10 +148,7 @@ function GpuChart({
/>
</section>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
<AreaChart
accessibilityLayer
data={gpuChartData}
@@ -225,38 +189,38 @@ function GpuChart({
</section>
</CardContent>
</Card>
);
)
}
function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[]);
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[])
const { cpu } = formatNezhaInfo(now, data);
const { cpu } = formatNezhaInfo(now, data)
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
let newData = [] as cpuChartData[];
const timestamp = Date.now().toString()
let newData = [] as cpuChartData[]
if (cpuChartData.length === 0) {
newData = [
{ timeStamp: timestamp, cpu: cpu },
{ timeStamp: timestamp, cpu: cpu },
];
]
} else {
newData = [...cpuChartData, { timeStamp: timestamp, cpu: cpu }];
newData = [...cpuChartData, { timeStamp: timestamp, cpu: cpu }]
}
if (newData.length > 30) {
newData.shift();
newData.shift()
}
setCpuChartData(newData);
setCpuChartData(newData)
}
}, [data]);
}, [data])
const chartConfig = {
cpu: {
label: "CPU",
},
} satisfies ChartConfig;
} satisfies ChartConfig
return (
<Card>
@@ -265,9 +229,7 @@ function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
<div className="flex items-center justify-between">
<p className="text-md font-medium">CPU</p>
<section className="flex items-center gap-2">
<p className="text-xs text-end w-10 font-medium">
{cpu.toFixed(0)}%
</p>
<p className="text-xs text-end w-10 font-medium">{cpu.toFixed(0)}%</p>
<AnimatedCircularProgressBar
className="size-3 text-[0px]"
max={100}
@@ -277,10 +239,7 @@ function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
/>
</section>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
<AreaChart
accessibilityLayer
data={cpuChartData}
@@ -321,61 +280,51 @@ function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
</section>
</CardContent>
</Card>
);
)
}
function ProcessChart({ now, data }: { now: number; data: NezhaServer }) {
const { t } = useTranslation();
const [processChartData, setProcessChartData] = useState(
[] as processChartData[],
);
const { t } = useTranslation()
const [processChartData, setProcessChartData] = useState([] as processChartData[])
const { process } = formatNezhaInfo(now, data);
const { process } = formatNezhaInfo(now, data)
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
let newData = [] as processChartData[];
const timestamp = Date.now().toString()
let newData = [] as processChartData[]
if (processChartData.length === 0) {
newData = [
{ timeStamp: timestamp, process: process },
{ timeStamp: timestamp, process: process },
];
]
} else {
newData = [
...processChartData,
{ timeStamp: timestamp, process: process },
];
newData = [...processChartData, { timeStamp: timestamp, process: process }]
}
if (newData.length > 30) {
newData.shift();
newData.shift()
}
setProcessChartData(newData);
setProcessChartData(newData)
}
}, [data]);
}, [data])
const chartConfig = {
process: {
label: "Process",
},
} satisfies ChartConfig;
} satisfies ChartConfig
return (
<Card>
<CardContent className="px-6 py-3">
<section className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<p className="text-md font-medium">
{t("serverDetailChart.process")}
</p>
<p className="text-md font-medium">{t("serverDetailChart.process")}</p>
<section className="flex items-center gap-2">
<p className="text-xs text-end w-10 font-medium">{process}</p>
</section>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
<AreaChart
accessibilityLayer
data={processChartData}
@@ -395,12 +344,7 @@ function ProcessChart({ now, data }: { now: number; data: NezhaServer }) {
interval="preserveStartEnd"
tickFormatter={(value) => formatRelativeTime(value)}
/>
<YAxis
tickLine={false}
axisLine={false}
mirror={true}
tickMargin={-15}
/>
<YAxis tickLine={false} axisLine={false} mirror={true} tickMargin={-15} />
<Area
isAnimationActive={false}
dataKey="process"
@@ -414,36 +358,33 @@ function ProcessChart({ now, data }: { now: number; data: NezhaServer }) {
</section>
</CardContent>
</Card>
);
)
}
function MemChart({ now, data }: { now: number; data: NezhaServer }) {
const { t } = useTranslation();
const [memChartData, setMemChartData] = useState([] as memChartData[]);
const { t } = useTranslation()
const [memChartData, setMemChartData] = useState([] as memChartData[])
const { mem, swap } = formatNezhaInfo(now, data);
const { mem, swap } = formatNezhaInfo(now, data)
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
let newData = [] as memChartData[];
const timestamp = Date.now().toString()
let newData = [] as memChartData[]
if (memChartData.length === 0) {
newData = [
{ timeStamp: timestamp, mem: mem, swap: swap },
{ timeStamp: timestamp, mem: mem, swap: swap },
];
]
} else {
newData = [
...memChartData,
{ timeStamp: timestamp, mem: mem, swap: swap },
];
newData = [...memChartData, { timeStamp: timestamp, mem: mem, swap: swap }]
}
if (newData.length > 30) {
newData.shift();
newData.shift()
}
setMemChartData(newData);
setMemChartData(newData)
}
}, [data]);
}, [data])
const chartConfig = {
mem: {
@@ -452,7 +393,7 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
swap: {
label: "Swap",
},
} satisfies ChartConfig;
} satisfies ChartConfig
return (
<Card>
@@ -461,9 +402,7 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
<div className="flex items-center justify-between">
<section className="flex items-center gap-4">
<div className="flex flex-col">
<p className=" text-xs text-muted-foreground">
{t("serverDetailChart.mem")}
</p>
<p className=" text-xs text-muted-foreground">{t("serverDetailChart.mem")}</p>
<div className="flex items-center gap-2">
<AnimatedCircularProgressBar
className="size-3 text-[0px]"
@@ -476,9 +415,7 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
</div>
</div>
<div className="flex flex-col">
<p className=" text-xs text-muted-foreground">
{t("serverDetailChart.swap")}
</p>
<p className=" text-xs text-muted-foreground">{t("serverDetailChart.swap")}</p>
<div className="flex items-center gap-2">
<AnimatedCircularProgressBar
className="size-3 text-[0px]"
@@ -493,14 +430,12 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
</section>
<section className="flex flex-col items-end gap-0.5">
<div className="flex text-[11px] font-medium items-center gap-2">
{formatBytes(data.state.mem_used)} /{" "}
{formatBytes(data.host.mem_total)}
{formatBytes(data.state.mem_used)} / {formatBytes(data.host.mem_total)}
</div>
<div className="flex text-[11px] font-medium items-center gap-2">
{data.host.swap_total ? (
<>
swap: {formatBytes(data.state.swap_used)} /{" "}
{formatBytes(data.host.swap_total)}
swap: {formatBytes(data.state.swap_used)} / {formatBytes(data.host.swap_total)}
</>
) : (
<>no swap</>
@@ -508,10 +443,7 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
</div>
</section>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
<AreaChart
accessibilityLayer
data={memChartData}
@@ -560,39 +492,39 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
</section>
</CardContent>
</Card>
);
)
}
function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
const { t } = useTranslation();
const [diskChartData, setDiskChartData] = useState([] as diskChartData[]);
const { t } = useTranslation()
const [diskChartData, setDiskChartData] = useState([] as diskChartData[])
const { disk } = formatNezhaInfo(now, data);
const { disk } = formatNezhaInfo(now, data)
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
let newData = [] as diskChartData[];
const timestamp = Date.now().toString()
let newData = [] as diskChartData[]
if (diskChartData.length === 0) {
newData = [
{ timeStamp: timestamp, disk: disk },
{ timeStamp: timestamp, disk: disk },
];
]
} else {
newData = [...diskChartData, { timeStamp: timestamp, disk: disk }];
newData = [...diskChartData, { timeStamp: timestamp, disk: disk }]
}
if (newData.length > 30) {
newData.shift();
newData.shift()
}
setDiskChartData(newData);
setDiskChartData(newData)
}
}, [data]);
}, [data])
const chartConfig = {
disk: {
label: "Disk",
},
} satisfies ChartConfig;
} satisfies ChartConfig
return (
<Card>
@@ -602,9 +534,7 @@ function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
<p className="text-md font-medium">{t("serverDetailChart.disk")}</p>
<section className="flex flex-col items-end gap-0.5">
<section className="flex items-center gap-2">
<p className="text-xs text-end w-10 font-medium">
{disk.toFixed(0)}%
</p>
<p className="text-xs text-end w-10 font-medium">{disk.toFixed(0)}%</p>
<AnimatedCircularProgressBar
className="size-3 text-[0px]"
max={100}
@@ -614,15 +544,11 @@ function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
/>
</section>
<div className="flex text-[11px] font-medium items-center gap-2">
{formatBytes(data.state.disk_used)} /{" "}
{formatBytes(data.host.disk_total)}
{formatBytes(data.state.disk_used)} / {formatBytes(data.host.disk_total)}
</div>
</section>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
<AreaChart
accessibilityLayer
data={diskChartData}
@@ -663,43 +589,38 @@ function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
</section>
</CardContent>
</Card>
);
)
}
function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
const { t } = useTranslation();
const [networkChartData, setNetworkChartData] = useState(
[] as networkChartData[],
);
const { t } = useTranslation()
const [networkChartData, setNetworkChartData] = useState([] as networkChartData[])
const { up, down } = formatNezhaInfo(now, data);
const { up, down } = formatNezhaInfo(now, data)
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
let newData = [] as networkChartData[];
const timestamp = Date.now().toString()
let newData = [] as networkChartData[]
if (networkChartData.length === 0) {
newData = [
{ timeStamp: timestamp, upload: up, download: down },
{ timeStamp: timestamp, upload: up, download: down },
];
]
} else {
newData = [
...networkChartData,
{ timeStamp: timestamp, upload: up, download: down },
];
newData = [...networkChartData, { timeStamp: timestamp, upload: up, download: down }]
}
if (newData.length > 30) {
newData.shift();
newData.shift()
}
setNetworkChartData(newData);
setNetworkChartData(newData)
}
}, [data]);
}, [data])
let maxDownload = Math.max(...networkChartData.map((item) => item.download));
maxDownload = Math.ceil(maxDownload);
let maxDownload = Math.max(...networkChartData.map((item) => item.download))
maxDownload = Math.ceil(maxDownload)
if (maxDownload < 1) {
maxDownload = 1;
maxDownload = 1
}
const chartConfig = {
@@ -709,7 +630,7 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
download: {
label: "Download",
},
} satisfies ChartConfig;
} satisfies ChartConfig
return (
<Card>
@@ -718,18 +639,14 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
<div className="flex items-center">
<section className="flex items-center gap-4">
<div className="flex flex-col w-20">
<p className="text-xs text-muted-foreground">
{t("serverDetailChart.upload")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetailChart.upload")}</p>
<div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]"></span>
<p className="text-xs font-medium">{up.toFixed(2)} M/s</p>
</div>
</div>
<div className="flex flex-col w-20">
<p className=" text-xs text-muted-foreground">
{t("serverDetailChart.download")}
</p>
<p className=" text-xs text-muted-foreground">{t("serverDetailChart.download")}</p>
<div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]"></span>
<p className="text-xs font-medium">{down.toFixed(2)} M/s</p>
@@ -737,10 +654,7 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
</div>
</section>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
<LineChart
accessibilityLayer
data={networkChartData}
@@ -792,37 +706,32 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
</section>
</CardContent>
</Card>
);
)
}
function ConnectChart({ now, data }: { now: number; data: NezhaServer }) {
const [connectChartData, setConnectChartData] = useState(
[] as connectChartData[],
);
const [connectChartData, setConnectChartData] = useState([] as connectChartData[])
const { tcp, udp } = formatNezhaInfo(now, data);
const { tcp, udp } = formatNezhaInfo(now, data)
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
let newData = [] as connectChartData[];
const timestamp = Date.now().toString()
let newData = [] as connectChartData[]
if (connectChartData.length === 0) {
newData = [
{ timeStamp: timestamp, tcp: tcp, udp: udp },
{ timeStamp: timestamp, tcp: tcp, udp: udp },
];
]
} else {
newData = [
...connectChartData,
{ timeStamp: timestamp, tcp: tcp, udp: udp },
];
newData = [...connectChartData, { timeStamp: timestamp, tcp: tcp, udp: udp }]
}
if (newData.length > 30) {
newData.shift();
newData.shift()
}
setConnectChartData(newData);
setConnectChartData(newData)
}
}, [data]);
}, [data])
const chartConfig = {
tcp: {
@@ -831,7 +740,7 @@ function ConnectChart({ now, data }: { now: number; data: NezhaServer }) {
udp: {
label: "UDP",
},
} satisfies ChartConfig;
} satisfies ChartConfig
return (
<Card>
@@ -855,10 +764,7 @@ function ConnectChart({ now, data }: { now: number; data: NezhaServer }) {
</div>
</section>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
<LineChart
accessibilityLayer
data={connectChartData}
@@ -907,5 +813,5 @@ function ConnectChart({ now, data }: { now: number; data: NezhaServer }) {
</section>
</CardContent>
</Card>
);
)
}

View File

@@ -1,41 +1,35 @@
import { BackIcon } from "@/components/Icon";
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading";
import ServerFlag from "@/components/ServerFlag";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { cn, formatNezhaInfo } from "@/lib/utils";
import { NezhaWebsocketResponse } from "@/types/nezha-api";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { formatBytes } from "@/lib/format";
import { BackIcon } from "@/components/Icon"
import ServerFlag from "@/components/ServerFlag"
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { formatBytes } from "@/lib/format"
import { cn, formatNezhaInfo } from "@/lib/utils"
import { NezhaWebsocketResponse } from "@/types/nezha-api"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
export default function ServerDetailOverview({
server_id,
}: {
server_id: string;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
export default function ServerDetailOverview({ server_id }: { server_id: string }) {
const { t } = useTranslation()
const navigate = useNavigate()
const { lastMessage, connected } = useWebSocketContext();
const { lastMessage, connected } = useWebSocketContext()
if (!connected) {
return <ServerDetailLoading />;
return <ServerDetailLoading />
}
const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
if (!nezhaWsData) {
return <ServerDetailLoading />;
return <ServerDetailLoading />
}
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id))
if (!server) {
return <ServerDetailLoading />;
return <ServerDetailLoading />
}
const {
@@ -57,7 +51,7 @@ export default function ServerDetailOverview({
net_out_transfer,
net_in_transfer,
last_active_time_string,
} = formatNezhaInfo(nezhaWsData.now, server);
} = formatNezhaInfo(nezhaWsData.now, server)
return (
<div>
@@ -72,9 +66,7 @@ export default function ServerDetailOverview({
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.status")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetail.status")}</p>
<Badge
className={cn(
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
@@ -93,13 +85,10 @@ export default function ServerDetailOverview({
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.uptime")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetail.uptime")}</p>
<div className="text-xs">
{" "}
{online ? (uptime / 86400).toFixed(0) : "N/A"}{" "}
{t("serverDetail.days")}
{online ? (uptime / 86400).toFixed(0) : "N/A"} {t("serverDetail.days")}
</div>
</section>
</CardContent>
@@ -109,9 +98,7 @@ export default function ServerDetailOverview({
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.version")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetail.version")}</p>
<div className="text-xs">{version} </div>
</section>
</CardContent>
@@ -121,9 +108,7 @@ export default function ServerDetailOverview({
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.arch")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetail.arch")}</p>
<div className="text-xs">{arch} </div>
</section>
</CardContent>
@@ -134,9 +119,7 @@ export default function ServerDetailOverview({
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.mem")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetail.mem")}</p>
<div className="text-xs">{formatBytes(mem_total)}</div>
</section>
</CardContent>
@@ -147,9 +130,7 @@ export default function ServerDetailOverview({
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.disk")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetail.disk")}</p>
<div className="text-xs">{formatBytes(disk_total)}</div>
</section>
</CardContent>
@@ -160,18 +141,11 @@ export default function ServerDetailOverview({
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.region")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p>
<section className="flex items-start gap-1">
<div className="text-xs text-start">
{country_code?.toUpperCase()}
</div>
<div className="text-xs text-start">{country_code?.toUpperCase()}</div>
{country_code && (
<ServerFlag
className="text-[11px] -mt-[1px]"
country_code={country_code}
/>
<ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />
)}
</section>
</section>
@@ -184,9 +158,7 @@ export default function ServerDetailOverview({
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.system")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetail.system")}</p>
<div className="text-xs">
{" "}
{platform} {platform_version ? " - " + platform_version : ""}
@@ -231,14 +203,9 @@ export default function ServerDetailOverview({
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.upload")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetail.upload")}</p>
{net_out_transfer ? (
<div className="text-xs">
{" "}
{formatBytes(net_out_transfer)}{" "}
</div>
<div className="text-xs"> {formatBytes(net_out_transfer)} </div>
) : (
<div className="text-xs"> {t("serverDetail.unknown")}</div>
)}
@@ -250,14 +217,9 @@ export default function ServerDetailOverview({
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.download")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetail.download")}</p>
{net_in_transfer ? (
<div className="text-xs">
{" "}
{formatBytes(net_in_transfer)}{" "}
</div>
<div className="text-xs"> {formatBytes(net_in_transfer)} </div>
) : (
<div className="text-xs"> {t("serverDetail.unknown")}</div>
)}
@@ -275,8 +237,7 @@ export default function ServerDetailOverview({
<section className="flex items-start flex-wrap gap-2">
{server?.state.temperatures.map((item, index) => (
<div className="text-xs flex items-center" key={index}>
<p className="font-semibold">{item.Name}</p>:{" "}
{item.Temperature.toFixed(2)} °C
<p className="font-semibold">{item.Name}</p>: {item.Temperature.toFixed(2)} °C
</div>
))}
</section>
@@ -289,9 +250,7 @@ export default function ServerDetailOverview({
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.lastActive")}
</p>
<p className="text-xs text-muted-foreground">{t("serverDetail.lastActive")}</p>
<div className="text-xs">
{last_active_time_string ? last_active_time_string : "N/A"}
</div>
@@ -300,5 +259,5 @@ export default function ServerDetailOverview({
</Card>
</section>
</div>
);
)
}

View File

@@ -1,38 +1,38 @@
import { cn } from "@/lib/utils";
import getUnicodeFlagIcon from "country-flag-icons/unicode";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils"
import getUnicodeFlagIcon from "country-flag-icons/unicode"
import { useEffect, useState } from "react"
export default function ServerFlag({
country_code,
className,
}: {
country_code: string;
className?: string;
country_code: string
className?: string
}) {
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false)
useEffect(() => {
const checkEmojiSupport = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试
if (!ctx) return;
ctx.fillStyle = "#000";
ctx.textBaseline = "top";
ctx.font = "32px Arial";
ctx.fillText(emojiFlag, 0, 0);
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
const emojiFlag = "🇺🇸" // 使用美国国旗作为测试
if (!ctx) return
ctx.fillStyle = "#000"
ctx.textBaseline = "top"
ctx.font = "32px Arial"
ctx.fillText(emojiFlag, 0, 0)
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0;
setSupportsEmojiFlags(support);
};
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0
setSupportsEmojiFlags(support)
}
checkEmojiSupport();
}, []);
checkEmojiSupport()
}, [])
if (!country_code) return null;
if (!country_code) return null
if (supportsEmojiFlags && country_code.toLowerCase() === "tw") {
country_code = "cn";
country_code = "cn"
}
return (
@@ -43,5 +43,5 @@ export default function ServerFlag({
getUnicodeFlagIcon(country_code)
)}
</span>
);
)
}

View File

@@ -1,23 +1,20 @@
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import { formatBytes } from "@/lib/format";
import {
ArrowDownCircleIcon,
ArrowUpCircleIcon,
} from "@heroicons/react/20/solid";
import { useStatus } from "@/hooks/use-status";
import useFilter from "@/hooks/use-filter";
import { Card, CardContent } from "@/components/ui/card"
import useFilter from "@/hooks/use-filter"
import { useStatus } from "@/hooks/use-status"
import { formatBytes } from "@/lib/format"
import { cn } from "@/lib/utils"
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
import { useTranslation } from "react-i18next"
type ServerOverviewProps = {
online: number;
offline: number;
total: number;
up: number;
down: number;
upSpeed: number;
downSpeed: number;
};
online: number
offline: number
total: number
up: number
down: number
upSpeed: number
downSpeed: number
}
export default function ServerOverview({
online,
@@ -28,25 +25,23 @@ export default function ServerOverview({
upSpeed,
downSpeed,
}: ServerOverviewProps) {
const { t } = useTranslation();
const { status, setStatus } = useStatus();
const { filter, setFilter } = useFilter();
const { t } = useTranslation()
const { status, setStatus } = useStatus()
const { filter, setFilter } = useFilter()
return (
<>
<section className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<Card
onClick={() => {
setFilter(false);
setStatus("all");
setFilter(false)
setStatus("all")
}}
className={cn("hover:border-blue-500 cursor-pointer transition-all")}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">
{t("serverOverview.totalServers")}
</p>
<p className="text-sm font-medium md:text-base">{t("serverOverview.totalServers")}</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
@@ -58,8 +53,8 @@ export default function ServerOverview({
</Card>
<Card
onClick={() => {
setFilter(false);
setStatus("online");
setFilter(false)
setStatus("online")
}}
className={cn(
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
@@ -86,8 +81,8 @@ export default function ServerOverview({
</Card>
<Card
onClick={() => {
setFilter(false);
setStatus("offline");
setFilter(false)
setStatus("offline")
}}
className={cn(
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
@@ -113,8 +108,8 @@ export default function ServerOverview({
</Card>
<Card
onClick={() => {
setStatus("all");
setFilter(true);
setStatus("all")
setFilter(true)
}}
className={cn(
"cursor-pointer hover:ring-purple-500 ring-1 ring-transparent transition-all",
@@ -126,9 +121,7 @@ export default function ServerOverview({
<CardContent className="flex h-full items-center relative px-6 py-3">
<section className="flex flex-col gap-1 w-full">
<div className="flex items-center w-full justify-between">
<p className="text-sm font-medium md:text-base">
{t("serverOverview.network")}
</p>
<p className="text-sm font-medium md:text-base">{t("serverOverview.network")}</p>
</div>
<section className="flex items-start flex-row z-[999] pr-2 sm:pr-0 gap-1">
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">
@@ -153,5 +146,5 @@ export default function ServerOverview({
</Card>
</section>
</>
);
)
}

View File

@@ -1,8 +1,8 @@
import { Progress } from "@/components/ui/progress";
import { Progress } from "@/components/ui/progress"
type ServerUsageBarProps = {
value: number;
};
value: number
}
export default function ServerUsageBar({ value }: ServerUsageBarProps) {
return (
@@ -10,14 +10,8 @@ export default function ServerUsageBar({ value }: ServerUsageBarProps) {
aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"}
value={value}
indicatorClassName={
value > 90
? "bg-red-500"
: value > 70
? "bg-orange-400"
: "bg-green-500"
}
indicatorClassName={value > 90 ? "bg-red-500" : value > 70 ? "bg-orange-400" : "bg-green-500"}
className={"h-[3px] rounded-sm"}
/>
);
)
}

View File

@@ -1,43 +1,42 @@
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";
import { CycleTransferStatsCard } from "./CycleTransferStats";
import { Loader } from "./loading/Loader";
import { useTranslation } from "react-i18next";
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import { fetchService } from "@/lib/nezha-api"
import { ServiceData } from "@/types/nezha-api"
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid"
import { useQuery } from "@tanstack/react-query"
import React from "react"
import { useTranslation } from "react-i18next"
import { CycleTransferStatsCard } from "./CycleTransferStats"
import ServiceTrackerClient from "./ServiceTrackerClient"
import { Loader } from "./loading/Loader"
export const ServiceTracker: React.FC = () => {
const { t } = useTranslation();
const { t } = useTranslation()
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 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;
serviceData.up.reduce((a, b) => a + b, 0) + serviceData.down.reduce((a, b) => a + b, 0)
const uptime = (totalUp / totalChecks) * 100
const avgDelay =
serviceData.delay.length > 0
? serviceData.delay.reduce((a, b) => a + b, 0) /
serviceData.delay.length
: 0;
? serviceData.delay.reduce((a, b) => a + b, 0) / serviceData.delay.length
: 0
return { days, uptime, avgDelay };
};
return { days, uptime, avgDelay }
}
if (isLoading) {
return (
@@ -45,49 +44,43 @@ export const ServiceTracker: React.FC = () => {
<Loader visible={true} />
{t("serviceTracker.loading")}
</div>
);
)
}
if (
!serviceData?.data?.services &&
!serviceData?.data?.cycle_transfer_stats
) {
if (!serviceData?.data?.services && !serviceData?.data?.cycle_transfer_stats) {
return (
<div className="mt-4 text-sm font-medium flex items-center gap-1">
<ExclamationTriangleIcon className="w-4 h-4" />
{t("serviceTracker.noService")}
</div>
);
)
}
return (
<div className="mt-4 w-full mx-auto ">
{serviceData.data.cycle_transfer_stats && (
<div>
<CycleTransferStatsCard
cycleStats={serviceData.data.cycle_transfer_stats}
/>
<CycleTransferStatsCard cycleStats={serviceData.data.cycle_transfer_stats} />
</div>
)}
{serviceData.data.services &&
Object.keys(serviceData.data.services).length > 0 && (
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
{Object.entries(serviceData.data.services).map(([name, data]) => {
const { days, uptime, avgDelay } = processServiceData(data);
return (
<ServiceTrackerClient
key={name}
days={days}
title={data.service_name}
uptime={uptime}
avgDelay={avgDelay}
/>
);
})}
</section>
)}
{serviceData.data.services && Object.keys(serviceData.data.services).length > 0 && (
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
{Object.entries(serviceData.data.services).map(([name, data]) => {
const { days, uptime, avgDelay } = processServiceData(data)
return (
<ServiceTrackerClient
key={name}
days={days}
title={data.service_name}
uptime={uptime}
avgDelay={avgDelay}
/>
)
})}
</section>
)}
</div>
);
};
)
}
export default ServiceTracker;
export default ServiceTracker

View File

@@ -1,17 +1,18 @@
import React from "react";
import { cn } from "@/lib/utils";
import { Separator } from "./ui/separator";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils"
import React from "react"
import { useTranslation } from "react-i18next"
import { Separator } from "./ui/separator"
interface ServiceTrackerProps {
days: Array<{
completed: boolean;
date?: Date;
}>;
className?: string;
title?: string;
uptime?: number;
avgDelay?: number;
completed: boolean
date?: Date
}>
className?: string
title?: string
uptime?: number
avgDelay?: number
}
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
@@ -21,7 +22,7 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
uptime = 100,
avgDelay = 0,
}) => {
const { t } = useTranslation();
const { t } = useTranslation()
return (
<div
className={cn(
@@ -55,9 +56,7 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
"flex-1 h-6 rounded-[5px] transition-colors",
day.completed ? "bg-green-600" : "bg-red-500/60",
)}
title={
day.date ? day.date.toLocaleDateString() : `Day ${index + 1}`
}
title={day.date ? day.date.toLocaleDateString() : `Day ${index + 1}`}
/>
))}
</div>
@@ -67,7 +66,7 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
<span>{t("serviceTracker.today")}</span>
</div>
</div>
);
};
)
}
export default ServiceTrackerClient;
export default ServiceTrackerClient

View File

@@ -1,17 +1,17 @@
import { cn } from "@/lib/utils";
import { m } from "framer-motion";
import { useTranslation } from "react-i18next";
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;
tabs: string[]
currentTab: string
setCurrentTab: (tab: string) => void
}) {
const { t } = useTranslation();
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">
@@ -43,5 +43,5 @@ export default function TabSwitch({
))}
</div>
</div>
);
)
}

View File

@@ -1,73 +1,60 @@
import { createContext, useEffect, useState, ReactNode } from "react";
import { ReactNode, createContext, useEffect, useState } from "react"
export type Theme = "dark" | "light" | "system";
export type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
children: ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
storageKey = "vite-ui-theme",
}: ThemeProviderProps) {
export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || "system",
);
)
useEffect(() => {
const root = window.document.documentElement;
const root = window.document.documentElement
root.classList.remove("light", "dark");
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
: "light"
root.classList.add(systemTheme);
const themeColor =
systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
return;
root.classList.add(systemTheme)
const themeColor = systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
return
}
root.classList.add(theme);
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
}, [theme]);
root.classList.add(theme)
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
};
}
return (
<ThemeProviderContext.Provider value={value}>
{children}
</ThemeProviderContext.Provider>
);
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>
}
export { ThemeProviderContext };
export { ThemeProviderContext }

View File

@@ -1,25 +1,26 @@
import { Button } from "@/components/ui/button";
import { Theme } from "@/components/ThemeProvider"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { Moon, Sun } from "lucide-react";
import { Theme } from "@/components/ThemeProvider";
import { useTheme } from "../hooks/use-theme";
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { useTranslation } from "react-i18next";
} from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"
import { CheckCircleIcon } from "@heroicons/react/20/solid"
import { Moon, Sun } from "lucide-react"
import { useTranslation } from "react-i18next"
import { useTheme } from "../hooks/use-theme"
export function ModeToggle() {
const { t } = useTranslation();
const { setTheme, theme } = useTheme();
const { t } = useTranslation()
const { setTheme, theme } = useTheme()
const handleSelect = (e: Event, newTheme: Theme) => {
e.preventDefault();
setTheme(newTheme);
};
e.preventDefault()
setTheme(newTheme)
}
return (
<DropdownMenu>
@@ -58,5 +59,5 @@ export function ModeToggle() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
)
}

View File

@@ -1,4 +1,4 @@
const bars = Array(8).fill(0);
const bars = Array(8).fill(0)
export const Loader = ({ visible }: { visible: boolean }) => {
return (
@@ -9,5 +9,5 @@ export const Loader = ({ visible }: { visible: boolean }) => {
))}
</div>
</div>
);
};
)
}

View File

@@ -1,6 +1,7 @@
import { Skeleton } from "@/components/ui/skeleton";
import { BackIcon } from "../Icon";
import { useNavigate } from "react-router-dom";
import { Skeleton } from "@/components/ui/skeleton"
import { useNavigate } from "react-router-dom"
import { BackIcon } from "../Icon"
export function ServerDetailChartLoading() {
return (
@@ -14,17 +15,17 @@ export function ServerDetailChartLoading() {
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</section>
</div>
);
)
}
export function ServerDetailLoading() {
const navigate = useNavigate();
const navigate = useNavigate()
return (
<div className="mx-auto w-full max-w-5xl px-0">
<div
onClick={() => {
navigate("/");
navigate("/")
}}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
>
@@ -33,5 +34,5 @@ export function ServerDetailLoading() {
</div>
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</div>
);
)
}

View File

@@ -1 +1 @@
export { domMax as default } from "framer-motion";
export { domMax as default } from "framer-motion"

View File

@@ -1,12 +1,11 @@
import { LazyMotion } from "framer-motion";
import { LazyMotion } from "framer-motion"
const loadFeatures = () =>
import("./framer-lazy-feature").then((res) => res.default);
const loadFeatures = () => import("./framer-lazy-feature").then((res) => res.default)
export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
return (
<LazyMotion features={loadFeatures} strict key="framer">
{children}
</LazyMotion>
);
};
)
}

View File

@@ -1,11 +1,11 @@
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
interface Props {
max: number;
value: number;
min: number;
className?: string;
primaryColor?: string;
max: number
value: number
min: number
className?: string
primaryColor?: string
}
export default function AnimatedCircularProgressBar({
@@ -15,9 +15,9 @@ export default function AnimatedCircularProgressBar({
primaryColor,
className,
}: Props) {
const circumference = 2 * Math.PI * 45;
const percentPx = circumference / 100;
const currentPercent = ((value - min) / (max - min)) * 100;
const circumference = 2 * Math.PI * 45
const percentPx = circumference / 100
const currentPercent = ((value - min) / (max - min)) * 100
return (
<div
@@ -37,12 +37,7 @@ export default function AnimatedCircularProgressBar({
} as React.CSSProperties
}
>
<svg
fill="none"
className="size-full"
strokeWidth="2"
viewBox="0 0 100 100"
>
<svg fill="none" className="size-full" strokeWidth="2" viewBox="0 0 100 100">
{currentPercent <= 90 && currentPercent >= 0 && (
<circle
cx="50"
@@ -62,8 +57,7 @@ export default function AnimatedCircularProgressBar({
transform:
"rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
transition: "all var(--transition-length) ease var(--delay)",
transformOrigin:
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties
}
/>
@@ -90,8 +84,7 @@ export default function AnimatedCircularProgressBar({
transitionProperty: "stroke-dasharray,transform",
transform:
"rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
transformOrigin:
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties
}
/>
@@ -103,5 +96,5 @@ export default function AnimatedCircularProgressBar({
{currentPercent}
</span>
</div>
);
)
}

View File

@@ -1,15 +1,13 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
@@ -21,16 +19,14 @@ const badgeVariants = cva(
variant: "default",
},
},
);
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}
export { Badge, badgeVariants };
export { Badge, badgeVariants }

View File

@@ -1,8 +1,7 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
@@ -10,12 +9,9 @@ const buttonVariants = cva(
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
@@ -31,26 +27,22 @@ const buttonVariants = cva(
size: "default",
},
},
);
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
)
},
);
Button.displayName = "Button";
)
Button.displayName = "Button"
export { Button, buttonVariants };
export { Button, buttonVariants }

View File

@@ -1,85 +1,58 @@
import { cn } from "@/lib/utils";
import * as React from "react";
import { cn } from "@/lib/utils"
import * as React from "react"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className,
)}
{...props}
/>
),
)
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
)
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
{...props}
/>
),
)
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
),
)
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
)
CardFooter.displayName = "CardFooter"
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -1,48 +1,45 @@
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
)
}
type ChartContextProps = {
config: ChartConfig;
};
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null);
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext);
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
throw new Error("useChart must be used within a <ChartContainer />")
}
return context;
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
config: ChartConfig
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
@@ -56,22 +53,18 @@ const ChartContainer = React.forwardRef<
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = "Chart";
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color,
);
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
if (!colorConfig.length) {
return null;
return null
}
return (
@@ -83,10 +76,8 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
@@ -95,20 +86,20 @@ ${colorConfig
.join("\n"),
}}
/>
);
};
)
}
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
@@ -129,49 +120,39 @@ const ChartTooltipContent = React.forwardRef<
},
ref,
) => {
const { config } = useChart();
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
return null
}
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
<div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
)
}
if (!value) {
return null;
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])
if (!active || !payload?.length) {
return null;
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot";
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
@@ -184,9 +165,9 @@ const ChartTooltipContent = React.forwardRef<
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
@@ -245,112 +226,94 @@ const ChartTooltipContent = React.forwardRef<
</>
)}
</div>
);
)
})}
</div>
</div>
);
)
},
);
ChartTooltipContent.displayName = "ChartTooltip";
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref,
) => {
const { config } = useChart();
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart()
if (!payload?.length) {
return null;
}
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex flex-wrap items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
ref={ref}
className={cn(
"flex flex-wrap items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
},
);
ChartLegendContent.displayName = "ChartLegend";
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
})
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string,
) {
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined;
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
: undefined
let configLabelKey: string = key;
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
}
export {
@@ -360,4 +323,4 @@ export {
ChartLegend,
ChartLegendContent,
ChartStyle,
};
}

View File

@@ -1,8 +1,7 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import * as React from "react"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
@@ -16,13 +15,11 @@ const Checkbox = React.forwardRef<
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox };
export { Checkbox }

View File

@@ -1,16 +1,15 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -24,8 +23,8 @@ const DialogOverlay = React.forwardRef<
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@@ -48,36 +47,21 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@@ -85,14 +69,11 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@@ -103,8 +84,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
@@ -117,4 +98,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
};
}

View File

@@ -1,25 +1,24 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
@@ -34,9 +33,8 @@ const DropdownMenuSubTrigger = React.forwardRef<
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
))
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -50,9 +48,8 @@ const DropdownMenuSubContent = React.forwardRef<
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
))
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -69,13 +66,13 @@ const DropdownMenuContent = React.forwardRef<
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
@@ -87,8 +84,8 @@ const DropdownMenuItem = React.forwardRef<
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -110,9 +107,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
))
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -133,26 +129,22 @@ const DropdownMenuRadioItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@@ -163,21 +155,13 @@ const DropdownMenuSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
@@ -195,4 +179,4 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};
}

View File

@@ -1,9 +1,7 @@
import * as React from "react";
import { cn } from "@/lib/utils"
import * as React from "react"
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
@@ -17,9 +15,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref}
{...props}
/>
);
)
},
);
Input.displayName = "Input";
)
Input.displayName = "Input"
export { Input };
export { Input }

View File

@@ -1,24 +1,18 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
import * as LabelPrimitive from "@radix-ui/react-label"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label };
export { Label }

View File

@@ -1,31 +1,24 @@
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import * as React from "react"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string;
indicatorClassName?: string
}
>(({ className, value, indicatorClassName, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className,
)}
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
{...props}
>
<ProgressPrimitive.Indicator
className={cn(
"h-full w-full flex-1 bg-primary transition-all",
indicatorClassName,
)}
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress };
export { Progress }

View File

@@ -1,29 +1,23 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
)}
{...props}
/>
))
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator };
export { Separator }

View File

@@ -1,15 +1,7 @@
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
}
export { Skeleton };
export { Skeleton }

View File

@@ -1,7 +1,6 @@
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from "react"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
@@ -21,7 +20,7 @@ const Switch = React.forwardRef<
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch };
export { Switch }

View File

@@ -1,40 +1,30 @@
import * as React from "react";
import { cn } from "@/lib/utils"
import * as React from "react"
import { cn } from "@/lib/utils";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
</div>
),
)
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
@@ -42,29 +32,25 @@ const TableFooter = React.forwardRef<
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className,
)}
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
),
)
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
@@ -78,8 +64,8 @@ const TableHead = React.forwardRef<
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
@@ -90,28 +76,15 @@ const TableCell = React.forwardRef<
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }