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

12
.prettierrc.json Normal file
View File

@ -0,0 +1,12 @@
{
"semi": false,
"singleQuote": false,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "all",
"importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss", "@trivago/prettier-plugin-sort-imports"]
}

BIN
bun.lockb

Binary file not shown.

View File

@ -1,8 +1,8 @@
import js from "@eslint/js"; import js from "@eslint/js"
import globals from "globals"; import reactHooks from "eslint-plugin-react-hooks"
import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"
import reactRefresh from "eslint-plugin-react-refresh"; import globals from "globals"
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint"
export default tseslint.config( export default tseslint.config(
{ ignores: ["dist"] }, { ignores: ["dist"] },
@ -19,12 +19,9 @@ export default tseslint.config(
}, },
rules: { rules: {
...reactHooks.configs.recommended.rules, ...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"warn",
{ allowConstantExport: true },
],
"react-hooks/exhaustive-deps": "off", "react-hooks/exhaustive-deps": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
}, },
}, },
); )

View File

@ -4,16 +4,14 @@
<script> <script>
// 在页面渲染前就执行主题初始化 // 在页面渲染前就执行主题初始化
try { try {
const storageKey = "vite-ui-theme"; const storageKey = "vite-ui-theme"
let theme = localStorage.getItem(storageKey); let theme = localStorage.getItem(storageKey)
if (theme === "system" || !theme) { if (theme === "system" || !theme) {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
? "dark"
: "light";
} }
document.documentElement.classList.add(theme); document.documentElement.classList.add(theme)
} catch (e) { } catch (e) {
document.documentElement.classList.add("light"); document.documentElement.classList.add("light")
} }
</script> </script>
<style> <style>
@ -65,53 +63,48 @@
} }
</style> </style>
<script> <script>
(function () { ;(function () {
const storageKey = "vite-ui-theme"; const storageKey = "vite-ui-theme"
const theme = localStorage.getItem(storageKey) || "system"; const theme = localStorage.getItem(storageKey) || "system"
const root = document.documentElement; const root = document.documentElement
function updateThemeColor(isDark) { function updateThemeColor(isDark) {
const themeColor = isDark ? "#242424" : "#fafafa"; const themeColor = isDark ? "#242424" : "#fafafa"
document document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
} }
function setTheme(newTheme) { function setTheme(newTheme) {
root.classList.remove("light", "dark"); root.classList.remove("light", "dark")
root.classList.add(newTheme); root.classList.add(newTheme)
updateThemeColor(newTheme === "dark"); updateThemeColor(newTheme === "dark")
} }
if (theme === "system") { if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
.matches
? "dark" ? "dark"
: "light"; : "light"
setTheme(systemTheme); setTheme(systemTheme)
window window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
.matchMedia("(prefers-color-scheme: dark)") setTheme(e.matches ? "dark" : "light")
.addEventListener("change", (e) => { })
setTheme(e.matches ? "dark" : "light");
});
} else { } else {
setTheme(theme); setTheme(theme)
} }
// Add loaded class after React has mounted // Add loaded class after React has mounted
window.addEventListener("load", () => { window.addEventListener("load", () => {
const root = document.getElementById("root"); const root = document.getElementById("root")
if (root) { if (root) {
// 使用 RAF 确保在下一帧渲染 // 使用 RAF 确保在下一帧渲染
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
root.classList.add("loaded"); root.classList.add("loaded")
}); })
}); })
} }
}); })
})(); })()
</script> </script>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/apple-touch-icon.png" /> <link rel="icon" type="image/png" href="/apple-touch-icon.png" />

View File

@ -23,6 +23,7 @@
"@tanstack/react-query": "^5.62.7", "@tanstack/react-query": "^5.62.7",
"@tanstack/react-query-devtools": "^5.62.7", "@tanstack/react-query-devtools": "^5.62.7",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/d3-geo": "^3.1.0", "@types/d3-geo": "^3.1.0",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -33,6 +34,7 @@
"i18next": "^24.1.0", "i18next": "^24.1.0",
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"prettier-plugin-tailwindcss": "^0.6.9",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-i18next": "^15.1.4", "react-i18next": "^15.1.4",

View File

@ -3,4 +3,4 @@ export default {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
}; }

View File

@ -1,18 +0,0 @@
/**
* Self-Destroy service worker
*/
self.addEventListener("install", function (e) {
self.skipWaiting();
});
self.addEventListener("activate", function (e) {
self.registration
.unregister()
.then(function () {
return self.clients.matchAll();
})
.then(function (clients) {
clients.forEach((client) => client.navigate(client.url));
});
});

View File

@ -1,7 +1,7 @@
const { execSync } = require("child_process"); const { execSync } = require("child_process")
// Get the short version of the git hash // Get the short version of the git hash
const gitHash = execSync("git rev-parse --short HEAD").toString().trim(); const gitHash = execSync("git rev-parse --short HEAD").toString().trim()
// Write it to stdout // Write it to stdout
console.log(gitHash); console.log(gitHash)

View File

