Compare commits
10 Commits
0c3479bb3b
...
39c1eca1b0
Author | SHA1 | Date | |
---|---|---|---|
|
39c1eca1b0 | ||
|
83122ad867 | ||
|
93a90f4a33 | ||
|
8a93bda5b6 | ||
|
8575aee27e | ||
|
4c7ffb509c | ||
|
2a44cc7afb | ||
|
72472f7cc1 | ||
|
a9a7a367c0 | ||
|
6b52a8dedb |
@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@next/next/no-img-element": "off",
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
}
|
||||
}
|
BIN
.github/1-dark.webp
vendored
Before Width: | Height: | Size: 140 KiB |
BIN
.github/1.webp
vendored
Before Width: | Height: | Size: 139 KiB |
BIN
.github/2-dark.webp
vendored
Before Width: | Height: | Size: 328 KiB |
BIN
.github/2.webp
vendored
Before Width: | Height: | Size: 226 KiB |
BIN
.github/3-dark.webp
vendored
Before Width: | Height: | Size: 119 KiB |
BIN
.github/3.webp
vendored
Before Width: | Height: | Size: 115 KiB |
BIN
.github/4-dark.webp
vendored
Before Width: | Height: | Size: 203 KiB |
BIN
.github/4.webp
vendored
Before Width: | Height: | Size: 135 KiB |
BIN
.github/v2-1.webp
vendored
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
.github/v2-2.webp
vendored
Normal file
After Width: | Height: | Size: 141 KiB |
BIN
.github/v2-3.webp
vendored
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
.github/v2-4.webp
vendored
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
.github/v2-dark.webp
vendored
Normal file
After Width: | Height: | Size: 183 KiB |
@ -23,11 +23,8 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Run linter and fix issues
|
||||
run: bun run lint:fix
|
||||
|
||||
- name: Run formatter
|
||||
run: bun run format
|
||||
- name: Run linter & formatter and fix issues
|
||||
run: bun run check:fix
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
13
README.md
@ -31,11 +31,8 @@
|
||||
|
||||
[环境变量介绍](https://nezhadash-docs.vercel.app/environment)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import { NezhaAPISafe, ServerApi } from "@/app/types/nezha-api"
|
||||
import { ServerDataWithTimestamp, useServerData } from "@/app/lib/server-data-context"
|
||||
import { NezhaAPISafe } from "@/app/types/nezha-api"
|
||||
import { ServerDetailChartLoading } from "@/components/loading/ServerDetailLoading"
|
||||
import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
|
||||
import { formatBytes, formatNezhaInfo, formatRelativeTime, nezhaFetcher } from "@/lib/utils"
|
||||
import { formatBytes, formatNezhaInfo, formatRelativeTime } from "@/lib/utils"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { Area, AreaChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
|
||||
import useSWRImmutable from "swr/immutable"
|
||||
|
||||
type cpuChartData = {
|
||||
timeStamp: string
|
||||
@ -52,16 +52,9 @@ export default function ServerDetailChartClient({
|
||||
}) {
|
||||
const t = useTranslations("ServerDetailChartClient")
|
||||
|
||||
const { data: allFallbackData } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
|
||||
const fallbackData = allFallbackData?.result?.find((item) => item.id === server_id)
|
||||
const { data: serverList, error, history } = useServerData()
|
||||
|
||||
const { data, error } = useSWRImmutable<NezhaAPISafe>(
|
||||
`/api/detail?server_id=${server_id}`,
|
||||
nezhaFetcher,
|
||||
{
|
||||
fallbackData,
|
||||
},
|
||||
)
|
||||
const data = serverList?.result?.find((item) => item.id === server_id)
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@ -77,23 +70,48 @@ export default function ServerDetailChartClient({
|
||||
|
||||
return (
|
||||
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
|
||||
<CpuChart data={data} />
|
||||
<ProcessChart data={data} />
|
||||
<DiskChart data={data} />
|
||||
<MemChart data={data} />
|
||||
<NetworkChart data={data} />
|
||||
<ConnectChart data={data} />
|
||||
<CpuChart data={data} history={history} />
|
||||
<ProcessChart data={data} history={history} />
|
||||
<DiskChart data={data} history={history} />
|
||||
<MemChart data={data} history={history} />
|
||||
<NetworkChart data={data} history={history} />
|
||||
<ConnectChart data={data} history={history} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function CpuChart({ data }: { data: NezhaAPISafe }) {
|
||||
function CpuChart({ history, data }: { history: ServerDataWithTimestamp[]; data: NezhaAPISafe }) {
|
||||
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[])
|
||||
const hasInitialized = useRef(false)
|
||||
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && history.length > 0) {
|
||||
const historyData = history
|
||||
.map((msg) => {
|
||||
const server = msg.data?.result?.find((item) => item.id === data.id)
|
||||
if (!server) return null
|
||||
const { cpu } = formatNezhaInfo(server)
|
||||
return {
|
||||
timeStamp: msg.timestamp.toString(),
|
||||
cpu: cpu,
|
||||
}
|
||||
})
|
||||
.filter((item): item is cpuChartData => item !== null)
|
||||
.reverse() // 保持时间顺序
|
||||
|
||||
setCpuChartData(historyData)
|
||||
hasInitialized.current = true
|
||||
setHistoryLoaded(true)
|
||||
} else if (history.length === 0) {
|
||||
setHistoryLoaded(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const { cpu } = formatNezhaInfo(data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && historyLoaded) {
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as cpuChartData[]
|
||||
if (cpuChartData.length === 0) {
|
||||
@ -109,7 +127,7 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
|
||||
}
|
||||
setCpuChartData(newData)
|
||||
}
|
||||
}, [data])
|
||||
}, [data, historyLoaded])
|
||||
|
||||
const chartConfig = {
|
||||
cpu: {
|
||||
@ -178,15 +196,45 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ProcessChart({ data }: { data: NezhaAPISafe }) {
|
||||
function ProcessChart({
|
||||
data,
|
||||
history,
|
||||
}: {
|
||||
data: NezhaAPISafe
|
||||
history: ServerDataWithTimestamp[]
|
||||
}) {
|
||||
const t = useTranslations("ServerDetailChartClient")
|
||||
|
||||
const [processChartData, setProcessChartData] = useState([] as processChartData[])
|
||||
const hasInitialized = useRef(false)
|
||||
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && history.length > 0) {
|
||||
const historyData = history
|
||||
.map((msg) => {
|
||||
const server = msg.data?.result?.find((item) => item.id === data.id)
|
||||
if (!server) return null
|
||||
const { process } = formatNezhaInfo(server)
|
||||
return {
|
||||
timeStamp: msg.timestamp.toString(),
|
||||
process: process,
|
||||
}
|
||||
})
|
||||
.filter((item): item is processChartData => item !== null)
|
||||
.reverse()
|
||||
|
||||
setProcessChartData(historyData)
|
||||
hasInitialized.current = true
|
||||
setHistoryLoaded(true)
|
||||
} else if (history.length === 0) {
|
||||
setHistoryLoaded(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const { process } = formatNezhaInfo(data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && historyLoaded) {
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as processChartData[]
|
||||
if (processChartData.length === 0) {
|
||||
@ -202,7 +250,7 @@ function ProcessChart({ data }: { data: NezhaAPISafe }) {
|
||||
}
|
||||
setProcessChartData(newData)
|
||||
}
|
||||
}, [data])
|
||||
}, [data, historyLoaded])
|
||||
|
||||
const chartConfig = {
|
||||
process: {
|
||||
@ -257,15 +305,40 @@ function ProcessChart({ data }: { data: NezhaAPISafe }) {
|
||||
)
|
||||
}
|
||||
|
||||
function MemChart({ data }: { data: NezhaAPISafe }) {
|
||||
function MemChart({ data, history }: { data: NezhaAPISafe; history: ServerDataWithTimestamp[] }) {
|
||||
const t = useTranslations("ServerDetailChartClient")
|
||||
|
||||
const [memChartData, setMemChartData] = useState([] as memChartData[])
|
||||
const hasInitialized = useRef(false)
|
||||
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && history.length > 0) {
|
||||
const historyData = history
|
||||
.map((msg) => {
|
||||
const server = msg.data?.result?.find((item) => item.id === data.id)
|
||||
if (!server) return null
|
||||
const { mem, swap } = formatNezhaInfo(server)
|
||||
return {
|
||||
timeStamp: msg.timestamp.toString(),
|
||||
mem: mem,
|
||||
swap: swap,
|
||||
}
|
||||
})
|
||||
.filter((item): item is memChartData => item !== null)
|
||||
.reverse()
|
||||
|
||||
setMemChartData(historyData)
|
||||
hasInitialized.current = true
|
||||
setHistoryLoaded(true)
|
||||
} else if (history.length === 0) {
|
||||
setHistoryLoaded(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const { mem, swap } = formatNezhaInfo(data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && historyLoaded) {
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as memChartData[]
|
||||
if (memChartData.length === 0) {
|
||||
@ -281,7 +354,7 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
|
||||
}
|
||||
setMemChartData(newData)
|
||||
}
|
||||
}, [data])
|
||||
}, [data, historyLoaded])
|
||||
|
||||
const chartConfig = {
|
||||
mem: {
|
||||
@ -386,15 +459,39 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
|
||||
)
|
||||
}
|
||||
|
||||
function DiskChart({ data }: { data: NezhaAPISafe }) {
|
||||
function DiskChart({ data, history }: { data: NezhaAPISafe; history: ServerDataWithTimestamp[] }) {
|
||||
const t = useTranslations("ServerDetailChartClient")
|
||||
|
||||
const [diskChartData, setDiskChartData] = useState([] as diskChartData[])
|
||||
const hasInitialized = useRef(false)
|
||||
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && history.length > 0) {
|
||||
const historyData = history
|
||||
.map((msg) => {
|
||||
const server = msg.data?.result?.find((item) => item.id === data.id)
|
||||
if (!server) return null
|
||||
const { disk } = formatNezhaInfo(server)
|
||||
return {
|
||||
timeStamp: msg.timestamp.toString(),
|
||||
disk: disk,
|
||||
}
|
||||
})
|
||||
.filter((item): item is diskChartData => item !== null)
|
||||
.reverse()
|
||||
|
||||
setDiskChartData(historyData)
|
||||
hasInitialized.current = true
|
||||
setHistoryLoaded(true)
|
||||
} else if (history.length === 0) {
|
||||
setHistoryLoaded(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const { disk } = formatNezhaInfo(data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && historyLoaded) {
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as diskChartData[]
|
||||
if (diskChartData.length === 0) {
|
||||
@ -410,7 +507,7 @@ function DiskChart({ data }: { data: NezhaAPISafe }) {
|
||||
}
|
||||
setDiskChartData(newData)
|
||||
}
|
||||
}, [data])
|
||||
}, [data, historyLoaded])
|
||||
|
||||
const chartConfig = {
|
||||
disk: {
|
||||
@ -484,15 +581,46 @@ function DiskChart({ data }: { data: NezhaAPISafe }) {
|
||||
)
|
||||
}
|
||||
|
||||
function NetworkChart({ data }: { data: NezhaAPISafe }) {
|
||||
function NetworkChart({
|
||||
data,
|
||||
history,
|
||||
}: {
|
||||
data: NezhaAPISafe
|
||||
history: ServerDataWithTimestamp[]
|
||||
}) {
|
||||
const t = useTranslations("ServerDetailChartClient")
|
||||
|
||||
const [networkChartData, setNetworkChartData] = useState([] as networkChartData[])
|
||||
const hasInitialized = useRef(false)
|
||||
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && history.length > 0) {
|
||||
const historyData = history
|
||||
.map((msg) => {
|
||||
const server = msg.data?.result?.find((item) => item.id === data.id)
|
||||
if (!server) return null
|
||||
const { up, down } = formatNezhaInfo(server)
|
||||
return {
|
||||
timeStamp: msg.timestamp.toString(),
|
||||
upload: up,
|
||||
download: down,
|
||||
}
|
||||
})
|
||||
.filter((item): item is networkChartData => item !== null)
|
||||
.reverse()
|
||||
|
||||
setNetworkChartData(historyData)
|
||||
hasInitialized.current = true
|
||||
setHistoryLoaded(true)
|
||||
} else if (history.length === 0) {
|
||||
setHistoryLoaded(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const { up, down } = formatNezhaInfo(data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && historyLoaded) {
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as networkChartData[]
|
||||
if (networkChartData.length === 0) {
|
||||
@ -508,7 +636,7 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
|
||||
}
|
||||
setNetworkChartData(newData)
|
||||
}
|
||||
}, [data])
|
||||
}, [data, historyLoaded])
|
||||
|
||||
let maxDownload = Math.max(...networkChartData.map((item) => item.download))
|
||||
maxDownload = Math.ceil(maxDownload)
|
||||
@ -602,13 +730,45 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ConnectChart({ data }: { data: NezhaAPISafe }) {
|
||||
function ConnectChart({
|
||||
data,
|
||||
history,
|
||||
}: {
|
||||
data: NezhaAPISafe
|
||||
history: ServerDataWithTimestamp[]
|
||||
}) {
|
||||
const [connectChartData, setConnectChartData] = useState([] as connectChartData[])
|
||||
const hasInitialized = useRef(false)
|
||||
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && history.length > 0) {
|
||||
const historyData = history
|
||||
.map((msg) => {
|
||||
const server = msg.data?.result?.find((item) => item.id === data.id)
|
||||
if (!server) return null
|
||||
const { tcp, udp } = formatNezhaInfo(server)
|
||||
return {
|
||||
timeStamp: msg.timestamp.toString(),
|
||||
tcp: tcp,
|
||||
udp: udp,
|
||||
}
|
||||
})
|
||||
.filter((item): item is connectChartData => item !== null)
|
||||
.reverse()
|
||||
|
||||
setConnectChartData(historyData)
|
||||
hasInitialized.current = true
|
||||
setHistoryLoaded(true)
|
||||
} else if (history.length === 0) {
|
||||
setHistoryLoaded(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const { tcp, udp } = formatNezhaInfo(data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && historyLoaded) {
|
||||
const timestamp = Date.now().toString()
|
||||
let newData = [] as connectChartData[]
|
||||
if (connectChartData.length === 0) {
|
||||
@ -624,7 +784,7 @@ function ConnectChart({ data }: { data: NezhaAPISafe }) {
|
||||
}
|
||||
setConnectChartData(newData)
|
||||
}
|
||||
}, [data])
|
||||
}, [data, historyLoaded])
|
||||
|
||||
const chartConfig = {
|
||||
tcp: {
|
||||
|
@ -1,18 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import { NezhaAPISafe, ServerApi } from "@/app/types/nezha-api"
|
||||
import { useServerData } from "@/app/lib/server-data-context"
|
||||
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 getEnv from "@/lib/env-entry"
|
||||
import { cn, formatBytes, nezhaFetcher } from "@/lib/utils"
|
||||
import { cn, formatBytes } from "@/lib/utils"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { notFound, useRouter } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import useSWR from "swr"
|
||||
import useSWRImmutable from "swr/immutable"
|
||||
|
||||
export default function ServerDetailClient({ server_id }: { server_id: number }) {
|
||||
const t = useTranslations("ServerDetailClient")
|
||||
@ -39,24 +36,13 @@ export default function ServerDetailClient({ server_id }: { server_id: number })
|
||||
}
|
||||
}
|
||||
|
||||
const { data: allFallbackData, isLoading } = useSWRImmutable<ServerApi>(
|
||||
"/api/server",
|
||||
nezhaFetcher,
|
||||
)
|
||||
const fallbackData = allFallbackData?.result?.find((item) => item.id === server_id)
|
||||
const { data: serverList, error, isLoading } = useServerData()
|
||||
const data = serverList?.result?.find((item) => item.id === server_id)
|
||||
|
||||
if (!fallbackData && !isLoading) {
|
||||
if (!data && !isLoading) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const { data, error } = useSWR<NezhaAPISafe>(`/api/detail?server_id=${server_id}`, nezhaFetcher, {
|
||||
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000,
|
||||
dedupingInterval: 1000,
|
||||
fallbackData,
|
||||
revalidateOnMount: false,
|
||||
revalidateIfStale: false,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
|
@ -1,8 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { ServerApi } from "@/app/types/nezha-api"
|
||||
import { nezhaFetcher } from "@/lib/utils"
|
||||
import useSWRImmutable from "swr/immutable"
|
||||
import { useServerData } from "@/app/lib/server-data-context"
|
||||
|
||||
import GlobalLoading from "../../../../components/loading/GlobalLoading"
|
||||
import { geoJsonString } from "../../../../lib/geo-json-string"
|
||||
@ -11,7 +9,7 @@ import GlobalInfo from "./GlobalInfo"
|
||||
import { InteractiveMap } from "./InteractiveMap"
|
||||
|
||||
export default function ServerGlobal() {
|
||||
const { data: nezhaServerList, error } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
|
||||
const { data: nezhaServerList, error } = useServerData()
|
||||
|
||||
if (error)
|
||||
return (
|
||||
|
@ -1,18 +1,18 @@
|
||||
"use client"
|
||||
|
||||
import { ServerApi } from "@/app/types/nezha-api"
|
||||
import { useServerData } from "@/app/lib/server-data-context"
|
||||
import ServerCard from "@/components/ServerCard"
|
||||
import ServerCardInline from "@/components/ServerCardInline"
|
||||
import Switch from "@/components/Switch"
|
||||
import { Loader } from "@/components/loading/Loader"
|
||||
import getEnv from "@/lib/env-entry"
|
||||
import { useFilter } from "@/lib/network-filter-context"
|
||||
import { useStatus } from "@/lib/status-context"
|
||||
import { cn, nezhaFetcher } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"
|
||||
import { useTranslations } from "next-intl"
|
||||
import dynamic from "next/dynamic"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import useSWR from "swr"
|
||||
|
||||
import GlobalLoading from "../../../../components/loading/GlobalLoading"
|
||||
|
||||
@ -70,10 +70,7 @@ export default function ServerListClient() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const { data, error } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
|
||||
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
|
||||
dedupingInterval: 1000,
|
||||
})
|
||||
const { data, error } = useServerData()
|
||||
|
||||
if (error)
|
||||
return (
|
||||
@ -83,7 +80,15 @@ export default function ServerListClient() {
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!data?.result) return null
|
||||
if (!data?.result)
|
||||
return (
|
||||
<div className="flex flex-col items-center min-h-96 justify-center ">
|
||||
<div className="font-semibold flex items-center gap-2 text-sm">
|
||||
<Loader visible={true} />
|
||||
{t("connecting")}...
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const { result } = data
|
||||
const sortedServers = result.sort((a, b) => {
|
||||
|
@ -1,31 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import { ServerApi } from "@/app/types/nezha-api"
|
||||
import { useServerData } from "@/app/lib/server-data-context"
|
||||
import { Loader } from "@/components/loading/Loader"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import getEnv from "@/lib/env-entry"
|
||||
import { useFilter } from "@/lib/network-filter-context"
|
||||
import { useStatus } from "@/lib/status-context"
|
||||
import { cn, formatBytes, nezhaFetcher } from "@/lib/utils"
|
||||
import { cn, formatBytes } from "@/lib/utils"
|
||||
import blogMan from "@/public/blog-man.webp"
|
||||
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
|
||||
import { useTranslations } from "next-intl"
|
||||
import Image from "next/image"
|
||||
import useSWRImmutable from "swr/immutable"
|
||||
|
||||
export default function ServerOverviewClient() {
|
||||
const { data, error, isLoading } = useServerData()
|
||||
const { status, setStatus } = useStatus()
|
||||
const { filter, setFilter } = useFilter()
|
||||
const t = useTranslations("ServerOverviewClient")
|
||||
|
||||
const { data, error, isLoading } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
|
||||
const disableCartoon = getEnv("NEXT_PUBLIC_DisableCartoon") === "true"
|
||||
|
||||
if (error) {
|
||||
const errorInfo = error as any
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p className="text-sm font-medium opacity-40">
|
||||
Error status:{error.status} {error.info?.cause ?? error.message}
|
||||
Error status:{errorInfo?.status} {errorInfo.info?.cause ?? errorInfo?.message}
|
||||
</p>
|
||||
<p className="text-sm font-medium opacity-40">{t("error_message")}</p>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Footer from "@/app/(main)/footer"
|
||||
import Header from "@/app/(main)/header"
|
||||
import { ServerDataProvider } from "@/app/lib/server-data-context"
|
||||
import { auth } from "@/auth"
|
||||
import { SignIn } from "@/components/SignIn"
|
||||
import getEnv from "@/lib/env-entry"
|
||||
@ -13,7 +14,9 @@ export default function MainLayout({ children }: DashboardProps) {
|
||||
<div className="flex min-h-screen w-full flex-col">
|
||||
<main className="flex min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 bg-background p-4 md:p-10 md:pt-8">
|
||||
<Header />
|
||||
<AuthProtected>{children}</AuthProtected>
|
||||
<AuthProtected>
|
||||
<ServerDataProvider>{children}</ServerDataProvider>
|
||||
</AuthProtected>
|
||||
<Footer />
|
||||
</main>
|
||||
</div>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { auth } from "@/auth"
|
||||
import getEnv from "@/lib/env-entry"
|
||||
import { GetServerIP } from "@/lib/serverFetch"
|
||||
import fs from "fs"
|
||||
import { AsnResponse, CityResponse, Reader } from "maxmind"
|
||||
import { redirect } from "next/navigation"
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import path from "path"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
|
62
app/lib/server-data-context.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
"use client"
|
||||
|
||||
import { ServerApi } from "@/app/types/nezha-api"
|
||||
import getEnv from "@/lib/env-entry"
|
||||
import { nezhaFetcher } from "@/lib/utils"
|
||||
import { ReactNode, createContext, useContext, useEffect, useState } from "react"
|
||||
import useSWR from "swr"
|
||||
|
||||
export interface ServerDataWithTimestamp {
|
||||
timestamp: number
|
||||
data: ServerApi
|
||||
}
|
||||
|
||||
interface ServerDataContextType {
|
||||
data: ServerApi | undefined
|
||||
error: Error | undefined
|
||||
isLoading: boolean
|
||||
history: ServerDataWithTimestamp[]
|
||||
}
|
||||
|
||||
const ServerDataContext = createContext<ServerDataContextType | undefined>(undefined)
|
||||
|
||||
const MAX_HISTORY_LENGTH = 30
|
||||
|
||||
export function ServerDataProvider({ children }: { children: ReactNode }) {
|
||||
const [history, setHistory] = useState<ServerDataWithTimestamp[]>([])
|
||||
|
||||
const { data, error, isLoading } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
|
||||
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
|
||||
dedupingInterval: 1000,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setHistory((prev) => {
|
||||
const newHistory = [
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
data: data,
|
||||
},
|
||||
...prev,
|
||||
].slice(0, MAX_HISTORY_LENGTH)
|
||||
|
||||
return newHistory
|
||||
})
|
||||
}
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<ServerDataContext.Provider value={{ data, error, isLoading, history }}>
|
||||
{children}
|
||||
</ServerDataContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useServerData() {
|
||||
const context = useContext(ServerDataContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useServerData must be used within a ServerDataProvider")
|
||||
}
|
||||
return context
|
||||
}
|
85
biome.json
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
|
||||
"files": { "ignoreUnknown": false, "ignore": [".next", "public"] },
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"useEditorconfig": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100,
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
},
|
||||
"organizeImports": { "enabled": true },
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": false,
|
||||
"complexity": { "noUselessTypeConstraint": "error" },
|
||||
"correctness": {
|
||||
"noUnusedVariables": "error",
|
||||
"useArrayLiterals": "off",
|
||||
"useExhaustiveDependencies": "off"
|
||||
},
|
||||
"style": { "noNamespace": "error", "useAsConstAssertion": "error" },
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off",
|
||||
"noExtraNonNullAssertion": "error",
|
||||
"noMisleadingInstantiator": "error",
|
||||
"noUnsafeDeclarationMerging": "error",
|
||||
"useNamespaceKeyword": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "asNeeded",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "double",
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["*.ts", "*.tsx", "*.mts", "*.cts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"noConstAssign": "off",
|
||||
"noGlobalObjectCalls": "off",
|
||||
"noInvalidBuiltinInstantiation": "off",
|
||||
"noInvalidConstructorSuper": "off",
|
||||
"noNewSymbol": "off",
|
||||
"noSetterReturn": "off",
|
||||
"noUndeclaredVariables": "off",
|
||||
"noUnreachable": "off",
|
||||
"noUnreachableSuper": "off"
|
||||
},
|
||||
"style": {
|
||||
"noArguments": "error",
|
||||
"noVar": "error",
|
||||
"useConst": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noClassAssign": "off",
|
||||
"noDuplicateClassMembers": "off",
|
||||
"noDuplicateObjectKeys": "off",
|
||||
"noDuplicateParameters": "off",
|
||||
"noFunctionAssign": "off",
|
||||
"noImportAssign": "off",
|
||||
"noRedeclare": "off",
|
||||
"noUnsafeNegation": "off",
|
||||
"useGetterReturn": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
[install]
|
||||
registry = "https://registry.npmmirror.com/"
|
@ -9,6 +9,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { localeItems } from "@/i18n-metadata"
|
||||
import { setUserLocale } from "@/i18n/locale"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid"
|
||||
import { useLocale } from "next-intl"
|
||||
import * as React from "react"
|
||||
@ -34,11 +35,20 @@ export function LanguageSwitcher() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||
{localeItems.map((item) => (
|
||||
{localeItems.map((item, index) => (
|
||||
<DropdownMenuItem
|
||||
key={item.code}
|
||||
onSelect={(e) => handleSelect(e, item.code)}
|
||||
className={locale === item.code ? "bg-muted gap-3" : ""}
|
||||
className={cn(
|
||||
{
|
||||
"bg-muted gap-3": locale === item.code,
|
||||
},
|
||||
{
|
||||
"rounded-t-[5px]": index === localeItems.length - 1,
|
||||
"rounded-[5px]": index !== 0 && index !== localeItems.length - 1,
|
||||
"rounded-b-[5px]": index === 0,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
|
@ -37,19 +37,19 @@ export function ModeToggle() {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||
<DropdownMenuItem
|
||||
className={cn({ "gap-3 bg-muted": theme === "light" })}
|
||||
className={cn("rounded-b-[5px]", { "gap-3 bg-muted": theme === "light" })}
|
||||
onSelect={(e) => handleSelect(e, "light")}
|
||||
>
|
||||
{t("Light")} {theme === "light" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn({ "gap-3 bg-muted": theme === "dark" })}
|
||||
className={cn("rounded-[5px]", { "gap-3 bg-muted": theme === "dark" })}
|
||||
onSelect={(e) => handleSelect(e, "dark")}
|
||||
>
|
||||
{t("Dark")} {theme === "dark" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn({ "gap-3 bg-muted": theme === "system" })}
|
||||
className={cn("rounded-t-[5px]", { "gap-3 bg-muted": theme === "system" })}
|
||||
onSelect={(e) => handleSelect(e, "system")}
|
||||
>
|
||||
{t("System")} {theme === "system" && <CheckCircleIcon className="size-4" />}
|
||||
|
@ -62,7 +62,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover p-1.5 text-popover-foreground shadow-2xl dark:shadow-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl dark:shadow-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
@ -6,19 +6,6 @@ export const localeItems = [
|
||||
{ code: "ja", name: "日本語" },
|
||||
{ code: "zh-t", name: "中文繁體" },
|
||||
{ code: "zh", name: "中文简体" },
|
||||
//{code: 'ar', name: 'العربية'},
|
||||
//{code: 'de', name: 'Deutsch'},
|
||||
//{code: 'es', name: 'Español'},
|
||||
//{code: 'fr', name: 'Français'},
|
||||
//{code: 'hi', name: 'हिन्दी'},
|
||||
//{code: 'id', name: 'Bahasa Indonesia'},
|
||||
//{code: 'it', name: 'Italiano'},
|
||||
//{code: 'ko', name: '한국어'},
|
||||
//{code: 'ms', name: 'Bahasa Melayu'},
|
||||
//{code: 'pt', name: 'Português'},
|
||||
//{code: 'ru', name: 'Русский'},
|
||||
//{code: 'th', name: 'ไทย'},
|
||||
//{code: 'vi', name: 'Tiếng Việt'},
|
||||
]
|
||||
|
||||
export const locales = localeItems.map((item) => item.code)
|
||||
|
@ -11,7 +11,8 @@
|
||||
},
|
||||
"ServerListClient": {
|
||||
"error_message": "Please check your environment variables and review the server console",
|
||||
"defaultTag": "All"
|
||||
"defaultTag": "All",
|
||||
"connecting": "Connecting"
|
||||
},
|
||||
"ServerCard": {
|
||||
"System": "System",
|
||||
|
@ -11,7 +11,8 @@
|
||||
},
|
||||
"ServerListClient": {
|
||||
"error_message": "環境変数を確認し、サーバーコンソールを確認してください",
|
||||
"defaultTag": "すべて"
|
||||
"defaultTag": "すべて",
|
||||
"connecting": "接続中"
|
||||
},
|
||||
"ServerCard": {
|
||||
"System": "システム",
|
||||
|
@ -11,7 +11,8 @@
|
||||
},
|
||||
"ServerListClient": {
|
||||
"error_message": "請檢查您的環境變數並檢查伺服器控制台",
|
||||
"defaultTag": "全部"
|
||||
"defaultTag": "全部",
|
||||
"connecting": "連接中"
|
||||
},
|
||||
"ServerCard": {
|
||||
"System": "系統",
|
||||
|
@ -11,7 +11,8 @@
|
||||
},
|
||||
"ServerListClient": {
|
||||
"error_message": "请检查您的环境变量并检查服务器控制台",
|
||||
"defaultTag": "全部"
|
||||
"defaultTag": "全部",
|
||||
"connecting": "连接中"
|
||||
},
|
||||
"ServerCard": {
|
||||
"System": "系统",
|
||||
|
14
package.json
@ -1,13 +1,15 @@
|
||||
{
|
||||
"name": "nezha-dash",
|
||||
"version": "1.9.1",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3040",
|
||||
"start": "node .next/standalone/server.js",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"format": "prettier --write .",
|
||||
"lint": "biome lint",
|
||||
"lint:fix": "biome lint --fix",
|
||||
"format": "biome format --write .",
|
||||
"check": "biome check",
|
||||
"check:fix": "biome check --fix",
|
||||
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
|
||||
"build-dev": "next build",
|
||||
"start-dev": "next start"
|
||||
@ -60,18 +62,16 @@
|
||||
"typescript-eslint": "^8.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@next/bundle-analyzer": "^15.1.2",
|
||||
"@tailwindcss/postcss": "^4.0.0-beta.8",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-next": "^15.1.2",
|
||||
"eslint-plugin-turbo": "^2.3.3",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"tailwindcss": "^4.0.0-beta.8",
|
||||
"typescript": "^5.7.2",
|
||||
"vercel": "^39.2.2"
|
||||
|