mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 01:21:56 +08:00
fix: prettier config
This commit is contained in:
parent
1483ce56fa
commit
9a2f3ea8e6
12
.prettierrc.json
Normal file
12
.prettierrc.json
Normal 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"]
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
import js from "@eslint/js"
|
||||
import reactHooks from "eslint-plugin-react-hooks"
|
||||
import reactRefresh from "eslint-plugin-react-refresh"
|
||||
import globals from "globals"
|
||||
import tseslint from "typescript-eslint"
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
@ -19,12 +19,9 @@ export default tseslint.config(
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
|
61
index.html
61
index.html
@ -4,16 +4,14 @@
|
||||
<script>
|
||||
// 在页面渲染前就执行主题初始化
|
||||
try {
|
||||
const storageKey = "vite-ui-theme";
|
||||
let theme = localStorage.getItem(storageKey);
|
||||
const storageKey = "vite-ui-theme"
|
||||
let theme = localStorage.getItem(storageKey)
|
||||
if (theme === "system" || !theme) {
|
||||
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
||||
}
|
||||
document.documentElement.classList.add(theme);
|
||||
document.documentElement.classList.add(theme)
|
||||
} catch (e) {
|
||||
document.documentElement.classList.add("light");
|
||||
document.documentElement.classList.add("light")
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@ -65,53 +63,48 @@
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
const storageKey = "vite-ui-theme";
|
||||
const theme = localStorage.getItem(storageKey) || "system";
|
||||
const root = document.documentElement;
|
||||
;(function () {
|
||||
const storageKey = "vite-ui-theme"
|
||||
const theme = localStorage.getItem(storageKey) || "system"
|
||||
const root = document.documentElement
|
||||
|
||||
function updateThemeColor(isDark) {
|
||||
const themeColor = isDark ? "#242424" : "#fafafa";
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute("content", themeColor);
|
||||
const themeColor = isDark ? "#242424" : "#fafafa"
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
||||
}
|
||||
|
||||
function setTheme(newTheme) {
|
||||
root.classList.remove("light", "dark");
|
||||
root.classList.add(newTheme);
|
||||
updateThemeColor(newTheme === "dark");
|
||||
root.classList.remove("light", "dark")
|
||||
root.classList.add(newTheme)
|
||||
updateThemeColor(newTheme === "dark")
|
||||
}
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
setTheme(systemTheme);
|
||||
: "light"
|
||||
setTheme(systemTheme)
|
||||
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", (e) => {
|
||||
setTheme(e.matches ? "dark" : "light");
|
||||
});
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
||||
setTheme(e.matches ? "dark" : "light")
|
||||
})
|
||||
} else {
|
||||
setTheme(theme);
|
||||
setTheme(theme)
|
||||
}
|
||||
|
||||
// Add loaded class after React has mounted
|
||||
window.addEventListener("load", () => {
|
||||
const root = document.getElementById("root");
|
||||
const root = document.getElementById("root")
|
||||
if (root) {
|
||||
// 使用 RAF 确保在下一帧渲染
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
root.classList.add("loaded");
|
||||
});
|
||||
});
|
||||
root.classList.add("loaded")
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
})();
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/apple-touch-icon.png" />
|
||||
|
@ -23,6 +23,7 @@
|
||||
"@tanstack/react-query": "^5.62.7",
|
||||
"@tanstack/react-query-devtools": "^5.62.7",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||
"@types/d3-geo": "^3.1.0",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@ -33,6 +34,7 @@
|
||||
"i18next": "^24.1.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"luxon": "^3.5.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-i18next": "^15.1.4",
|
||||
|
@ -3,4 +3,4 @@ export default {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
18
public/sw.js
18
public/sw.js
@ -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));
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
const { execSync } = require("child_process");
|
||||
const { execSync } = require("child_process")
|
||||
|
||||
// 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
|
||||
console.log(gitHash);
|
||||
console.log(gitHash)
|
||||
|
23
src/App.tsx
23
src/App.tsx
@ -1,11 +1,12 @@
|
||||
import React from "react";
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import Header from "./components/Header";
|
||||
import Footer from "./components/Footer";
|
||||
import Server from "./pages/Server";
|
||||
import ServerDetail from "./pages/ServerDetail";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import ErrorPage from "./pages/ErrorPage";
|
||||
import React from "react"
|
||||
import { Route, BrowserRouter as Router, Routes } from "react-router-dom"
|
||||
|
||||
import Footer from "./components/Footer"
|
||||
import Header from "./components/Header"
|
||||
import ErrorPage from "./pages/ErrorPage"
|
||||
import NotFound from "./pages/NotFound"
|
||||
import Server from "./pages/Server"
|
||||
import ServerDetail from "./pages/ServerDetail"
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
@ -23,7 +24,7 @@ const App: React.FC = () => {
|
||||
</main>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React from "react";
|
||||
import { CycleTransferStats } from "@/types/nezha-api";
|
||||
import { CycleTransferStatsClient } from "./CycleTransferStatsClient";
|
||||
import { CycleTransferStats } from "@/types/nezha-api"
|
||||
import React from "react"
|
||||
|
||||
import { CycleTransferStatsClient } from "./CycleTransferStatsClient"
|
||||
|
||||
interface CycleTransferStatsProps {
|
||||
cycleStats: CycleTransferStats;
|
||||
className?: string;
|
||||
cycleStats: CycleTransferStats
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({
|
||||
@ -15,41 +16,39 @@ export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-4">
|
||||
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
|
||||
if (!cycleData.server_name) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return Object.entries(cycleData.server_name).map(
|
||||
([serverId, serverName]) => {
|
||||
const transfer = cycleData.transfer?.[serverId] || 0;
|
||||
const nextUpdate = cycleData.next_update?.[serverId];
|
||||
return Object.entries(cycleData.server_name).map(([serverId, serverName]) => {
|
||||
const transfer = cycleData.transfer?.[serverId] || 0
|
||||
const nextUpdate = cycleData.next_update?.[serverId]
|
||||
|
||||
if (!transfer && !nextUpdate) {
|
||||
return null;
|
||||
}
|
||||
if (!transfer && !nextUpdate) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<CycleTransferStatsClient
|
||||
key={`${cycleId}-${serverId}`}
|
||||
name={cycleData.name}
|
||||
from={cycleData.from}
|
||||
to={cycleData.to}
|
||||
max={cycleData.max}
|
||||
serverStats={[
|
||||
{
|
||||
serverId,
|
||||
serverName,
|
||||
transfer,
|
||||
nextUpdate: nextUpdate || "",
|
||||
},
|
||||
]}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
return (
|
||||
<CycleTransferStatsClient
|
||||
key={`${cycleId}-${serverId}`}
|
||||
name={cycleData.name}
|
||||
from={cycleData.from}
|
||||
to={cycleData.to}
|
||||
max={cycleData.max}
|
||||
serverStats={[
|
||||
{
|
||||
serverId,
|
||||
serverName,
|
||||
transfer,
|
||||
nextUpdate: nextUpdate || "",
|
||||
},
|
||||
]}
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default CycleTransferStatsCard;
|
||||
export default CycleTransferStatsCard
|
||||
|
@ -1,28 +1,34 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar";
|
||||
import { CircleStackIcon } from "@heroicons/react/24/outline";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CircleStackIcon } from "@heroicons/react/24/outline"
|
||||
import React from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar"
|
||||
|
||||
interface CycleTransferStatsClientProps {
|
||||
name: string;
|
||||
from: string;
|
||||
to: string;
|
||||
max: number;
|
||||
name: string
|
||||
from: string
|
||||
to: string
|
||||
max: number
|
||||
serverStats: Array<{
|
||||
serverId: string;
|
||||
serverName: string;
|
||||
transfer: number;
|
||||
nextUpdate: string;
|
||||
}>;
|
||||
className?: string;
|
||||
serverId: string
|
||||
serverName: string
|
||||
transfer: number
|
||||
nextUpdate: string
|
||||
}>
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const CycleTransferStatsClient: React.FC<
|
||||
CycleTransferStatsClientProps
|
||||
> = ({ name, from, to, max, serverStats, className }) => {
|
||||
const { t } = useTranslation();
|
||||
export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> = ({
|
||||
name,
|
||||
from,
|
||||
to,
|
||||
max,
|
||||
serverStats,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@ -31,7 +37,7 @@ export const CycleTransferStatsClient: React.FC<
|
||||
)}
|
||||
>
|
||||
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => {
|
||||
const progress = (transfer / max) * 100;
|
||||
const progress = (transfer / max) * 100
|
||||
|
||||
return (
|
||||
<div key={serverId}>
|
||||
@ -40,8 +46,7 @@ export const CycleTransferStatsClient: React.FC<
|
||||
{name}
|
||||
</div>
|
||||
<span className="text-stone-600 dark:text-stone-400 text-xs">
|
||||
{new Date(from).toLocaleDateString()} -{" "}
|
||||
{new Date(to).toLocaleDateString()}
|
||||
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
|
||||
</span>
|
||||
</section>
|
||||
|
||||
@ -51,9 +56,7 @@ export const CycleTransferStatsClient: React.FC<
|
||||
<span className="text-sm font-semibold">{serverName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xs text-end w-10 font-medium">
|
||||
{progress.toFixed(0)}%
|
||||
</p>
|
||||
<p className="text-xs text-end w-10 font-medium">{progress.toFixed(0)}%</p>
|
||||
<AnimatedCircularProgressBar
|
||||
className="size-4 text-[0px]"
|
||||
max={100}
|
||||
@ -82,15 +85,14 @@ export const CycleTransferStatsClient: React.FC<
|
||||
|
||||
<section className="flex justify-between items-center mt-2">
|
||||
<div className="text-xs text-stone-500 dark:text-stone-400">
|
||||
{t("cycleTransfer.nextUpdate")}:{" "}
|
||||
{new Date(nextUpdate).toLocaleString()}
|
||||
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default CycleTransferStatsClient;
|
||||
export default CycleTransferStatsClient
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { fetchSetting } from "@/lib/nezha-api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { fetchSetting } from "@/lib/nezha-api"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import React from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: settingData } = useQuery({
|
||||
queryKey: ["setting"],
|
||||
queryFn: () => fetchSetting(),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4">
|
||||
@ -26,10 +26,7 @@ const Footer: React.FC = () => {
|
||||
</div>
|
||||
<p>
|
||||
{t("footer.themeBy")}
|
||||
<a
|
||||
href={"https://github.com/hamster1963/nezha-dash"}
|
||||
target="_blank"
|
||||
>
|
||||
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank">
|
||||
nezha-dash
|
||||
</a>
|
||||
{import.meta.env.VITE_GIT_HASH && (
|
||||
@ -39,7 +36,7 @@ const Footer: React.FC = () => {
|
||||
</section>
|
||||
</section>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
export default Footer
|
||||
|
@ -1,41 +1,35 @@
|
||||
import { geoJsonString } from "@/lib/geo-json-string";
|
||||
import { NezhaServer } from "@/types/nezha-api";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { geoEquirectangular, geoPath } from "d3-geo";
|
||||
import { countryCoordinates } from "@/lib/geo-limit";
|
||||
import MapTooltip from "./MapTooltip";
|
||||
import useTooltip from "@/hooks/use-tooltip";
|
||||
import { formatNezhaInfo } from "@/lib/utils";
|
||||
import useTooltip from "@/hooks/use-tooltip"
|
||||
import { geoJsonString } from "@/lib/geo-json-string"
|
||||
import { countryCoordinates } from "@/lib/geo-limit"
|
||||
import { formatNezhaInfo } from "@/lib/utils"
|
||||
import { NezhaServer } from "@/types/nezha-api"
|
||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default function GlobalMap({
|
||||
serverList,
|
||||
now,
|
||||
}: {
|
||||
serverList: NezhaServer[];
|
||||
now: number;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const countryList: string[] = [];
|
||||
const serverCounts: { [key: string]: number } = {};
|
||||
import MapTooltip from "./MapTooltip"
|
||||
|
||||
export default function GlobalMap({ serverList, now }: { serverList: NezhaServer[]; now: number }) {
|
||||
const { t } = useTranslation()
|
||||
const countryList: string[] = []
|
||||
const serverCounts: { [key: string]: number } = {}
|
||||
|
||||
serverList.forEach((server) => {
|
||||
if (server.country_code) {
|
||||
const countryCode = server.country_code.toUpperCase();
|
||||
const countryCode = server.country_code.toUpperCase()
|
||||
if (!countryList.includes(countryCode)) {
|
||||
countryList.push(countryCode);
|
||||
countryList.push(countryCode)
|
||||
}
|
||||
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1;
|
||||
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const width = 900;
|
||||
const height = 500;
|
||||
const width = 900
|
||||
const height = 500
|
||||
|
||||
const geoJson = JSON.parse(geoJsonString);
|
||||
const geoJson = JSON.parse(geoJsonString)
|
||||
const filteredFeatures = geoJson.features.filter(
|
||||
(feature: { properties: { iso_a3_eh: string } }) =>
|
||||
feature.properties.iso_a3_eh !== "",
|
||||
);
|
||||
(feature: { properties: { iso_a3_eh: string } }) => feature.properties.iso_a3_eh !== "",
|
||||
)
|
||||
|
||||
return (
|
||||
<section className="flex flex-col gap-4 mt-8">
|
||||
@ -54,24 +48,24 @@ export default function GlobalMap({
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
interface InteractiveMapProps {
|
||||
countries: string[];
|
||||
serverCounts: { [key: string]: number };
|
||||
width: number;
|
||||
height: number;
|
||||
countries: string[]
|
||||
serverCounts: { [key: string]: number }
|
||||
width: number
|
||||
height: number
|
||||
filteredFeatures: {
|
||||
type: "Feature";
|
||||
type: "Feature"
|
||||
properties: {
|
||||
iso_a2_eh: string;
|
||||
[key: string]: string;
|
||||
};
|
||||
geometry: never;
|
||||
}[];
|
||||
nezhaServerList: NezhaServer[];
|
||||
now: number;
|
||||
iso_a2_eh: string
|
||||
[key: string]: string
|
||||
}
|
||||
geometry: never
|
||||
}[]
|
||||
nezhaServerList: NezhaServer[]
|
||||
now: number
|
||||
}
|
||||
|
||||
export function InteractiveMap({
|
||||
@ -83,20 +77,17 @@ export function InteractiveMap({
|
||||
nezhaServerList,
|
||||
now,
|
||||
}: InteractiveMapProps) {
|
||||
const { setTooltipData } = useTooltip();
|
||||
const { setTooltipData } = useTooltip()
|
||||
|
||||
const projection = geoEquirectangular()
|
||||
.scale(140)
|
||||
.translate([width / 2, height / 2])
|
||||
.rotate([-12, 0, 0]);
|
||||
.rotate([-12, 0, 0])
|
||||
|
||||
const path = geoPath().projection(projection);
|
||||
const path = geoPath().projection(projection)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[2/1]"
|
||||
onMouseLeave={() => setTooltipData(null)}
|
||||
>
|
||||
<div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}>
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
@ -120,11 +111,9 @@ export function InteractiveMap({
|
||||
onMouseEnter={() => setTooltipData(null)}
|
||||
/>
|
||||
{filteredFeatures.map((feature, index) => {
|
||||
const isHighlighted = countries.includes(
|
||||
feature.properties.iso_a2_eh,
|
||||
);
|
||||
const isHighlighted = countries.includes(feature.properties.iso_a2_eh)
|
||||
|
||||
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0;
|
||||
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0
|
||||
|
||||
return (
|
||||
<path
|
||||
@ -137,30 +126,29 @@ export function InteractiveMap({
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
if (!isHighlighted) {
|
||||
setTooltipData(null);
|
||||
return;
|
||||
setTooltipData(null)
|
||||
return
|
||||
}
|
||||
if (path.centroid(feature)) {
|
||||
const countryCode = feature.properties.iso_a2_eh;
|
||||
const countryCode = feature.properties.iso_a2_eh
|
||||
const countryServers = nezhaServerList
|
||||
.filter(
|
||||
(server: NezhaServer) =>
|
||||
server.country_code?.toUpperCase() === countryCode,
|
||||
(server: NezhaServer) => server.country_code?.toUpperCase() === countryCode,
|
||||
)
|
||||
.map((server: NezhaServer) => ({
|
||||
name: server.name,
|
||||
status: formatNezhaInfo(now, server).online,
|
||||
}));
|
||||
}))
|
||||
setTooltipData({
|
||||
centroid: path.centroid(feature),
|
||||
country: feature.properties.name,
|
||||
count: serverCount,
|
||||
servers: countryServers,
|
||||
});
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
)
|
||||
})}
|
||||
|
||||
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
|
||||
@ -168,18 +156,18 @@ export function InteractiveMap({
|
||||
// 检查该国家是否已经在 filteredFeatures 中
|
||||
const isInFilteredFeatures = filteredFeatures.some(
|
||||
(feature) => feature.properties.iso_a2_eh === countryCode,
|
||||
);
|
||||
)
|
||||
|
||||
// 如果已经在 filteredFeatures 中,跳过
|
||||
if (isInFilteredFeatures) return null;
|
||||
if (isInFilteredFeatures) return null
|
||||
|
||||
// 获取国家的经纬度
|
||||
const coords = countryCoordinates[countryCode];
|
||||
if (!coords) return null;
|
||||
const coords = countryCoordinates[countryCode]
|
||||
if (!coords) return null
|
||||
|
||||
// 使用投影函数将经纬度转换为 SVG 坐标
|
||||
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0];
|
||||
const serverCount = serverCounts[countryCode] || 0;
|
||||
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0]
|
||||
const serverCount = serverCounts[countryCode] || 0
|
||||
|
||||
return (
|
||||
<g
|
||||
@ -188,19 +176,18 @@ export function InteractiveMap({
|
||||
const countryServers = nezhaServerList
|
||||
.filter(
|
||||
(server: NezhaServer) =>
|
||||
server.country_code?.toUpperCase() ===
|
||||
countryCode.toUpperCase(),
|
||||
server.country_code?.toUpperCase() === countryCode.toUpperCase(),
|
||||
)
|
||||
.map((server: NezhaServer) => ({
|
||||
name: server.name,
|
||||
status: formatNezhaInfo(now, server).online,
|
||||
}));
|
||||
}))
|
||||
setTooltipData({
|
||||
centroid: [x, y],
|
||||
country: coords.name,
|
||||
count: serverCount,
|
||||
servers: countryServers,
|
||||
});
|
||||
})
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
@ -211,11 +198,11 @@ export function InteractiveMap({
|
||||
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
<MapTooltip />
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { m } from "framer-motion";
|
||||
import { cn } from "@/lib/utils"
|
||||
import { m } from "framer-motion"
|
||||
|
||||
export default function GroupSwitch({
|
||||
tabs,
|
||||
currentTab,
|
||||
setCurrentTab,
|
||||
}: {
|
||||
tabs: string[];
|
||||
currentTab: string;
|
||||
setCurrentTab: (tab: string) => void;
|
||||
tabs: string[]
|
||||
currentTab: string
|
||||
setCurrentTab: (tab: string) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]">
|
||||
@ -41,5 +41,5 @@ export default function GroupSwitch({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,72 +1,71 @@
|
||||
import { ModeToggle } from "@/components/ThemeSwitcher";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { DateTime } from "luxon";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { LanguageSwitcher } from "./LanguageSwitcher";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ModeToggle } from "@/components/ThemeSwitcher"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { DateTime } from "luxon"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
import { LanguageSwitcher } from "./LanguageSwitcher"
|
||||
|
||||
function Header() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { data: settingData, isLoading } = useQuery({
|
||||
queryKey: ["setting"],
|
||||
queryFn: () => fetchSetting(),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
})
|
||||
|
||||
const siteName = settingData?.data?.site_name;
|
||||
const siteName = settingData?.data?.site_name
|
||||
|
||||
const InjectContext = useCallback((content: string) => {
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = content;
|
||||
const tempDiv = document.createElement("div")
|
||||
tempDiv.innerHTML = content
|
||||
|
||||
const handlers: { [key: string]: (element: HTMLElement) => void } = {
|
||||
SCRIPT: (element) => {
|
||||
const script = document.createElement("script");
|
||||
const script = document.createElement("script")
|
||||
if ((element as HTMLScriptElement).src) {
|
||||
script.src = (element as HTMLScriptElement).src;
|
||||
script.src = (element as HTMLScriptElement).src
|
||||
} else {
|
||||
script.textContent = element.textContent;
|
||||
script.textContent = element.textContent
|
||||
}
|
||||
document.body.appendChild(script);
|
||||
document.body.appendChild(script)
|
||||
},
|
||||
STYLE: (element) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = element.textContent;
|
||||
document.head.appendChild(style);
|
||||
const style = document.createElement("style")
|
||||
style.textContent = element.textContent
|
||||
document.head.appendChild(style)
|
||||
},
|
||||
DEFAULT: (element) => {
|
||||
document.body.appendChild(element);
|
||||
document.body.appendChild(element)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Array.from(tempDiv.childNodes).forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as HTMLElement;
|
||||
(handlers[element.tagName] || handlers.DEFAULT)(element);
|
||||
const element = node as HTMLElement
|
||||
;(handlers[element.tagName] || handlers.DEFAULT)(element)
|
||||
} else if (node.nodeType === Node.TEXT_NODE) {
|
||||
document.body.appendChild(
|
||||
document.createTextNode(node.textContent || ""),
|
||||
);
|
||||
document.body.appendChild(document.createTextNode(node.textContent || ""))
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = siteName || "NEZHA";
|
||||
}, [siteName]);
|
||||
document.title = siteName || "NEZHA"
|
||||
}, [siteName])
|
||||
|
||||
useEffect(() => {
|
||||
if (settingData?.data?.custom_code) {
|
||||
InjectContext(settingData?.data?.custom_code);
|
||||
InjectContext(settingData?.data?.custom_code)
|
||||
}
|
||||
}, [settingData?.data?.custom_code]);
|
||||
}, [settingData?.data?.custom_code])
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-5xl">
|
||||
@ -89,13 +88,8 @@ function Header() {
|
||||
) : (
|
||||
siteName || "NEZHA"
|
||||
)}
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 hidden h-4 w-[1px] md:block"
|
||||
/>
|
||||
<p className="hidden text-sm font-medium opacity-40 md:block">
|
||||
{t("nezha")}
|
||||
</p>
|
||||
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
|
||||
<p className="hidden text-sm font-medium opacity-40 md:block">{t("nezha")}</p>
|
||||
</section>
|
||||
<section className="flex items-center gap-2">
|
||||
<DashboardLink />
|
||||
@ -105,17 +99,17 @@ function Header() {
|
||||
</section>
|
||||
<Overview />
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DashboardLink() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
const { data: userData } = useQuery({
|
||||
queryKey: ["login-user"],
|
||||
queryFn: () => fetchLoginUser(),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@ -129,37 +123,37 @@ function DashboardLink() {
|
||||
{userData?.data?.id && t("dashboard")}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// https://github.com/streamich/react-use/blob/master/src/useInterval.ts
|
||||
const useInterval = (callback: () => void, delay: number | null) => {
|
||||
const savedCallback = useRef<() => void>(() => {});
|
||||
const savedCallback = useRef<() => void>(() => {})
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
});
|
||||
savedCallback.current = callback
|
||||
})
|
||||
useEffect(() => {
|
||||
if (delay !== null) {
|
||||
const interval = setInterval(() => savedCallback.current(), delay || 0);
|
||||
return () => clearInterval(interval);
|
||||
const interval = setInterval(() => savedCallback.current(), delay || 0)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
return undefined;
|
||||
}, [delay]);
|
||||
};
|
||||
return undefined
|
||||
}, [delay])
|
||||
}
|
||||
function Overview() {
|
||||
const { t } = useTranslation();
|
||||
const [mouted, setMounted] = useState(false);
|
||||
const { t } = useTranslation()
|
||||
const [mouted, setMounted] = useState(false)
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
const timeOption = DateTime.TIME_SIMPLE;
|
||||
timeOption.hour12 = true;
|
||||
setMounted(true)
|
||||
}, [])
|
||||
const timeOption = DateTime.TIME_SIMPLE
|
||||
timeOption.hour12 = true
|
||||
const [timeString, setTimeString] = useState(
|
||||
DateTime.now().setLocale("en-US").toLocaleString(timeOption),
|
||||
);
|
||||
)
|
||||
useInterval(() => {
|
||||
setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption));
|
||||
}, 1000);
|
||||
setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption))
|
||||
}, 1000)
|
||||
return (
|
||||
<section className={"mt-10 flex flex-col md:mt-16"}>
|
||||
<p className="text-base font-semibold">👋 {t("overview")}</p>
|
||||
@ -172,6 +166,6 @@ function Overview() {
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
)
|
||||
}
|
||||
export default Header;
|
||||
export default Header
|
||||
|
@ -3,7 +3,7 @@ export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
<svg viewBox="0 0 496 512" fill="white" {...props}>
|
||||
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
|
||||
</svg>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function BackIcon() {
|
||||
@ -28,5 +28,5 @@ export function BackIcon() {
|
||||
height="20"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,31 +1,30 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const locale = i18n.language;
|
||||
const locale = i18n.language
|
||||
|
||||
const handleSelect = (e: Event, newLocale: string) => {
|
||||
e.preventDefault(); // 阻止默认的关闭行为
|
||||
i18n.changeLanguage(newLocale);
|
||||
};
|
||||
e.preventDefault() // 阻止默认的关闭行为
|
||||
i18n.changeLanguage(newLocale)
|
||||
}
|
||||
|
||||
const localeItems = [
|
||||
{ name: t("language.zh-CN"), code: "zh-CN" },
|
||||
{ name: t("language.zh-TW"), code: "zh-TW" },
|
||||
{ name: t("language.en"), code: "en" },
|
||||
];
|
||||
]
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@ -46,11 +45,10 @@ export function LanguageSwitcher() {
|
||||
onSelect={(e) => handleSelect(e, item.code)}
|
||||
className={locale === item.code ? "bg-muted gap-3" : ""}
|
||||
>
|
||||
{item.name}{" "}
|
||||
{locale === item.code && <CheckCircleIcon className="size-4" />}
|
||||
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import useTooltip from "@/hooks/use-tooltip";
|
||||
import { AnimatePresence, m } from "framer-motion";
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useTooltip from "@/hooks/use-tooltip"
|
||||
import { AnimatePresence, m } from "framer-motion"
|
||||
import { memo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const MapTooltip = memo(function MapTooltip() {
|
||||
const { t } = useTranslation();
|
||||
const { tooltipData } = useTooltip();
|
||||
const { t } = useTranslation()
|
||||
const { tooltipData } = useTooltip()
|
||||
|
||||
if (!tooltipData) return null;
|
||||
if (!tooltipData) return null
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
@ -23,14 +23,12 @@ const MapTooltip = memo(function MapTooltip() {
|
||||
transform: "translate(20%, -50%)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.stopPropagation();
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{tooltipData.country === "China"
|
||||
? "Mainland China"
|
||||
: tooltipData.country}
|
||||
{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}
|
||||
</p>
|
||||
<p className="text-neutral-600 dark:text-neutral-400 mb-1">
|
||||
{tooltipData.count} {t("map.Servers")}
|
||||
@ -56,7 +54,7 @@ const MapTooltip = memo(function MapTooltip() {
|
||||
</div>
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
export default MapTooltip;
|
||||
export default MapTooltip
|
||||
|
@ -1,12 +1,6 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
@ -14,33 +8,28 @@ import {
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { fetchMonitor } from "@/lib/nezha-api";
|
||||
import { formatTime } from "@/lib/utils";
|
||||
import { formatRelativeTime } from "@/lib/utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as React from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||||
import NetworkChartLoading from "./NetworkChartLoading";
|
||||
import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api";
|
||||
import { Switch } from "./ui/switch";
|
||||
import { Label } from "./ui/label";
|
||||
} from "@/components/ui/chart"
|
||||
import { fetchMonitor } from "@/lib/nezha-api"
|
||||
import { formatTime } from "@/lib/utils"
|
||||
import { formatRelativeTime } from "@/lib/utils"
|
||||
import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import * as React from "react"
|
||||
import { useCallback, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
|
||||
|
||||
import NetworkChartLoading from "./NetworkChartLoading"
|
||||
import { Label } from "./ui/label"
|
||||
import { Switch } from "./ui/switch"
|
||||
|
||||
interface ResultItem {
|
||||
created_at: number;
|
||||
[key: string]: number;
|
||||
created_at: number
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
export function NetworkChart({
|
||||
server_id,
|
||||
show,
|
||||
}: {
|
||||
server_id: number;
|
||||
show: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
export function NetworkChart({ server_id, show }: { server_id: number; show: boolean }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: monitorData } = useQuery({
|
||||
queryKey: ["monitor", server_id],
|
||||
@ -49,29 +38,27 @@ export function NetworkChart({
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
})
|
||||
|
||||
if (!monitorData) return <NetworkChartLoading />;
|
||||
if (!monitorData) return <NetworkChartLoading />
|
||||
|
||||
if (monitorData?.success && !monitorData.data) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p className="text-sm font-medium opacity-40"></p>
|
||||
<p className="text-sm font-medium opacity-40 mb-4">
|
||||
{t("monitor.noData")}
|
||||
</p>
|
||||
<p className="text-sm font-medium opacity-40 mb-4">{t("monitor.noData")}</p>
|
||||
</div>
|
||||
<NetworkChartLoading />
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const transformedData = transformData(monitorData.data);
|
||||
const transformedData = transformData(monitorData.data)
|
||||
|
||||
const formattedData = formatData(monitorData.data);
|
||||
const formattedData = formatData(monitorData.data)
|
||||
|
||||
const chartDataKey = Object.keys(transformedData);
|
||||
const chartDataKey = Object.keys(transformedData)
|
||||
|
||||
const initChartConfig = {
|
||||
avg_delay: {
|
||||
@ -80,10 +67,10 @@ export function NetworkChart({
|
||||
...chartDataKey.reduce((acc, key) => {
|
||||
acc[key] = {
|
||||
label: key,
|
||||
};
|
||||
return acc;
|
||||
}
|
||||
return acc
|
||||
}, {} as ChartConfig),
|
||||
} satisfies ChartConfig;
|
||||
} satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<NetworkChartClient
|
||||
@ -93,7 +80,7 @@ export function NetworkChart({
|
||||
serverName={monitorData.data[0].server_name}
|
||||
formattedData={formattedData}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export const NetworkChartClient = React.memo(function NetworkChart({
|
||||
@ -103,33 +90,33 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
||||
serverName,
|
||||
formattedData,
|
||||
}: {
|
||||
chartDataKey: string[];
|
||||
chartConfig: ChartConfig;
|
||||
chartData: ServerMonitorChart;
|
||||
serverName: string;
|
||||
formattedData: ResultItem[];
|
||||
chartDataKey: string[]
|
||||
chartConfig: ChartConfig
|
||||
chartData: ServerMonitorChart
|
||||
serverName: string
|
||||
formattedData: ResultItem[]
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
|
||||
const defaultChart = "All";
|
||||
const defaultChart = "All"
|
||||
|
||||
const [activeChart, setActiveChart] = React.useState(defaultChart);
|
||||
const [isPeakEnabled, setIsPeakEnabled] = React.useState(false);
|
||||
const [activeChart, setActiveChart] = React.useState(defaultChart)
|
||||
const [isPeakEnabled, setIsPeakEnabled] = React.useState(false)
|
||||
|
||||
const handleButtonClick = useCallback(
|
||||
(chart: string) => {
|
||||
setActiveChart((prev) => (prev === chart ? defaultChart : chart));
|
||||
setActiveChart((prev) => (prev === chart ? defaultChart : chart))
|
||||
},
|
||||
[defaultChart],
|
||||
);
|
||||
)
|
||||
|
||||
const getColorByIndex = useCallback(
|
||||
(chart: string) => {
|
||||
const index = chartDataKey.indexOf(chart);
|
||||
return `hsl(var(--chart-${(index % 10) + 1}))`;
|
||||
const index = chartDataKey.indexOf(chart)
|
||||
return `hsl(var(--chart-${(index % 10) + 1}))`
|
||||
},
|
||||
[chartDataKey],
|
||||
);
|
||||
)
|
||||
|
||||
const chartButtons = useMemo(
|
||||
() =>
|
||||
@ -140,16 +127,14 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
||||
className={`relative z-30 flex cursor-pointer grow basis-0 flex-col justify-center gap-1 border-b border-neutral-200 dark:border-neutral-800 px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6`}
|
||||
onClick={() => handleButtonClick(key)}
|
||||
>
|
||||
<span className="whitespace-nowrap text-xs text-muted-foreground">
|
||||
{key}
|
||||
</span>
|
||||
<span className="whitespace-nowrap text-xs text-muted-foreground">{key}</span>
|
||||
<span className="text-md font-bold leading-none sm:text-lg">
|
||||
{chartData[key][chartData[key].length - 1].avg_delay.toFixed(2)}ms
|
||||
</span>
|
||||
</button>
|
||||
)),
|
||||
[chartDataKey, activeChart, chartData, handleButtonClick],
|
||||
);
|
||||
)
|
||||
|
||||
const chartLines = useMemo(() => {
|
||||
if (activeChart !== defaultChart) {
|
||||
@ -162,7 +147,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
||||
dataKey="avg_delay"
|
||||
stroke={getColorByIndex(activeChart)}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
return chartDataKey.map((key) => (
|
||||
<Line
|
||||
@ -175,65 +160,50 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
||||
stroke={getColorByIndex(key)}
|
||||
connectNulls={true}
|
||||
/>
|
||||
));
|
||||
}, [activeChart, defaultChart, chartDataKey, getColorByIndex]);
|
||||
))
|
||||
}, [activeChart, defaultChart, chartDataKey, getColorByIndex])
|
||||
|
||||
const processedData = useMemo(() => {
|
||||
if (!isPeakEnabled) {
|
||||
return activeChart === defaultChart
|
||||
? formattedData
|
||||
: chartData[activeChart];
|
||||
return activeChart === defaultChart ? formattedData : chartData[activeChart]
|
||||
}
|
||||
|
||||
// 如果开启了削峰,对数据进行处理
|
||||
const data = (
|
||||
activeChart === defaultChart ? formattedData : chartData[activeChart]
|
||||
) as ResultItem[];
|
||||
const windowSize = 7; // 增加到7个点的移动平均
|
||||
const weights = [0.1, 0.1, 0.15, 0.3, 0.15, 0.1, 0.1]; // 加权平均的权重
|
||||
) as ResultItem[]
|
||||
const windowSize = 7 // 增加到7个点的移动平均
|
||||
const weights = [0.1, 0.1, 0.15, 0.3, 0.15, 0.1, 0.1] // 加权平均的权重
|
||||
|
||||
return data.map((point, index) => {
|
||||
if (index < windowSize - 1) return point;
|
||||
if (index < windowSize - 1) return point
|
||||
|
||||
const window = data.slice(index - windowSize + 1, index + 1);
|
||||
const smoothed = { ...point } as ResultItem;
|
||||
const window = data.slice(index - windowSize + 1, index + 1)
|
||||
const smoothed = { ...point } as ResultItem
|
||||
|
||||
if (activeChart === defaultChart) {
|
||||
// 处理所有线路的数据
|
||||
chartDataKey.forEach((key) => {
|
||||
const values = window
|
||||
.map((w) => w[key])
|
||||
.filter((v) => v !== undefined && v !== null) as number[];
|
||||
.filter((v) => v !== undefined && v !== null) as number[]
|
||||
if (values.length === windowSize) {
|
||||
smoothed[key] = values.reduce(
|
||||
(acc, val, idx) => acc + val * weights[idx],
|
||||
0,
|
||||
);
|
||||
smoothed[key] = values.reduce((acc, val, idx) => acc + val * weights[idx], 0)
|
||||
}
|
||||
});
|
||||
})
|
||||
} else {
|
||||
// 处理单条线路的数据
|
||||
const values = window
|
||||
.map((w) => w.avg_delay)
|
||||
.filter((v) => v !== undefined && v !== null) as number[];
|
||||
.filter((v) => v !== undefined && v !== null) as number[]
|
||||
if (values.length === windowSize) {
|
||||
smoothed.avg_delay = values.reduce(
|
||||
(acc, val, idx) => acc + val * weights[idx],
|
||||
0,
|
||||
);
|
||||
smoothed.avg_delay = values.reduce((acc, val, idx) => acc + val * weights[idx], 0)
|
||||
}
|
||||
}
|
||||
|
||||
return smoothed;
|
||||
});
|
||||
}, [
|
||||
isPeakEnabled,
|
||||
activeChart,
|
||||
formattedData,
|
||||
chartData,
|
||||
chartDataKey,
|
||||
defaultChart,
|
||||
]);
|
||||
return smoothed
|
||||
})
|
||||
}, [isPeakEnabled, activeChart, formattedData, chartData, chartDataKey, defaultChart])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -246,11 +216,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
||||
{chartDataKey.length} {t("monitor.monitorCount")}
|
||||
</CardDescription>
|
||||
<div className="flex items-center mt-0.5 space-x-2">
|
||||
<Switch
|
||||
id="Peak"
|
||||
checked={isPeakEnabled}
|
||||
onCheckedChange={setIsPeakEnabled}
|
||||
/>
|
||||
<Switch id="Peak" checked={isPeakEnabled} onCheckedChange={setIsPeakEnabled} />
|
||||
<Label className="text-xs" htmlFor="Peak">
|
||||
Peak cut
|
||||
</Label>
|
||||
@ -259,15 +225,8 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
||||
<div className="flex flex-wrap w-full">{chartButtons}</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pr-2 pl-0 py-4 sm:pt-6 sm:pb-6 sm:pr-6 sm:pl-2">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[250px] w-full"
|
||||
>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={processedData}
|
||||
margin={{ left: 12, right: 12 }}
|
||||
>
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
|
||||
<LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="created_at"
|
||||
@ -292,67 +251,64 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
||||
indicator={"line"}
|
||||
labelKey="created_at"
|
||||
labelFormatter={(_, payload) => {
|
||||
return formatTime(payload[0].payload.created_at);
|
||||
return formatTime(payload[0].payload.created_at)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{activeChart === defaultChart && (
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
)}
|
||||
{activeChart === defaultChart && <ChartLegend content={<ChartLegendContent />} />}
|
||||
{chartLines}
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const transformData = (data: NezhaMonitor[]) => {
|
||||
const monitorData: ServerMonitorChart = {};
|
||||
const monitorData: ServerMonitorChart = {}
|
||||
|
||||
data.forEach((item) => {
|
||||
const monitorName = item.monitor_name;
|
||||
const monitorName = item.monitor_name
|
||||
|
||||
if (!monitorData[monitorName]) {
|
||||
monitorData[monitorName] = [];
|
||||
monitorData[monitorName] = []
|
||||
}
|
||||
|
||||
for (let i = 0; i < item.created_at.length; i++) {
|
||||
monitorData[monitorName].push({
|
||||
created_at: item.created_at[i],
|
||||
avg_delay: item.avg_delay[i],
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return monitorData;
|
||||
};
|
||||
return monitorData
|
||||
}
|
||||
|
||||
const formatData = (rawData: NezhaMonitor[]) => {
|
||||
const result: { [time: number]: ResultItem } = {};
|
||||
const result: { [time: number]: ResultItem } = {}
|
||||
|
||||
const allTimes = new Set<number>();
|
||||
const allTimes = new Set<number>()
|
||||
rawData.forEach((item) => {
|
||||
item.created_at.forEach((time) => allTimes.add(time));
|
||||
});
|
||||
item.created_at.forEach((time) => allTimes.add(time))
|
||||
})
|
||||
|
||||
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b);
|
||||
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b)
|
||||
|
||||
rawData.forEach((item) => {
|
||||
const { monitor_name, created_at, avg_delay } = item;
|
||||
const { monitor_name, created_at, avg_delay } = item
|
||||
|
||||
allTimeArray.forEach((time) => {
|
||||
if (!result[time]) {
|
||||
result[time] = { created_at: time };
|
||||
result[time] = { created_at: time }
|
||||
}
|
||||
|
||||
const timeIndex = created_at.indexOf(time);
|
||||
const timeIndex = created_at.indexOf(time)
|
||||
// @ts-expect-error - avg_delay is an array
|
||||
result[time][monitor_name] =
|
||||
timeIndex !== -1 ? avg_delay[timeIndex] : null;
|
||||
});
|
||||
});
|
||||
result[time][monitor_name] = timeIndex !== -1 ? avg_delay[timeIndex] : null
|
||||
})
|
||||
})
|
||||
|
||||
return Object.values(result).sort((a, b) => a.created_at - b.created_at);
|
||||
};
|
||||
return Object.values(result).sort((a, b) => a.created_at - b.created_at)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Loader } from "@/components/loading/Loader";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Loader } from "@/components/loading/Loader"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
|
||||
export default function NetworkChartLoading() {
|
||||
return (
|
||||
@ -19,5 +19,5 @@ export default function NetworkChartLoading() {
|
||||
<div className="aspect-auto h-[250px] w-full"></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,28 +1,17 @@
|
||||
import ServerFlag from "@/components/ServerFlag";
|
||||
import ServerUsageBar from "@/components/ServerUsageBar";
|
||||
import ServerFlag from "@/components/ServerFlag"
|
||||
import ServerUsageBar from "@/components/ServerUsageBar"
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { cn, formatNezhaInfo, getDaysBetweenDates, parsePublicNote } from "@/lib/utils"
|
||||
import { NezhaServer } from "@/types/nezha-api"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
import {
|
||||
cn,
|
||||
formatNezhaInfo,
|
||||
parsePublicNote,
|
||||
getDaysBetweenDates,
|
||||
} from "@/lib/utils";
|
||||
import { NezhaServer } from "@/types/nezha-api";
|
||||
import { Card } from "./ui/card";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import { Badge } from "./ui/badge"
|
||||
import { Card } from "./ui/card"
|
||||
|
||||
export default function ServerCard({
|
||||
now,
|
||||
serverInfo,
|
||||
}: {
|
||||
now: number;
|
||||
serverInfo: NezhaServer;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
export default function ServerCard({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
name,
|
||||
country_code,
|
||||
@ -35,23 +24,20 @@ export default function ServerCard({
|
||||
net_in_transfer,
|
||||
net_out_transfer,
|
||||
public_note,
|
||||
} = formatNezhaInfo(now, serverInfo);
|
||||
} = formatNezhaInfo(now, serverInfo)
|
||||
|
||||
const showFlag = true;
|
||||
const showFlag = true
|
||||
|
||||
const parsedData = parsePublicNote(public_note);
|
||||
const parsedData = parsePublicNote(public_note)
|
||||
|
||||
let daysLeft = 0;
|
||||
let isNeverExpire = false;
|
||||
let daysLeft = 0
|
||||
let isNeverExpire = false
|
||||
|
||||
if (parsedData?.billingDataMod?.endDate) {
|
||||
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
|
||||
isNeverExpire = true;
|
||||
isNeverExpire = true
|
||||
} else {
|
||||
daysLeft = getDaysBetweenDates(
|
||||
parsedData.billingDataMod.endDate,
|
||||
new Date(now).toISOString(),
|
||||
);
|
||||
daysLeft = getDaysBetweenDates(parsedData.billingDataMod.endDate, new Date(now).toISOString())
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,11 +77,7 @@ export default function ServerCard({
|
||||
剩余时间: {isNeverExpire ? "永久" : daysLeft + "天"}
|
||||
</p>
|
||||
) : (
|
||||
<p
|
||||
className={cn(
|
||||
"text-[10px] text-muted-foreground text-red-600",
|
||||
)}
|
||||
>
|
||||
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
|
||||
已过期: {daysLeft * -1} 天
|
||||
</p>
|
||||
))}
|
||||
@ -105,47 +87,29 @@ export default function ServerCard({
|
||||
<section className={cn("grid grid-cols-5 items-center gap-3")}>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{cpu.toFixed(2)}%
|
||||
</div>
|
||||
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={cpu} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.mem")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{mem.toFixed(2)}%
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={mem} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.stg")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{stg.toFixed(2)}%
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={stg} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.upload")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{up >= 1024
|
||||
? `${(up / 1024).toFixed(2)}G/s`
|
||||
: `${up.toFixed(2)}M/s`}
|
||||
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.download")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{down >= 1024
|
||||
? `${(down / 1024).toFixed(2)}G/s`
|
||||
: `${down.toFixed(2)}M/s`}
|
||||
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -179,24 +143,16 @@ export default function ServerCard({
|
||||
>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
showFlag ? "min-w-[17px]" : "min-w-0",
|
||||
)}
|
||||
className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}
|
||||
>
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<p
|
||||
className={cn(
|
||||
"break-all font-bold tracking-tight",
|
||||
showFlag ? "text-xs" : "text-sm",
|
||||
)}
|
||||
>
|
||||
<p className={cn("break-all font-bold tracking-tight", showFlag ? "text-xs" : "text-sm")}>
|
||||
{name}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,33 +1,24 @@
|
||||
import ServerFlag from "@/components/ServerFlag";
|
||||
import ServerUsageBar from "@/components/ServerUsageBar";
|
||||
import ServerFlag from "@/components/ServerFlag"
|
||||
import ServerUsageBar from "@/components/ServerUsageBar"
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
|
||||
import { cn, formatNezhaInfo, getDaysBetweenDates, parsePublicNote } from "@/lib/utils"
|
||||
import { NezhaServer } from "@/types/nezha-api"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
import {
|
||||
cn,
|
||||
formatNezhaInfo,
|
||||
getDaysBetweenDates,
|
||||
parsePublicNote,
|
||||
} from "@/lib/utils";
|
||||
import { NezhaServer } from "@/types/nezha-api";
|
||||
import { Card } from "./ui/card";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
GetFontLogoClass,
|
||||
GetOsName,
|
||||
MageMicrosoftWindows,
|
||||
} from "@/lib/logo-class";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { Card } from "./ui/card"
|
||||
import { Separator } from "./ui/separator"
|
||||
|
||||
export default function ServerCardInline({
|
||||
now,
|
||||
serverInfo,
|
||||
}: {
|
||||
now: number;
|
||||
serverInfo: NezhaServer;
|
||||
now: number
|
||||
serverInfo: NezhaServer
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
name,
|
||||
country_code,
|
||||
@ -42,23 +33,20 @@ export default function ServerCardInline({
|
||||
net_in_transfer,
|
||||
net_out_transfer,
|
||||
public_note,
|
||||
} = formatNezhaInfo(now, serverInfo);
|
||||
} = formatNezhaInfo(now, serverInfo)
|
||||
|
||||
const showFlag = true;
|
||||
const showFlag = true
|
||||
|
||||
const parsedData = parsePublicNote(public_note);
|
||||
const parsedData = parsePublicNote(public_note)
|
||||
|
||||
let daysLeft = 0;
|
||||
let isNeverExpire = false;
|
||||
let daysLeft = 0
|
||||
let isNeverExpire = false
|
||||
|
||||
if (parsedData?.billingDataMod?.endDate) {
|
||||
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
|
||||
isNeverExpire = true;
|
||||
isNeverExpire = true
|
||||
} else {
|
||||
daysLeft = getDaysBetweenDates(
|
||||
parsedData.billingDataMod.endDate,
|
||||
new Date(now).toISOString(),
|
||||
);
|
||||
daysLeft = getDaysBetweenDates(parsedData.billingDataMod.endDate, new Date(now).toISOString())
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,11 +90,7 @@ export default function ServerCardInline({
|
||||
剩余时间: {isNeverExpire ? "永久" : daysLeft + "天"}
|
||||
</p>
|
||||
) : (
|
||||
<p
|
||||
className={cn(
|
||||
"text-[10px] text-muted-foreground text-red-600",
|
||||
)}
|
||||
>
|
||||
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
|
||||
已过期: {daysLeft * -1} 天
|
||||
</p>
|
||||
))}
|
||||
@ -115,9 +99,7 @@ export default function ServerCardInline({
|
||||
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
|
||||
<div
|
||||
className={"items-center flex flex-row gap-2 whitespace-nowrap"}
|
||||
>
|
||||
<div className={"items-center flex flex-row gap-2 whitespace-nowrap"}>
|
||||
<div className="text-xs font-semibold">
|
||||
{platform.includes("Windows") ? (
|
||||
<MageMicrosoftWindows className="size-[10px]" />
|
||||
@ -126,81 +108,53 @@ export default function ServerCardInline({
|
||||
)}
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.system")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p>
|
||||
<div className="flex items-center text-[10.5px] font-semibold">
|
||||
{platform.includes("Windows")
|
||||
? "Windows"
|
||||
: GetOsName(platform)}
|
||||
{platform.includes("Windows") ? "Windows" : GetOsName(platform)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-20 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.uptime")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.uptime")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{(uptime / 86400).toFixed(0)} {t("serverCard.days")}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{cpu.toFixed(2)}%
|
||||
</div>
|
||||
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={cpu} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.mem")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{mem.toFixed(2)}%
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={mem} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.stg")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{stg.toFixed(2)}%
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={stg} />
|
||||
</div>
|
||||
<div className={"flex w-16 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.upload")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{up >= 1024
|
||||
? `${(up / 1024).toFixed(2)}G/s`
|
||||
: `${up.toFixed(2)}M/s`}
|
||||
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-16 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.download")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{down >= 1024
|
||||
? `${(down / 1024).toFixed(2)}G/s`
|
||||
: `${down.toFixed(2)}M/s`}
|
||||
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-20 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.totalUpload")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.totalUpload")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{formatBytes(net_out_transfer)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-20 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.totalDownload")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.totalDownload")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{formatBytes(net_in_transfer)}
|
||||
</div>
|
||||
@ -222,10 +176,7 @@ export default function ServerCardInline({
|
||||
>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
showFlag ? "min-w-[17px]" : "min-w-0",
|
||||
)}
|
||||
className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}
|
||||
>
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
</div>
|
||||
@ -241,5 +192,5 @@ export default function ServerCardInline({
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,115 +1,92 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart";
|
||||
import { formatNezhaInfo, formatRelativeTime } from "@/lib/utils";
|
||||
import { NezhaServer, NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { ServerDetailChartLoading } from "./loading/ServerDetailLoading";
|
||||
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar";
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { formatNezhaInfo, formatRelativeTime } from "@/lib/utils"
|
||||
import { NezhaServer, NezhaWebsocketResponse } from "@/types/nezha-api"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Area, AreaChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
|
||||
|
||||
import { ServerDetailChartLoading } from "./loading/ServerDetailLoading"
|
||||
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar"
|
||||
|
||||
type gpuChartData = {
|
||||
timeStamp: string;
|
||||
gpu: number;
|
||||
};
|
||||
timeStamp: string
|
||||
gpu: number
|
||||
}
|
||||
|
||||
type cpuChartData = {
|
||||
timeStamp: string;
|
||||
cpu: number;
|
||||
};
|
||||
timeStamp: string
|
||||
cpu: number
|
||||
}
|
||||
|
||||
type processChartData = {
|
||||
timeStamp: string;
|
||||
process: number;
|
||||
};
|
||||
timeStamp: string
|
||||
process: number
|
||||
}
|
||||
|
||||
type diskChartData = {
|
||||
timeStamp: string;
|
||||
disk: number;
|
||||
};
|
||||
timeStamp: string
|
||||
disk: number
|
||||
}
|
||||
|
||||
type memChartData = {
|
||||
timeStamp: string;
|
||||
mem: number;
|
||||
swap: number;
|
||||
};
|
||||
timeStamp: string
|
||||
mem: number
|
||||
swap: number
|
||||
}
|
||||
|
||||
type networkChartData = {
|
||||
timeStamp: string;
|
||||
upload: number;
|
||||
download: number;
|
||||
};
|
||||
timeStamp: string
|
||||
upload: number
|
||||
download: number
|
||||
}
|
||||
|
||||
type connectChartData = {
|
||||
timeStamp: string;
|
||||
tcp: number;
|
||||
udp: number;
|
||||
};
|
||||
timeStamp: string
|
||||
tcp: number
|
||||
udp: number
|
||||
}
|
||||
|
||||
export default function ServerDetailChart({
|
||||
server_id,
|
||||
}: {
|
||||
server_id: string;
|
||||
}) {
|
||||
const { lastMessage, connected } = useWebSocketContext();
|
||||
export default function ServerDetailChart({ server_id }: { server_id: string }) {
|
||||
const { lastMessage, connected } = useWebSocketContext()
|
||||
|
||||
if (!connected) {
|
||||
return <ServerDetailChartLoading />;
|
||||
return <ServerDetailChartLoading />
|
||||
}
|
||||
|
||||
const nezhaWsData = lastMessage
|
||||
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
|
||||
: null;
|
||||
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
|
||||
|
||||
if (!nezhaWsData) {
|
||||
return <ServerDetailChartLoading />;
|
||||
return <ServerDetailChartLoading />
|
||||
}
|
||||
|
||||
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
|
||||
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id))
|
||||
|
||||
if (!server) {
|
||||
return <ServerDetailChartLoading />;
|
||||
return <ServerDetailChartLoading />
|
||||
}
|
||||
|
||||
const { online } = formatNezhaInfo(nezhaWsData.now, server);
|
||||
const { online } = formatNezhaInfo(nezhaWsData.now, server)
|
||||
|
||||
if (!online) {
|
||||
return <ServerDetailChartLoading />;
|
||||
return <ServerDetailChartLoading />
|
||||
}
|
||||
|
||||
const gpuStats = server.state.gpu || [];
|
||||
const gpuList = server.host.gpu || [];
|
||||
const gpuStats = server.state.gpu || []
|
||||
const gpuList = server.host.gpu || []
|
||||
|
||||
return (
|
||||
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
|
||||
<CpuChart now={nezhaWsData.now} data={server} />
|
||||
{gpuStats.length >= 1 && gpuList.length === gpuStats.length ? (
|
||||
gpuList.map((gpu, index) => (
|
||||
<GpuChart
|
||||
now={nezhaWsData.now}
|
||||
gpuStat={gpuStats[index]}
|
||||
gpuName={gpu}
|
||||
key={index}
|
||||
/>
|
||||
<GpuChart now={nezhaWsData.now} gpuStat={gpuStats[index]} gpuName={gpu} key={index} />
|
||||
))
|
||||
) : gpuStats.length > 0 ? (
|
||||
gpuStats.map((gpu, index) => (
|
||||
<GpuChart
|
||||
now={nezhaWsData.now}
|
||||
gpuStat={gpu}
|
||||
gpuName={`#${index + 1}`}
|
||||
key={index}
|
||||
/>
|
||||
<GpuChart now={nezhaWsData.now} gpuStat={gpu} gpuName={`#${index + 1}`} key={index} />
|
||||
))
|
||||
) : (
|
||||
<></>
|
||||
@ -120,44 +97,36 @@ export default function ServerDetailChart({
|
||||
<NetworkChart now={nezhaWsData.now} data={server} />
|
||||
<ConnectChart now={nezhaWsData.now} data={server} />
|
||||
</section>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function GpuChart({
|
||||
now,
|
||||
gpuStat,
|
||||
gpuName,
|
||||
}: {
|
||||
now: number;
|
||||
gpuStat: number;
|
||||
gpuName?: string;
|
||||
}) {
|
||||
const [gpuChartData, setGpuChartData] = useState([] as gpuChartData[]);
|
||||
function GpuChart({ now, gpuStat, gpuName }: { now: number; gpuStat: number; gpuName?: string }) {
|
||||
const [gpuChartData, setGpuChartData] = useState([] as gpuChartData[])
|
||||
|
||||
useEffect(() => {
|
||||
if (gpuStat) {
|
||||
const timestamp = Date.now().toString();
|
||||
let newData = [] as gpuChartData[];
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as gpuChartData[]
|
||||
if (gpuChartData.length === 0) {
|
||||
newData = [
|
||||
{ timeStamp: timestamp, gpu: gpuStat },
|
||||
{ timeStamp: timestamp, gpu: gpuStat },
|
||||
];
|
||||
]
|
||||
} else {
|
||||
newData = [...gpuChartData, { timeStamp: timestamp, gpu: gpuStat }];
|
||||
newData = [...gpuChartData, { timeStamp: timestamp, gpu: gpuStat }]
|
||||
}
|
||||
if (newData.length > 30) {
|
||||
newData.shift();
|
||||
newData.shift()
|
||||
}
|
||||
setGpuChartData(newData);
|
||||
setGpuChartData(newData)
|
||||
}
|
||||
}, [now, gpuStat]);
|
||||
}, [now, gpuStat])
|
||||
|
||||
const chartConfig = {
|
||||
gpu: {
|
||||
label: "GPU",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
} satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -169,9 +138,7 @@ function GpuChart({
|
||||
{gpuName && <p className="text-xs mt-1 mb-1.5">GPU: {gpuName}</p>}
|
||||
</section>
|
||||
<section className="flex items-center gap-2">
|
||||
<p className="text-xs text-end w-10 font-medium">
|
||||
{gpuStat.toFixed(0)}%
|
||||
</p>
|
||||
<p className="text-xs text-end w-10 font-medium">{gpuStat.toFixed(0)}%</p>
|
||||
<AnimatedCircularProgressBar
|
||||
className="size-3 text-[0px]"
|
||||
max={100}
|
||||
@ -181,10 +148,7 @@ function GpuChart({
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[130px] w-full"
|
||||
>
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={gpuChartData}
|
||||
@ -225,38 +189,38 @@ function GpuChart({
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[]);
|
||||
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[])
|
||||
|
||||
const { cpu } = formatNezhaInfo(now, data);
|
||||
const { cpu } = formatNezhaInfo(now, data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const timestamp = Date.now().toString();
|
||||
let newData = [] as cpuChartData[];
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as cpuChartData[]
|
||||
if (cpuChartData.length === 0) {
|
||||
newData = [
|
||||
{ timeStamp: timestamp, cpu: cpu },
|
||||
{ timeStamp: timestamp, cpu: cpu },
|
||||
];
|
||||
]
|
||||
} else {
|
||||
newData = [...cpuChartData, { timeStamp: timestamp, cpu: cpu }];
|
||||
newData = [...cpuChartData, { timeStamp: timestamp, cpu: cpu }]
|
||||
}
|
||||
if (newData.length > 30) {
|
||||
newData.shift();
|
||||
newData.shift()
|
||||
}
|
||||
setCpuChartData(newData);
|
||||
setCpuChartData(newData)
|
||||
}
|
||||
}, [data]);
|
||||
}, [data])
|
||||
|
||||
const chartConfig = {
|
||||
cpu: {
|
||||
label: "CPU",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
} satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -265,9 +229,7 @@ function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-md font-medium">CPU</p>
|
||||
<section className="flex items-center gap-2">
|
||||
<p className="text-xs text-end w-10 font-medium">
|
||||
{cpu.toFixed(0)}%
|
||||
</p>
|
||||
<p className="text-xs text-end w-10 font-medium">{cpu.toFixed(0)}%</p>
|
||||
<AnimatedCircularProgressBar
|
||||
className="size-3 text-[0px]"
|
||||
max={100}
|
||||
@ -277,10 +239,7 @@ function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[130px] w-full"
|
||||
>
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={cpuChartData}
|
||||
@ -321,61 +280,51 @@ function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ProcessChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
const { t } = useTranslation();
|
||||
const [processChartData, setProcessChartData] = useState(
|
||||
[] as processChartData[],
|
||||
);
|
||||
const { t } = useTranslation()
|
||||
const [processChartData, setProcessChartData] = useState([] as processChartData[])
|
||||
|
||||
const { process } = formatNezhaInfo(now, data);
|
||||
const { process } = formatNezhaInfo(now, data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const timestamp = Date.now().toString();
|
||||
let newData = [] as processChartData[];
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as processChartData[]
|
||||
if (processChartData.length === 0) {
|
||||
newData = [
|
||||
{ timeStamp: timestamp, process: process },
|
||||
{ timeStamp: timestamp, process: process },
|
||||
];
|
||||
]
|
||||
} else {
|
||||
newData = [
|
||||
...processChartData,
|
||||
{ timeStamp: timestamp, process: process },
|
||||
];
|
||||
newData = [...processChartData, { timeStamp: timestamp, process: process }]
|
||||
}
|
||||
if (newData.length > 30) {
|
||||
newData.shift();
|
||||
newData.shift()
|
||||
}
|
||||
setProcessChartData(newData);
|
||||
setProcessChartData(newData)
|
||||
}
|
||||
}, [data]);
|
||||
}, [data])
|
||||
|
||||
const chartConfig = {
|
||||
process: {
|
||||
label: "Process",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
} satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-md font-medium">
|
||||
{t("serverDetailChart.process")}
|
||||
</p>
|
||||
<p className="text-md font-medium">{t("serverDetailChart.process")}</p>
|
||||
<section className="flex items-center gap-2">
|
||||
<p className="text-xs text-end w-10 font-medium">{process}</p>
|
||||
</section>
|
||||
</div>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[130px] w-full"
|
||||
>
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={processChartData}
|
||||
@ -395,12 +344,7 @@ function ProcessChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
interval="preserveStartEnd"
|
||||
tickFormatter={(value) => formatRelativeTime(value)}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
mirror={true}
|
||||
tickMargin={-15}
|
||||
/>
|
||||
<YAxis tickLine={false} axisLine={false} mirror={true} tickMargin={-15} />
|
||||
<Area
|
||||
isAnimationActive={false}
|
||||
dataKey="process"
|
||||
@ -414,36 +358,33 @@ function ProcessChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
const { t } = useTranslation();
|
||||
const [memChartData, setMemChartData] = useState([] as memChartData[]);
|
||||
const { t } = useTranslation()
|
||||
const [memChartData, setMemChartData] = useState([] as memChartData[])
|
||||
|
||||
const { mem, swap } = formatNezhaInfo(now, data);
|
||||
const { mem, swap } = formatNezhaInfo(now, data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const timestamp = Date.now().toString();
|
||||
let newData = [] as memChartData[];
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as memChartData[]
|
||||
if (memChartData.length === 0) {
|
||||
newData = [
|
||||
{ timeStamp: timestamp, mem: mem, swap: swap },
|
||||
{ timeStamp: timestamp, mem: mem, swap: swap },
|
||||
];
|
||||
]
|
||||
} else {
|
||||
newData = [
|
||||
...memChartData,
|
||||
{ timeStamp: timestamp, mem: mem, swap: swap },
|
||||
];
|
||||
newData = [...memChartData, { timeStamp: timestamp, mem: mem, swap: swap }]
|
||||
}
|
||||
if (newData.length > 30) {
|
||||
newData.shift();
|
||||
newData.shift()
|
||||
}
|
||||
setMemChartData(newData);
|
||||
setMemChartData(newData)
|
||||
}
|
||||
}, [data]);
|
||||
}, [data])
|
||||
|
||||
const chartConfig = {
|
||||
mem: {
|
||||
@ -452,7 +393,7 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
swap: {
|
||||
label: "Swap",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
} satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -461,9 +402,7 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
<div className="flex items-center justify-between">
|
||||
<section className="flex items-center gap-4">
|
||||
<div className="flex flex-col">
|
||||
<p className=" text-xs text-muted-foreground">
|
||||
{t("serverDetailChart.mem")}
|
||||
</p>
|
||||
<p className=" text-xs text-muted-foreground">{t("serverDetailChart.mem")}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<AnimatedCircularProgressBar
|
||||
className="size-3 text-[0px]"
|
||||
@ -476,9 +415,7 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className=" text-xs text-muted-foreground">
|
||||
{t("serverDetailChart.swap")}
|
||||
</p>
|
||||
<p className=" text-xs text-muted-foreground">{t("serverDetailChart.swap")}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<AnimatedCircularProgressBar
|
||||
className="size-3 text-[0px]"
|
||||
@ -493,14 +430,12 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
</section>
|
||||
<section className="flex flex-col items-end gap-0.5">
|
||||
<div className="flex text-[11px] font-medium items-center gap-2">
|
||||
{formatBytes(data.state.mem_used)} /{" "}
|
||||
{formatBytes(data.host.mem_total)}
|
||||
{formatBytes(data.state.mem_used)} / {formatBytes(data.host.mem_total)}
|
||||
</div>
|
||||
<div className="flex text-[11px] font-medium items-center gap-2">
|
||||
{data.host.swap_total ? (
|
||||
<>
|
||||
swap: {formatBytes(data.state.swap_used)} /{" "}
|
||||
{formatBytes(data.host.swap_total)}
|
||||
swap: {formatBytes(data.state.swap_used)} / {formatBytes(data.host.swap_total)}
|
||||
</>
|
||||
) : (
|
||||
<>no swap</>
|
||||
@ -508,10 +443,7 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[130px] w-full"
|
||||
>
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={memChartData}
|
||||
@ -560,39 +492,39 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
const { t } = useTranslation();
|
||||
const [diskChartData, setDiskChartData] = useState([] as diskChartData[]);
|
||||
const { t } = useTranslation()
|
||||
const [diskChartData, setDiskChartData] = useState([] as diskChartData[])
|
||||
|
||||
const { disk } = formatNezhaInfo(now, data);
|
||||
const { disk } = formatNezhaInfo(now, data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const timestamp = Date.now().toString();
|
||||
let newData = [] as diskChartData[];
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as diskChartData[]
|
||||
if (diskChartData.length === 0) {
|
||||
newData = [
|
||||
{ timeStamp: timestamp, disk: disk },
|
||||
{ timeStamp: timestamp, disk: disk },
|
||||
];
|
||||
]
|
||||
} else {
|
||||
newData = [...diskChartData, { timeStamp: timestamp, disk: disk }];
|
||||
newData = [...diskChartData, { timeStamp: timestamp, disk: disk }]
|
||||
}
|
||||
if (newData.length > 30) {
|
||||
newData.shift();
|
||||
newData.shift()
|
||||
}
|
||||
setDiskChartData(newData);
|
||||
setDiskChartData(newData)
|
||||
}
|
||||
}, [data]);
|
||||
}, [data])
|
||||
|
||||
const chartConfig = {
|
||||
disk: {
|
||||
label: "Disk",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
} satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -602,9 +534,7 @@ function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
<p className="text-md font-medium">{t("serverDetailChart.disk")}</p>
|
||||
<section className="flex flex-col items-end gap-0.5">
|
||||
<section className="flex items-center gap-2">
|
||||
<p className="text-xs text-end w-10 font-medium">
|
||||
{disk.toFixed(0)}%
|
||||
</p>
|
||||
<p className="text-xs text-end w-10 font-medium">{disk.toFixed(0)}%</p>
|
||||
<AnimatedCircularProgressBar
|
||||
className="size-3 text-[0px]"
|
||||
max={100}
|
||||
@ -614,15 +544,11 @@ function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
/>
|
||||
</section>
|
||||
<div className="flex text-[11px] font-medium items-center gap-2">
|
||||
{formatBytes(data.state.disk_used)} /{" "}
|
||||
{formatBytes(data.host.disk_total)}
|
||||
{formatBytes(data.state.disk_used)} / {formatBytes(data.host.disk_total)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[130px] w-full"
|
||||
>
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={diskChartData}
|
||||
@ -663,43 +589,38 @@ function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
const { t } = useTranslation();
|
||||
const [networkChartData, setNetworkChartData] = useState(
|
||||
[] as networkChartData[],
|
||||
);
|
||||
const { t } = useTranslation()
|
||||
const [networkChartData, setNetworkChartData] = useState([] as networkChartData[])
|
||||
|
||||
const { up, down } = formatNezhaInfo(now, data);
|
||||
const { up, down } = formatNezhaInfo(now, data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const timestamp = Date.now().toString();
|
||||
let newData = [] as networkChartData[];
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as networkChartData[]
|
||||
if (networkChartData.length === 0) {
|
||||
newData = [
|
||||
{ timeStamp: timestamp, upload: up, download: down },
|
||||
{ timeStamp: timestamp, upload: up, download: down },
|
||||
];
|
||||
]
|
||||
} else {
|
||||
newData = [
|
||||
...networkChartData,
|
||||
{ timeStamp: timestamp, upload: up, download: down },
|
||||
];
|
||||
newData = [...networkChartData, { timeStamp: timestamp, upload: up, download: down }]
|
||||
}
|
||||
if (newData.length > 30) {
|
||||
newData.shift();
|
||||
newData.shift()
|
||||
}
|
||||
setNetworkChartData(newData);
|
||||
setNetworkChartData(newData)
|
||||
}
|
||||
}, [data]);
|
||||
}, [data])
|
||||
|
||||
let maxDownload = Math.max(...networkChartData.map((item) => item.download));
|
||||
maxDownload = Math.ceil(maxDownload);
|
||||
let maxDownload = Math.max(...networkChartData.map((item) => item.download))
|
||||
maxDownload = Math.ceil(maxDownload)
|
||||
if (maxDownload < 1) {
|
||||
maxDownload = 1;
|
||||
maxDownload = 1
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
@ -709,7 +630,7 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
download: {
|
||||
label: "Download",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
} satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -718,18 +639,14 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
<div className="flex items-center">
|
||||
<section className="flex items-center gap-4">
|
||||
<div className="flex flex-col w-20">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetailChart.upload")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetailChart.upload")}</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]"></span>
|
||||
<p className="text-xs font-medium">{up.toFixed(2)} M/s</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-20">
|
||||
<p className=" text-xs text-muted-foreground">
|
||||
{t("serverDetailChart.download")}
|
||||
</p>
|
||||
<p className=" text-xs text-muted-foreground">{t("serverDetailChart.download")}</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]"></span>
|
||||
<p className="text-xs font-medium">{down.toFixed(2)} M/s</p>
|
||||
@ -737,10 +654,7 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[130px] w-full"
|
||||
>
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={networkChartData}
|
||||
@ -792,37 +706,32 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ConnectChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
const [connectChartData, setConnectChartData] = useState(
|
||||
[] as connectChartData[],
|
||||
);
|
||||
const [connectChartData, setConnectChartData] = useState([] as connectChartData[])
|
||||
|
||||
const { tcp, udp } = formatNezhaInfo(now, data);
|
||||
const { tcp, udp } = formatNezhaInfo(now, data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const timestamp = Date.now().toString();
|
||||
let newData = [] as connectChartData[];
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as connectChartData[]
|
||||
if (connectChartData.length === 0) {
|
||||
newData = [
|
||||
{ timeStamp: timestamp, tcp: tcp, udp: udp },
|
||||
{ timeStamp: timestamp, tcp: tcp, udp: udp },
|
||||
];
|
||||
]
|
||||
} else {
|
||||
newData = [
|
||||
...connectChartData,
|
||||
{ timeStamp: timestamp, tcp: tcp, udp: udp },
|
||||
];
|
||||
newData = [...connectChartData, { timeStamp: timestamp, tcp: tcp, udp: udp }]
|
||||
}
|
||||
if (newData.length > 30) {
|
||||
newData.shift();
|
||||
newData.shift()
|
||||
}
|
||||
setConnectChartData(newData);
|
||||
setConnectChartData(newData)
|
||||
}
|
||||
}, [data]);
|
||||
}, [data])
|
||||
|
||||
const chartConfig = {
|
||||
tcp: {
|
||||
@ -831,7 +740,7 @@ function ConnectChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
udp: {
|
||||
label: "UDP",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
} satisfies ChartConfig
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -855,10 +764,7 @@ function ConnectChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[130px] w-full"
|
||||
>
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={connectChartData}
|
||||
@ -907,5 +813,5 @@ function ConnectChart({ now, data }: { now: number; data: NezhaServer }) {
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,41 +1,35 @@
|
||||
import { BackIcon } from "@/components/Icon";
|
||||
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading";
|
||||
import ServerFlag from "@/components/ServerFlag";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||
import { cn, formatNezhaInfo } from "@/lib/utils";
|
||||
import { NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import { BackIcon } from "@/components/Icon"
|
||||
import ServerFlag from "@/components/ServerFlag"
|
||||
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { cn, formatNezhaInfo } from "@/lib/utils"
|
||||
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
export default function ServerDetailOverview({
|
||||
server_id,
|
||||
}: {
|
||||
server_id: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
export default function ServerDetailOverview({ server_id }: { server_id: string }) {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { lastMessage, connected } = useWebSocketContext();
|
||||
const { lastMessage, connected } = useWebSocketContext()
|
||||
|
||||
if (!connected) {
|
||||
return <ServerDetailLoading />;
|
||||
return <ServerDetailLoading />
|
||||
}
|
||||
|
||||
const nezhaWsData = lastMessage
|
||||
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
|
||||
: null;
|
||||
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
|
||||
|
||||
if (!nezhaWsData) {
|
||||
return <ServerDetailLoading />;
|
||||
return <ServerDetailLoading />
|
||||
}
|
||||
|
||||
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
|
||||
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id))
|
||||
|
||||
if (!server) {
|
||||
return <ServerDetailLoading />;
|
||||
return <ServerDetailLoading />
|
||||
}
|
||||
|
||||
const {
|
||||
@ -57,7 +51,7 @@ export default function ServerDetailOverview({
|
||||
net_out_transfer,
|
||||
net_in_transfer,
|
||||
last_active_time_string,
|
||||
} = formatNezhaInfo(nezhaWsData.now, server);
|
||||
} = formatNezhaInfo(nezhaWsData.now, server)
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -72,9 +66,7 @@ export default function ServerDetailOverview({
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.status")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.status")}</p>
|
||||
<Badge
|
||||
className={cn(
|
||||
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
|
||||
@ -93,13 +85,10 @@ export default function ServerDetailOverview({
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.uptime")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.uptime")}</p>
|
||||
<div className="text-xs">
|
||||
{" "}
|
||||
{online ? (uptime / 86400).toFixed(0) : "N/A"}{" "}
|
||||
{t("serverDetail.days")}
|
||||
{online ? (uptime / 86400).toFixed(0) : "N/A"} {t("serverDetail.days")}
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
@ -109,9 +98,7 @@ export default function ServerDetailOverview({
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.version")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.version")}</p>
|
||||
<div className="text-xs">{version} </div>
|
||||
</section>
|
||||
</CardContent>
|
||||
@ -121,9 +108,7 @@ export default function ServerDetailOverview({
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.arch")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.arch")}</p>
|
||||
<div className="text-xs">{arch} </div>
|
||||
</section>
|
||||
</CardContent>
|
||||
@ -134,9 +119,7 @@ export default function ServerDetailOverview({
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.mem")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.mem")}</p>
|
||||
<div className="text-xs">{formatBytes(mem_total)}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
@ -147,9 +130,7 @@ export default function ServerDetailOverview({
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.disk")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.disk")}</p>
|
||||
<div className="text-xs">{formatBytes(disk_total)}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
@ -160,18 +141,11 @@ export default function ServerDetailOverview({
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.region")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p>
|
||||
<section className="flex items-start gap-1">
|
||||
<div className="text-xs text-start">
|
||||
{country_code?.toUpperCase()}
|
||||
</div>
|
||||
<div className="text-xs text-start">{country_code?.toUpperCase()}</div>
|
||||
{country_code && (
|
||||
<ServerFlag
|
||||
className="text-[11px] -mt-[1px]"
|
||||
country_code={country_code}
|
||||
/>
|
||||
<ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
@ -184,9 +158,7 @@ export default function ServerDetailOverview({
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.system")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.system")}</p>
|
||||
<div className="text-xs">
|
||||
{" "}
|
||||
{platform} {platform_version ? " - " + platform_version : ""}
|
||||
@ -231,14 +203,9 @@ export default function ServerDetailOverview({
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.upload")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.upload")}</p>
|
||||
{net_out_transfer ? (
|
||||
<div className="text-xs">
|
||||
{" "}
|
||||
{formatBytes(net_out_transfer)}{" "}
|
||||
</div>
|
||||
<div className="text-xs"> {formatBytes(net_out_transfer)} </div>
|
||||
) : (
|
||||
<div className="text-xs"> {t("serverDetail.unknown")}</div>
|
||||
)}
|
||||
@ -250,14 +217,9 @@ export default function ServerDetailOverview({
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.download")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.download")}</p>
|
||||
{net_in_transfer ? (
|
||||
<div className="text-xs">
|
||||
{" "}
|
||||
{formatBytes(net_in_transfer)}{" "}
|
||||
</div>
|
||||
<div className="text-xs"> {formatBytes(net_in_transfer)} </div>
|
||||
) : (
|
||||
<div className="text-xs"> {t("serverDetail.unknown")}</div>
|
||||
)}
|
||||
@ -275,8 +237,7 @@ export default function ServerDetailOverview({
|
||||
<section className="flex items-start flex-wrap gap-2">
|
||||
{server?.state.temperatures.map((item, index) => (
|
||||
<div className="text-xs flex items-center" key={index}>
|
||||
<p className="font-semibold">{item.Name}</p>:{" "}
|
||||
{item.Temperature.toFixed(2)} °C
|
||||
<p className="font-semibold">{item.Name}</p>: {item.Temperature.toFixed(2)} °C
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
@ -289,9 +250,7 @@ export default function ServerDetailOverview({
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.lastActive")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.lastActive")}</p>
|
||||
<div className="text-xs">
|
||||
{last_active_time_string ? last_active_time_string : "N/A"}
|
||||
</div>
|
||||
@ -300,5 +259,5 @@ export default function ServerDetailOverview({
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,38 +1,38 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import getUnicodeFlagIcon from "country-flag-icons/unicode";
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils"
|
||||
import getUnicodeFlagIcon from "country-flag-icons/unicode"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export default function ServerFlag({
|
||||
country_code,
|
||||
className,
|
||||
}: {
|
||||
country_code: string;
|
||||
className?: string;
|
||||
country_code: string
|
||||
className?: string
|
||||
}) {
|
||||
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
|
||||
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkEmojiSupport = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试
|
||||
if (!ctx) return;
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.textBaseline = "top";
|
||||
ctx.font = "32px Arial";
|
||||
ctx.fillText(emojiFlag, 0, 0);
|
||||
const canvas = document.createElement("canvas")
|
||||
const ctx = canvas.getContext("2d")
|
||||
const emojiFlag = "🇺🇸" // 使用美国国旗作为测试
|
||||
if (!ctx) return
|
||||
ctx.fillStyle = "#000"
|
||||
ctx.textBaseline = "top"
|
||||
ctx.font = "32px Arial"
|
||||
ctx.fillText(emojiFlag, 0, 0)
|
||||
|
||||
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0;
|
||||
setSupportsEmojiFlags(support);
|
||||
};
|
||||
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0
|
||||
setSupportsEmojiFlags(support)
|
||||
}
|
||||
|
||||
checkEmojiSupport();
|
||||
}, []);
|
||||
checkEmojiSupport()
|
||||
}, [])
|
||||
|
||||
if (!country_code) return null;
|
||||
if (!country_code) return null
|
||||
|
||||
if (supportsEmojiFlags && country_code.toLowerCase() === "tw") {
|
||||
country_code = "cn";
|
||||
country_code = "cn"
|
||||
}
|
||||
|
||||
return (
|
||||
@ -43,5 +43,5 @@ export default function ServerFlag({
|
||||
getUnicodeFlagIcon(country_code)
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,23 +1,20 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import {
|
||||
ArrowDownCircleIcon,
|
||||
ArrowUpCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { useStatus } from "@/hooks/use-status";
|
||||
import useFilter from "@/hooks/use-filter";
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import useFilter from "@/hooks/use-filter"
|
||||
import { useStatus } from "@/hooks/use-status"
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
type ServerOverviewProps = {
|
||||
online: number;
|
||||
offline: number;
|
||||
total: number;
|
||||
up: number;
|
||||
down: number;
|
||||
upSpeed: number;
|
||||
downSpeed: number;
|
||||
};
|
||||
online: number
|
||||
offline: number
|
||||
total: number
|
||||
up: number
|
||||
down: number
|
||||
upSpeed: number
|
||||
downSpeed: number
|
||||
}
|
||||
|
||||
export default function ServerOverview({
|
||||
online,
|
||||
@ -28,25 +25,23 @@ export default function ServerOverview({
|
||||
upSpeed,
|
||||
downSpeed,
|
||||
}: ServerOverviewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { status, setStatus } = useStatus();
|
||||
const { filter, setFilter } = useFilter();
|
||||
const { t } = useTranslation()
|
||||
const { status, setStatus } = useStatus()
|
||||
const { filter, setFilter } = useFilter()
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<Card
|
||||
onClick={() => {
|
||||
setFilter(false);
|
||||
setStatus("all");
|
||||
setFilter(false)
|
||||
setStatus("all")
|
||||
}}
|
||||
className={cn("hover:border-blue-500 cursor-pointer transition-all")}
|
||||
>
|
||||
<CardContent className="flex h-full items-center px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium md:text-base">
|
||||
{t("serverOverview.totalServers")}
|
||||
</p>
|
||||
<p className="text-sm font-medium md:text-base">{t("serverOverview.totalServers")}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
|
||||
@ -58,8 +53,8 @@ export default function ServerOverview({
|
||||
</Card>
|
||||
<Card
|
||||
onClick={() => {
|
||||
setFilter(false);
|
||||
setStatus("online");
|
||||
setFilter(false)
|
||||
setStatus("online")
|
||||
}}
|
||||
className={cn(
|
||||
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
|
||||
@ -86,8 +81,8 @@ export default function ServerOverview({
|
||||
</Card>
|
||||
<Card
|
||||
onClick={() => {
|
||||
setFilter(false);
|
||||
setStatus("offline");
|
||||
setFilter(false)
|
||||
setStatus("offline")
|
||||
}}
|
||||
className={cn(
|
||||
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
|
||||
@ -113,8 +108,8 @@ export default function ServerOverview({
|
||||
</Card>
|
||||
<Card
|
||||
onClick={() => {
|
||||
setStatus("all");
|
||||
setFilter(true);
|
||||
setStatus("all")
|
||||
setFilter(true)
|
||||
}}
|
||||
className={cn(
|
||||
"cursor-pointer hover:ring-purple-500 ring-1 ring-transparent transition-all",
|
||||
@ -126,9 +121,7 @@ export default function ServerOverview({
|
||||
<CardContent className="flex h-full items-center relative px-6 py-3">
|
||||
<section className="flex flex-col gap-1 w-full">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<p className="text-sm font-medium md:text-base">
|
||||
{t("serverOverview.network")}
|
||||
</p>
|
||||
<p className="text-sm font-medium md:text-base">{t("serverOverview.network")}</p>
|
||||
</div>
|
||||
<section className="flex items-start flex-row z-[999] pr-2 sm:pr-0 gap-1">
|
||||
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">
|
||||
@ -153,5 +146,5 @@ export default function ServerOverview({
|
||||
</Card>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
type ServerUsageBarProps = {
|
||||
value: number;
|
||||
};
|
||||
value: number
|
||||
}
|
||||
|
||||
export default function ServerUsageBar({ value }: ServerUsageBarProps) {
|
||||
return (
|
||||
@ -10,14 +10,8 @@ export default function ServerUsageBar({ value }: ServerUsageBarProps) {
|
||||
aria-label={"Server Usage Bar"}
|
||||
aria-labelledby={"Server Usage Bar"}
|
||||
value={value}
|
||||
indicatorClassName={
|
||||
value > 90
|
||||
? "bg-red-500"
|
||||
: value > 70
|
||||
? "bg-orange-400"
|
||||
: "bg-green-500"
|
||||
}
|
||||
indicatorClassName={value > 90 ? "bg-red-500" : value > 70 ? "bg-orange-400" : "bg-green-500"}
|
||||
className={"h-[3px] rounded-sm"}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,43 +1,42 @@
|
||||
import React from "react";
|
||||
import ServiceTrackerClient from "./ServiceTrackerClient";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchService } from "@/lib/nezha-api";
|
||||
import { ServiceData } from "@/types/nezha-api";
|
||||
import { CycleTransferStatsCard } from "./CycleTransferStats";
|
||||
import { Loader } from "./loading/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||
import { fetchService } from "@/lib/nezha-api"
|
||||
import { ServiceData } from "@/types/nezha-api"
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import React from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { CycleTransferStatsCard } from "./CycleTransferStats"
|
||||
import ServiceTrackerClient from "./ServiceTrackerClient"
|
||||
import { Loader } from "./loading/Loader"
|
||||
|
||||
export const ServiceTracker: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
const { data: serviceData, isLoading } = useQuery({
|
||||
queryKey: ["service"],
|
||||
queryFn: () => fetchService(),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
})
|
||||
|
||||
const processServiceData = (serviceData: ServiceData) => {
|
||||
const days = serviceData.up.map((up, index) => ({
|
||||
completed: up > serviceData.down[index],
|
||||
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
|
||||
}));
|
||||
}))
|
||||
|
||||
const totalUp = serviceData.up.reduce((a, b) => a + b, 0);
|
||||
const totalUp = serviceData.up.reduce((a, b) => a + b, 0)
|
||||
const totalChecks =
|
||||
serviceData.up.reduce((a, b) => a + b, 0) +
|
||||
serviceData.down.reduce((a, b) => a + b, 0);
|
||||
const uptime = (totalUp / totalChecks) * 100;
|
||||
serviceData.up.reduce((a, b) => a + b, 0) + serviceData.down.reduce((a, b) => a + b, 0)
|
||||
const uptime = (totalUp / totalChecks) * 100
|
||||
|
||||
const avgDelay =
|
||||
serviceData.delay.length > 0
|
||||
? serviceData.delay.reduce((a, b) => a + b, 0) /
|
||||
serviceData.delay.length
|
||||
: 0;
|
||||
? serviceData.delay.reduce((a, b) => a + b, 0) / serviceData.delay.length
|
||||
: 0
|
||||
|
||||
return { days, uptime, avgDelay };
|
||||
};
|
||||
return { days, uptime, avgDelay }
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@ -45,49 +44,43 @@ export const ServiceTracker: React.FC = () => {
|
||||
<Loader visible={true} />
|
||||
{t("serviceTracker.loading")}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
!serviceData?.data?.services &&
|
||||
!serviceData?.data?.cycle_transfer_stats
|
||||
) {
|
||||
if (!serviceData?.data?.services && !serviceData?.data?.cycle_transfer_stats) {
|
||||
return (
|
||||
<div className="mt-4 text-sm font-medium flex items-center gap-1">
|
||||
<ExclamationTriangleIcon className="w-4 h-4" />
|
||||
{t("serviceTracker.noService")}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 w-full mx-auto ">
|
||||
{serviceData.data.cycle_transfer_stats && (
|
||||
<div>
|
||||
<CycleTransferStatsCard
|
||||
cycleStats={serviceData.data.cycle_transfer_stats}
|
||||
/>
|
||||
<CycleTransferStatsCard cycleStats={serviceData.data.cycle_transfer_stats} />
|
||||
</div>
|
||||
)}
|
||||
{serviceData.data.services &&
|
||||
Object.keys(serviceData.data.services).length > 0 && (
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
|
||||
{Object.entries(serviceData.data.services).map(([name, data]) => {
|
||||
const { days, uptime, avgDelay } = processServiceData(data);
|
||||
return (
|
||||
<ServiceTrackerClient
|
||||
key={name}
|
||||
days={days}
|
||||
title={data.service_name}
|
||||
uptime={uptime}
|
||||
avgDelay={avgDelay}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
)}
|
||||
{serviceData.data.services && Object.keys(serviceData.data.services).length > 0 && (
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
|
||||
{Object.entries(serviceData.data.services).map(([name, data]) => {
|
||||
const { days, uptime, avgDelay } = processServiceData(data)
|
||||
return (
|
||||
<ServiceTrackerClient
|
||||
key={name}
|
||||
days={days}
|
||||
title={data.service_name}
|
||||
uptime={uptime}
|
||||
avgDelay={avgDelay}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceTracker;
|
||||
export default ServiceTracker
|
||||
|
@ -1,17 +1,18 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Separator } from "./ui/separator"
|
||||
|
||||
interface ServiceTrackerProps {
|
||||
days: Array<{
|
||||
completed: boolean;
|
||||
date?: Date;
|
||||
}>;
|
||||
className?: string;
|
||||
title?: string;
|
||||
uptime?: number;
|
||||
avgDelay?: number;
|
||||
completed: boolean
|
||||
date?: Date
|
||||
}>
|
||||
className?: string
|
||||
title?: string
|
||||
uptime?: number
|
||||
avgDelay?: number
|
||||
}
|
||||
|
||||
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
|
||||
@ -21,7 +22,7 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
|
||||
uptime = 100,
|
||||
avgDelay = 0,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@ -55,9 +56,7 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
|
||||
"flex-1 h-6 rounded-[5px] transition-colors",
|
||||
day.completed ? "bg-green-600" : "bg-red-500/60",
|
||||
)}
|
||||
title={
|
||||
day.date ? day.date.toLocaleDateString() : `Day ${index + 1}`
|
||||
}
|
||||
title={day.date ? day.date.toLocaleDateString() : `Day ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -67,7 +66,7 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
|
||||
<span>{t("serviceTracker.today")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceTrackerClient;
|
||||
export default ServiceTrackerClient
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { m } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils"
|
||||
import { m } from "framer-motion"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export default function TabSwitch({
|
||||
tabs,
|
||||
currentTab,
|
||||
setCurrentTab,
|
||||
}: {
|
||||
tabs: string[];
|
||||
currentTab: string;
|
||||
setCurrentTab: (tab: string) => void;
|
||||
tabs: string[]
|
||||
currentTab: string
|
||||
setCurrentTab: (tab: string) => void
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="z-50 flex flex-col items-start rounded-[50px]">
|
||||
<div className="flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800">
|
||||
@ -43,5 +43,5 @@ export default function TabSwitch({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,73 +1,60 @@
|
||||
import { createContext, useEffect, useState, ReactNode } from "react";
|
||||
import { ReactNode, createContext, useEffect, useState } from "react"
|
||||
|
||||
export type Theme = "dark" | "light" | "system";
|
||||
export type Theme = "dark" | "light" | "system"
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
children: ReactNode
|
||||
defaultTheme?: Theme
|
||||
storageKey?: string
|
||||
}
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
}
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
storageKey = "vite-ui-theme",
|
||||
}: ThemeProviderProps) {
|
||||
export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || "system",
|
||||
);
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
const root = window.document.documentElement
|
||||
|
||||
root.classList.remove("light", "dark");
|
||||
root.classList.remove("light", "dark")
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
: "light"
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
const themeColor =
|
||||
systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute("content", themeColor);
|
||||
return;
|
||||
root.classList.add(systemTheme)
|
||||
const themeColor = systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
||||
return
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute("content", themeColor);
|
||||
}, [theme]);
|
||||
root.classList.add(theme)
|
||||
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
||||
}, [theme])
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
localStorage.setItem(storageKey, theme)
|
||||
setTheme(theme)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>
|
||||
}
|
||||
|
||||
export { ThemeProviderContext };
|
||||
export { ThemeProviderContext }
|
||||
|
@ -1,25 +1,26 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Theme } from "@/components/ThemeProvider"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { Theme } from "@/components/ThemeProvider";
|
||||
import { useTheme } from "../hooks/use-theme";
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useTheme } from "../hooks/use-theme"
|
||||
|
||||
export function ModeToggle() {
|
||||
const { t } = useTranslation();
|
||||
const { setTheme, theme } = useTheme();
|
||||
const { t } = useTranslation()
|
||||
const { setTheme, theme } = useTheme()
|
||||
|
||||
const handleSelect = (e: Event, newTheme: Theme) => {
|
||||
e.preventDefault();
|
||||
setTheme(newTheme);
|
||||
};
|
||||
e.preventDefault()
|
||||
setTheme(newTheme)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@ -58,5 +59,5 @@ export function ModeToggle() {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
const bars = Array(8).fill(0);
|
||||
const bars = Array(8).fill(0)
|
||||
|
||||
export const Loader = ({ visible }: { visible: boolean }) => {
|
||||
return (
|
||||
@ -9,5 +9,5 @@ export const Loader = ({ visible }: { visible: boolean }) => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { BackIcon } from "../Icon";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
import { BackIcon } from "../Icon"
|
||||
|
||||
export function ServerDetailChartLoading() {
|
||||
return (
|
||||
@ -14,17 +15,17 @@ export function ServerDetailChartLoading() {
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function ServerDetailLoading() {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-5xl px-0">
|
||||
<div
|
||||
onClick={() => {
|
||||
navigate("/");
|
||||
navigate("/")
|
||||
}}
|
||||
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
|
||||
>
|
||||
@ -33,5 +34,5 @@ export function ServerDetailLoading() {
|
||||
</div>
|
||||
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export { domMax as default } from "framer-motion";
|
||||
export { domMax as default } from "framer-motion"
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { LazyMotion } from "framer-motion";
|
||||
import { LazyMotion } from "framer-motion"
|
||||
|
||||
const loadFeatures = () =>
|
||||
import("./framer-lazy-feature").then((res) => res.default);
|
||||
const loadFeatures = () => import("./framer-lazy-feature").then((res) => res.default)
|
||||
|
||||
export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<LazyMotion features={loadFeatures} strict key="framer">
|
||||
{children}
|
||||
</LazyMotion>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface Props {
|
||||
max: number;
|
||||
value: number;
|
||||
min: number;
|
||||
className?: string;
|
||||
primaryColor?: string;
|
||||
max: number
|
||||
value: number
|
||||
min: number
|
||||
className?: string
|
||||
primaryColor?: string
|
||||
}
|
||||
|
||||
export default function AnimatedCircularProgressBar({
|
||||
@ -15,9 +15,9 @@ export default function AnimatedCircularProgressBar({
|
||||
primaryColor,
|
||||
className,
|
||||
}: Props) {
|
||||
const circumference = 2 * Math.PI * 45;
|
||||
const percentPx = circumference / 100;
|
||||
const currentPercent = ((value - min) / (max - min)) * 100;
|
||||
const circumference = 2 * Math.PI * 45
|
||||
const percentPx = circumference / 100
|
||||
const currentPercent = ((value - min) / (max - min)) * 100
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -37,12 +37,7 @@ export default function AnimatedCircularProgressBar({
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
className="size-full"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
<svg fill="none" className="size-full" strokeWidth="2" viewBox="0 0 100 100">
|
||||
{currentPercent <= 90 && currentPercent >= 0 && (
|
||||
<circle
|
||||
cx="50"
|
||||
@ -62,8 +57,7 @@ export default function AnimatedCircularProgressBar({
|
||||
transform:
|
||||
"rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
|
||||
transition: "all var(--transition-length) ease var(--delay)",
|
||||
transformOrigin:
|
||||
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
@ -90,8 +84,7 @@ export default function AnimatedCircularProgressBar({
|
||||
transitionProperty: "stroke-dasharray,transform",
|
||||
transform:
|
||||
"rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
|
||||
transformOrigin:
|
||||
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
@ -103,5 +96,5 @@ export default function AnimatedCircularProgressBar({
|
||||
{currentPercent}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,15 +1,13 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
import { type VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
@ -21,16 +19,14 @@ const badgeVariants = cva(
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
export { Badge, badgeVariants }
|
||||
|
@ -1,8 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { type VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
@ -10,12 +9,9 @@ const buttonVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
@ -31,26 +27,22 @@ const buttonVariants = cva(
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
)
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants };
|
||||
export { Button, buttonVariants }
|
||||
|
@ -1,85 +1,58 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as React from "react"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
|
@ -1,48 +1,45 @@
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context;
|
||||
return context
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"];
|
||||
config: ChartConfig
|
||||
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"]
|
||||
}
|
||||
>(({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
@ -56,22 +53,18 @@ const ChartContainer = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
});
|
||||
ChartContainer.displayName = "Chart";
|
||||
)
|
||||
})
|
||||
ChartContainer.displayName = "Chart"
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color,
|
||||
);
|
||||
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
@ -83,10 +76,8 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
@ -95,20 +86,20 @@ ${colorConfig
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
@ -129,49 +120,39 @@ const ChartTooltipContent = React.forwardRef<
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart();
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
<div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -184,9 +165,9 @@ const ChartTooltipContent = React.forwardRef<
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -245,112 +226,94 @@ const ChartTooltipContent = React.forwardRef<
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
);
|
||||
ChartTooltipContent.displayName = "ChartTooltip";
|
||||
)
|
||||
ChartTooltipContent.displayName = "ChartTooltip"
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart();
|
||||
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
ChartLegendContent.displayName = "ChartLegend";
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
ChartLegendContent.displayName = "ChartLegend"
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string,
|
||||
) {
|
||||
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key;
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config];
|
||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
|
||||
}
|
||||
|
||||
export {
|
||||
@ -360,4 +323,4 @@ export {
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
@ -16,13 +15,11 @@ const Checkbox = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox };
|
||||
export { Checkbox }
|
||||
|
@ -1,16 +1,15 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
@ -24,8 +23,8 @@ const DialogOverlay = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
@ -48,36 +47,21 @@ const DialogContent = React.forwardRef<
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
@ -85,14 +69,11 @@ const DialogTitle = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
@ -103,8 +84,8 @@ const DialogDescription = React.forwardRef<
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
@ -117,4 +98,4 @@ export {
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
}
|
||||
|
@ -1,25 +1,24 @@
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
@ -34,9 +33,8 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
@ -50,9 +48,8 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
))
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
@ -69,13 +66,13 @@ const DropdownMenuContent = React.forwardRef<
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
@ -87,8 +84,8 @@ const DropdownMenuItem = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
@ -110,9 +107,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
@ -133,26 +129,22 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
@ -163,21 +155,13 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
@ -195,4 +179,4 @@ export {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
@ -17,9 +15,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input };
|
||||
export { Input }
|
||||
|
@ -1,24 +1,18 @@
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { type VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label };
|
||||
export { Label }
|
||||
|
@ -1,31 +1,24 @@
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import * as React from "react"
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
|
||||
indicatorClassName?: string;
|
||||
indicatorClassName?: string
|
||||
}
|
||||
>(({ className, value, indicatorClassName, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className,
|
||||
)}
|
||||
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className={cn(
|
||||
"h-full w-full flex-1 bg-primary transition-all",
|
||||
indicatorClassName,
|
||||
)}
|
||||
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress };
|
||||
export { Progress }
|
||||
|
@ -1,29 +1,23 @@
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import * as React from "react"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator };
|
||||
export { Separator }
|
||||
|
@ -1,15 +1,7 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
export { Skeleton }
|
||||
|
@ -1,7 +1,6 @@
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
import * as React from "react"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
@ -21,7 +20,7 @@ const Switch = React.forwardRef<
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch };
|
||||
export { Switch }
|
||||
|
@ -1,40 +1,30 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
||||
</div>
|
||||
),
|
||||
)
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
@ -42,29 +32,25 @@ const TableFooter = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
@ -78,8 +64,8 @@ const TableHead = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
@ -90,28 +76,15 @@ const TableCell = React.forwardRef<
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
};
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { createContext } from "react";
|
||||
import { createContext } from "react"
|
||||
|
||||
export interface FilterContextType {
|
||||
filter: boolean;
|
||||
setFilter: (filter: boolean) => void;
|
||||
filter: boolean
|
||||
setFilter: (filter: boolean) => void
|
||||
}
|
||||
|
||||
export const FilterContext = createContext<FilterContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
export const FilterContext = createContext<FilterContextType | undefined>(undefined)
|
||||
|
@ -1,14 +1,11 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { ReactNode, useState } from "react";
|
||||
import { FilterContext } from "./filter-context";
|
||||
import { ReactNode, useState } from "react"
|
||||
|
||||
import { FilterContext } from "./filter-context"
|
||||
|
||||
export function FilterProvider({ children }: { children: ReactNode }) {
|
||||
const [filter, setFilter] = useState<boolean>(false);
|
||||
const [filter, setFilter] = useState<boolean>(false)
|
||||
|
||||
return (
|
||||
<FilterContext.Provider value={{ filter, setFilter }}>
|
||||
{children}
|
||||
</FilterContext.Provider>
|
||||
);
|
||||
return <FilterContext.Provider value={{ filter, setFilter }}>{children}</FilterContext.Provider>
|
||||
}
|
||||
|
@ -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 {
|
||||
status: Status;
|
||||
setStatus: (status: Status) => void;
|
||||
status: Status
|
||||
setStatus: (status: Status) => void
|
||||
}
|
||||
|
||||
export const StatusContext = createContext<StatusContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
export const StatusContext = createContext<StatusContextType | undefined>(undefined)
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Status, StatusContext } from "./status-context";
|
||||
import { ReactNode, useState } from "react"
|
||||
|
||||
import { Status, StatusContext } from "./status-context"
|
||||
|
||||
export function StatusProvider({ children }: { children: ReactNode }) {
|
||||
const [status, setStatus] = useState<Status>("all");
|
||||
const [status, setStatus] = useState<Status>("all")
|
||||
|
||||
return (
|
||||
<StatusContext.Provider value={{ status, setStatus }}>
|
||||
{children}
|
||||
</StatusContext.Provider>
|
||||
);
|
||||
return <StatusContext.Provider value={{ status, setStatus }}>{children}</StatusContext.Provider>
|
||||
}
|
||||
|
@ -1,20 +1,18 @@
|
||||
import { createContext } from "react";
|
||||
import { createContext } from "react"
|
||||
|
||||
export interface TooltipData {
|
||||
centroid: [number, number];
|
||||
country: string;
|
||||
count: number;
|
||||
centroid: [number, number]
|
||||
country: string
|
||||
count: number
|
||||
servers: Array<{
|
||||
name: string;
|
||||
status: boolean;
|
||||
}>;
|
||||
name: string
|
||||
status: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
interface TooltipContextType {
|
||||
tooltipData: TooltipData | null;
|
||||
setTooltipData: (data: TooltipData | null) => void;
|
||||
tooltipData: TooltipData | null
|
||||
setTooltipData: (data: TooltipData | null) => void
|
||||
}
|
||||
|
||||
export const TooltipContext = createContext<TooltipContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
export const TooltipContext = createContext<TooltipContextType | undefined>(undefined)
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { ReactNode, useState } from "react";
|
||||
import { TooltipContext, TooltipData } from "./tooltip-context";
|
||||
import { ReactNode, useState } from "react"
|
||||
|
||||
import { TooltipContext, TooltipData } from "./tooltip-context"
|
||||
|
||||
export function TooltipProvider({ children }: { children: ReactNode }) {
|
||||
const [tooltipData, setTooltipData] = useState<TooltipData | null>(null);
|
||||
const [tooltipData, setTooltipData] = useState<TooltipData | null>(null)
|
||||
|
||||
return (
|
||||
<TooltipContext.Provider value={{ tooltipData, setTooltipData }}>
|
||||
{children}
|
||||
</TooltipContext.Provider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { createContext } from "react";
|
||||
import { createContext } from "react"
|
||||
|
||||
export interface WebSocketContextType {
|
||||
lastMessage: { data: string } | null;
|
||||
connected: boolean;
|
||||
lastMessage: { data: string } | null
|
||||
connected: boolean
|
||||
}
|
||||
|
||||
export const WebSocketContext = createContext<WebSocketContextType>({
|
||||
lastMessage: null,
|
||||
connected: false,
|
||||
});
|
||||
})
|
||||
|
@ -1,81 +1,75 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { WebSocketContext, WebSocketContextType } from "./websocket-context";
|
||||
import React, { useEffect, useRef, useState } from "react"
|
||||
|
||||
import { WebSocketContext, WebSocketContextType } from "./websocket-context"
|
||||
|
||||
interface WebSocketProviderProps {
|
||||
url: string;
|
||||
children: React.ReactNode;
|
||||
url: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
|
||||
url,
|
||||
children,
|
||||
}) => {
|
||||
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const ws = useRef<WebSocket | null>(null);
|
||||
const reconnectTimeout = useRef<NodeJS.Timeout>(null);
|
||||
const maxReconnectAttempts = 30;
|
||||
const reconnectAttempts = useRef(0);
|
||||
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, children }) => {
|
||||
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null)
|
||||
const [connected, setConnected] = useState(false)
|
||||
const ws = useRef<WebSocket | null>(null)
|
||||
const reconnectTimeout = useRef<NodeJS.Timeout>(null)
|
||||
const maxReconnectAttempts = 30
|
||||
const reconnectAttempts = useRef(0)
|
||||
|
||||
const connect = () => {
|
||||
try {
|
||||
const wsUrl = new URL(url, window.location.origin);
|
||||
wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
|
||||
const wsUrl = new URL(url, window.location.origin)
|
||||
wsUrl.protocol = wsUrl.protocol.replace("http", "ws")
|
||||
|
||||
ws.current = new WebSocket(wsUrl.toString());
|
||||
ws.current = new WebSocket(wsUrl.toString())
|
||||
|
||||
ws.current.onopen = () => {
|
||||
console.log("WebSocket connected");
|
||||
setConnected(true);
|
||||
reconnectAttempts.current = 0;
|
||||
};
|
||||
console.log("WebSocket connected")
|
||||
setConnected(true)
|
||||
reconnectAttempts.current = 0
|
||||
}
|
||||
|
||||
ws.current.onclose = () => {
|
||||
console.log("WebSocket disconnected");
|
||||
setConnected(false);
|
||||
console.log("WebSocket disconnected")
|
||||
setConnected(false)
|
||||
|
||||
// 重连逻辑
|
||||
if (reconnectAttempts.current < maxReconnectAttempts) {
|
||||
reconnectTimeout.current = setTimeout(() => {
|
||||
reconnectAttempts.current++;
|
||||
connect();
|
||||
}, 3000);
|
||||
reconnectAttempts.current++
|
||||
connect()
|
||||
}, 3000)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ws.current.onmessage = (event) => {
|
||||
setLastMessage({ data: event.data });
|
||||
};
|
||||
setLastMessage({ data: event.data })
|
||||
}
|
||||
|
||||
ws.current.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
};
|
||||
console.error("WebSocket error:", error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("WebSocket connection error:", error);
|
||||
console.error("WebSocket connection error:", error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
connect()
|
||||
|
||||
return () => {
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
ws.current.close()
|
||||
}
|
||||
if (reconnectTimeout.current) {
|
||||
clearTimeout(reconnectTimeout.current);
|
||||
clearTimeout(reconnectTimeout.current)
|
||||
}
|
||||
};
|
||||
}, [url]);
|
||||
}
|
||||
}, [url])
|
||||
|
||||
const contextValue: WebSocketContextType = {
|
||||
lastMessage,
|
||||
connected,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<WebSocketContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
);
|
||||
};
|
||||
return <WebSocketContext.Provider value={contextValue}>{children}</WebSocketContext.Provider>
|
||||
}
|
||||
|
@ -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 context = useContext(FilterContext);
|
||||
const context = useContext(FilterContext)
|
||||
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
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
import { StatusContext } from "../context/status-context";
|
||||
import { useContext } from "react"
|
||||
|
||||
import { StatusContext } from "../context/status-context"
|
||||
|
||||
export function useStatus() {
|
||||
const context = useContext(StatusContext);
|
||||
const context = useContext(StatusContext)
|
||||
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
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { useContext } from "react";
|
||||
import { ThemeProviderContext } from "../components/ThemeProvider";
|
||||
import { useContext } from "react"
|
||||
|
||||
import { ThemeProviderContext } from "../components/ThemeProvider"
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
const context = useContext(ThemeProviderContext)
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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 = () => {
|
||||
const context = useContext(TooltipContext);
|
||||
const context = useContext(TooltipContext)
|
||||
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
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
import { WebSocketContext } from "../context/websocket-context";
|
||||
import { useContext } from "react"
|
||||
|
||||
import { WebSocketContext } from "../context/websocket-context"
|
||||
|
||||
export const useWebSocketContext = () => {
|
||||
const context = useContext(WebSocketContext);
|
||||
const context = useContext(WebSocketContext)
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useWebSocketContext must be used within a WebSocketProvider",
|
||||
);
|
||||
throw new Error("useWebSocketContext must be used within a WebSocketProvider")
|
||||
}
|
||||
return context;
|
||||
};
|
||||
return context
|
||||
}
|
||||
|
24
src/i18n.js
24
src/i18n.js
@ -1,9 +1,9 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import i18n from "i18next"
|
||||
import { initReactI18next } from "react-i18next"
|
||||
|
||||
import enTranslation from "./locales/en/translation.json";
|
||||
import zhCNTranslation from "./locales/zh-CN/translation.json";
|
||||
import zhTWTranslation from "./locales/zh-TW/translation.json";
|
||||
import enTranslation from "./locales/en/translation.json"
|
||||
import zhCNTranslation from "./locales/zh-CN/translation.json"
|
||||
import zhTWTranslation from "./locales/zh-TW/translation.json"
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
@ -15,11 +15,11 @@ const resources = {
|
||||
"zh-TW": {
|
||||
translation: zhTWTranslation,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const getStoredLanguage = () => {
|
||||
return localStorage.getItem("language") || "zh-CN";
|
||||
};
|
||||
return localStorage.getItem("language") || "zh-CN"
|
||||
}
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
@ -28,11 +28,11 @@ i18n.use(initReactI18next).init({
|
||||
interpolation: {
|
||||
escapeValue: false, // react已经安全地转义
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// 添加语言改变时的处理函数
|
||||
i18n.on("languageChanged", (lng) => {
|
||||
localStorage.setItem("language", lng);
|
||||
});
|
||||
localStorage.setItem("language", lng)
|
||||
})
|
||||
|
||||
export default i18n;
|
||||
export default i18n
|
||||
|
@ -1,21 +1,11 @@
|
||||
export function formatBytes(bytes: number, decimals: number = 2) {
|
||||
if (!+bytes) return "0 Bytes";
|
||||
if (!+bytes) return "0 Bytes"
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = [
|
||||
"Bytes",
|
||||
"KiB",
|
||||
"MiB",
|
||||
"GiB",
|
||||
"TiB",
|
||||
"PiB",
|
||||
"EiB",
|
||||
"ZiB",
|
||||
"YiB",
|
||||
];
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const sizes = ["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
@ -1,7 +1,4 @@
|
||||
export const countryCoordinates: Record<
|
||||
string,
|
||||
{ lat: number; lng: number; name: string }
|
||||
> = {
|
||||
export const countryCoordinates: Record<string, { lat: number; lng: number; name: string }> = {
|
||||
// 亚洲
|
||||
AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗
|
||||
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" }, // 西撒哈拉
|
||||
ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚
|
||||
ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { SVGProps } from "react";
|
||||
import type { SVGProps } from "react"
|
||||
|
||||
export function GetFontLogoClass(platform: string): string {
|
||||
if (
|
||||
@ -47,24 +47,24 @@ export function GetFontLogoClass(platform: string): string {
|
||||
"zorin",
|
||||
].indexOf(platform) > -1
|
||||
) {
|
||||
return platform;
|
||||
return platform
|
||||
}
|
||||
if (platform == "darwin") {
|
||||
return "apple";
|
||||
return "apple"
|
||||
}
|
||||
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
|
||||
return "tux";
|
||||
return "tux"
|
||||
}
|
||||
if (platform == "amazon") {
|
||||
return "redhat";
|
||||
return "redhat"
|
||||
}
|
||||
if (platform == "arch") {
|
||||
return "archlinux";
|
||||
return "archlinux"
|
||||
}
|
||||
if (platform.toLowerCase().includes("opensuse")) {
|
||||
return "opensuse";
|
||||
return "opensuse"
|
||||
}
|
||||
return "tux";
|
||||
return "tux"
|
||||
}
|
||||
|
||||
export function GetOsName(platform: string): string {
|
||||
@ -110,39 +110,33 @@ export function GetOsName(platform: string): string {
|
||||
"zorin",
|
||||
].indexOf(platform) > -1
|
||||
) {
|
||||
return platform.charAt(0).toUpperCase() + platform.slice(1);
|
||||
return platform.charAt(0).toUpperCase() + platform.slice(1)
|
||||
}
|
||||
if (platform == "darwin") {
|
||||
return "macOS";
|
||||
return "macOS"
|
||||
}
|
||||
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
|
||||
return "Linux";
|
||||
return "Linux"
|
||||
}
|
||||
if (platform == "amazon") {
|
||||
return "Redhat";
|
||||
return "Redhat"
|
||||
}
|
||||
if (platform == "arch") {
|
||||
return "Archlinux";
|
||||
return "Archlinux"
|
||||
}
|
||||
if (platform.toLowerCase().includes("opensuse")) {
|
||||
return "Opensuse";
|
||||
return "Opensuse"
|
||||
}
|
||||
return "Linux";
|
||||
return "Linux"
|
||||
}
|
||||
|
||||
export function MageMicrosoftWindows(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
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"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -4,51 +4,49 @@ import {
|
||||
ServerGroupResponse,
|
||||
ServiceResponse,
|
||||
SettingResponse,
|
||||
} from "@/types/nezha-api";
|
||||
} from "@/types/nezha-api"
|
||||
|
||||
export const fetchServerGroup = async (): Promise<ServerGroupResponse> => {
|
||||
const response = await fetch("/api/v1/server-group");
|
||||
const data = await response.json();
|
||||
const response = await fetch("/api/v1/server-group")
|
||||
const data = await response.json()
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
throw new Error(data.error)
|
||||
}
|
||||
return data;
|
||||
};
|
||||
return data
|
||||
}
|
||||
|
||||
export const fetchLoginUser = async (): Promise<LoginUserResponse> => {
|
||||
const response = await fetch("/api/v1/profile");
|
||||
const data = await response.json();
|
||||
const response = await fetch("/api/v1/profile")
|
||||
const data = await response.json()
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
throw new Error(data.error)
|
||||
}
|
||||
return data;
|
||||
};
|
||||
return data
|
||||
}
|
||||
|
||||
export const fetchMonitor = async (
|
||||
server_id: number,
|
||||
): Promise<MonitorResponse> => {
|
||||
const response = await fetch(`/api/v1/service/${server_id}`);
|
||||
const data = await response.json();
|
||||
export const fetchMonitor = async (server_id: number): Promise<MonitorResponse> => {
|
||||
const response = await fetch(`/api/v1/service/${server_id}`)
|
||||
const data = await response.json()
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
throw new Error(data.error)
|
||||
}
|
||||
return data;
|
||||
};
|
||||
return data
|
||||
}
|
||||
|
||||
export const fetchService = async (): Promise<ServiceResponse> => {
|
||||
const response = await fetch("/api/v1/service");
|
||||
const data = await response.json();
|
||||
const response = await fetch("/api/v1/service")
|
||||
const data = await response.json()
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
throw new Error(data.error)
|
||||
}
|
||||
return data;
|
||||
};
|
||||
return data
|
||||
}
|
||||
|
||||
export const fetchSetting = async (): Promise<SettingResponse> => {
|
||||
const response = await fetch("/api/v1/setting");
|
||||
const data = await response.json();
|
||||
const response = await fetch("/api/v1/setting")
|
||||
const data = await response.json()
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
throw new Error(data.error)
|
||||
}
|
||||
return data;
|
||||
};
|
||||
return data
|
||||
}
|
||||
|
122
src/lib/utils.ts
122
src/lib/utils.ts
@ -1,15 +1,15 @@
|
||||
import { NezhaServer } from "@/types/nezha-api";
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { NezhaServer } from "@/types/nezha-api"
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
|
||||
const lastActiveTime = serverInfo.last_active.startsWith("000")
|
||||
? 0
|
||||
: parseISOTimestamp(serverInfo.last_active);
|
||||
: parseISOTimestamp(serverInfo.last_active)
|
||||
return {
|
||||
...serverInfo,
|
||||
cpu: serverInfo.state.cpu || 0,
|
||||
@ -17,9 +17,7 @@ export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
|
||||
process: serverInfo.state.process_count || 0,
|
||||
up: serverInfo.state.net_out_speed / 1024 / 1024 || 0,
|
||||
down: serverInfo.state.net_in_speed / 1024 / 1024 || 0,
|
||||
last_active_time_string: lastActiveTime
|
||||
? new Date(lastActiveTime).toLocaleString()
|
||||
: "",
|
||||
last_active_time_string: lastActiveTime ? new Date(lastActiveTime).toLocaleString() : "",
|
||||
online: now - lastActiveTime <= 30000,
|
||||
uptime: serverInfo.state.uptime || 0,
|
||||
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_15: serverInfo.state.load_15?.toFixed(2) || 0.0,
|
||||
public_note: handlePublicNote(serverInfo.id, serverInfo.public_note || ""),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getDaysBetweenDates(date1: string, date2: string): number {
|
||||
const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数
|
||||
const firstDate = new Date(date1);
|
||||
const secondDate = new Date(date2);
|
||||
const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
|
||||
const firstDate = new Date(date1)
|
||||
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) =>
|
||||
fetch(url)
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
throw new Error(res.statusText)
|
||||
}
|
||||
return res.json();
|
||||
return res.json()
|
||||
})
|
||||
.then((data) => data.data)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
console.error(err)
|
||||
throw err
|
||||
})
|
||||
|
||||
export const nezhaFetcher = async (url: string) => {
|
||||
const res = await fetch(url);
|
||||
const res = await fetch(url)
|
||||
|
||||
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>
|
||||
error.info = await res.json();
|
||||
error.info = await res.json()
|
||||
// @ts-expect-error - res.status is a number
|
||||
error.status = res.status;
|
||||
throw error;
|
||||
error.status = res.status
|
||||
throw error
|
||||
}
|
||||
|
||||
return res.json();
|
||||
};
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export function parseISOTimestamp(isoString: string): number {
|
||||
return new Date(isoString).getTime();
|
||||
return new Date(isoString).getTime()
|
||||
}
|
||||
|
||||
export function formatRelativeTime(timestamp: number): string {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||
const now = Date.now()
|
||||
const diff = now - timestamp
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000)
|
||||
|
||||
if (hours > 24) {
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d`;
|
||||
const days = Math.floor(hours / 24)
|
||||
return `${days}d`
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h`;
|
||||
return `${hours}h`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m`;
|
||||
return `${minutes}m`
|
||||
} else if (seconds >= 0) {
|
||||
return `${seconds}s`;
|
||||
return `${seconds}s`
|
||||
}
|
||||
return "0s";
|
||||
return "0s"
|
||||
}
|
||||
|
||||
export function formatTime(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
const seconds = date.getSeconds().toString().padStart(2, "0");
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
const date = new Date(timestamp)
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hours = date.getHours().toString().padStart(2, "0")
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0")
|
||||
const seconds = date.getSeconds().toString().padStart(2, "0")
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
interface BillingData {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
autoRenewal: string;
|
||||
cycle: string;
|
||||
amount: string;
|
||||
startDate: string
|
||||
endDate: string
|
||||
autoRenewal: string
|
||||
cycle: string
|
||||
amount: string
|
||||
}
|
||||
|
||||
// interface PlanData {
|
||||
@ -140,16 +138,16 @@ interface BillingData {
|
||||
// }
|
||||
|
||||
interface PublicNoteData {
|
||||
billingDataMod: BillingData;
|
||||
billingDataMod: BillingData
|
||||
// planDataMod: PlanData;
|
||||
}
|
||||
|
||||
export function parsePublicNote(publicNote: string): PublicNoteData | null {
|
||||
try {
|
||||
if (!publicNote) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const data = JSON.parse(publicNote);
|
||||
const data = JSON.parse(publicNote)
|
||||
return {
|
||||
billingDataMod: {
|
||||
startDate: data.billingDataMod.startDate || "",
|
||||
@ -167,26 +165,26 @@ export function parsePublicNote(publicNote: string): PublicNoteData | null {
|
||||
// networkRoute: data.planDataMod.networkRoute || "",
|
||||
// extra: data.planDataMod.extra || "",
|
||||
// },
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing public note:", error);
|
||||
return null;
|
||||
console.error("Error parsing public note:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle public_note with sessionStorage
|
||||
export function handlePublicNote(serverId: number, publicNote: string): string {
|
||||
const storageKey = `server_${serverId}_public_note`;
|
||||
const storedNote = sessionStorage.getItem(storageKey);
|
||||
const storageKey = `server_${serverId}_public_note`
|
||||
const storedNote = sessionStorage.getItem(storageKey)
|
||||
|
||||
if (!publicNote && storedNote) {
|
||||
return storedNote;
|
||||
return storedNote
|
||||
}
|
||||
|
||||
if (publicNote) {
|
||||
sessionStorage.setItem(storageKey, publicNote);
|
||||
return publicNote;
|
||||
sessionStorage.setItem(storageKey, publicNote)
|
||||
return publicNote
|
||||
}
|
||||
|
||||
return "";
|
||||
return ""
|
||||
}
|
||||
|
33
src/main.tsx
33
src/main.tsx
@ -1,19 +1,20 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
import "./i18n";
|
||||
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";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { Toaster } from "sonner"
|
||||
|
||||
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(
|
||||
<React.StrictMode>
|
||||
@ -45,4 +46,4 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
</ThemeProvider>
|
||||
</MotionProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
)
|
||||
|
@ -1,23 +1,21 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
interface ErrorPageProps {
|
||||
code?: string | number;
|
||||
message?: string;
|
||||
code?: string | number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export default function ErrorPage({ code = "500", message }: ErrorPageProps) {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<h1 className="text-4xl font-semibold">{code}</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
{message || t("error.somethingWentWrong")}
|
||||
</p>
|
||||
<p className="text-xl text-muted-foreground">{message || t("error.somethingWentWrong")}</p>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => window.location.reload()} variant="outline">
|
||||
{t("error.tryAgain")}
|
||||
@ -28,5 +26,5 @@ export default function ErrorPage({ code = "500", message }: ErrorPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,22 +1,20 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
export default function NotFound() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<h1 className="text-4xl font-semibold">404</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
{t("error.pageNotFound")}
|
||||
</p>
|
||||
<p className="text-xl text-muted-foreground">{t("error.pageNotFound")}</p>
|
||||
<Button onClick={() => navigate("/")} className="mt-2">
|
||||
{t("error.backToHome")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,67 +1,60 @@
|
||||
import { NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||
import ServerCard from "@/components/ServerCard";
|
||||
import { cn, formatNezhaInfo } from "@/lib/utils";
|
||||
import ServerOverview from "@/components/ServerOverview";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchServerGroup } from "@/lib/nezha-api";
|
||||
import GroupSwitch from "@/components/GroupSwitch";
|
||||
import { ServerGroup } from "@/types/nezha-api";
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ChartBarSquareIcon,
|
||||
ViewColumnsIcon,
|
||||
MapIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { ServiceTracker } from "@/components/ServiceTracker";
|
||||
import ServerCardInline from "@/components/ServerCardInline";
|
||||
import { Loader } from "@/components/loading/Loader";
|
||||
import GlobalMap from "@/components/GlobalMap";
|
||||
import { useStatus } from "@/hooks/use-status";
|
||||
import useFilter from "@/hooks/use-filter";
|
||||
import GlobalMap from "@/components/GlobalMap"
|
||||
import GroupSwitch from "@/components/GroupSwitch"
|
||||
import ServerCard from "@/components/ServerCard"
|
||||
import ServerCardInline from "@/components/ServerCardInline"
|
||||
import ServerOverview from "@/components/ServerOverview"
|
||||
import { ServiceTracker } from "@/components/ServiceTracker"
|
||||
import { Loader } from "@/components/loading/Loader"
|
||||
import useFilter from "@/hooks/use-filter"
|
||||
import { useStatus } from "@/hooks/use-status"
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
||||
import { fetchServerGroup } from "@/lib/nezha-api"
|
||||
import { cn, formatNezhaInfo } from "@/lib/utils"
|
||||
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
||||
import { ServerGroup } from "@/types/nezha-api"
|
||||
import { ChartBarSquareIcon, MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export default function Servers() {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation()
|
||||
const { data: groupData } = useQuery({
|
||||
queryKey: ["server-group"],
|
||||
queryFn: () => fetchServerGroup(),
|
||||
});
|
||||
const { lastMessage, connected } = useWebSocketContext();
|
||||
const { status } = useStatus();
|
||||
const { filter } = useFilter();
|
||||
const [showServices, setShowServices] = useState<string>("0");
|
||||
const [showMap, setShowMap] = useState<string>("0");
|
||||
const [inline, setInline] = useState<string>("0");
|
||||
const [currentGroup, setCurrentGroup] = useState<string>("All");
|
||||
})
|
||||
const { lastMessage, connected } = useWebSocketContext()
|
||||
const { status } = useStatus()
|
||||
const { filter } = useFilter()
|
||||
const [showServices, setShowServices] = useState<string>("0")
|
||||
const [showMap, setShowMap] = useState<string>("0")
|
||||
const [inline, setInline] = useState<string>("0")
|
||||
const [currentGroup, setCurrentGroup] = useState<string>("All")
|
||||
|
||||
useEffect(() => {
|
||||
const showServicesState = localStorage.getItem("showServices");
|
||||
const showServicesState = localStorage.getItem("showServices")
|
||||
if (showServicesState !== null) {
|
||||
setShowServices(showServicesState);
|
||||
setShowServices(showServicesState)
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const inlineState = localStorage.getItem("inline");
|
||||
const inlineState = localStorage.getItem("inline")
|
||||
if (inlineState !== null) {
|
||||
setInline(inlineState);
|
||||
setInline(inlineState)
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const groupTabs = [
|
||||
"All",
|
||||
...(groupData?.data?.map((item: ServerGroup) => item.group.name) || []),
|
||||
];
|
||||
const groupTabs = ["All", ...(groupData?.data?.map((item: ServerGroup) => item.group.name) || [])]
|
||||
|
||||
useEffect(() => {
|
||||
const hasShownToast = sessionStorage.getItem("websocket-connected-toast");
|
||||
const hasShownToast = sessionStorage.getItem("websocket-connected-toast")
|
||||
if (connected && !hasShownToast) {
|
||||
toast.success(t("info.websocketConnected"));
|
||||
sessionStorage.setItem("websocket-connected-toast", "true");
|
||||
toast.success(t("info.websocketConnected"))
|
||||
sessionStorage.setItem("websocket-connected-toast", "true")
|
||||
}
|
||||
}, [connected]);
|
||||
}, [connected])
|
||||
|
||||
if (!connected) {
|
||||
return (
|
||||
@ -71,42 +64,37 @@ export default function Servers() {
|
||||
{t("info.websocketConnecting")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const nezhaWsData = lastMessage
|
||||
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
|
||||
: null;
|
||||
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
|
||||
|
||||
if (!nezhaWsData) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center ">
|
||||
<p className="font-semibold text-sm">{t("info.processing")}</p>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
let filteredServers =
|
||||
nezhaWsData?.servers?.filter((server) => {
|
||||
if (currentGroup === "All") return true;
|
||||
if (currentGroup === "All") return true
|
||||
const group = groupData?.data?.find(
|
||||
(g: ServerGroup) =>
|
||||
g.group.name === currentGroup &&
|
||||
Array.isArray(g.servers) &&
|
||||
g.servers.includes(server.id),
|
||||
);
|
||||
return !!group;
|
||||
}) || [];
|
||||
)
|
||||
return !!group
|
||||
}) || []
|
||||
|
||||
const totalServers = filteredServers.length || 0;
|
||||
const totalServers = filteredServers.length || 0
|
||||
const onlineServers =
|
||||
filteredServers.filter(
|
||||
(server) => formatNezhaInfo(nezhaWsData.now, server).online,
|
||||
)?.length || 0;
|
||||
filteredServers.filter((server) => formatNezhaInfo(nezhaWsData.now, server).online)?.length || 0
|
||||
const offlineServers =
|
||||
filteredServers.filter(
|
||||
(server) => !formatNezhaInfo(nezhaWsData.now, server).online,
|
||||
)?.length || 0;
|
||||
filteredServers.filter((server) => !formatNezhaInfo(nezhaWsData.now, server).online)?.length ||
|
||||
0
|
||||
const up =
|
||||
filteredServers.reduce(
|
||||
(total, server) =>
|
||||
@ -114,7 +102,7 @@ export default function Servers() {
|
||||
? total + (server.state?.net_out_transfer ?? 0)
|
||||
: total,
|
||||
0,
|
||||
) || 0;
|
||||
) || 0
|
||||
const down =
|
||||
filteredServers.reduce(
|
||||
(total, server) =>
|
||||
@ -122,7 +110,7 @@ export default function Servers() {
|
||||
? total + (server.state?.net_in_transfer ?? 0)
|
||||
: total,
|
||||
0,
|
||||
) || 0;
|
||||
) || 0
|
||||
|
||||
const upSpeed =
|
||||
filteredServers.reduce(
|
||||
@ -131,7 +119,7 @@ export default function Servers() {
|
||||
? total + (server.state?.net_out_speed ?? 0)
|
||||
: total,
|
||||
0,
|
||||
) || 0;
|
||||
) || 0
|
||||
const downSpeed =
|
||||
filteredServers.reduce(
|
||||
(total, server) =>
|
||||
@ -139,43 +127,33 @@ export default function Servers() {
|
||||
? total + (server.state?.net_in_speed ?? 0)
|
||||
: total,
|
||||
0,
|
||||
) || 0;
|
||||
) || 0
|
||||
|
||||
filteredServers =
|
||||
status === "all"
|
||||
? filteredServers
|
||||
: filteredServers.filter((server) =>
|
||||
[status].includes(
|
||||
formatNezhaInfo(nezhaWsData.now, server).online
|
||||
? "online"
|
||||
: "offline",
|
||||
),
|
||||
);
|
||||
[status].includes(formatNezhaInfo(nezhaWsData.now, server).online ? "online" : "offline"),
|
||||
)
|
||||
|
||||
if (filter) {
|
||||
filteredServers.sort((a, b) => {
|
||||
if (
|
||||
!formatNezhaInfo(nezhaWsData.now, a).online &&
|
||||
formatNezhaInfo(nezhaWsData.now, b).online
|
||||
)
|
||||
return 1;
|
||||
if (
|
||||
formatNezhaInfo(nezhaWsData.now, a).online &&
|
||||
!formatNezhaInfo(nezhaWsData.now, b).online
|
||||
)
|
||||
return -1;
|
||||
if (!formatNezhaInfo(nezhaWsData.now, a).online && formatNezhaInfo(nezhaWsData.now, b).online)
|
||||
return 1
|
||||
if (formatNezhaInfo(nezhaWsData.now, a).online && !formatNezhaInfo(nezhaWsData.now, b).online)
|
||||
return -1
|
||||
if (
|
||||
!formatNezhaInfo(nezhaWsData.now, a).online &&
|
||||
!formatNezhaInfo(nezhaWsData.now, b).online
|
||||
)
|
||||
return 0;
|
||||
return 0
|
||||
return (
|
||||
formatNezhaInfo(nezhaWsData.now, b).state.net_in_speed +
|
||||
formatNezhaInfo(nezhaWsData.now, b).state.net_out_speed -
|
||||
(formatNezhaInfo(nezhaWsData.now, a).state.net_in_speed +
|
||||
formatNezhaInfo(nezhaWsData.now, a).state.net_out_speed)
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@ -192,13 +170,12 @@ export default function Servers() {
|
||||
<section className="flex mt-6 items-center gap-2 w-full overflow-hidden">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowMap(showMap === "0" ? "1" : "0");
|
||||
setShowMap(showMap === "0" ? "1" : "0")
|
||||
}}
|
||||
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)] ",
|
||||
{
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500":
|
||||
showMap === "1",
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": showMap === "1",
|
||||
},
|
||||
)}
|
||||
>
|
||||
@ -206,17 +183,13 @@ export default function Servers() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowServices(showServices === "0" ? "1" : "0");
|
||||
localStorage.setItem(
|
||||
"showServices",
|
||||
showServices === "0" ? "1" : "0",
|
||||
);
|
||||
setShowServices(showServices === "0" ? "1" : "0")
|
||||
localStorage.setItem("showServices", showServices === "0" ? "1" : "0")
|
||||
}}
|
||||
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)] ",
|
||||
{
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500":
|
||||
showServices === "1",
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": showServices === "1",
|
||||
},
|
||||
)}
|
||||
>
|
||||
@ -224,54 +197,38 @@ export default function Servers() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setInline(inline === "0" ? "1" : "0");
|
||||
localStorage.setItem("inline", inline === "0" ? "1" : "0");
|
||||
setInline(inline === "0" ? "1" : "0")
|
||||
localStorage.setItem("inline", inline === "0" ? "1" : "0")
|
||||
}}
|
||||
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)] ",
|
||||
{
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500":
|
||||
inline === "1",
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": inline === "1",
|
||||
},
|
||||
)}
|
||||
>
|
||||
<ViewColumnsIcon className="size-[13px]" />
|
||||
</button>
|
||||
<GroupSwitch
|
||||
tabs={groupTabs}
|
||||
currentTab={currentGroup}
|
||||
setCurrentTab={setCurrentGroup}
|
||||
/>
|
||||
<GroupSwitch tabs={groupTabs} currentTab={currentGroup} setCurrentTab={setCurrentGroup} />
|
||||
</section>
|
||||
{showMap === "1" && (
|
||||
<GlobalMap
|
||||
now={nezhaWsData.now}
|
||||
serverList={nezhaWsData?.servers || []}
|
||||
/>
|
||||
<GlobalMap now={nezhaWsData.now} serverList={nezhaWsData?.servers || []} />
|
||||
)}
|
||||
{showServices === "1" && <ServiceTracker />}
|
||||
{inline === "1" && (
|
||||
<section className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6">
|
||||
{filteredServers.map((serverInfo) => (
|
||||
<ServerCardInline
|
||||
now={nezhaWsData.now}
|
||||
key={serverInfo.id}
|
||||
serverInfo={serverInfo}
|
||||
/>
|
||||
<ServerCardInline now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} />
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
{inline === "0" && (
|
||||
<section className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6">
|
||||
{filteredServers.map((serverInfo) => (
|
||||
<ServerCard
|
||||
now={nezhaWsData.now}
|
||||
key={serverInfo.id}
|
||||
serverInfo={serverInfo}
|
||||
/>
|
||||
<ServerCard now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} />
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,26 +1,26 @@
|
||||
import { NetworkChart } from "@/components/NetworkChart";
|
||||
import ServerDetailChart from "@/components/ServerDetailChart";
|
||||
import ServerDetailOverview from "@/components/ServerDetailOverview";
|
||||
import TabSwitch from "@/components/TabSwitch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { NetworkChart } from "@/components/NetworkChart"
|
||||
import ServerDetailChart from "@/components/ServerDetailChart"
|
||||
import ServerDetailOverview from "@/components/ServerDetailOverview"
|
||||
import TabSwitch from "@/components/TabSwitch"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useNavigate, useParams } from "react-router-dom"
|
||||
|
||||
export default function ServerDetail() {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
||||
}, []);
|
||||
window.scrollTo({ top: 0, left: 0, behavior: "instant" })
|
||||
}, [])
|
||||
|
||||
const tabs = ["Detail", "Network"];
|
||||
const [currentTab, setCurrentTab] = useState(tabs[0]);
|
||||
const tabs = ["Detail", "Network"]
|
||||
const [currentTab, setCurrentTab] = useState(tabs[0])
|
||||
|
||||
const { id: server_id } = useParams();
|
||||
const { id: server_id } = useParams()
|
||||
|
||||
if (!server_id) {
|
||||
navigate("/404");
|
||||
return null;
|
||||
navigate("/404")
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
@ -29,11 +29,7 @@ export default function ServerDetail() {
|
||||
<section className="flex items-center my-2 w-full">
|
||||
<Separator className="flex-1" />
|
||||
<div className="flex justify-center w-full max-w-[200px]">
|
||||
<TabSwitch
|
||||
tabs={tabs}
|
||||
currentTab={currentTab}
|
||||
setCurrentTab={setCurrentTab}
|
||||
/>
|
||||
<TabSwitch tabs={tabs} currentTab={currentTab} setCurrentTab={setCurrentTab} />
|
||||
</div>
|
||||
<Separator className="flex-1" />
|
||||
</section>
|
||||
@ -41,11 +37,8 @@ export default function ServerDetail() {
|
||||
<ServerDetailChart server_id={server_id} />
|
||||
</div>
|
||||
<div style={{ display: currentTab === tabs[1] ? "block" : "none" }}>
|
||||
<NetworkChart
|
||||
server_id={Number(server_id)}
|
||||
show={currentTab === tabs[1]}
|
||||
/>
|
||||
<NetworkChart server_id={Number(server_id)} show={currentTab === tabs[1]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
4
src/types/css.d.ts
vendored
4
src/types/css.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
declare module "*.css" {
|
||||
const css: { [key: string]: string };
|
||||
export default css;
|
||||
const css: { [key: string]: string }
|
||||
export default css
|
||||
}
|
||||
|
@ -1,151 +1,151 @@
|
||||
export interface NezhaWebsocketResponse {
|
||||
now: number;
|
||||
servers: NezhaServer[];
|
||||
now: number
|
||||
servers: NezhaServer[]
|
||||
}
|
||||
|
||||
export interface NezhaServer {
|
||||
id: number;
|
||||
name: string;
|
||||
public_note: string;
|
||||
last_active: string;
|
||||
country_code: string;
|
||||
host: NezhaServerHost;
|
||||
state: NezhaServerStatus;
|
||||
id: number
|
||||
name: string
|
||||
public_note: string
|
||||
last_active: string
|
||||
country_code: string
|
||||
host: NezhaServerHost
|
||||
state: NezhaServerStatus
|
||||
}
|
||||
|
||||
export interface NezhaServerHost {
|
||||
platform: string;
|
||||
platform_version: string;
|
||||
cpu: string[];
|
||||
gpu: string[];
|
||||
mem_total: number;
|
||||
disk_total: number;
|
||||
swap_total: number;
|
||||
arch: string;
|
||||
boot_time: number;
|
||||
version: string;
|
||||
platform: string
|
||||
platform_version: string
|
||||
cpu: string[]
|
||||
gpu: string[]
|
||||
mem_total: number
|
||||
disk_total: number
|
||||
swap_total: number
|
||||
arch: string
|
||||
boot_time: number
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface NezhaServerStatus {
|
||||
cpu: number;
|
||||
mem_used: number;
|
||||
swap_used: number;
|
||||
disk_used: number;
|
||||
net_in_transfer: number;
|
||||
net_out_transfer: number;
|
||||
net_in_speed: number;
|
||||
net_out_speed: number;
|
||||
uptime: number;
|
||||
load_1: number;
|
||||
load_5: number;
|
||||
load_15: number;
|
||||
tcp_conn_count: number;
|
||||
udp_conn_count: number;
|
||||
process_count: number;
|
||||
temperatures: temperature[];
|
||||
gpu: number[];
|
||||
cpu: number
|
||||
mem_used: number
|
||||
swap_used: number
|
||||
disk_used: number
|
||||
net_in_transfer: number
|
||||
net_out_transfer: number
|
||||
net_in_speed: number
|
||||
net_out_speed: number
|
||||
uptime: number
|
||||
load_1: number
|
||||
load_5: number
|
||||
load_15: number
|
||||
tcp_conn_count: number
|
||||
udp_conn_count: number
|
||||
process_count: number
|
||||
temperatures: temperature[]
|
||||
gpu: number[]
|
||||
}
|
||||
|
||||
interface temperature {
|
||||
Name: string;
|
||||
Temperature: number;
|
||||
Name: string
|
||||
Temperature: number
|
||||
}
|
||||
|
||||
export interface ServerGroupResponse {
|
||||
success: boolean;
|
||||
data: ServerGroup[];
|
||||
success: boolean
|
||||
data: ServerGroup[]
|
||||
}
|
||||
|
||||
export interface ServerGroup {
|
||||
group: {
|
||||
id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
name: string;
|
||||
};
|
||||
servers: number[];
|
||||
id: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
name: string
|
||||
}
|
||||
servers: number[]
|
||||
}
|
||||
|
||||
export interface LoginUserResponse {
|
||||
success: boolean;
|
||||
success: boolean
|
||||
data: {
|
||||
id: number;
|
||||
username: string;
|
||||
password: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
id: number
|
||||
username: string
|
||||
password: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface MonitorResponse {
|
||||
success: boolean;
|
||||
data: NezhaMonitor[];
|
||||
success: boolean
|
||||
data: NezhaMonitor[]
|
||||
}
|
||||
|
||||
export type ServerMonitorChart = {
|
||||
[key: string]: {
|
||||
created_at: number;
|
||||
avg_delay: number;
|
||||
}[];
|
||||
};
|
||||
created_at: number
|
||||
avg_delay: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface NezhaMonitor {
|
||||
monitor_id: number;
|
||||
monitor_name: string;
|
||||
server_id: number;
|
||||
server_name: string;
|
||||
created_at: number[];
|
||||
avg_delay: number[];
|
||||
monitor_id: number
|
||||
monitor_name: string
|
||||
server_id: number
|
||||
server_name: string
|
||||
created_at: number[]
|
||||
avg_delay: number[]
|
||||
}
|
||||
|
||||
export interface ServiceResponse {
|
||||
success: boolean;
|
||||
success: boolean
|
||||
data: {
|
||||
services: {
|
||||
[key: string]: ServiceData;
|
||||
};
|
||||
cycle_transfer_stats: CycleTransferStats;
|
||||
};
|
||||
[key: string]: ServiceData
|
||||
}
|
||||
cycle_transfer_stats: CycleTransferStats
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServiceData {
|
||||
service_name: string;
|
||||
current_up: number;
|
||||
current_down: number;
|
||||
total_up: number;
|
||||
total_down: number;
|
||||
delay: number[];
|
||||
up: number[];
|
||||
down: number[];
|
||||
service_name: string
|
||||
current_up: number
|
||||
current_down: number
|
||||
total_up: number
|
||||
total_down: number
|
||||
delay: number[]
|
||||
up: number[]
|
||||
down: number[]
|
||||
}
|
||||
|
||||
export interface CycleTransferStats {
|
||||
[key: string]: CycleTransferData;
|
||||
[key: string]: CycleTransferData
|
||||
}
|
||||
|
||||
export interface CycleTransferData {
|
||||
name: string;
|
||||
from: string;
|
||||
to: string;
|
||||
max: number;
|
||||
min: number;
|
||||
name: string
|
||||
from: string
|
||||
to: string
|
||||
max: number
|
||||
min: number
|
||||
server_name: {
|
||||
[key: string]: string;
|
||||
};
|
||||
[key: string]: string
|
||||
}
|
||||
transfer: {
|
||||
[key: string]: number;
|
||||
};
|
||||
[key: string]: number
|
||||
}
|
||||
next_update: {
|
||||
[key: string]: string;
|
||||
};
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SettingResponse {
|
||||
success: boolean;
|
||||
success: boolean
|
||||
data: {
|
||||
language: string;
|
||||
site_name: string;
|
||||
custom_code: string;
|
||||
version: string;
|
||||
};
|
||||
language: string
|
||||
site_name: string
|
||||
custom_code: string
|
||||
version: string
|
||||
}
|
||||
}
|
||||
|
@ -56,4 +56,4 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
};
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
|
@ -1,17 +1,17 @@
|
||||
import path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { execSync } from "child_process";
|
||||
import react from "@vitejs/plugin-react-swc"
|
||||
import { execSync } from "child_process"
|
||||
import path from "path"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
// Get git commit hash
|
||||
const getGitHash = () => {
|
||||
try {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
return execSync("git rev-parse --short HEAD").toString().trim()
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return "unknown";
|
||||
console.log(e)
|
||||
return "unknown"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
@ -50,15 +50,11 @@ export default defineConfig({
|
||||
assetFileNames: `assets/[name].[hash].[ext]`,
|
||||
manualChunks(id) {
|
||||
if (id.includes("node_modules")) {
|
||||
return id
|
||||
.toString()
|
||||
.split("node_modules/")[1]
|
||||
.split("/")[0]
|
||||
.toString();
|
||||
return id.toString().split("node_modules/")[1].split("/")[0].toString()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 1500,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user