@ -1,11 +1,12 @@
import React from "react"; import React from "react"
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { Route, BrowserRouter as Router, Routes } from "react-router-dom"
import Header from "./components/Header";
import Footer from "./components/Footer"; import Footer from "./components/Footer"
import Server from "./pages/Server"; import Header from "./components/Header"
import ServerDetail from "./pages/ServerDetail"; import ErrorPage from "./pages/ErrorPage"
import NotFound from "./pages/NotFound"; import NotFound from "./pages/NotFound"
import ErrorPage from "./pages/ErrorPage"; import Server from "./pages/Server"
import ServerDetail from "./pages/ServerDetail"
const App: React.FC = () => { const App: React.FC = () => {
return ( return (
@ -23,7 +24,7 @@ const App: React.FC = () => {
</main> </main>
</div> </div>
</Router> </Router>
); )
}; }
export default App; export default App

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,72 +1,71 @@
import { ModeToggle } from "@/components/ThemeSwitcher"; import { ModeToggle } from "@/components/ThemeSwitcher"
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator"
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton"
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"; import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query"
import { DateTime } from "luxon"; import { DateTime } from "luxon"
import { useEffect, useRef, useState, useCallback } from "react"; import { useCallback, useEffect, useRef, useState } from "react"
import { LanguageSwitcher } from "./LanguageSwitcher"; import { useTranslation } from "react-i18next"
import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"
import { useNavigate } from "react-router-dom";
import { LanguageSwitcher } from "./LanguageSwitcher"
function Header() { function Header() {
const { t } = useTranslation(); const { t } = useTranslation()
const navigate = useNavigate(); const navigate = useNavigate()
const { data: settingData, isLoading } = useQuery({ const { data: settingData, isLoading } = useQuery({
queryKey: ["setting"], queryKey: ["setting"],
queryFn: () => fetchSetting(), queryFn: () => fetchSetting(),
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
}); })
const siteName = settingData?.data?.site_name; const siteName = settingData?.data?.site_name
const InjectContext = useCallback((content: string) => { const InjectContext = useCallback((content: string) => {
const tempDiv = document.createElement("div"); const tempDiv = document.createElement("div")
tempDiv.innerHTML = content; tempDiv.innerHTML = content
const handlers: { [key: string]: (element: HTMLElement) => void } = { const handlers: { [key: string]: (element: HTMLElement) => void } = {
SCRIPT: (element) => { SCRIPT: (element) => {
const script = document.createElement("script"); const script = document.createElement("script")
if ((element as HTMLScriptElement).src) { if ((element as HTMLScriptElement).src) {
script.src = (element as HTMLScriptElement).src; script.src = (element as HTMLScriptElement).src
} else { } else {
script.textContent = element.textContent; script.textContent = element.textContent
} }
document.body.appendChild(script); document.body.appendChild(script)
}, },
STYLE: (element) => { STYLE: (element) => {
const style = document.createElement("style"); const style = document.createElement("style")
style.textContent = element.textContent; style.textContent = element.textContent
document.head.appendChild(style); document.head.appendChild(style)
}, },
DEFAULT: (element) => { DEFAULT: (element) => {
document.body.appendChild(element); document.body.appendChild(element)
}, },
}; }
Array.from(tempDiv.childNodes).forEach((node) => { Array.from(tempDiv.childNodes).forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) { if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement; const element = node as HTMLElement
(handlers[element.tagName] || handlers.DEFAULT)(element); ;(handlers[element.tagName] || handlers.DEFAULT)(element)
} else if (node.nodeType === Node.TEXT_NODE) { } else if (node.nodeType === Node.TEXT_NODE) {
document.body.appendChild( document.body.appendChild(document.createTextNode(node.textContent || ""))
document.createTextNode(node.textContent || ""),
);
} }
}); })
}, []); }, [])
useEffect(() => { useEffect(() => {
document.title = siteName || "NEZHA"; document.title = siteName || "NEZHA"
}, [siteName]); }, [siteName])
useEffect(() => { useEffect(() => {
if (settingData?.data?.custom_code) { if (settingData?.data?.custom_code) {
InjectContext(settingData?.data?.custom_code); InjectContext(settingData?.data?.custom_code)
} }
}, [settingData?.data?.custom_code]); }, [settingData?.data?.custom_code])
return ( return (
<div className="mx-auto w-full max-w-5xl"> <div className="mx-auto w-full max-w-5xl">
@ -89,13 +88,8 @@ function Header() {
) : ( ) : (
siteName || "NEZHA" siteName || "NEZHA"
)} )}
<Separator <Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
orientation="vertical" <p className="hidden text-sm font-medium opacity-40 md:block">{t("nezha")}</p>
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>
<section className="flex items-center gap-2"> <section className="flex items-center gap-2">
<DashboardLink /> <DashboardLink />
@ -105,17 +99,17 @@ function Header() {
</section> </section>
<Overview /> <Overview />
</div> </div>
); )
} }
function DashboardLink() { function DashboardLink() {
const { t } = useTranslation(); const { t } = useTranslation()
const { data: userData } = useQuery({ const { data: userData } = useQuery({
queryKey: ["login-user"], queryKey: ["login-user"],
queryFn: () => fetchLoginUser(), queryFn: () => fetchLoginUser(),
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
}); })
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -129,37 +123,37 @@ function DashboardLink() {
{userData?.data?.id && t("dashboard")} {userData?.data?.id && t("dashboard")}
</a> </a>
</div> </div>
); )
} }
// https://github.com/streamich/react-use/blob/master/src/useInterval.ts // https://github.com/streamich/react-use/blob/master/src/useInterval.ts
const useInterval = (callback: () => void, delay: number | null) => { const useInterval = (callback: () => void, delay: number | null) => {
const savedCallback = useRef<() => void>(() => {}); const savedCallback = useRef<() => void>(() => {})
useEffect(() => { useEffect(() => {
savedCallback.current = callback; savedCallback.current = callback
}); })
useEffect(() => { useEffect(() => {
if (delay !== null) { if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0); const interval = setInterval(() => savedCallback.current(), delay || 0)
return () => clearInterval(interval); return () => clearInterval(interval)
} }
return undefined; return undefined
}, [delay]); }, [delay])
}; }
function Overview() { function Overview() {
const { t } = useTranslation(); const { t } = useTranslation()
const [mouted, setMounted] = useState(false); const [mouted, setMounted] = useState(false)
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true)
}, []); }, [])
const timeOption = DateTime.TIME_SIMPLE; const timeOption = DateTime.TIME_SIMPLE
timeOption.hour12 = true; timeOption.hour12 = true
const [timeString, setTimeString] = useState( const [timeString, setTimeString] = useState(
DateTime.now().setLocale("en-US").toLocaleString(timeOption), DateTime.now().setLocale("en-US").toLocaleString(timeOption),
); )
useInterval(() => { useInterval(() => {
setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption)); setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption))
}, 1000); }, 1000)
return ( return (
<section className={"mt-10 flex flex-col md:mt-16"}> <section className={"mt-10 flex flex-col md:mt-16"}>
<p className="text-base font-semibold">👋 {t("overview")}</p> <p className="text-base font-semibold">👋 {t("overview")}</p>
@ -172,6 +166,6 @@ function Overview() {
)} )}
</div> </div>
</section> </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}> <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" /> <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> </svg>
); )
} }
export function BackIcon() { export function BackIcon() {
@ -28,5 +28,5 @@ export function BackIcon() {
height="20" 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 { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu"
import { CheckCircleIcon } from "@heroicons/react/20/solid"
import { CheckCircleIcon } from "@heroicons/react/20/solid"; import { useTranslation } from "react-i18next"
import { useTranslation } from "react-i18next";
export function LanguageSwitcher() { 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) => { const handleSelect = (e: Event, newLocale: string) => {
e.preventDefault(); // 阻止默认的关闭行为 e.preventDefault() // 阻止默认的关闭行为
i18n.changeLanguage(newLocale); i18n.changeLanguage(newLocale)
}; }
const localeItems = [ const localeItems = [
{ name: t("language.zh-CN"), code: "zh-CN" }, { name: t("language.zh-CN"), code: "zh-CN" },
{ name: t("language.zh-TW"), code: "zh-TW" }, { name: t("language.zh-TW"), code: "zh-TW" },
{ name: t("language.en"), code: "en" }, { name: t("language.en"), code: "en" },
]; ]
return ( return (
<DropdownMenu> <DropdownMenu>
@ -46,11 +45,10 @@ export function LanguageSwitcher() {
onSelect={(e) => handleSelect(e, item.code)} onSelect={(e) => handleSelect(e, item.code)}
className={locale === item.code ? "bg-muted gap-3" : ""} className={locale === item.code ? "bg-muted gap-3" : ""}
> >
{item.name}{" "} {item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
{locale === item.code && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); )
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,17 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import { m } from "framer-motion"; import { m } from "framer-motion"
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next"
export default function TabSwitch({ export default function TabSwitch({
tabs, tabs,
currentTab, currentTab,
setCurrentTab, setCurrentTab,
}: { }: {
tabs: string[]; tabs: string[]
currentTab: string; currentTab: string
setCurrentTab: (tab: string) => void; setCurrentTab: (tab: string) => void
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation()
return ( return (
<div className="z-50 flex flex-col items-start rounded-[50px]"> <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"> <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>
</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 = { type ThemeProviderProps = {
children: ReactNode; children: ReactNode
defaultTheme?: Theme; defaultTheme?: Theme
storageKey?: string; storageKey?: string
}; }
type ThemeProviderState = { type ThemeProviderState = {
theme: Theme; theme: Theme
setTheme: (theme: Theme) => void; setTheme: (theme: Theme) => void
}; }
const initialState: ThemeProviderState = { const initialState: ThemeProviderState = {
theme: "system", theme: "system",
setTheme: () => null, setTheme: () => null,
}; }
const ThemeProviderContext = createContext<ThemeProviderState>(initialState); const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({ export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeProviderProps) {
children,
storageKey = "vite-ui-theme",
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>( const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || "system", () => (localStorage.getItem(storageKey) as Theme) || "system",
); )
useEffect(() => { useEffect(() => {
const root = window.document.documentElement; const root = window.document.documentElement
root.classList.remove("light", "dark"); root.classList.remove("light", "dark")
if (theme === "system") { if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
.matches
? "dark" ? "dark"
: "light"; : "light"
root.classList.add(systemTheme); root.classList.add(systemTheme)
const themeColor = const themeColor = systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"; document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
document return
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
return;
} }
root.classList.add(theme); root.classList.add(theme)
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"; const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
document document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
.querySelector('meta[name="theme-color"]') }, [theme])
?.setAttribute("content", themeColor);
}, [theme]);
const value = { const value = {
theme, theme,
setTheme: (theme: Theme) => { setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme); localStorage.setItem(storageKey, theme)
setTheme(theme); setTheme(theme)
}, },
}; }
return ( return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>
<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 { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import { Moon, Sun } from "lucide-react"; import { CheckCircleIcon } from "@heroicons/react/20/solid"
import { Theme } from "@/components/ThemeProvider"; import { Moon, Sun } from "lucide-react"
import { useTheme } from "../hooks/use-theme"; import { useTranslation } from "react-i18next"
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { useTranslation } from "react-i18next"; import { useTheme } from "../hooks/use-theme"
export function ModeToggle() { export function ModeToggle() {
const { t } = useTranslation(); const { t } = useTranslation()
const { setTheme, theme } = useTheme(); const { setTheme, theme } = useTheme()
const handleSelect = (e: Event, newTheme: Theme) => { const handleSelect = (e: Event, newTheme: Theme) => {
e.preventDefault(); e.preventDefault()
setTheme(newTheme); setTheme(newTheme)
}; }
return ( return (
<DropdownMenu> <DropdownMenu>
@ -58,5 +59,5 @@ export function ModeToggle() {
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </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 }) => { export const Loader = ({ visible }: { visible: boolean }) => {
return ( return (
@ -9,5 +9,5 @@ export const Loader = ({ visible }: { visible: boolean }) => {
))} ))}
</div> </div>
</div> </div>
); )
}; }

View File

@ -1,6 +1,7 @@
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton"
import { BackIcon } from "../Icon"; import { useNavigate } from "react-router-dom"
import { useNavigate } from "react-router-dom";
import { BackIcon } from "../Icon"
export function ServerDetailChartLoading() { export function ServerDetailChartLoading() {
return ( return (
@ -14,17 +15,17 @@ export function ServerDetailChartLoading() {
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</section> </section>
</div> </div>
); )
} }
export function ServerDetailLoading() { export function ServerDetailLoading() {
const navigate = useNavigate(); const navigate = useNavigate()
return ( return (
<div className="mx-auto w-full max-w-5xl px-0"> <div className="mx-auto w-full max-w-5xl px-0">
<div <div
onClick={() => { onClick={() => {
navigate("/"); navigate("/")
}} }}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl" 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> </div>
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</div> </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 = () => const loadFeatures = () => import("./framer-lazy-feature").then((res) => res.default)
import("./framer-lazy-feature").then((res) => res.default);
export const MotionProvider = ({ children }: { children: React.ReactNode }) => { export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
return ( return (
<LazyMotion features={loadFeatures} strict key="framer"> <LazyMotion features={loadFeatures} strict key="framer">
{children} {children}
</LazyMotion> </LazyMotion>
); )
}; }

View File

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

View File

@ -1,15 +1,13 @@
import * as React from "react"; import { cn } from "@/lib/utils"
import { cva, type VariantProps } from "class-variance-authority"; import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils";
const badgeVariants = cva( 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", "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: { variants: {
variant: { variant: {
default: default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: destructive:
@ -21,16 +19,14 @@ const badgeVariants = cva(
variant: "default", variant: "default",
}, },
}, },
); )
export interface BadgeProps export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {} VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return <div className={cn(badgeVariants({ variant }), className)} {...props} />
<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 { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"; import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils";
const buttonVariants = cva( 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", "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: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
"bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
outline: secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
"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", ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
@ -31,26 +27,22 @@ const buttonVariants = cva(
size: "default", size: "default",
}, },
}, },
); )
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean; asChild?: boolean
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"; const Comp = asChild ? Slot : "button"
return ( return (
<Comp <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
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 { cn } from "@/lib/utils"
import * as React from "react"; import * as React from "react"
const Card = React.forwardRef< const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div
>(({ className, ...props }, ref) => ( ref={ref}
<div className={cn(
ref={ref} "rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className={cn( className,
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none", )}
className, {...props}
)} />
{...props} ),
/> )
)); Card.displayName = "Card"
Card.displayName = "Card";
const CardHeader = React.forwardRef< const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<div )
ref={ref} CardHeader.displayName = "CardHeader"
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef< const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLHeadingElement> <h3
>(({ className, ...props }, ref) => ( ref={ref}
<h3 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
ref={ref} {...props}
className={cn( />
"text-2xl font-semibold leading-none tracking-tight", ),
className, )
)} CardTitle.displayName = "CardTitle"
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<p <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
ref={ref} ))
className={cn("text-sm text-muted-foreground", className)} CardDescription.displayName = "CardDescription"
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef< const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> )
)); CardContent.displayName = "CardContent"
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef< const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<div )
ref={ref} CardFooter.displayName = "CardFooter"
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export { export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

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

View File

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

View File

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

View File

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

View File

@ -1,31 +1,24 @@
import * as React from "react"; import { cn } from "@/lib/utils"
import * as ProgressPrimitive from "@radix-ui/react-progress"; import * as ProgressPrimitive from "@radix-ui/react-progress"
import * as React from "react"
import { cn } from "@/lib/utils";
const Progress = React.forwardRef< const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>, React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & { React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string; indicatorClassName?: string
} }
>(({ className, value, indicatorClassName, ...props }, ref) => ( >(({ className, value, indicatorClassName, ...props }, ref) => (
<ProgressPrimitive.Root <ProgressPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className,
)}
{...props} {...props}
> >
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
className={cn( className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
"h-full w-full flex-1 bg-primary transition-all",
indicatorClassName,
)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </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 { cn } from "@/lib/utils"
import * as SeparatorPrimitive from "@radix-ui/react-separator"; import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react"
import { cn } from "@/lib/utils";
const Separator = React.forwardRef< const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>( >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
( <SeparatorPrimitive.Root
{ className, orientation = "horizontal", decorative = true, ...props }, ref={ref}
ref, decorative={decorative}
) => ( orientation={orientation}
<SeparatorPrimitive.Root className={cn(
ref={ref} "shrink-0 bg-border",
decorative={decorative} orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
orientation={orientation} className,
className={cn( )}
"shrink-0 bg-border", {...props}
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", />
className, ))
)} Separator.displayName = SeparatorPrimitive.Root.displayName
{...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({ function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
className, return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
...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 { cn } from "@/lib/utils"
import * as SwitchPrimitives from "@radix-ui/react-switch"; import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from "react"
import { cn } from "@/lib/utils";
const Switch = React.forwardRef< const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>, React.ElementRef<typeof SwitchPrimitives.Root>,
@ -21,7 +20,7 @@ const Switch = React.forwardRef<
)} )}
/> />
</SwitchPrimitives.Root> </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) => (
const Table = React.forwardRef< <div className="relative w-full overflow-auto">
HTMLTableElement, <table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
React.HTMLAttributes<HTMLTableElement> </div>
>(({ className, ...props }, ref) => ( ),
<div className="relative w-full overflow-auto"> )
<table Table.displayName = "Table"
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef< const TableHeader = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement> React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
)); ))
TableHeader.displayName = "TableHeader"; TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef< const TableBody = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement> React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tbody <tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
ref={ref} ))
className={cn("[&_tr:last-child]:border-0", className)} TableBody.displayName = "TableBody"
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef< const TableFooter = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
@ -42,29 +32,25 @@ const TableFooter = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tfoot <tfoot
ref={ref} ref={ref}
className={cn( className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className,
)}
{...props} {...props}
/> />
)); ))
TableFooter.displayName = "TableFooter"; TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef< const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
HTMLTableRowElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableRowElement> <tr
>(({ className, ...props }, ref) => ( ref={ref}
<tr className={cn(
ref={ref} "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className={cn( className,
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", )}
className, {...props}
)} />
{...props} ),
/> )
)); TableRow.displayName = "TableRow"
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef< const TableHead = React.forwardRef<
HTMLTableCellElement, HTMLTableCellElement,
@ -78,8 +64,8 @@ const TableHead = React.forwardRef<
)} )}
{...props} {...props}
/> />
)); ))
TableHead.displayName = "TableHead"; TableHead.displayName = "TableHead"
const TableCell = React.forwardRef< const TableCell = React.forwardRef<
HTMLTableCellElement, HTMLTableCellElement,
@ -90,28 +76,15 @@ const TableCell = React.forwardRef<
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props} {...props}
/> />
)); ))
TableCell.displayName = "TableCell"; TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef< const TableCaption = React.forwardRef<
HTMLTableCaptionElement, HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement> React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<caption <caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
ref={ref} ))
className={cn("mt-4 text-sm text-muted-foreground", className)} TableCaption.displayName = "TableCaption"
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export { export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@ -1,10 +1,8 @@
import { createContext } from "react"; import { createContext } from "react"
export interface FilterContextType { export interface FilterContextType {
filter: boolean; filter: boolean
setFilter: (filter: boolean) => void; setFilter: (filter: boolean) => void
} }
export const FilterContext = createContext<FilterContextType | undefined>( export const FilterContext = createContext<FilterContextType | undefined>(undefined)
undefined,
);

View File

@ -1,14 +1,11 @@
"use client"; "use client"
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react"
import { FilterContext } from "./filter-context";
import { FilterContext } from "./filter-context"
export function FilterProvider({ children }: { children: ReactNode }) { export function FilterProvider({ children }: { children: ReactNode }) {
const [filter, setFilter] = useState<boolean>(false); const [filter, setFilter] = useState<boolean>(false)
return ( return <FilterContext.Provider value={{ filter, setFilter }}>{children}</FilterContext.Provider>
<FilterContext.Provider value={{ filter, setFilter }}>
{children}
</FilterContext.Provider>
);
} }

View File

@ -1,12 +1,10 @@
import { createContext } from "react"; import { createContext } from "react"
export type Status = "all" | "online" | "offline"; export type Status = "all" | "online" | "offline"
export interface StatusContextType { export interface StatusContextType {
status: Status; status: Status
setStatus: (status: Status) => void; setStatus: (status: Status) => void
} }
export const StatusContext = createContext<StatusContextType | undefined>( export const StatusContext = createContext<StatusContextType | undefined>(undefined)
undefined,
);

View File

@ -1,12 +1,9 @@
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react"
import { Status, StatusContext } from "./status-context";
import { Status, StatusContext } from "./status-context"
export function StatusProvider({ children }: { children: ReactNode }) { export function StatusProvider({ children }: { children: ReactNode }) {
const [status, setStatus] = useState<Status>("all"); const [status, setStatus] = useState<Status>("all")
return ( return <StatusContext.Provider value={{ status, setStatus }}>{children}</StatusContext.Provider>
<StatusContext.Provider value={{ status, setStatus }}>
{children}
</StatusContext.Provider>
);
} }

View File

@ -1,20 +1,18 @@
import { createContext } from "react"; import { createContext } from "react"
export interface TooltipData { export interface TooltipData {
centroid: [number, number]; centroid: [number, number]
country: string; country: string
count: number; count: number
servers: Array<{ servers: Array<{
name: string; name: string
status: boolean; status: boolean
}>; }>
} }
interface TooltipContextType { interface TooltipContextType {
tooltipData: TooltipData | null; tooltipData: TooltipData | null
setTooltipData: (data: TooltipData | null) => void; setTooltipData: (data: TooltipData | null) => void
} }
export const TooltipContext = createContext<TooltipContextType | undefined>( export const TooltipContext = createContext<TooltipContextType | undefined>(undefined)
undefined,
);

View File

@ -1,12 +1,13 @@
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react"
import { TooltipContext, TooltipData } from "./tooltip-context";
import { TooltipContext, TooltipData } from "./tooltip-context"
export function TooltipProvider({ children }: { children: ReactNode }) { export function TooltipProvider({ children }: { children: ReactNode }) {
const [tooltipData, setTooltipData] = useState<TooltipData | null>(null); const [tooltipData, setTooltipData] = useState<TooltipData | null>(null)
return ( return (
<TooltipContext.Provider value={{ tooltipData, setTooltipData }}> <TooltipContext.Provider value={{ tooltipData, setTooltipData }}>
{children} {children}
</TooltipContext.Provider> </TooltipContext.Provider>
); )
} }

View File

@ -1,11 +1,11 @@
import { createContext } from "react"; import { createContext } from "react"
export interface WebSocketContextType { export interface WebSocketContextType {
lastMessage: { data: string } | null; lastMessage: { data: string } | null
connected: boolean; connected: boolean
} }
export const WebSocketContext = createContext<WebSocketContextType>({ export const WebSocketContext = createContext<WebSocketContextType>({
lastMessage: null, lastMessage: null,
connected: false, connected: false,
}); })

View File

@ -1,81 +1,75 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react"
import { WebSocketContext, WebSocketContextType } from "./websocket-context";
import { WebSocketContext, WebSocketContextType } from "./websocket-context"
interface WebSocketProviderProps { interface WebSocketProviderProps {
url: string; url: string
children: React.ReactNode; children: React.ReactNode
} }
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, children }) => {
url, const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null)
children, const [connected, setConnected] = useState(false)
}) => { const ws = useRef<WebSocket | null>(null)
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null); const reconnectTimeout = useRef<NodeJS.Timeout>(null)
const [connected, setConnected] = useState(false); const maxReconnectAttempts = 30
const ws = useRef<WebSocket | null>(null); const reconnectAttempts = useRef(0)
const reconnectTimeout = useRef<NodeJS.Timeout>(null);
const maxReconnectAttempts = 30;
const reconnectAttempts = useRef(0);
const connect = () => { const connect = () => {
try { try {
const wsUrl = new URL(url, window.location.origin); const wsUrl = new URL(url, window.location.origin)
wsUrl.protocol = wsUrl.protocol.replace("http", "ws"); wsUrl.protocol = wsUrl.protocol.replace("http", "ws")
ws.current = new WebSocket(wsUrl.toString()); ws.current = new WebSocket(wsUrl.toString())
ws.current.onopen = () => { ws.current.onopen = () => {
console.log("WebSocket connected"); console.log("WebSocket connected")
setConnected(true); setConnected(true)
reconnectAttempts.current = 0; reconnectAttempts.current = 0
}; }
ws.current.onclose = () => { ws.current.onclose = () => {
console.log("WebSocket disconnected"); console.log("WebSocket disconnected")
setConnected(false); setConnected(false)
// 重连逻辑 // 重连逻辑
if (reconnectAttempts.current < maxReconnectAttempts) { if (reconnectAttempts.current < maxReconnectAttempts) {
reconnectTimeout.current = setTimeout(() => { reconnectTimeout.current = setTimeout(() => {
reconnectAttempts.current++; reconnectAttempts.current++
connect(); connect()
}, 3000); }, 3000)
} }
}; }
ws.current.onmessage = (event) => { ws.current.onmessage = (event) => {
setLastMessage({ data: event.data }); setLastMessage({ data: event.data })
}; }
ws.current.onerror = (error) => { ws.current.onerror = (error) => {
console.error("WebSocket error:", error); console.error("WebSocket error:", error)
}; }
} catch (error) { } catch (error) {
console.error("WebSocket connection error:", error); console.error("WebSocket connection error:", error)
} }
}; }
useEffect(() => { useEffect(() => {
connect(); connect()
return () => { return () => {
if (ws.current) { if (ws.current) {
ws.current.close(); ws.current.close()
} }
if (reconnectTimeout.current) { if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current); clearTimeout(reconnectTimeout.current)
} }
}; }
}, [url]); }, [url])
const contextValue: WebSocketContextType = { const contextValue: WebSocketContextType = {
lastMessage, lastMessage,
connected, connected,
}; }
return ( return <WebSocketContext.Provider value={contextValue}>{children}</WebSocketContext.Provider>
<WebSocketContext.Provider value={contextValue}> }
{children}
</WebSocketContext.Provider>
);
};

