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
|
- name: Install dependencies
|
||||||
run: bun install
|
run: bun install
|
||||||
|
|
||||||
- name: Run linter and fix issues
|
- name: Run linter & formatter and fix issues
|
||||||
run: bun run lint:fix
|
run: bun run check:fix
|
||||||
|
|
||||||
- name: Run formatter
|
|
||||||
run: bun run format
|
|
||||||
|
|
||||||
- name: Check for changes
|
- name: Check for changes
|
||||||
id: check_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)
|
[环境变量介绍](https://nezhadash-docs.vercel.app/environment)
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
"use client"
|
"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 { ServerDetailChartLoading } from "@/components/loading/ServerDetailLoading"
|
||||||
import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar"
|
import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
|
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
|
||||||
import { formatBytes, formatNezhaInfo, formatRelativeTime, nezhaFetcher } from "@/lib/utils"
|
import { formatBytes, formatNezhaInfo, formatRelativeTime } from "@/lib/utils"
|
||||||
import { useTranslations } from "next-intl"
|
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 { Area, AreaChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
|
||||||
import useSWRImmutable from "swr/immutable"
|
|
||||||
|
|
||||||
type cpuChartData = {
|
type cpuChartData = {
|
||||||
timeStamp: string
|
timeStamp: string
|
||||||
@ -52,16 +52,9 @@ export default function ServerDetailChartClient({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTranslations("ServerDetailChartClient")
|
const t = useTranslations("ServerDetailChartClient")
|
||||||
|
|
||||||
const { data: allFallbackData } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
|
const { data: serverList, error, history } = useServerData()
|
||||||
const fallbackData = allFallbackData?.result?.find((item) => item.id === server_id)
|
|
||||||
|
|
||||||
const { data, error } = useSWRImmutable<NezhaAPISafe>(
|
const data = serverList?.result?.find((item) => item.id === server_id)
|
||||||
`/api/detail?server_id=${server_id}`,
|
|
||||||
nezhaFetcher,
|
|
||||||
{
|
|
||||||
fallbackData,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@ -77,23 +70,48 @@ export default function ServerDetailChartClient({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
|
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
|
||||||
<CpuChart data={data} />
|
<CpuChart data={data} history={history} />
|
||||||
<ProcessChart data={data} />
|
<ProcessChart data={data} history={history} />
|
||||||
<DiskChart data={data} />
|
<DiskChart data={data} history={history} />
|
||||||
<MemChart data={data} />
|
<MemChart data={data} history={history} />
|
||||||
<NetworkChart data={data} />
|
<NetworkChart data={data} history={history} />
|
||||||
<ConnectChart data={data} />
|
<ConnectChart data={data} history={history} />
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CpuChart({ data }: { data: NezhaAPISafe }) {
|
function CpuChart({ history, data }: { history: ServerDataWithTimestamp[]; data: NezhaAPISafe }) {
|
||||||
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[])
|
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)
|
const { cpu } = formatNezhaInfo(data)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as cpuChartData[]
|
let newData = [] as cpuChartData[]
|
||||||
if (cpuChartData.length === 0) {
|
if (cpuChartData.length === 0) {
|
||||||
@ -109,7 +127,7 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
|
|||||||
}
|
}
|
||||||
setCpuChartData(newData)
|
setCpuChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data])
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
cpu: {
|
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 t = useTranslations("ServerDetailChartClient")
|
||||||
|
|
||||||
const [processChartData, setProcessChartData] = useState([] as processChartData[])
|
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)
|
const { process } = formatNezhaInfo(data)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as processChartData[]
|
let newData = [] as processChartData[]
|
||||||
if (processChartData.length === 0) {
|
if (processChartData.length === 0) {
|
||||||
@ -202,7 +250,7 @@ function ProcessChart({ data }: { data: NezhaAPISafe }) {
|
|||||||
}
|
}
|
||||||
setProcessChartData(newData)
|
setProcessChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data])
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
process: {
|
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 t = useTranslations("ServerDetailChartClient")
|
||||||
|
|
||||||
const [memChartData, setMemChartData] = useState([] as memChartData[])
|
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)
|
const { mem, swap } = formatNezhaInfo(data)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as memChartData[]
|
let newData = [] as memChartData[]
|
||||||
if (memChartData.length === 0) {
|
if (memChartData.length === 0) {
|
||||||
@ -281,7 +354,7 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
|
|||||||
}
|
}
|
||||||
setMemChartData(newData)
|
setMemChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data])
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
mem: {
|
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 t = useTranslations("ServerDetailChartClient")
|
||||||
|
|
||||||
const [diskChartData, setDiskChartData] = useState([] as diskChartData[])
|
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)
|
const { disk } = formatNezhaInfo(data)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as diskChartData[]
|
let newData = [] as diskChartData[]
|
||||||
if (diskChartData.length === 0) {
|
if (diskChartData.length === 0) {
|
||||||
@ -410,7 +507,7 @@ function DiskChart({ data }: { data: NezhaAPISafe }) {
|
|||||||
}
|
}
|
||||||
setDiskChartData(newData)
|
setDiskChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data])
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
disk: {
|
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 t = useTranslations("ServerDetailChartClient")
|
||||||
|
|
||||||
const [networkChartData, setNetworkChartData] = useState([] as networkChartData[])
|
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)
|
const { up, down } = formatNezhaInfo(data)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as networkChartData[]
|
let newData = [] as networkChartData[]
|
||||||
if (networkChartData.length === 0) {
|
if (networkChartData.length === 0) {
|
||||||
@ -508,7 +636,7 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
|
|||||||
}
|
}
|
||||||
setNetworkChartData(newData)
|
setNetworkChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data])
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
let maxDownload = Math.max(...networkChartData.map((item) => item.download))
|
let maxDownload = Math.max(...networkChartData.map((item) => item.download))
|
||||||
maxDownload = Math.ceil(maxDownload)
|
maxDownload = Math.ceil(maxDownload)
|
||||||
@ -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 [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)
|
const { tcp, udp } = formatNezhaInfo(data)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as connectChartData[]
|
let newData = [] as connectChartData[]
|
||||||
if (connectChartData.length === 0) {
|
if (connectChartData.length === 0) {
|
||||||
@ -624,7 +784,7 @@ function ConnectChart({ data }: { data: NezhaAPISafe }) {
|
|||||||
}
|
}
|
||||||
setConnectChartData(newData)
|
setConnectChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data])
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
tcp: {
|
tcp: {
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { NezhaAPISafe, ServerApi } from "@/app/types/nezha-api"
|
import { useServerData } from "@/app/lib/server-data-context"
|
||||||
import { BackIcon } from "@/components/Icon"
|
import { BackIcon } from "@/components/Icon"
|
||||||
import ServerFlag from "@/components/ServerFlag"
|
import ServerFlag from "@/components/ServerFlag"
|
||||||
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
|
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import getEnv from "@/lib/env-entry"
|
import { cn, formatBytes } from "@/lib/utils"
|
||||||
import { cn, formatBytes, nezhaFetcher } from "@/lib/utils"
|
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { notFound, useRouter } from "next/navigation"
|
import { notFound, useRouter } from "next/navigation"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import useSWR from "swr"
|
|
||||||
import useSWRImmutable from "swr/immutable"
|
|
||||||
|
|
||||||
export default function ServerDetailClient({ server_id }: { server_id: number }) {
|
export default function ServerDetailClient({ server_id }: { server_id: number }) {
|
||||||
const t = useTranslations("ServerDetailClient")
|
const t = useTranslations("ServerDetailClient")
|
||||||
@ -39,24 +36,13 @@ export default function ServerDetailClient({ server_id }: { server_id: number })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: allFallbackData, isLoading } = useSWRImmutable<ServerApi>(
|
const { data: serverList, error, isLoading } = useServerData()
|
||||||
"/api/server",
|
const data = serverList?.result?.find((item) => item.id === server_id)
|
||||||
nezhaFetcher,
|
|
||||||
)
|
|
||||||
const fallbackData = allFallbackData?.result?.find((item) => item.id === server_id)
|
|
||||||
|
|
||||||
if (!fallbackData && !isLoading) {
|
if (!data && !isLoading) {
|
||||||
notFound()
|
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) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { ServerApi } from "@/app/types/nezha-api"
|
import { useServerData } from "@/app/lib/server-data-context"
|
||||||
import { nezhaFetcher } from "@/lib/utils"
|
|
||||||
import useSWRImmutable from "swr/immutable"
|
|
||||||
|
|
||||||
import GlobalLoading from "../../../../components/loading/GlobalLoading"
|
import GlobalLoading from "../../../../components/loading/GlobalLoading"
|
||||||
import { geoJsonString } from "../../../../lib/geo-json-string"
|
import { geoJsonString } from "../../../../lib/geo-json-string"
|
||||||
@ -11,7 +9,7 @@ import GlobalInfo from "./GlobalInfo"
|
|||||||
import { InteractiveMap } from "./InteractiveMap"
|
import { InteractiveMap } from "./InteractiveMap"
|
||||||
|
|
||||||
export default function ServerGlobal() {
|
export default function ServerGlobal() {
|
||||||
const { data: nezhaServerList, error } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
|
const { data: nezhaServerList, error } = useServerData()
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { ServerApi } from "@/app/types/nezha-api"
|
import { useServerData } from "@/app/lib/server-data-context"
|
||||||
import ServerCard from "@/components/ServerCard"
|
import ServerCard from "@/components/ServerCard"
|
||||||
import ServerCardInline from "@/components/ServerCardInline"
|
import ServerCardInline from "@/components/ServerCardInline"
|
||||||
import Switch from "@/components/Switch"
|
import Switch from "@/components/Switch"
|
||||||
|
import { Loader } from "@/components/loading/Loader"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { useFilter } from "@/lib/network-filter-context"
|
import { useFilter } from "@/lib/network-filter-context"
|
||||||
import { useStatus } from "@/lib/status-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 { MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import dynamic from "next/dynamic"
|
import dynamic from "next/dynamic"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import useSWR from "swr"
|
|
||||||
|
|
||||||
import GlobalLoading from "../../../../components/loading/GlobalLoading"
|
import GlobalLoading from "../../../../components/loading/GlobalLoading"
|
||||||
|
|
||||||
@ -70,10 +70,7 @@ export default function ServerListClient() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { data, error } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
|
const { data, error } = useServerData()
|
||||||
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
|
|
||||||
dedupingInterval: 1000,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
@ -83,7 +80,15 @@ export default function ServerListClient() {
|
|||||||
</div>
|
</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 { result } = data
|
||||||
const sortedServers = result.sort((a, b) => {
|
const sortedServers = result.sort((a, b) => {
|
||||||
|
@ -1,31 +1,30 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { ServerApi } from "@/app/types/nezha-api"
|
import { useServerData } from "@/app/lib/server-data-context"
|
||||||
import { Loader } from "@/components/loading/Loader"
|
import { Loader } from "@/components/loading/Loader"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { useFilter } from "@/lib/network-filter-context"
|
import { useFilter } from "@/lib/network-filter-context"
|
||||||
import { useStatus } from "@/lib/status-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 blogMan from "@/public/blog-man.webp"
|
||||||
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
|
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import useSWRImmutable from "swr/immutable"
|
|
||||||
|
|
||||||
export default function ServerOverviewClient() {
|
export default function ServerOverviewClient() {
|
||||||
|
const { data, error, isLoading } = useServerData()
|
||||||
const { status, setStatus } = useStatus()
|
const { status, setStatus } = useStatus()
|
||||||
const { filter, setFilter } = useFilter()
|
const { filter, setFilter } = useFilter()
|
||||||
const t = useTranslations("ServerOverviewClient")
|
const t = useTranslations("ServerOverviewClient")
|
||||||
|
|
||||||
const { data, error, isLoading } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
|
|
||||||
const disableCartoon = getEnv("NEXT_PUBLIC_DisableCartoon") === "true"
|
const disableCartoon = getEnv("NEXT_PUBLIC_DisableCartoon") === "true"
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
const errorInfo = error as any
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<p className="text-sm font-medium opacity-40">
|
<p 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>
|
||||||
<p className="text-sm font-medium opacity-40">{t("error_message")}</p>
|
<p className="text-sm font-medium opacity-40">{t("error_message")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Footer from "@/app/(main)/footer"
|
import Footer from "@/app/(main)/footer"
|
||||||
import Header from "@/app/(main)/header"
|
import Header from "@/app/(main)/header"
|
||||||
|
import { ServerDataProvider } from "@/app/lib/server-data-context"
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth"
|
||||||
import { SignIn } from "@/components/SignIn"
|
import { SignIn } from "@/components/SignIn"
|
||||||
import getEnv from "@/lib/env-entry"
|
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">
|
<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">
|
<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 />
|
<Header />
|
||||||
<AuthProtected>{children}</AuthProtected>
|
<AuthProtected>
|
||||||
|
<ServerDataProvider>{children}</ServerDataProvider>
|
||||||
|
</AuthProtected>
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { GetServerIP } from "@/lib/serverFetch"
|
import { GetServerIP } from "@/lib/serverFetch"
|
||||||
import fs from "fs"
|
|
||||||
import { AsnResponse, CityResponse, Reader } from "maxmind"
|
import { AsnResponse, CityResponse, Reader } from "maxmind"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
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"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { localeItems } from "@/i18n-metadata"
|
import { localeItems } from "@/i18n-metadata"
|
||||||
import { setUserLocale } from "@/i18n/locale"
|
import { setUserLocale } from "@/i18n/locale"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
import { CheckCircleIcon } from "@heroicons/react/20/solid"
|
import { CheckCircleIcon } from "@heroicons/react/20/solid"
|
||||||
import { useLocale } from "next-intl"
|
import { useLocale } from "next-intl"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
@ -34,11 +35,20 @@ export function LanguageSwitcher() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||||
{localeItems.map((item) => (
|
{localeItems.map((item, index) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={item.code}
|
key={item.code}
|
||||||
onSelect={(e) => handleSelect(e, 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" />}
|
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -37,19 +37,19 @@ export function ModeToggle() {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||||
<DropdownMenuItem
|
<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")}
|
onSelect={(e) => handleSelect(e, "light")}
|
||||||
>
|
>
|
||||||
{t("Light")} {theme === "light" && <CheckCircleIcon className="size-4" />}
|
{t("Light")} {theme === "light" && <CheckCircleIcon className="size-4" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<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")}
|
onSelect={(e) => handleSelect(e, "dark")}
|
||||||
>
|
>
|
||||||
{t("Dark")} {theme === "dark" && <CheckCircleIcon className="size-4" />}
|
{t("Dark")} {theme === "dark" && <CheckCircleIcon className="size-4" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<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")}
|
onSelect={(e) => handleSelect(e, "system")}
|
||||||
>
|
>
|
||||||
{t("System")} {theme === "system" && <CheckCircleIcon className="size-4" />}
|
{t("System")} {theme === "system" && <CheckCircleIcon className="size-4" />}
|
||||||
|
@ -62,7 +62,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -6,19 +6,6 @@ export const localeItems = [
|
|||||||
{ code: "ja", name: "日本語" },
|
{ code: "ja", name: "日本語" },
|
||||||
{ code: "zh-t", name: "中文繁體" },
|
{ code: "zh-t", name: "中文繁體" },
|
||||||
{ code: "zh", 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)
|
export const locales = localeItems.map((item) => item.code)
|
||||||
|
@ -11,7 +11,8 @@
|
|||||||
},
|
},
|
||||||
"ServerListClient": {
|
"ServerListClient": {
|
||||||
"error_message": "Please check your environment variables and review the server console",
|
"error_message": "Please check your environment variables and review the server console",
|
||||||
"defaultTag": "All"
|
"defaultTag": "All",
|
||||||
|
"connecting": "Connecting"
|
||||||
},
|
},
|
||||||
"ServerCard": {
|
"ServerCard": {
|
||||||
"System": "System",
|
"System": "System",
|
||||||
|
@ -11,7 +11,8 @@
|
|||||||
},
|
},
|
||||||
"ServerListClient": {
|
"ServerListClient": {
|
||||||
"error_message": "環境変数を確認し、サーバーコンソールを確認してください",
|
"error_message": "環境変数を確認し、サーバーコンソールを確認してください",
|
||||||
"defaultTag": "すべて"
|
"defaultTag": "すべて",
|
||||||
|
"connecting": "接続中"
|
||||||
},
|
},
|
||||||
"ServerCard": {
|
"ServerCard": {
|
||||||
"System": "システム",
|
"System": "システム",
|
||||||
|
@ -11,7 +11,8 @@
|
|||||||
},
|
},
|
||||||
"ServerListClient": {
|
"ServerListClient": {
|
||||||
"error_message": "請檢查您的環境變數並檢查伺服器控制台",
|
"error_message": "請檢查您的環境變數並檢查伺服器控制台",
|
||||||
"defaultTag": "全部"
|
"defaultTag": "全部",
|
||||||
|
"connecting": "連接中"
|
||||||
},
|
},
|
||||||
"ServerCard": {
|
"ServerCard": {
|
||||||
"System": "系統",
|
"System": "系統",
|
||||||
|
@ -11,7 +11,8 @@
|
|||||||
},
|
},
|
||||||
"ServerListClient": {
|
"ServerListClient": {
|
||||||
"error_message": "请检查您的环境变量并检查服务器控制台",
|
"error_message": "请检查您的环境变量并检查服务器控制台",
|
||||||
"defaultTag": "全部"
|
"defaultTag": "全部",
|
||||||
|
"connecting": "连接中"
|
||||||
},
|
},
|
||||||
"ServerCard": {
|
"ServerCard": {
|
||||||
"System": "系统",
|
"System": "系统",
|
||||||
|
14
package.json
@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "nezha-dash",
|
"name": "nezha-dash",
|
||||||
"version": "1.9.1",
|
"version": "2.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3040",
|
"dev": "next dev -p 3040",
|
||||||
"start": "node .next/standalone/server.js",
|
"start": "node .next/standalone/server.js",
|
||||||
"lint": "next lint",
|
"lint": "biome lint",
|
||||||
"lint:fix": "next lint --fix",
|
"lint:fix": "biome lint --fix",
|
||||||
"format": "prettier --write .",
|
"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": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
|
||||||
"build-dev": "next build",
|
"build-dev": "next build",
|
||||||
"start-dev": "next start"
|
"start-dev": "next start"
|
||||||
@ -60,18 +62,16 @@
|
|||||||
"typescript-eslint": "^8.18.2"
|
"typescript-eslint": "^8.18.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "1.9.4",
|
||||||
"@next/bundle-analyzer": "^15.1.2",
|
"@next/bundle-analyzer": "^15.1.2",
|
||||||
"@tailwindcss/postcss": "^4.0.0-beta.8",
|
"@tailwindcss/postcss": "^4.0.0-beta.8",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"eslint": "^9.17.0",
|
|
||||||
"eslint-config-next": "^15.1.2",
|
"eslint-config-next": "^15.1.2",
|
||||||
"eslint-plugin-turbo": "^2.3.3",
|
"eslint-plugin-turbo": "^2.3.3",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.4.2",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
|
||||||
"tailwindcss": "^4.0.0-beta.8",
|
"tailwindcss": "^4.0.0-beta.8",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vercel": "^39.2.2"
|
"vercel": "^39.2.2"
|
||||||
|