Compare commits
No commits in common. "39c1eca1b0060d567b221f8abe141b6f8f8973d3" and "0c3479bb3bf355a834fe1df4a4c03699d929a8a5" have entirely different histories.
39c1eca1b0
...
0c3479bb3b
8
.eslintrc.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"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
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
.github/1.webp
vendored
Normal file
After Width: | Height: | Size: 139 KiB |
BIN
.github/2-dark.webp
vendored
Normal file
After Width: | Height: | Size: 328 KiB |
BIN
.github/2.webp
vendored
Normal file
After Width: | Height: | Size: 226 KiB |
BIN
.github/3-dark.webp
vendored
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
.github/3.webp
vendored
Normal file
After Width: | Height: | Size: 115 KiB |
BIN
.github/4-dark.webp
vendored
Normal file
After Width: | Height: | Size: 203 KiB |
BIN
.github/4.webp
vendored
Normal file
After Width: | Height: | Size: 135 KiB |
BIN
.github/v2-1.webp
vendored
Before Width: | Height: | Size: 185 KiB |
BIN
.github/v2-2.webp
vendored
Before Width: | Height: | Size: 141 KiB |
BIN
.github/v2-3.webp
vendored
Before Width: | Height: | Size: 126 KiB |
BIN
.github/v2-4.webp
vendored
Before Width: | Height: | Size: 142 KiB |
BIN
.github/v2-dark.webp
vendored
Before Width: | Height: | Size: 183 KiB |
@ -23,8 +23,11 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install
|
run: bun install
|
||||||
|
|
||||||
- name: Run linter & formatter and fix issues
|
- name: Run linter and fix issues
|
||||||
run: bun run check:fix
|
run: bun run lint:fix
|
||||||
|
|
||||||
|
- name: Run formatter
|
||||||
|
run: bun run format
|
||||||
|
|
||||||
- name: Check for changes
|
- name: Check for changes
|
||||||
id: check_changes
|
id: check_changes
|
||||||
|
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"]
|
||||||
|
}
|
13
README.md
@ -31,8 +31,11 @@
|
|||||||
|
|
||||||
[环境变量介绍](https://nezhadash-docs.vercel.app/environment)
|
[环境变量介绍](https://nezhadash-docs.vercel.app/environment)
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { ServerDataWithTimestamp, useServerData } from "@/app/lib/server-data-context"
|
import { NezhaAPISafe, ServerApi } from "@/app/types/nezha-api"
|
||||||
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 } from "@/lib/utils"
|
import { formatBytes, formatNezhaInfo, formatRelativeTime, nezhaFetcher } from "@/lib/utils"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, 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,9 +52,16 @@ export default function ServerDetailChartClient({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTranslations("ServerDetailChartClient")
|
const t = useTranslations("ServerDetailChartClient")
|
||||||
|
|
||||||
const { data: serverList, error, history } = useServerData()
|
const { data: allFallbackData } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
|
||||||
|
const fallbackData = allFallbackData?.result?.find((item) => item.id === server_id)
|
||||||
|
|
||||||
const data = serverList?.result?.find((item) => item.id === server_id)
|
const { data, error } = useSWRImmutable<NezhaAPISafe>(
|
||||||
|
`/api/detail?server_id=${server_id}`,
|
||||||
|
nezhaFetcher,
|
||||||
|
{
|
||||||
|
fallbackData,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@ -70,48 +77,23 @@ 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} history={history} />
|
<CpuChart data={data} />
|
||||||
<ProcessChart data={data} history={history} />
|
<ProcessChart data={data} />
|
||||||
<DiskChart data={data} history={history} />
|
<DiskChart data={data} />
|
||||||
<MemChart data={data} history={history} />
|
<MemChart data={data} />
|
||||||
<NetworkChart data={data} history={history} />
|
<NetworkChart data={data} />
|
||||||
<ConnectChart data={data} history={history} />
|
<ConnectChart data={data} />
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CpuChart({ history, data }: { history: ServerDataWithTimestamp[]; data: NezhaAPISafe }) {
|
function CpuChart({ data }: { 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 && historyLoaded) {
|
if (data) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as cpuChartData[]
|
let newData = [] as cpuChartData[]
|
||||||
if (cpuChartData.length === 0) {
|
if (cpuChartData.length === 0) {
|
||||||
@ -127,7 +109,7 @@ function CpuChart({ history, data }: { history: ServerDataWithTimestamp[]; data:
|
|||||||
}
|
}
|
||||||
setCpuChartData(newData)
|
setCpuChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data, historyLoaded])
|
}, [data])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
cpu: {
|
cpu: {
|
||||||
@ -196,45 +178,15 @@ function CpuChart({ history, data }: { history: ServerDataWithTimestamp[]; data:
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProcessChart({
|
function ProcessChart({ data }: { data: NezhaAPISafe }) {
|
||||||
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 && historyLoaded) {
|
if (data) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as processChartData[]
|
let newData = [] as processChartData[]
|
||||||
if (processChartData.length === 0) {
|
if (processChartData.length === 0) {
|
||||||
@ -250,7 +202,7 @@ function ProcessChart({
|
|||||||
}
|
}
|
||||||
setProcessChartData(newData)
|
setProcessChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data, historyLoaded])
|
}, [data])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
process: {
|
process: {
|
||||||
@ -305,40 +257,15 @@ function ProcessChart({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MemChart({ data, history }: { data: NezhaAPISafe; history: ServerDataWithTimestamp[] }) {
|
function MemChart({ data }: { data: NezhaAPISafe }) {
|
||||||
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 && historyLoaded) {
|
if (data) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as memChartData[]
|
let newData = [] as memChartData[]
|
||||||
if (memChartData.length === 0) {
|
if (memChartData.length === 0) {
|
||||||
@ -354,7 +281,7 @@ function MemChart({ data, history }: { data: NezhaAPISafe; history: ServerDataWi
|
|||||||
}
|
}
|
||||||
setMemChartData(newData)
|
setMemChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data, historyLoaded])
|
}, [data])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
mem: {
|
mem: {
|
||||||
@ -459,39 +386,15 @@ function MemChart({ data, history }: { data: NezhaAPISafe; history: ServerDataWi
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DiskChart({ data, history }: { data: NezhaAPISafe; history: ServerDataWithTimestamp[] }) {
|
function DiskChart({ data }: { data: NezhaAPISafe }) {
|
||||||
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 && historyLoaded) {
|
if (data) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as diskChartData[]
|
let newData = [] as diskChartData[]
|
||||||
if (diskChartData.length === 0) {
|
if (diskChartData.length === 0) {
|
||||||
@ -507,7 +410,7 @@ function DiskChart({ data, history }: { data: NezhaAPISafe; history: ServerDataW
|
|||||||
}
|
}
|
||||||
setDiskChartData(newData)
|
setDiskChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data, historyLoaded])
|
}, [data])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
disk: {
|
disk: {
|
||||||
@ -581,46 +484,15 @@ function DiskChart({ data, history }: { data: NezhaAPISafe; history: ServerDataW
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NetworkChart({
|
function NetworkChart({ data }: { data: NezhaAPISafe }) {
|
||||||
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 && historyLoaded) {
|
if (data) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as networkChartData[]
|
let newData = [] as networkChartData[]
|
||||||
if (networkChartData.length === 0) {
|
if (networkChartData.length === 0) {
|
||||||
@ -636,7 +508,7 @@ function NetworkChart({
|
|||||||
}
|
}
|
||||||
setNetworkChartData(newData)
|
setNetworkChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data, historyLoaded])
|
}, [data])
|
||||||
|
|
||||||
let maxDownload = Math.max(...networkChartData.map((item) => item.download))
|
let maxDownload = Math.max(...networkChartData.map((item) => item.download))
|
||||||
maxDownload = Math.ceil(maxDownload)
|
maxDownload = Math.ceil(maxDownload)
|
||||||
@ -730,45 +602,13 @@ function NetworkChart({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConnectChart({
|
function ConnectChart({ data }: { data: NezhaAPISafe }) {
|
||||||
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 && historyLoaded) {
|
if (data) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
let newData = [] as connectChartData[]
|
let newData = [] as connectChartData[]
|
||||||
if (connectChartData.length === 0) {
|
if (connectChartData.length === 0) {
|
||||||
@ -784,7 +624,7 @@ function ConnectChart({
|
|||||||
}
|
}
|
||||||
setConnectChartData(newData)
|
setConnectChartData(newData)
|
||||||
}
|
}
|
||||||
}, [data, historyLoaded])
|
}, [data])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
tcp: {
|
tcp: {
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useServerData } from "@/app/lib/server-data-context"
|
import { NezhaAPISafe, ServerApi } from "@/app/types/nezha-api"
|
||||||
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 { cn, formatBytes } from "@/lib/utils"
|
import getEnv from "@/lib/env-entry"
|
||||||
|
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")
|
||||||
@ -36,13 +39,24 @@ export default function ServerDetailClient({ server_id }: { server_id: number })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: serverList, error, isLoading } = useServerData()
|
const { data: allFallbackData, isLoading } = useSWRImmutable<ServerApi>(
|
||||||
const data = serverList?.result?.find((item) => item.id === server_id)
|
"/api/server",
|
||||||
|
nezhaFetcher,
|
||||||
|
)
|
||||||
|
const fallbackData = allFallbackData?.result?.find((item) => item.id === server_id)
|
||||||
|
|
||||||
if (!data && !isLoading) {
|
if (!fallbackData && !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,6 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useServerData } from "@/app/lib/server-data-context"
|
import { ServerApi } from "@/app/types/nezha-api"
|
||||||
|
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"
|
||||||
@ -9,7 +11,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 } = useServerData()
|
const { data: nezhaServerList, error } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useServerData } from "@/app/lib/server-data-context"
|
import { ServerApi } from "@/app/types/nezha-api"
|
||||||
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 } from "@/lib/utils"
|
import { cn, nezhaFetcher } 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,7 +70,10 @@ export default function ServerListClient() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { data, error } = useServerData()
|
const { data, error } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
|
||||||
|
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
|
||||||
|
dedupingInterval: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
@ -80,15 +83,7 @@ export default function ServerListClient() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!data?.result)
|
if (!data?.result) return null
|
||||||
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,30 +1,31 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useServerData } from "@/app/lib/server-data-context"
|
import { ServerApi } from "@/app/types/nezha-api"
|
||||||
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 } from "@/lib/utils"
|
import { cn, formatBytes, nezhaFetcher } 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:{errorInfo?.status} {errorInfo.info?.cause ?? errorInfo?.message}
|
Error status:{error.status} {error.info?.cause ?? error.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,6 +1,5 @@
|
|||||||
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"
|
||||||
@ -14,9 +13,7 @@ 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>
|
<AuthProtected>{children}</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"
|
||||||
|
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
"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
@ -1,85 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
2
bunfig.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[install]
|
||||||
|
registry = "https://registry.npmmirror.com/"
|
@ -9,7 +9,6 @@ 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"
|
||||||
@ -35,20 +34,11 @@ 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, index) => (
|
{localeItems.map((item) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={item.code}
|
key={item.code}
|
||||||
onSelect={(e) => handleSelect(e, item.code)}
|
onSelect={(e) => handleSelect(e, item.code)}
|
||||||
className={cn(
|
className={locale === item.code ? "bg-muted gap-3" : ""}
|
||||||
{
|
|
||||||
"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("rounded-b-[5px]", { "gap-3 bg-muted": theme === "light" })}
|
className={cn({ "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("rounded-[5px]", { "gap-3 bg-muted": theme === "dark" })}
|
className={cn({ "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("rounded-t-[5px]", { "gap-3 bg-muted": theme === "system" })}
|
className={cn({ "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 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.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",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -6,6 +6,19 @@ 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,8 +11,7 @@
|
|||||||
},
|
},
|
||||||
"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,8 +11,7 @@
|
|||||||
},
|
},
|
||||||
"ServerListClient": {
|
"ServerListClient": {
|
||||||
"error_message": "環境変数を確認し、サーバーコンソールを確認してください",
|
"error_message": "環境変数を確認し、サーバーコンソールを確認してください",
|
||||||
"defaultTag": "すべて",
|
"defaultTag": "すべて"
|
||||||
"connecting": "接続中"
|
|
||||||
},
|
},
|
||||||
"ServerCard": {
|
"ServerCard": {
|
||||||
"System": "システム",
|
"System": "システム",
|
||||||
|
@ -11,8 +11,7 @@
|
|||||||
},
|
},
|
||||||
"ServerListClient": {
|
"ServerListClient": {
|
||||||
"error_message": "請檢查您的環境變數並檢查伺服器控制台",
|
"error_message": "請檢查您的環境變數並檢查伺服器控制台",
|
||||||
"defaultTag": "全部",
|
"defaultTag": "全部"
|
||||||
"connecting": "連接中"
|
|
||||||
},
|
},
|
||||||
"ServerCard": {
|
"ServerCard": {
|
||||||
"System": "系統",
|
"System": "系統",
|
||||||
|
@ -11,8 +11,7 @@
|
|||||||
},
|
},
|
||||||
"ServerListClient": {
|
"ServerListClient": {
|
||||||
"error_message": "请检查您的环境变量并检查服务器控制台",
|
"error_message": "请检查您的环境变量并检查服务器控制台",
|
||||||
"defaultTag": "全部",
|
"defaultTag": "全部"
|
||||||
"connecting": "连接中"
|
|
||||||
},
|
},
|
||||||
"ServerCard": {
|
"ServerCard": {
|
||||||
"System": "系统",
|
"System": "系统",
|
||||||
|
14
package.json
@ -1,15 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "nezha-dash",
|
"name": "nezha-dash",
|
||||||
"version": "2.0.0",
|
"version": "1.9.1",
|
||||||
"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": "biome lint",
|
"lint": "next lint",
|
||||||
"lint:fix": "biome lint --fix",
|
"lint:fix": "next lint --fix",
|
||||||
"format": "biome format --write .",
|
"format": "prettier --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"
|
||||||
@ -62,16 +60,18 @@
|
|||||||
"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"
|
||||||
|