View File

@ -1,12 +1,12 @@
import { useContext } from "react"; import { FilterContext, FilterContextType } from "@/context/filter-context"
import { FilterContext, FilterContextType } from "@/context/filter-context"; import { useContext } from "react"
const useFilter = (): FilterContextType => { const useFilter = (): FilterContextType => {
const context = useContext(FilterContext); const context = useContext(FilterContext)
if (context === undefined) { if (context === undefined) {
throw new Error("useFilter must be used within a FilterProvider"); throw new Error("useFilter must be used within a FilterProvider")
} }
return context; return context
}; }
export default useFilter; export default useFilter

View File

@ -1,10 +1,11 @@
import { useContext } from "react"; import { useContext } from "react"
import { StatusContext } from "../context/status-context";
import { StatusContext } from "../context/status-context"
export function useStatus() { export function useStatus() {
const context = useContext(StatusContext); const context = useContext(StatusContext)
if (context === undefined) { if (context === undefined) {
throw new Error("useStatus must be used within a StatusProvider"); throw new Error("useStatus must be used within a StatusProvider")
} }
return context; return context
} }

View File

@ -1,12 +1,13 @@
import { useContext } from "react"; import { useContext } from "react"
import { ThemeProviderContext } from "../components/ThemeProvider";
import { ThemeProviderContext } from "../components/ThemeProvider"
export const useTheme = () => { export const useTheme = () => {
const context = useContext(ThemeProviderContext); const context = useContext(ThemeProviderContext)
if (context === undefined) { if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider"); throw new Error("useTheme must be used within a ThemeProvider")
} }
return context; return context
}; }

View File

@ -1,12 +1,12 @@
import { useContext } from "react"; import { TooltipContext } from "@/context/tooltip-context"
import { TooltipContext } from "@/context/tooltip-context"; import { useContext } from "react"
export const useTooltip = () => { export const useTooltip = () => {
const context = useContext(TooltipContext); const context = useContext(TooltipContext)
if (context === undefined) { if (context === undefined) {
throw new Error("useTooltip must be used within a TooltipProvider"); throw new Error("useTooltip must be used within a TooltipProvider")
} }
return context; return context
}; }
export default useTooltip; export default useTooltip

View File

@ -1,12 +1,11 @@
import { useContext } from "react"; import { useContext } from "react"
import { WebSocketContext } from "../context/websocket-context";
import { WebSocketContext } from "../context/websocket-context"
export const useWebSocketContext = () => { export const useWebSocketContext = () => {
const context = useContext(WebSocketContext); const context = useContext(WebSocketContext)
if (context === undefined) { if (context === undefined) {
throw new Error( throw new Error("useWebSocketContext must be used within a WebSocketProvider")
"useWebSocketContext must be used within a WebSocketProvider",
);
} }
return context; return context
}; }

View File

@ -1,9 +1,9 @@
import i18n from "i18next"; import i18n from "i18next"
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next"
import enTranslation from "./locales/en/translation.json"; import enTranslation from "./locales/en/translation.json"
import zhCNTranslation from "./locales/zh-CN/translation.json"; import zhCNTranslation from "./locales/zh-CN/translation.json"
import zhTWTranslation from "./locales/zh-TW/translation.json"; import zhTWTranslation from "./locales/zh-TW/translation.json"
const resources = { const resources = {
en: { en: {
@ -15,11 +15,11 @@ const resources = {
"zh-TW": { "zh-TW": {
translation: zhTWTranslation, translation: zhTWTranslation,
}, },
}; }
const getStoredLanguage = () => { const getStoredLanguage = () => {
return localStorage.getItem("language") || "zh-CN"; return localStorage.getItem("language") || "zh-CN"
}; }
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
resources, resources,
@ -28,11 +28,11 @@ i18n.use(initReactI18next).init({
interpolation: { interpolation: {
escapeValue: false, // react已经安全地转义 escapeValue: false, // react已经安全地转义
}, },
}); })
// 添加语言改变时的处理函数 // 添加语言改变时的处理函数
i18n.on("languageChanged", (lng) => { i18n.on("languageChanged", (lng) => {
localStorage.setItem("language", lng); localStorage.setItem("language", lng)
}); })
export default i18n; export default i18n

View File

@ -1,21 +1,11 @@
export function formatBytes(bytes: number, decimals: number = 2) { export function formatBytes(bytes: number, decimals: number = 2) {
if (!+bytes) return "0 Bytes"; if (!+bytes) return "0 Bytes"
const k = 1024; const k = 1024
const dm = decimals < 0 ? 0 : decimals; const dm = decimals < 0 ? 0 : decimals
const sizes = [ const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
"Bytes",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
} }

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,4 @@
export const countryCoordinates: Record< export const countryCoordinates: Record<string, { lat: number; lng: number; name: string }> = {
string,
{ lat: number; lng: number; name: string }
> = {
// 亚洲 // 亚洲
AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗 AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗
AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚 AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚
@ -208,4 +205,4 @@ export const countryCoordinates: Record<
EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉 EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉
ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚 ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚
ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦 ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦
}; }

View File

@ -1,4 +1,4 @@
import type { SVGProps } from "react"; import type { SVGProps } from "react"
export function GetFontLogoClass(platform: string): string { export function GetFontLogoClass(platform: string): string {
if ( if (
@ -47,24 +47,24 @@ export function GetFontLogoClass(platform: string): string {
"zorin", "zorin",
].indexOf(platform) > -1 ].indexOf(platform) > -1
) { ) {
return platform; return platform
} }
if (platform == "darwin") { if (platform == "darwin") {
return "apple"; return "apple"
} }
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) { if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "tux"; return "tux"
} }
if (platform == "amazon") { if (platform == "amazon") {
return "redhat"; return "redhat"
} }
if (platform == "arch") { if (platform == "arch") {
return "archlinux"; return "archlinux"
} }
if (platform.toLowerCase().includes("opensuse")) { if (platform.toLowerCase().includes("opensuse")) {
return "opensuse"; return "opensuse"
} }
return "tux"; return "tux"
} }
export function GetOsName(platform: string): string { export function GetOsName(platform: string): string {
@ -110,39 +110,33 @@ export function GetOsName(platform: string): string {
"zorin", "zorin",
].indexOf(platform) > -1 ].indexOf(platform) > -1
) { ) {
return platform.charAt(0).toUpperCase() + platform.slice(1); return platform.charAt(0).toUpperCase() + platform.slice(1)
} }
if (platform == "darwin") { if (platform == "darwin") {
return "macOS"; return "macOS"
} }
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) { if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "Linux"; return "Linux"
} }
if (platform == "amazon") { if (platform == "amazon") {
return "Redhat"; return "Redhat"
} }
if (platform == "arch") { if (platform == "arch") {
return "Archlinux"; return "Archlinux"
} }
if (platform.toLowerCase().includes("opensuse")) { if (platform.toLowerCase().includes("opensuse")) {
return "Opensuse"; return "Opensuse"
} }
return "Linux"; return "Linux"
} }
export function MageMicrosoftWindows(props: SVGProps<SVGSVGElement>) { export function MageMicrosoftWindows(props: SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path <path
fill="currentColor" fill="currentColor"
d="M2.75 7.189V2.865c0-.102 0-.115.115-.115h8.622c.128 0 .14 0 .14.128V11.5c0 .128 0 .128-.14.128H2.865c-.102 0-.115 0-.115-.116zM7.189 21.25H2.865c-.102 0-.115 0-.115-.116V12.59c0-.128 0-.128.128-.128h8.635c.102 0 .115 0 .115.115v8.57c0 .09 0 .103-.116.103zM21.25 7.189v4.31c0 .116 0 .116-.116.116h-8.557c-.102 0-.128 0-.128-.115V2.865c0-.09 0-.102.115-.102h8.48c.206 0 .206 0 .206.205zm-8.763 9.661v-4.273c0-.09 0-.115.103-.09h8.621c.026 0 0 .09 0 .142v8.518a.06.06 0 0 1-.017.06a.06.06 0 0 1-.06.017H12.54s-.09 0-.077-.09V16.85z" d="M2.75 7.189V2.865c0-.102 0-.115.115-.115h8.622c.128 0 .14 0 .14.128V11.5c0 .128 0 .128-.14.128H2.865c-.102 0-.115 0-.115-.116zM7.189 21.25H2.865c-.102 0-.115 0-.115-.116V12.59c0-.128 0-.128.128-.128h8.635c.102 0 .115 0 .115.115v8.57c0 .09 0 .103-.116.103zM21.25 7.189v4.31c0 .116 0 .116-.116.116h-8.557c-.102 0-.128 0-.128-.115V2.865c0-.09 0-.102.115-.102h8.48c.206 0 .206 0 .206.205zm-8.763 9.661v-4.273c0-.09 0-.115.103-.09h8.621c.026 0 0 .09 0 .142v8.518a.06.06 0 0 1-.017.06a.06.06 0 0 1-.06.017H12.54s-.09 0-.077-.09V16.85z"
></path> ></path>
</svg> </svg>
); )
} }

View File

@ -4,51 +4,49 @@ import {
ServerGroupResponse, ServerGroupResponse,
ServiceResponse, ServiceResponse,
SettingResponse, SettingResponse,
} from "@/types/nezha-api"; } from "@/types/nezha-api"
export const fetchServerGroup = async (): Promise<ServerGroupResponse> => { export const fetchServerGroup = async (): Promise<ServerGroupResponse> => {
const response = await fetch("/api/v1/server-group"); const response = await fetch("/api/v1/server-group")
const data = await response.json(); const data = await response.json()
if (data.error) { if (data.error) {
throw new Error(data.error); throw new Error(data.error)
} }
return data; return data
}; }
export const fetchLoginUser = async (): Promise<LoginUserResponse> => { export const fetchLoginUser = async (): Promise<LoginUserResponse> => {
const response = await fetch("/api/v1/profile"); const response = await fetch("/api/v1/profile")
const data = await response.json(); const data = await response.json()
if (data.error) { if (data.error) {
throw new Error(data.error); throw new Error(data.error)
} }
return data; return data
}; }
export const fetchMonitor = async ( export const fetchMonitor = async (server_id: number): Promise<MonitorResponse> => {
server_id: number, const response = await fetch(`/api/v1/service/${server_id}`)
): Promise<MonitorResponse> => { const data = await response.json()
const response = await fetch(`/api/v1/service/${server_id}`);
const data = await response.json();
if (data.error) { if (data.error) {
throw new Error(data.error); throw new Error(data.error)
} }
return data; return data
}; }
export const fetchService = async (): Promise<ServiceResponse> => { export const fetchService = async (): Promise<ServiceResponse> => {
const response = await fetch("/api/v1/service"); const response = await fetch("/api/v1/service")
const data = await response.json(); const data = await response.json()
if (data.error) { if (data.error) {
throw new Error(data.error); throw new Error(data.error)
} }
return data; return data
}; }
export const fetchSetting = async (): Promise<SettingResponse> => { export const fetchSetting = async (): Promise<SettingResponse> => {
const response = await fetch("/api/v1/setting"); const response = await fetch("/api/v1/setting")
const data = await response.json(); const data = await response.json()
if (data.error) { if (data.error) {
throw new Error(data.error); throw new Error(data.error)
} }
return data; return data
}; }

View File

@ -1,15 +1,15 @@
import { NezhaServer } from "@/types/nezha-api"; import { NezhaServer } from "@/types/nezha-api"
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs))
} }
export function formatNezhaInfo(now: number, serverInfo: NezhaServer) { export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
const lastActiveTime = serverInfo.last_active.startsWith("000") const lastActiveTime = serverInfo.last_active.startsWith("000")
? 0 ? 0
: parseISOTimestamp(serverInfo.last_active); : parseISOTimestamp(serverInfo.last_active)
return { return {
...serverInfo, ...serverInfo,
cpu: serverInfo.state.cpu || 0, cpu: serverInfo.state.cpu || 0,
@ -17,9 +17,7 @@ export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
process: serverInfo.state.process_count || 0, process: serverInfo.state.process_count || 0,
up: serverInfo.state.net_out_speed / 1024 / 1024 || 0, up: serverInfo.state.net_out_speed / 1024 / 1024 || 0,
down: serverInfo.state.net_in_speed / 1024 / 1024 || 0, down: serverInfo.state.net_in_speed / 1024 / 1024 || 0,
last_active_time_string: lastActiveTime last_active_time_string: lastActiveTime ? new Date(lastActiveTime).toLocaleString() : "",
? new Date(lastActiveTime).toLocaleString()
: "",
online: now - lastActiveTime <= 30000, online: now - lastActiveTime <= 30000,
uptime: serverInfo.state.uptime || 0, uptime: serverInfo.state.uptime || 0,
version: serverInfo.host.version || null, version: serverInfo.host.version || null,
@ -45,88 +43,88 @@ export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
load_5: serverInfo.state.load_5?.toFixed(2) || 0.0, load_5: serverInfo.state.load_5?.toFixed(2) || 0.0,
load_15: serverInfo.state.load_15?.toFixed(2) || 0.0, load_15: serverInfo.state.load_15?.toFixed(2) || 0.0,
public_note: handlePublicNote(serverInfo.id, serverInfo.public_note || ""), public_note: handlePublicNote(serverInfo.id, serverInfo.public_note || ""),
}; }
} }
export function getDaysBetweenDates(date1: string, date2: string): number { export function getDaysBetweenDates(date1: string, date2: string): number {
const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数 const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
const firstDate = new Date(date1); const firstDate = new Date(date1)
const secondDate = new Date(date2); const secondDate = new Date(date2)
// 计算两个日期之间的天数差异 // 计算两个日期之间的天数差异
return Math.round((firstDate.getTime() - secondDate.getTime()) / oneDay); return Math.round((firstDate.getTime() - secondDate.getTime()) / oneDay)
} }
export const fetcher = (url: string) => export const fetcher = (url: string) =>
fetch(url) fetch(url)
.then((res) => { .then((res) => {
if (!res.ok) { if (!res.ok) {
throw new Error(res.statusText); throw new Error(res.statusText)
} }
return res.json(); return res.json()
}) })
.then((data) => data.data) .then((data) => data.data)
.catch((err) => { .catch((err) => {
console.error(err); console.error(err)
throw err; throw err
}); })
export const nezhaFetcher = async (url: string) => { export const nezhaFetcher = async (url: string) => {
const res = await fetch(url); const res = await fetch(url)
if (!res.ok) { if (!res.ok) {
const error = new Error("An error occurred while fetching the data."); const error = new Error("An error occurred while fetching the data.")
// @ts-expect-error - res.json() returns a Promise<any> // @ts-expect-error - res.json() returns a Promise<any>
error.info = await res.json(); error.info = await res.json()
// @ts-expect-error - res.status is a number // @ts-expect-error - res.status is a number
error.status = res.status; error.status = res.status
throw error; throw error
} }
return res.json(); return res.json()
}; }
export function parseISOTimestamp(isoString: string): number { export function parseISOTimestamp(isoString: string): number {
return new Date(isoString).getTime(); return new Date(isoString).getTime()
} }
export function formatRelativeTime(timestamp: number): string { export function formatRelativeTime(timestamp: number): string {
const now = Date.now(); const now = Date.now()
const diff = now - timestamp; const diff = now - timestamp
const hours = Math.floor(diff / (1000 * 60 * 60)); const hours = Math.floor(diff / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((diff % (1000 * 60)) / 1000); const seconds = Math.floor((diff % (1000 * 60)) / 1000)
if (hours > 24) { if (hours > 24) {
const days = Math.floor(hours / 24); const days = Math.floor(hours / 24)
return `${days}d`; return `${days}d`
} else if (hours > 0) { } else if (hours > 0) {
return `${hours}h`; return `${hours}h`
} else if (minutes > 0) { } else if (minutes > 0) {
return `${minutes}m`; return `${minutes}m`
} else if (seconds >= 0) { } else if (seconds >= 0) {
return `${seconds}s`; return `${seconds}s`
} }
return "0s"; return "0s"
} }
export function formatTime(timestamp: number): string { export function formatTime(timestamp: number): string {
const date = new Date(timestamp); const date = new Date(timestamp)
const year = date.getFullYear(); const year = date.getFullYear()
const month = date.getMonth() + 1; const month = date.getMonth() + 1
const day = date.getDate(); const day = date.getDate()
const hours = date.getHours().toString().padStart(2, "0"); const hours = date.getHours().toString().padStart(2, "0")
const minutes = date.getMinutes().toString().padStart(2, "0"); const minutes = date.getMinutes().toString().padStart(2, "0")
const seconds = date.getSeconds().toString().padStart(2, "0"); const seconds = date.getSeconds().toString().padStart(2, "0")
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
} }
interface BillingData { interface BillingData {
startDate: string; startDate: string
endDate: string; endDate: string
autoRenewal: string; autoRenewal: string
cycle: string; cycle: string
amount: string; amount: string
} }
// interface PlanData { // interface PlanData {
@ -140,16 +138,16 @@ interface BillingData {
// } // }
interface PublicNoteData { interface PublicNoteData {
billingDataMod: BillingData; billingDataMod: BillingData
// planDataMod: PlanData; // planDataMod: PlanData;
} }
export function parsePublicNote(publicNote: string): PublicNoteData | null { export function parsePublicNote(publicNote: string): PublicNoteData | null {
try { try {
if (!publicNote) { if (!publicNote) {
return null; return null
} }
const data = JSON.parse(publicNote); const data = JSON.parse(publicNote)
return { return {
billingDataMod: { billingDataMod: {
startDate: data.billingDataMod.startDate || "", startDate: data.billingDataMod.startDate || "",
@ -167,26 +165,26 @@ export function parsePublicNote(publicNote: string): PublicNoteData | null {
// networkRoute: data.planDataMod.networkRoute || "", // networkRoute: data.planDataMod.networkRoute || "",
// extra: data.planDataMod.extra || "", // extra: data.planDataMod.extra || "",
// }, // },
}; }
} catch (error) { } catch (error) {
console.error("Error parsing public note:", error); console.error("Error parsing public note:", error)
return null; return null
} }
} }
// Function to handle public_note with sessionStorage // Function to handle public_note with sessionStorage
export function handlePublicNote(serverId: number, publicNote: string): string { export function handlePublicNote(serverId: number, publicNote: string): string {
const storageKey = `server_${serverId}_public_note`; const storageKey = `server_${serverId}_public_note`
const storedNote = sessionStorage.getItem(storageKey); const storedNote = sessionStorage.getItem(storageKey)
if (!publicNote && storedNote) { if (!publicNote && storedNote) {
return storedNote; return storedNote
} }
if (publicNote) { if (publicNote) {
sessionStorage.setItem(storageKey, publicNote); sessionStorage.setItem(storageKey, publicNote)
return publicNote; return publicNote
} }
return ""; return ""
} }

View File

@ -1,19 +1,20 @@
import React from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import ReactDOM from "react-dom/client"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import App from "./App"; import React from "react"
import "./index.css"; import ReactDOM from "react-dom/client"
import "./i18n"; import { Toaster } from "sonner"
import { ThemeProvider } from "./components/ThemeProvider";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "sonner";
import { MotionProvider } from "./components/motion/motion-provider";
import { WebSocketProvider } from "./context/websocket-provider";
import { StatusProvider } from "./context/status-provider";
import { FilterProvider } from "./context/network-filter-context";
import { TooltipProvider } from "./context/tooltip-provider";
const queryClient = new QueryClient(); import App from "./App"
import { ThemeProvider } from "./components/ThemeProvider"
import { MotionProvider } from "./components/motion/motion-provider"
import { FilterProvider } from "./context/network-filter-context"
import { StatusProvider } from "./context/status-provider"
import { TooltipProvider } from "./context/tooltip-provider"
import { WebSocketProvider } from "./context/websocket-provider"
import "./i18n"
import "./index.css"
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
@ -45,4 +46,4 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
</ThemeProvider> </ThemeProvider>
</MotionProvider> </MotionProvider>
</React.StrictMode>, </React.StrictMode>,
); )

View File

@ -1,23 +1,21 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"
import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"
interface ErrorPageProps { interface ErrorPageProps {
code?: string | number; code?: string | number
message?: string; message?: string
} }
export default function ErrorPage({ code = "500", message }: ErrorPageProps) { export default function ErrorPage({ code = "500", message }: ErrorPageProps) {
const navigate = useNavigate(); const navigate = useNavigate()
const { t } = useTranslation(); const { t } = useTranslation()
return ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<h1 className="text-4xl font-semibold">{code}</h1> <h1 className="text-4xl font-semibold">{code}</h1>
<p className="text-xl text-muted-foreground"> <p className="text-xl text-muted-foreground">{message || t("error.somethingWentWrong")}</p>
{message || t("error.somethingWentWrong")}
</p>
<div className="flex gap-2"> <div className="flex gap-2">
<Button onClick={() => window.location.reload()} variant="outline"> <Button onClick={() => window.location.reload()} variant="outline">
{t("error.tryAgain")} {t("error.tryAgain")}
@ -28,5 +26,5 @@ export default function ErrorPage({ code = "500", message }: ErrorPageProps) {
</div> </div>
</div> </div>
</div> </div>
); )
} }

View File

@ -1,22 +1,20 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"
import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"
export default function NotFound() { export default function NotFound() {
const navigate = useNavigate(); const navigate = useNavigate()
const { t } = useTranslation(); const { t } = useTranslation()
return ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<h1 className="text-4xl font-semibold">404</h1> <h1 className="text-4xl font-semibold">404</h1>
<p className="text-xl text-muted-foreground"> <p className="text-xl text-muted-foreground">{t("error.pageNotFound")}</p>
{t("error.pageNotFound")}
</p>
<Button onClick={() => navigate("/")} className="mt-2"> <Button onClick={() => navigate("/")} className="mt-2">
{t("error.backToHome")} {t("error.backToHome")}
</Button> </Button>
</div> </div>
</div> </div>
); )
} }

View File

@ -1,67 +1,60 @@
import { NezhaWebsocketResponse } from "@/types/nezha-api"; import GlobalMap from "@/components/GlobalMap"
import ServerCard from "@/components/ServerCard"; import GroupSwitch from "@/components/GroupSwitch"
import { cn, formatNezhaInfo } from "@/lib/utils"; import ServerCard from "@/components/ServerCard"
import ServerOverview from "@/components/ServerOverview"; import ServerCardInline from "@/components/ServerCardInline"
import { useEffect, useState } from "react"; import ServerOverview from "@/components/ServerOverview"
import { toast } from "sonner"; import { ServiceTracker } from "@/components/ServiceTracker"
import { useQuery } from "@tanstack/react-query"; import { Loader } from "@/components/loading/Loader"
import { fetchServerGroup } from "@/lib/nezha-api"; import useFilter from "@/hooks/use-filter"
import GroupSwitch from "@/components/GroupSwitch"; import { useStatus } from "@/hooks/use-status"
import { ServerGroup } from "@/types/nezha-api"; import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { useWebSocketContext } from "@/hooks/use-websocket-context"; import { fetchServerGroup } from "@/lib/nezha-api"
import { useTranslation } from "react-i18next"; import { cn, formatNezhaInfo } from "@/lib/utils"
import { import { NezhaWebsocketResponse } from "@/types/nezha-api"
ChartBarSquareIcon, import { ServerGroup } from "@/types/nezha-api"
ViewColumnsIcon, import { ChartBarSquareIcon, MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"
MapIcon, import { useQuery } from "@tanstack/react-query"
} from "@heroicons/react/20/solid"; import { useEffect, useState } from "react"
import { ServiceTracker } from "@/components/ServiceTracker"; import { useTranslation } from "react-i18next"
import ServerCardInline from "@/components/ServerCardInline"; import { toast } from "sonner"
import { Loader } from "@/components/loading/Loader";
import GlobalMap from "@/components/GlobalMap";
import { useStatus } from "@/hooks/use-status";
import useFilter from "@/hooks/use-filter";
export default function Servers() { export default function Servers() {
const { t } = useTranslation(); const { t } = useTranslation()
const { data: groupData } = useQuery({ const { data: groupData } = useQuery({
queryKey: ["server-group"], queryKey: ["server-group"],
queryFn: () => fetchServerGroup(), queryFn: () => fetchServerGroup(),
}); })
const { lastMessage, connected } = useWebSocketContext(); const { lastMessage, connected } = useWebSocketContext()
const { status } = useStatus(); const { status } = useStatus()
const { filter } = useFilter(); const { filter } = useFilter()
const [showServices, setShowServices] = useState<string>("0"); const [showServices, setShowServices] = useState<string>("0")
const [showMap, setShowMap] = useState<string>("0"); const [showMap, setShowMap] = useState<string>("0")
const [inline, setInline] = useState<string>("0"); const [inline, setInline] = useState<string>("0")
const [currentGroup, setCurrentGroup] = useState<string>("All"); const [currentGroup, setCurrentGroup] = useState<string>("All")
useEffect(() => { useEffect(() => {
const showServicesState = localStorage.getItem("showServices"); const showServicesState = localStorage.getItem("showServices")
if (showServicesState !== null) { if (showServicesState !== null) {
setShowServices(showServicesState); setShowServices(showServicesState)
} }
}, []); }, [])
useEffect(() => { useEffect(() => {
const inlineState = localStorage.getItem("inline"); const inlineState = localStorage.getItem("inline")
if (inlineState !== null) { if (inlineState !== null) {
setInline(inlineState); setInline(inlineState)
} }
}, []); }, [])
const groupTabs = [ const groupTabs = ["All", ...(groupData?.data?.map((item: ServerGroup) => item.group.name) || [])]
"All",
...(groupData?.data?.map((item: ServerGroup) => item.group.name) || []),
];
useEffect(() => { useEffect(() => {
const hasShownToast = sessionStorage.getItem("websocket-connected-toast"); const hasShownToast = sessionStorage.getItem("websocket-connected-toast")
if (connected && !hasShownToast) { if (connected && !hasShownToast) {
toast.success(t("info.websocketConnected")); toast.success(t("info.websocketConnected"))
sessionStorage.setItem("websocket-connected-toast", "true"); sessionStorage.setItem("websocket-connected-toast", "true")
} }
}, [connected]); }, [connected])
if (!connected) { if (!connected) {
return ( return (
@ -71,42 +64,37 @@ export default function Servers() {
{t("info.websocketConnecting")} {t("info.websocketConnecting")}
</div> </div>
</div> </div>
); )
} }
const nezhaWsData = lastMessage const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
if (!nezhaWsData) { if (!nezhaWsData) {
return ( return (
<div className="flex flex-col items-center justify-center "> <div className="flex flex-col items-center justify-center ">
<p className="font-semibold text-sm">{t("info.processing")}</p> <p className="font-semibold text-sm">{t("info.processing")}</p>
</div> </div>
); )
} }
let filteredServers = let filteredServers =
nezhaWsData?.servers?.filter((server) => { nezhaWsData?.servers?.filter((server) => {
if (currentGroup === "All") return true; if (currentGroup === "All") return true
const group = groupData?.data?.find( const group = groupData?.data?.find(
(g: ServerGroup) => (g: ServerGroup) =>
g.group.name === currentGroup && g.group.name === currentGroup &&
Array.isArray(g.servers) && Array.isArray(g.servers) &&
g.servers.includes(server.id), g.servers.includes(server.id),
); )
return !!group; return !!group
}) || []; }) || []
const totalServers = filteredServers.length || 0; const totalServers = filteredServers.length || 0
const onlineServers = const onlineServers =
filteredServers.filter( filteredServers.filter((server) => formatNezhaInfo(nezhaWsData.now, server).online)?.length || 0
(server) => formatNezhaInfo(nezhaWsData.now, server).online,
)?.length || 0;
const offlineServers = const offlineServers =
filteredServers.filter( filteredServers.filter((server) => !formatNezhaInfo(nezhaWsData.now, server).online)?.length ||
(server) => !formatNezhaInfo(nezhaWsData.now, server).online, 0
)?.length || 0;
const up = const up =
filteredServers.reduce( filteredServers.reduce(
(total, server) => (total, server) =>
@ -114,7 +102,7 @@ export default function Servers() {
? total + (server.state?.net_out_transfer ?? 0) ? total + (server.state?.net_out_transfer ?? 0)
: total, : total,
0, 0,
) || 0; ) || 0
const down = const down =
filteredServers.reduce( filteredServers.reduce(
(total, server) => (total, server) =>
@ -122,7 +110,7 @@ export default function Servers() {
? total + (server.state?.net_in_transfer ?? 0) ? total + (server.state?.net_in_transfer ?? 0)
: total, : total,
0, 0,
) || 0; ) || 0
const upSpeed = const upSpeed =
filteredServers.reduce( filteredServers.reduce(
@ -131,7 +119,7 @@ export default function Servers() {
? total + (server.state?.net_out_speed ?? 0) ? total + (server.state?.net_out_speed ?? 0)
: total, : total,
0, 0,
) || 0; ) || 0
const downSpeed = const downSpeed =
filteredServers.reduce( filteredServers.reduce(
(total, server) => (total, server) =>
@ -139,43 +127,33 @@ export default function Servers() {
? total + (server.state?.net_in_speed ?? 0) ? total + (server.state?.net_in_speed ?? 0)
: total, : total,
0, 0,
) || 0; ) || 0
filteredServers = filteredServers =
status === "all" status === "all"
? filteredServers ? filteredServers
: filteredServers.filter((server) => : filteredServers.filter((server) =>
[status].includes( [status].includes(formatNezhaInfo(nezhaWsData.now, server).online ? "online" : "offline"),
formatNezhaInfo(nezhaWsData.now, server).online )
? "online"
: "offline",
),
);
if (filter) { if (filter) {
filteredServers.sort((a, b) => { filteredServers.sort((a, b) => {
if ( if (!formatNezhaInfo(nezhaWsData.now, a).online && formatNezhaInfo(nezhaWsData.now, b).online)
!formatNezhaInfo(nezhaWsData.now, a).online && return 1
formatNezhaInfo(nezhaWsData.now, b).online if (formatNezhaInfo(nezhaWsData.now, a).online && !formatNezhaInfo(nezhaWsData.now, b).online)
) return -1
return 1;
if (
formatNezhaInfo(nezhaWsData.now, a).online &&
!formatNezhaInfo(nezhaWsData.now, b).online
)
return -1;
if ( if (
!formatNezhaInfo(nezhaWsData.now, a).online && !formatNezhaInfo(nezhaWsData.now, a).online &&
!formatNezhaInfo(nezhaWsData.now, b).online !formatNezhaInfo(nezhaWsData.now, b).online
) )
return 0; return 0
return ( return (
formatNezhaInfo(nezhaWsData.now, b).state.net_in_speed + formatNezhaInfo(nezhaWsData.now, b).state.net_in_speed +
formatNezhaInfo(nezhaWsData.now, b).state.net_out_speed - formatNezhaInfo(nezhaWsData.now, b).state.net_out_speed -
(formatNezhaInfo(nezhaWsData.now, a).state.net_in_speed + (formatNezhaInfo(nezhaWsData.now, a).state.net_in_speed +
formatNezhaInfo(nezhaWsData.now, a).state.net_out_speed) formatNezhaInfo(nezhaWsData.now, a).state.net_out_speed)
); )
}); })
} }
return ( return (
@ -192,13 +170,12 @@ export default function Servers() {
<section className="flex mt-6 items-center gap-2 w-full overflow-hidden"> <section className="flex mt-6 items-center gap-2 w-full overflow-hidden">
<button <button
onClick={() => { onClick={() => {
setShowMap(showMap === "0" ? "1" : "0"); setShowMap(showMap === "0" ? "1" : "0")
}} }}
className={cn( className={cn(
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ", "rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
{ {
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": "shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": showMap === "1",
showMap === "1",
}, },
)} )}
> >
@ -206,17 +183,13 @@ export default function Servers() {
</button> </button>
<button <button
onClick={() => { onClick={() => {
setShowServices(showServices === "0" ? "1" : "0"); setShowServices(showServices === "0" ? "1" : "0")
localStorage.setItem( localStorage.setItem("showServices", showServices === "0" ? "1" : "0")
"showServices",
showServices === "0" ? "1" : "0",
);
}} }}
className={cn( className={cn(
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ", "rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
{ {
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": "shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": showServices === "1",
showServices === "1",
}, },
)} )}
> >
@ -224,54 +197,38 @@ export default function Servers() {
</button> </button>
<button <button
onClick={() => { onClick={() => {
setInline(inline === "0" ? "1" : "0"); setInline(inline === "0" ? "1" : "0")
localStorage.setItem("inline", inline === "0" ? "1" : "0"); localStorage.setItem("inline", inline === "0" ? "1" : "0")
}} }}
className={cn( className={cn(
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ", "rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
{ {
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": "shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": inline === "1",
inline === "1",
}, },
)} )}
> >
<ViewColumnsIcon className="size-[13px]" /> <ViewColumnsIcon className="size-[13px]" />
</button> </button>
<GroupSwitch <GroupSwitch tabs={groupTabs} currentTab={currentGroup} setCurrentTab={setCurrentGroup} />
tabs={groupTabs}
currentTab={currentGroup}
setCurrentTab={setCurrentGroup}
/>
</section> </section>
{showMap === "1" && ( {showMap === "1" && (
<GlobalMap <GlobalMap now={nezhaWsData.now} serverList={nezhaWsData?.servers || []} />
now={nezhaWsData.now}
serverList={nezhaWsData?.servers || []}
/>
)} )}
{showServices === "1" && <ServiceTracker />} {showServices === "1" && <ServiceTracker />}
{inline === "1" && ( {inline === "1" && (
<section className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6"> <section className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6">
{filteredServers.map((serverInfo) => ( {filteredServers.map((serverInfo) => (
<ServerCardInline <ServerCardInline now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} />
now={nezhaWsData.now}
key={serverInfo.id}
serverInfo={serverInfo}
/>
))} ))}
</section> </section>
)} )}
{inline === "0" && ( {inline === "0" && (
<section className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6"> <section className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6">
{filteredServers.map((serverInfo) => ( {filteredServers.map((serverInfo) => (
<ServerCard <ServerCard now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} />
now={nezhaWsData.now}
key={serverInfo.id}
serverInfo={serverInfo}
/>
))} ))}
</section> </section>
)} )}
</div> </div>
); )
} }

View File

@ -1,26 +1,26 @@
import { NetworkChart } from "@/components/NetworkChart"; import { NetworkChart } from "@/components/NetworkChart"
import ServerDetailChart from "@/components/ServerDetailChart"; import ServerDetailChart from "@/components/ServerDetailChart"
import ServerDetailOverview from "@/components/ServerDetailOverview"; import ServerDetailOverview from "@/components/ServerDetailOverview"
import TabSwitch from "@/components/TabSwitch"; import TabSwitch from "@/components/TabSwitch"
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator"
import { useState, useEffect } from "react"; import { useEffect, useState } from "react"
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom"
export default function ServerDetail() { export default function ServerDetail() {
const navigate = useNavigate(); const navigate = useNavigate()
useEffect(() => { useEffect(() => {
window.scrollTo({ top: 0, left: 0, behavior: "instant" }); window.scrollTo({ top: 0, left: 0, behavior: "instant" })
}, []); }, [])
const tabs = ["Detail", "Network"]; const tabs = ["Detail", "Network"]
const [currentTab, setCurrentTab] = useState(tabs[0]); const [currentTab, setCurrentTab] = useState(tabs[0])
const { id: server_id } = useParams(); const { id: server_id } = useParams()
if (!server_id) { if (!server_id) {
navigate("/404"); navigate("/404")
return null; return null
} }
return ( return (
@ -29,11 +29,7 @@ export default function ServerDetail() {
<section className="flex items-center my-2 w-full"> <section className="flex items-center my-2 w-full">
<Separator className="flex-1" /> <Separator className="flex-1" />
<div className="flex justify-center w-full max-w-[200px]"> <div className="flex justify-center w-full max-w-[200px]">
<TabSwitch <TabSwitch tabs={tabs} currentTab={currentTab} setCurrentTab={setCurrentTab} />
tabs={tabs}
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
</div> </div>
<Separator className="flex-1" /> <Separator className="flex-1" />
</section> </section>
@ -41,11 +37,8 @@ export default function ServerDetail() {
<ServerDetailChart server_id={server_id} /> <ServerDetailChart server_id={server_id} />
</div> </div>
<div style={{ display: currentTab === tabs[1] ? "block" : "none" }}> <div style={{ display: currentTab === tabs[1] ? "block" : "none" }}>
<NetworkChart <NetworkChart server_id={Number(server_id)} show={currentTab === tabs[1]} />
server_id={Number(server_id)}
show={currentTab === tabs[1]}
/>
</div> </div>
</div> </div>
); )
} }

4
src/types/css.d.ts vendored
View File

@ -1,4 +1,4 @@
declare module "*.css" { declare module "*.css" {
const css: { [key: string]: string }; const css: { [key: string]: string }
export default css; export default css
} }

View File

@ -1,151 +1,151 @@
export interface NezhaWebsocketResponse { export interface NezhaWebsocketResponse {
now: number; now: number
servers: NezhaServer[]; servers: NezhaServer[]
} }
export interface NezhaServer { export interface NezhaServer {
id: number; id: number
name: string; name: string
public_note: string; public_note: string
last_active: string; last_active: string
country_code: string; country_code: string
host: NezhaServerHost; host: NezhaServerHost
state: NezhaServerStatus; state: NezhaServerStatus
} }
export interface NezhaServerHost { export interface NezhaServerHost {
platform: string; platform: string
platform_version: string; platform_version: string
cpu: string[]; cpu: string[]
gpu: string[]; gpu: string[]
mem_total: number; mem_total: number
disk_total: number; disk_total: number
swap_total: number; swap_total: number
arch: string; arch: string
boot_time: number; boot_time: number
version: string; version: string
} }
export interface NezhaServerStatus { export interface NezhaServerStatus {
cpu: number; cpu: number
mem_used: number; mem_used: number
swap_used: number; swap_used: number
disk_used: number; disk_used: number
net_in_transfer: number; net_in_transfer: number
net_out_transfer: number; net_out_transfer: number
net_in_speed: number; net_in_speed: number
net_out_speed: number; net_out_speed: number
uptime: number; uptime: number
load_1: number; load_1: number
load_5: number; load_5: number
load_15: number; load_15: number
tcp_conn_count: number; tcp_conn_count: number
udp_conn_count: number; udp_conn_count: number
process_count: number; process_count: number
temperatures: temperature[]; temperatures: temperature[]
gpu: number[]; gpu: number[]
} }
interface temperature { interface temperature {
Name: string; Name: string
Temperature: number; Temperature: number
} }
export interface ServerGroupResponse { export interface ServerGroupResponse {
success: boolean; success: boolean
data: ServerGroup[]; data: ServerGroup[]
} }
export interface ServerGroup { export interface ServerGroup {
group: { group: {
id: number; id: number
created_at: string; created_at: string
updated_at: string; updated_at: string
name: string; name: string
}; }
servers: number[]; servers: number[]
} }
export interface LoginUserResponse { export interface LoginUserResponse {
success: boolean; success: boolean
data: { data: {
id: number; id: number
username: string; username: string
password: string; password: string
created_at: string; created_at: string
updated_at: string; updated_at: string
}; }
} }
export interface MonitorResponse { export interface MonitorResponse {
success: boolean; success: boolean
data: NezhaMonitor[]; data: NezhaMonitor[]
} }
export type ServerMonitorChart = { export type ServerMonitorChart = {
[key: string]: { [key: string]: {
created_at: number; created_at: number
avg_delay: number; avg_delay: number
}[]; }[]
}; }
export interface NezhaMonitor { export interface NezhaMonitor {
monitor_id: number; monitor_id: number
monitor_name: string; monitor_name: string
server_id: number; server_id: number
server_name: string; server_name: string
created_at: number[]; created_at: number[]
avg_delay: number[]; avg_delay: number[]
} }
export interface ServiceResponse { export interface ServiceResponse {
success: boolean; success: boolean
data: { data: {
services: { services: {
[key: string]: ServiceData; [key: string]: ServiceData
}; }
cycle_transfer_stats: CycleTransferStats; cycle_transfer_stats: CycleTransferStats
}; }
} }
export interface ServiceData { export interface ServiceData {
service_name: string; service_name: string
current_up: number; current_up: number
current_down: number; current_down: number
total_up: number; total_up: number
total_down: number; total_down: number
delay: number[]; delay: number[]
up: number[]; up: number[]
down: number[]; down: number[]
} }
export interface CycleTransferStats { export interface CycleTransferStats {
[key: string]: CycleTransferData; [key: string]: CycleTransferData
} }
export interface CycleTransferData { export interface CycleTransferData {
name: string; name: string
from: string; from: string
to: string; to: string
max: number; max: number
min: number; min: number
server_name: { server_name: {
[key: string]: string; [key: string]: string
}; }
transfer: { transfer: {
[key: string]: number; [key: string]: number
}; }
next_update: { next_update: {
[key: string]: string; [key: string]: string
}; }
} }
export interface SettingResponse { export interface SettingResponse {
success: boolean; success: boolean
data: { data: {
language: string; language: string
site_name: string; site_name: string
custom_code: string; custom_code: string
version: string; version: string
}; }
} }

View File

@ -56,4 +56,4 @@ module.exports = {
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [require("tailwindcss-animate")],
}; }

View File

@ -1,9 +1,6 @@
{ {
"files": [], "files": [],
"references": [ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {

View File

@ -1,17 +1,17 @@
import path from "path"; import react from "@vitejs/plugin-react-swc"
import { defineConfig } from "vite"; import { execSync } from "child_process"
import react from "@vitejs/plugin-react-swc"; import path from "path"
import { execSync } from "child_process"; import { defineConfig } from "vite"
// Get git commit hash // Get git commit hash
const getGitHash = () => { const getGitHash = () => {
try { try {
return execSync("git rev-parse --short HEAD").toString().trim(); return execSync("git rev-parse --short HEAD").toString().trim()
} catch (e) { } catch (e) {
console.log(e); console.log(e)
return "unknown"; return "unknown"
} }
}; }
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
@ -50,15 +50,11 @@ export default defineConfig({
assetFileNames: `assets/[name].[hash].[ext]`, assetFileNames: `assets/[name].[hash].[ext]`,
manualChunks(id) { manualChunks(id) {
if (id.includes("node_modules")) { if (id.includes("node_modules")) {
return id return id.toString().split("node_modules/")[1].split("/")[0].toString()
.toString()
.split("node_modules/")[1]
.split("/")[0]
.toString();
} }
}, },
}, },
}, },
chunkSizeWarningLimit: 1500, chunkSizeWarningLimit: 1500,
}, },
}); })