mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
updates
This commit is contained in:
16
main.go
16
main.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
_ "monitor-site/migrations"
|
_ "monitor-site/migrations"
|
||||||
@@ -151,6 +152,12 @@ func main() {
|
|||||||
if newStatus == "down" || newStatus == "paused" {
|
if newStatus == "down" || newStatus == "paused" {
|
||||||
deleteServerConnection(newRecord)
|
deleteServerConnection(newRecord)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if server is set to pending, try to connect
|
||||||
|
if newStatus == "pending" {
|
||||||
|
go updateServer(newRecord)
|
||||||
|
}
|
||||||
|
|
||||||
// alerts
|
// alerts
|
||||||
handleStatusAlerts(newStatus, oldRecord)
|
handleStatusAlerts(newStatus, oldRecord)
|
||||||
return nil
|
return nil
|
||||||
@@ -218,6 +225,13 @@ func updateServer(record *models.Record) {
|
|||||||
// get server stats from agent
|
// get server stats from agent
|
||||||
systemData, err := requestJson(&server)
|
systemData, err := requestJson(&server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err.Error() == "retry" {
|
||||||
|
// if previous connection was closed, try again
|
||||||
|
app.Logger().Error("Existing SSH connection closed. Retrying...", "host", server.Host, "port", server.Port)
|
||||||
|
deleteServerConnection(record)
|
||||||
|
updateServer(record)
|
||||||
|
return
|
||||||
|
}
|
||||||
app.Logger().Error("Failed to get server stats: ", "err", err.Error())
|
app.Logger().Error("Failed to get server stats: ", "err", err.Error())
|
||||||
updateServerStatus(record, "down")
|
updateServerStatus(record, "down")
|
||||||
return
|
return
|
||||||
@@ -307,7 +321,7 @@ func getServerConnection(server *Server) (*ssh.Client, error) {
|
|||||||
func requestJson(server *Server) (SystemData, error) {
|
func requestJson(server *Server) (SystemData, error) {
|
||||||
session, err := server.Client.NewSession()
|
session, err := server.Client.NewSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return SystemData{}, err
|
return SystemData{}, errors.New("retry")
|
||||||
}
|
}
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
|
||||||
|
BIN
site/bun.lockb
BIN
site/bun.lockb
Binary file not shown.
@@ -26,6 +26,8 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-time": "^3.1.0",
|
||||||
"lucide-react": "^0.407.0",
|
"lucide-react": "^0.407.0",
|
||||||
"nanostores": "^0.10.3",
|
"nanostores": "^0.10.3",
|
||||||
"pocketbase": "^0.21.3",
|
"pocketbase": "^0.21.3",
|
||||||
|
@@ -6,7 +6,8 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime } from '@/lib/stores'
|
||||||
import { cn } from '@/lib/utils'
|
import { chartTimeData, cn } from '@/lib/utils'
|
||||||
|
import { ChartTimes } from '@/types'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
@@ -19,16 +20,20 @@ export default function ChartTimeSelect({ className }: { className?: string }) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select defaultValue="1h" value={chartTime} onValueChange={(value) => $chartTime.set(value)}>
|
<Select
|
||||||
|
defaultValue="1h"
|
||||||
|
value={chartTime}
|
||||||
|
onValueChange={(value: ChartTimes) => $chartTime.set(value)}
|
||||||
|
>
|
||||||
<SelectTrigger className={cn(className, 'w-40 px-5')}>
|
<SelectTrigger className={cn(className, 'w-40 px-5')}>
|
||||||
<SelectValue placeholder="1h" />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="1h">1 hour</SelectItem>
|
{Object.entries(chartTimeData).map(([value, { label }]) => (
|
||||||
<SelectItem value="12h">12 hours</SelectItem>
|
<SelectItem key={label} value={value}>
|
||||||
<SelectItem value="24h">24 hours</SelectItem>
|
{label}
|
||||||
<SelectItem value="1w">1 week</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="30d">30 days</SelectItem>
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)
|
)
|
||||||
|
@@ -8,14 +8,20 @@ import {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from '@/components/ui/chart'
|
} from '@/components/ui/chart'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils'
|
import { formatShortDate, hourWithMinutes } from '@/lib/utils'
|
||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
|
|
||||||
export default function ContainerCpuChart({
|
export default function ContainerCpuChart({
|
||||||
chartData,
|
chartData,
|
||||||
|
ticks,
|
||||||
}: {
|
}: {
|
||||||
chartData: Record<string, number | string>[]
|
chartData: Record<string, number | string>[]
|
||||||
|
ticks: number[]
|
||||||
}) {
|
}) {
|
||||||
|
if (!chartData.length || !ticks.length) {
|
||||||
|
return <Spinner />
|
||||||
|
}
|
||||||
|
|
||||||
const chartConfig = useMemo(() => {
|
const chartConfig = useMemo(() => {
|
||||||
let config = {} as Record<
|
let config = {} as Record<
|
||||||
string,
|
string,
|
||||||
@@ -45,18 +51,12 @@ export default function ContainerCpuChart({
|
|||||||
const hue = ((i * 360) / length) % 360
|
const hue = ((i * 360) / length) % 360
|
||||||
config[key] = {
|
config[key] = {
|
||||||
label: key,
|
label: key,
|
||||||
color: `hsl(${hue}, 60%, 60%)`,
|
color: `hsl(${hue}, 60%, 55%)`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return config satisfies ChartConfig
|
return config satisfies ChartConfig
|
||||||
}, [chartData])
|
}, [chartData])
|
||||||
|
|
||||||
const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData])
|
|
||||||
|
|
||||||
if (!chartData.length) {
|
|
||||||
return <Spinner />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
|
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
|
||||||
<AreaChart
|
<AreaChart
|
||||||
@@ -86,7 +86,7 @@ export default function ContainerCpuChart({
|
|||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickMargin={8}
|
tickMargin={8}
|
||||||
minTickGap={30}
|
minTickGap={30}
|
||||||
tickFormatter={formatShortTime}
|
tickFormatter={hourWithMinutes}
|
||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
// cursor={false}
|
// cursor={false}
|
||||||
@@ -100,9 +100,10 @@ export default function ContainerCpuChart({
|
|||||||
{Object.keys(chartConfig).map((key) => (
|
{Object.keys(chartConfig).map((key) => (
|
||||||
<Area
|
<Area
|
||||||
key={key}
|
key={key}
|
||||||
|
// isAnimationActive={chartData.length < 20}
|
||||||
animateNewValues={false}
|
animateNewValues={false}
|
||||||
dataKey={key}
|
dataKey={key}
|
||||||
type="monotone"
|
type="monotoneX"
|
||||||
fill={chartConfig[key].color}
|
fill={chartConfig[key].color}
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.4}
|
||||||
stroke={chartConfig[key].color}
|
stroke={chartConfig[key].color}
|
||||||
|
@@ -8,11 +8,17 @@ import {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from '@/components/ui/chart'
|
} from '@/components/ui/chart'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils'
|
import { formatShortDate, hourWithMinutes } from '@/lib/utils'
|
||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
|
|
||||||
export default function ({ chartData }: { chartData: Record<string, number | string>[] }) {
|
export default function ({
|
||||||
if (!chartData.length) {
|
chartData,
|
||||||
|
ticks,
|
||||||
|
}: {
|
||||||
|
chartData: Record<string, number | string>[]
|
||||||
|
ticks: number[]
|
||||||
|
}) {
|
||||||
|
if (!chartData.length || !ticks.length) {
|
||||||
return <Spinner />
|
return <Spinner />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,14 +51,12 @@ export default function ({ chartData }: { chartData: Record<string, number | str
|
|||||||
const hue = ((i * 360) / length) % 360
|
const hue = ((i * 360) / length) % 360
|
||||||
config[key] = {
|
config[key] = {
|
||||||
label: key,
|
label: key,
|
||||||
color: `hsl(${hue}, 60%, 60%)`,
|
color: `hsl(${hue}, 60%, 55%)`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return config satisfies ChartConfig
|
return config satisfies ChartConfig
|
||||||
}, [chartData])
|
}, [chartData])
|
||||||
|
|
||||||
const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
|
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
|
||||||
<AreaChart
|
<AreaChart
|
||||||
@@ -72,7 +76,7 @@ export default function ({ chartData }: { chartData: Record<string, number | str
|
|||||||
allowDecimals={false}
|
allowDecimals={false}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
unit={' GiB'}
|
unit={' GB'}
|
||||||
tickFormatter={(x) => {
|
tickFormatter={(x) => {
|
||||||
x = x / 1024
|
x = x / 1024
|
||||||
return x % 1 === 0 ? x : x.toFixed(1)
|
return x % 1 === 0 ? x : x.toFixed(1)
|
||||||
@@ -88,7 +92,7 @@ export default function ({ chartData }: { chartData: Record<string, number | str
|
|||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickMargin={8}
|
tickMargin={8}
|
||||||
minTickGap={30}
|
minTickGap={30}
|
||||||
tickFormatter={formatShortTime}
|
tickFormatter={hourWithMinutes}
|
||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
// cursor={false}
|
// cursor={false}
|
||||||
@@ -102,10 +106,10 @@ export default function ({ chartData }: { chartData: Record<string, number | str
|
|||||||
{Object.keys(chartConfig).map((key) => (
|
{Object.keys(chartConfig).map((key) => (
|
||||||
<Area
|
<Area
|
||||||
key={key}
|
key={key}
|
||||||
// isAnimationActive={false}
|
isAnimationActive={chartData.length < 20}
|
||||||
animateNewValues={false}
|
animateNewValues={false}
|
||||||
dataKey={key}
|
dataKey={key}
|
||||||
type="monotone"
|
type="monotoneX"
|
||||||
fill={chartConfig[key].color}
|
fill={chartConfig[key].color}
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.4}
|
||||||
stroke={chartConfig[key].color}
|
stroke={chartConfig[key].color}
|
||||||
|
@@ -6,9 +6,10 @@ import {
|
|||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from '@/components/ui/chart'
|
} from '@/components/ui/chart'
|
||||||
import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils'
|
import { chartTimeData, formatShortDate } from '@/lib/utils'
|
||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
import { useMemo } from 'react'
|
import { useStore } from '@nanostores/react'
|
||||||
|
import { $chartTime } from '@/lib/stores'
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
cpu: {
|
cpu: {
|
||||||
@@ -17,12 +18,17 @@ const chartConfig = {
|
|||||||
},
|
},
|
||||||
} satisfies ChartConfig
|
} satisfies ChartConfig
|
||||||
|
|
||||||
export default function CpuChart({ chartData }: { chartData: { time: number; cpu: number }[] }) {
|
export default function CpuChart({
|
||||||
if (!chartData?.length) {
|
chartData,
|
||||||
|
ticks,
|
||||||
|
}: {
|
||||||
|
chartData: { time: number; cpu: number }[]
|
||||||
|
ticks: number[]
|
||||||
|
}) {
|
||||||
|
if (!chartData.length || !ticks.length) {
|
||||||
return <Spinner />
|
return <Spinner />
|
||||||
}
|
}
|
||||||
|
const chartTime = useStore($chartTime)
|
||||||
const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
|
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
|
||||||
@@ -42,10 +48,10 @@ export default function CpuChart({ chartData }: { chartData: { time: number; cpu
|
|||||||
ticks={ticks}
|
ticks={ticks}
|
||||||
type="number"
|
type="number"
|
||||||
scale={'time'}
|
scale={'time'}
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
minTickGap={35}
|
minTickGap={35}
|
||||||
tickFormatter={formatShortTime}
|
tickMargin={8}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={chartTimeData[chartTime].format}
|
||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
animationEasing="ease-out"
|
animationEasing="ease-out"
|
||||||
@@ -60,7 +66,7 @@ export default function CpuChart({ chartData }: { chartData: { time: number; cpu
|
|||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="cpu"
|
dataKey="cpu"
|
||||||
type="monotone"
|
type="monotoneX"
|
||||||
fill="var(--color-cpu)"
|
fill="var(--color-cpu)"
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.4}
|
||||||
stroke="var(--color-cpu)"
|
stroke="var(--color-cpu)"
|
||||||
|
@@ -6,26 +6,25 @@ import {
|
|||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from '@/components/ui/chart'
|
} from '@/components/ui/chart'
|
||||||
import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils'
|
import { formatShortDate, hourWithMinutes } from '@/lib/utils'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
// for (const data of chartData) {
|
|
||||||
// data.month = formatDateShort(data.month)
|
|
||||||
// }
|
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
diskUsed: {
|
diskUsed: {
|
||||||
label: 'Disk Usage',
|
label: 'Disk Usage',
|
||||||
color: 'hsl(var(--chart-3))',
|
color: 'hsl(var(--chart-4))',
|
||||||
},
|
},
|
||||||
} satisfies ChartConfig
|
} satisfies ChartConfig
|
||||||
|
|
||||||
export default function DiskChart({
|
export default function DiskChart({
|
||||||
chartData,
|
chartData,
|
||||||
|
ticks,
|
||||||
}: {
|
}: {
|
||||||
chartData: { time: number; disk: number; diskUsed: number }[]
|
chartData: { time: number; disk: number; diskUsed: number }[]
|
||||||
|
ticks: number[]
|
||||||
}) {
|
}) {
|
||||||
if (!chartData.length) {
|
if (!chartData.length || !ticks.length) {
|
||||||
return <Spinner />
|
return <Spinner />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +32,6 @@ export default function DiskChart({
|
|||||||
return Math.round(chartData[0]?.disk)
|
return Math.round(chartData[0]?.disk)
|
||||||
}, [chartData])
|
}, [chartData])
|
||||||
|
|
||||||
const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData])
|
|
||||||
|
|
||||||
// const ticks = useMemo(() => {
|
// const ticks = useMemo(() => {
|
||||||
// let ticks = [0]
|
// let ticks = [0]
|
||||||
// for (let i = 1; i < diskSize; i += diskSize / 5) {
|
// for (let i = 1; i < diskSize; i += diskSize / 5) {
|
||||||
@@ -65,7 +62,7 @@ export default function DiskChart({
|
|||||||
minTickGap={8}
|
minTickGap={8}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
unit={' GiB'}
|
unit={' GB'}
|
||||||
/>
|
/>
|
||||||
{/* todo: short time if first date is same day, otherwise short date */}
|
{/* todo: short time if first date is same day, otherwise short date */}
|
||||||
<XAxis
|
<XAxis
|
||||||
@@ -78,13 +75,13 @@ export default function DiskChart({
|
|||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickMargin={8}
|
tickMargin={8}
|
||||||
minTickGap={30}
|
minTickGap={30}
|
||||||
tickFormatter={formatShortTime}
|
tickFormatter={hourWithMinutes}
|
||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
cursor={false}
|
// cursor={false}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit=" GiB"
|
unit=" GB"
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
/>
|
/>
|
||||||
@@ -92,7 +89,7 @@ export default function DiskChart({
|
|||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="diskUsed"
|
dataKey="diskUsed"
|
||||||
type="monotone"
|
type="monotoneX"
|
||||||
fill="var(--color-diskUsed)"
|
fill="var(--color-diskUsed)"
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.4}
|
||||||
stroke="var(--color-diskUsed)"
|
stroke="var(--color-diskUsed)"
|
||||||
|
97
site/src/components/charts/disk-io-chart.tsx
Normal file
97
site/src/components/charts/disk-io-chart.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChartConfig,
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
} from '@/components/ui/chart'
|
||||||
|
import { formatShortDate, hourWithMinutes } from '@/lib/utils'
|
||||||
|
import Spinner from '../spinner'
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
read: {
|
||||||
|
label: 'Read',
|
||||||
|
color: 'hsl(var(--chart-5))',
|
||||||
|
},
|
||||||
|
write: {
|
||||||
|
label: 'Write',
|
||||||
|
color: 'hsl(var(--chart-3))',
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig
|
||||||
|
|
||||||
|
export default function DiskIoChart({
|
||||||
|
chartData,
|
||||||
|
ticks,
|
||||||
|
}: {
|
||||||
|
chartData: { time: number; read: number; write: number }[]
|
||||||
|
ticks: number[]
|
||||||
|
}) {
|
||||||
|
if (!chartData.length || !ticks.length) {
|
||||||
|
return <Spinner />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
|
||||||
|
<AreaChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={chartData}
|
||||||
|
margin={{
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 10,
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<YAxis
|
||||||
|
width={75}
|
||||||
|
domain={[0, 'auto']}
|
||||||
|
// ticks={ticks}
|
||||||
|
tickCount={9}
|
||||||
|
minTickGap={8}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
unit={' MB/s'}
|
||||||
|
/>
|
||||||
|
{/* todo: short time if first date is same day, otherwise short date */}
|
||||||
|
<XAxis
|
||||||
|
dataKey="time"
|
||||||
|
domain={[ticks[0], ticks.at(-1)!]}
|
||||||
|
ticks={ticks}
|
||||||
|
type="number"
|
||||||
|
scale={'time'}
|
||||||
|
tickLine={true}
|
||||||
|
axisLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
minTickGap={30}
|
||||||
|
tickFormatter={hourWithMinutes}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
// cursor={false}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
unit=" MB/s"
|
||||||
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
||||||
|
indicator="line"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey="read"
|
||||||
|
type="monotoneX"
|
||||||
|
fill="var(--color-read)"
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke="var(--color-read)"
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey="write"
|
||||||
|
type="monotoneX"
|
||||||
|
fill="var(--color-write)"
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke="var(--color-write)"
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
)
|
||||||
|
}
|
@@ -6,21 +6,21 @@ import {
|
|||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from '@/components/ui/chart'
|
} from '@/components/ui/chart'
|
||||||
import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils'
|
import { formatShortDate, hourWithMinutes } from '@/lib/utils'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
|
|
||||||
export default function MemChart({
|
export default function MemChart({
|
||||||
chartData,
|
chartData,
|
||||||
|
ticks,
|
||||||
}: {
|
}: {
|
||||||
chartData: { time: number; mem: number; memUsed: number; memCache: number }[]
|
chartData: { time: number; mem: number; memUsed: number; memCache: number }[]
|
||||||
|
ticks: number[]
|
||||||
}) {
|
}) {
|
||||||
if (!chartData.length) {
|
if (!chartData.length || !ticks.length) {
|
||||||
return <Spinner />
|
return <Spinner />
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData])
|
|
||||||
|
|
||||||
const totalMem = useMemo(() => {
|
const totalMem = useMemo(() => {
|
||||||
return Math.ceil(chartData[0]?.mem)
|
return Math.ceil(chartData[0]?.mem)
|
||||||
}, [chartData])
|
}, [chartData])
|
||||||
@@ -56,7 +56,7 @@ export default function MemChart({
|
|||||||
tickLine={false}
|
tickLine={false}
|
||||||
allowDecimals={false}
|
allowDecimals={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(v) => `${v} GiB`}
|
tickFormatter={(v) => `${v} GB`}
|
||||||
/>
|
/>
|
||||||
{/* todo: short time if first date is same day, otherwise short date */}
|
{/* todo: short time if first date is same day, otherwise short date */}
|
||||||
<XAxis
|
<XAxis
|
||||||
@@ -69,7 +69,7 @@ export default function MemChart({
|
|||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickMargin={8}
|
tickMargin={8}
|
||||||
minTickGap={30}
|
minTickGap={30}
|
||||||
tickFormatter={formatShortTime}
|
tickFormatter={hourWithMinutes}
|
||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
// cursor={false}
|
// cursor={false}
|
||||||
@@ -77,7 +77,7 @@ export default function MemChart({
|
|||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit="GiB"
|
unit="GB"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
itemSorter={(a, b) => a.name.localeCompare(b.name)}
|
itemSorter={(a, b) => a.name.localeCompare(b.name)}
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
||||||
@@ -87,7 +87,7 @@ export default function MemChart({
|
|||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="memUsed"
|
dataKey="memUsed"
|
||||||
type="monotone"
|
type="monotoneX"
|
||||||
fill="var(--color-memUsed)"
|
fill="var(--color-memUsed)"
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.4}
|
||||||
stroke="var(--color-memUsed)"
|
stroke="var(--color-memUsed)"
|
||||||
@@ -95,7 +95,7 @@ export default function MemChart({
|
|||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="memCache"
|
dataKey="memCache"
|
||||||
type="monotone"
|
type="monotoneX"
|
||||||
fill="var(--color-memCache)"
|
fill="var(--color-memCache)"
|
||||||
fillOpacity={0.2}
|
fillOpacity={0.2}
|
||||||
strokeOpacity={0.3}
|
strokeOpacity={0.3}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { $updatedSystem, $systems, pb } from '@/lib/stores'
|
import { $updatedSystem, $systems, pb, $chartTime } from '@/lib/stores'
|
||||||
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
|
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
|
||||||
import { Suspense, lazy, useEffect, useMemo, useState } from 'react'
|
import { Suspense, lazy, useEffect, useMemo, useState } from 'react'
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
||||||
@@ -6,8 +6,10 @@ import { useStore } from '@nanostores/react'
|
|||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
import { ClockArrowUp, CpuIcon, GlobeIcon } from 'lucide-react'
|
import { ClockArrowUp, CpuIcon, GlobeIcon } from 'lucide-react'
|
||||||
import ChartTimeSelect from '../charts/chart-time-select'
|
import ChartTimeSelect from '../charts/chart-time-select'
|
||||||
import { cn, getPbTimestamp } from '@/lib/utils'
|
import { chartTimeData, cn, getPbTimestamp } from '@/lib/utils'
|
||||||
import { Separator } from '../ui/separator'
|
import { Separator } from '../ui/separator'
|
||||||
|
import { scaleTime } from 'd3-scale'
|
||||||
|
import DiskIoChart from '../charts/disk-io-chart'
|
||||||
|
|
||||||
const CpuChart = lazy(() => import('../charts/cpu-chart'))
|
const CpuChart = lazy(() => import('../charts/cpu-chart'))
|
||||||
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
|
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
|
||||||
@@ -23,6 +25,8 @@ function timestampToBrowserTime(timestamp: string) {
|
|||||||
export default function ServerDetail({ name }: { name: string }) {
|
export default function ServerDetail({ name }: { name: string }) {
|
||||||
const servers = useStore($systems)
|
const servers = useStore($systems)
|
||||||
const updatedSystem = useStore($updatedSystem)
|
const updatedSystem = useStore($updatedSystem)
|
||||||
|
const chartTime = useStore($chartTime)
|
||||||
|
const [ticks, setTicks] = useState([] as number[])
|
||||||
const [server, setServer] = useState({} as SystemRecord)
|
const [server, setServer] = useState({} as SystemRecord)
|
||||||
const [containers, setContainers] = useState([] as ContainerStatsRecord[])
|
const [containers, setContainers] = useState([] as ContainerStatsRecord[])
|
||||||
|
|
||||||
@@ -34,6 +38,9 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
const [diskChartData, setDiskChartData] = useState(
|
const [diskChartData, setDiskChartData] = useState(
|
||||||
[] as { time: number; disk: number; diskUsed: number }[]
|
[] as { time: number; disk: number; diskUsed: number }[]
|
||||||
)
|
)
|
||||||
|
const [diskIoChartData, setDiskIoChartData] = useState(
|
||||||
|
[] as { time: number; read: number; write: number }[]
|
||||||
|
)
|
||||||
const [dockerCpuChartData, setDockerCpuChartData] = useState(
|
const [dockerCpuChartData, setDockerCpuChartData] = useState(
|
||||||
[] as Record<string, number | string>[]
|
[] as Record<string, number | string>[]
|
||||||
)
|
)
|
||||||
@@ -44,7 +51,6 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = `${name} / Beszel`
|
document.title = `${name} / Beszel`
|
||||||
return () => {
|
return () => {
|
||||||
console.log('unmounting')
|
|
||||||
setServer({} as SystemRecord)
|
setServer({} as SystemRecord)
|
||||||
setCpuChartData([])
|
setCpuChartData([])
|
||||||
setMemChartData([])
|
setMemChartData([])
|
||||||
@@ -55,14 +61,14 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
}, [name])
|
}, [name])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (server?.id && server.name === name) {
|
if (server.id && server.name === name) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const matchingServer = servers.find((s) => s.name === name) as SystemRecord
|
const matchingServer = servers.find((s) => s.name === name) as SystemRecord
|
||||||
if (matchingServer) {
|
if (matchingServer) {
|
||||||
setServer(matchingServer)
|
setServer(matchingServer)
|
||||||
}
|
}
|
||||||
}, [name, server])
|
}, [name, server, servers])
|
||||||
|
|
||||||
// if visiting directly, make sure server gets set when servers are loaded
|
// if visiting directly, make sure server gets set when servers are loaded
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
@@ -77,17 +83,14 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
|
|
||||||
// get stats
|
// get stats
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!('id' in server)) {
|
if (!server.id) {
|
||||||
console.log('no id in server')
|
|
||||||
return
|
return
|
||||||
} else {
|
|
||||||
console.log('id in server')
|
|
||||||
}
|
}
|
||||||
pb.collection<SystemStatsRecord>('system_stats')
|
pb.collection<SystemStatsRecord>('system_stats')
|
||||||
.getFullList({
|
.getFullList({
|
||||||
filter: `system="${server.id}" && created > "${getPbTimestamp('1h')}"`,
|
filter: `system="${server.id}" && created > "${getPbTimestamp('1h')}"`,
|
||||||
fields: 'created,stats',
|
fields: 'created,stats',
|
||||||
sort: '-created',
|
sort: 'created',
|
||||||
})
|
})
|
||||||
.then((records) => {
|
.then((records) => {
|
||||||
// console.log('sctats', records)
|
// console.log('sctats', records)
|
||||||
@@ -101,17 +104,15 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
}
|
}
|
||||||
}, [updatedSystem])
|
}, [updatedSystem])
|
||||||
|
|
||||||
// get cpu data
|
// create cpu / mem / disk data for charts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!serverStats.length) {
|
if (!serverStats.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('stats', serverStats)
|
|
||||||
// let maxCpu = 0
|
|
||||||
const cpuData = [] as typeof cpuChartData
|
const cpuData = [] as typeof cpuChartData
|
||||||
const memData = [] as typeof memChartData
|
const memData = [] as typeof memChartData
|
||||||
const diskData = [] as typeof diskChartData
|
const diskData = [] as typeof diskChartData
|
||||||
|
const diskIoData = [] as typeof diskIoChartData
|
||||||
for (let { created, stats } of serverStats) {
|
for (let { created, stats } of serverStats) {
|
||||||
const time = new Date(created).getTime()
|
const time = new Date(created).getTime()
|
||||||
cpuData.push({ time, cpu: stats.cpu })
|
cpuData.push({ time, cpu: stats.cpu })
|
||||||
@@ -122,18 +123,33 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
memCache: stats.mb,
|
memCache: stats.mb,
|
||||||
})
|
})
|
||||||
diskData.push({ time, disk: stats.d, diskUsed: stats.du })
|
diskData.push({ time, disk: stats.d, diskUsed: stats.du })
|
||||||
|
diskIoData.push({ time, read: stats.dr, write: stats.dw })
|
||||||
}
|
}
|
||||||
setCpuChartData(cpuData.reverse())
|
setCpuChartData(cpuData)
|
||||||
setMemChartData(memData.reverse())
|
setMemChartData(memData)
|
||||||
setDiskChartData(diskData.reverse())
|
setDiskChartData(diskData)
|
||||||
|
setDiskIoChartData(diskIoData)
|
||||||
}, [serverStats])
|
}, [serverStats])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!serverStats.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const now = new Date()
|
||||||
|
const startTime = chartTimeData[chartTime].getOffset(now)
|
||||||
|
const scale = scaleTime([startTime.getTime(), now], [0, cpuChartData.length])
|
||||||
|
setTicks(scale.ticks().map((d) => d.getTime()))
|
||||||
|
}, [chartTime, serverStats])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!server.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
pb.collection<ContainerStatsRecord>('container_stats')
|
pb.collection<ContainerStatsRecord>('container_stats')
|
||||||
.getFullList({
|
.getFullList({
|
||||||
filter: `system="${server.id}" && created > "${getPbTimestamp('1h')}"`,
|
filter: `system="${server.id}" && created > "${getPbTimestamp('1h')}"`,
|
||||||
fields: 'created,stats',
|
fields: 'created,stats',
|
||||||
sort: '-created',
|
sort: 'created',
|
||||||
})
|
})
|
||||||
.then((records) => {
|
.then((records) => {
|
||||||
setContainers(records)
|
setContainers(records)
|
||||||
@@ -158,19 +174,18 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
dockerMemData.push(memData)
|
dockerMemData.push(memData)
|
||||||
}
|
}
|
||||||
// console.log('containerMemData', containerMemData)
|
// console.log('containerMemData', containerMemData)
|
||||||
setDockerCpuChartData(dockerCpuData.reverse())
|
setDockerCpuChartData(dockerCpuData)
|
||||||
setDockerMemChartData(dockerMemData.reverse())
|
setDockerMemChartData(dockerMemData)
|
||||||
}, [containers])
|
}, [containers])
|
||||||
const uptime = useMemo(() => {
|
const uptime = useMemo(() => {
|
||||||
console.log('making uptime')
|
|
||||||
let uptime = server.info?.u || 0
|
let uptime = server.info?.u || 0
|
||||||
if (uptime < 172800) {
|
if (uptime < 172800) {
|
||||||
return `${Math.floor(uptime / 3600)} hours`
|
return `${Math.trunc(uptime / 3600)} hours`
|
||||||
}
|
}
|
||||||
return `${Math.floor(server.info?.u / 86400)} days`
|
return `${Math.trunc(server.info?.u / 86400)} days`
|
||||||
}, [server.info?.u])
|
}, [server.info?.u])
|
||||||
|
|
||||||
if (!('id' in server)) {
|
if (!server.id) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,25 +235,34 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
title="Total CPU Usage"
|
title="Total CPU Usage"
|
||||||
description="Average system-wide CPU utilization as a percentage"
|
description="Average system-wide CPU utilization as a percentage"
|
||||||
>
|
>
|
||||||
<CpuChart chartData={cpuChartData} />
|
<CpuChart chartData={cpuChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{dockerCpuChartData.length > 0 && (
|
{dockerCpuChartData.length > 0 && (
|
||||||
<ChartCard title="Docker CPU Usage" description="CPU utilization of docker containers">
|
<ChartCard title="Docker CPU Usage" description="CPU utilization of docker containers">
|
||||||
<ContainerCpuChart chartData={dockerCpuChartData} />
|
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
<ChartCard title="Total Memory Usage" description="Precise utilization at the recorded time">
|
<ChartCard title="Total Memory Usage" description="Precise utilization at the recorded time">
|
||||||
<MemChart chartData={memChartData} />
|
<MemChart chartData={memChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{dockerMemChartData.length > 0 && (
|
{dockerMemChartData.length > 0 && (
|
||||||
<ChartCard title="Docker Memory Usage" description="Memory usage of docker containers">
|
<ChartCard title="Docker Memory Usage" description="Memory usage of docker containers">
|
||||||
<ContainerMemChart chartData={dockerMemChartData} />
|
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
<ChartCard title="Disk Usage" description="Precise usage at the recorded time">
|
<ChartCard
|
||||||
<DiskChart chartData={diskChartData} />
|
title="Disk Usage"
|
||||||
|
description="Usage of partition where the root filesystem is mounted"
|
||||||
|
>
|
||||||
|
<DiskChart chartData={diskChartData} ticks={ticks} />
|
||||||
|
</ChartCard>
|
||||||
|
<ChartCard
|
||||||
|
title="Disk I/O"
|
||||||
|
description="Throughput of disk where the root filesystem is mounted"
|
||||||
|
>
|
||||||
|
<DiskIoChart chartData={diskIoChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import PocketBase from 'pocketbase'
|
import PocketBase from 'pocketbase'
|
||||||
import { atom } from 'nanostores'
|
import { atom, WritableAtom } from 'nanostores'
|
||||||
import { AlertRecord, SystemRecord } from '@/types'
|
import { AlertRecord, ChartTimes, SystemRecord } from '@/types'
|
||||||
import { createRouter } from '@nanostores/router'
|
import { createRouter } from '@nanostores/router'
|
||||||
|
|
||||||
/** PocketBase JS Client */
|
/** PocketBase JS Client */
|
||||||
@@ -35,4 +35,4 @@ export const $alerts = atom([] as AlertRecord[])
|
|||||||
export const $publicKey = atom('')
|
export const $publicKey = atom('')
|
||||||
|
|
||||||
/** Chart time period */
|
/** Chart time period */
|
||||||
export const $chartTime = atom('1h')
|
export const $chartTime = atom('1h') as WritableAtom<ChartTimes>
|
||||||
|
@@ -2,9 +2,10 @@ import { toast } from '@/components/ui/use-toast'
|
|||||||
import { type ClassValue, clsx } from 'clsx'
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
import { $alerts, $systems, pb } from './stores'
|
import { $alerts, $systems, pb } from './stores'
|
||||||
import { AlertRecord, SystemRecord } from '@/types'
|
import { AlertRecord, ChartTimes, SystemRecord } from '@/types'
|
||||||
import { RecordModel, RecordSubscription } from 'pocketbase'
|
import { RecordModel, RecordSubscription } from 'pocketbase'
|
||||||
import { WritableAtom } from 'nanostores'
|
import { WritableAtom } from 'nanostores'
|
||||||
|
import { timeDay, timeHour } from 'd3-time'
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
@@ -42,32 +43,35 @@ export const updateAlerts = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const shortTimeFormatter = new Intl.DateTimeFormat(undefined, {
|
const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, {
|
||||||
// day: 'numeric',
|
|
||||||
// month: 'numeric',
|
|
||||||
// year: '2-digit',
|
|
||||||
// hour12: false,
|
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
})
|
})
|
||||||
export const formatShortTime = (timestamp: string) => {
|
export const hourWithMinutes = (timestamp: string) => {
|
||||||
// console.log('ts', timestamp)
|
return hourWithMinutesFormatter.format(new Date(timestamp))
|
||||||
return shortTimeFormatter.format(new Date(timestamp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const shortDateFormatter = new Intl.DateTimeFormat(undefined, {
|
const shortDateFormatter = new Intl.DateTimeFormat(undefined, {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
// year: '2-digit',
|
|
||||||
// hour12: false,
|
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
})
|
})
|
||||||
export const formatShortDate = (timestamp: string) => {
|
export const formatShortDate = (timestamp: string) => {
|
||||||
console.log('ts', timestamp)
|
// console.log('ts', timestamp)
|
||||||
return shortDateFormatter.format(new Date(timestamp))
|
return shortDateFormatter.format(new Date(timestamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dayFormatter = new Intl.DateTimeFormat(undefined, {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
// dateStyle: 'medium',
|
||||||
|
})
|
||||||
|
export const formatDay = (timestamp: string) => {
|
||||||
|
// console.log('ts', timestamp)
|
||||||
|
return dayFormatter.format(new Date(timestamp))
|
||||||
|
}
|
||||||
|
|
||||||
export const updateFavicon = (newIconUrl: string) =>
|
export const updateFavicon = (newIconUrl: string) =>
|
||||||
((document.querySelector("link[rel='icon']") as HTMLLinkElement).href = newIconUrl)
|
((document.querySelector("link[rel='icon']") as HTMLLinkElement).href = newIconUrl)
|
||||||
|
|
||||||
@@ -103,33 +107,42 @@ export function updateRecordList<T extends RecordModel>(
|
|||||||
$store.set(newRecords)
|
$store.set(newRecords)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPbTimestamp(timeString: string) {
|
export function getPbTimestamp(timeString: ChartTimes) {
|
||||||
const now = new Date()
|
const d = chartTimeData[timeString].getOffset(new Date())
|
||||||
let timeValue = parseInt(timeString.slice(0, -1))
|
const year = d.getUTCFullYear()
|
||||||
let unit = timeString.slice(-1)
|
const month = String(d.getUTCMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getUTCDate()).padStart(2, '0')
|
||||||
if (unit === 'h') {
|
const hours = String(d.getUTCHours()).padStart(2, '0')
|
||||||
now.setUTCHours(now.getUTCHours() - timeValue)
|
const minutes = String(d.getUTCMinutes()).padStart(2, '0')
|
||||||
} else {
|
const seconds = String(d.getUTCSeconds()).padStart(2, '0')
|
||||||
// d
|
|
||||||
now.setUTCDate(now.getUTCDate() - timeValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
const year = now.getUTCFullYear()
|
|
||||||
const month = String(now.getUTCMonth() + 1).padStart(2, '0')
|
|
||||||
const day = String(now.getUTCDate()).padStart(2, '0')
|
|
||||||
const hours = String(now.getUTCHours()).padStart(2, '0')
|
|
||||||
const minutes = String(now.getUTCMinutes()).padStart(2, '0')
|
|
||||||
const seconds = String(now.getUTCSeconds()).padStart(2, '0')
|
|
||||||
|
|
||||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calculateXaxisTicks = (chartData: any[]) => {
|
export const chartTimeData = {
|
||||||
const ticks: number[] = []
|
'1h': {
|
||||||
const lastDate = chartData.at(-1)!.time
|
label: '1 hour',
|
||||||
for (let i = 60; i >= 0; i--) {
|
format: (timestamp: string) => hourWithMinutes(timestamp),
|
||||||
ticks.push(lastDate - i * 60 * 1000)
|
getOffset: (endTime: Date) => timeHour.offset(endTime, -1),
|
||||||
}
|
},
|
||||||
return ticks
|
'12h': {
|
||||||
|
label: '12 hours',
|
||||||
|
format: (timestamp: string) => hourWithMinutes(timestamp),
|
||||||
|
getOffset: (endTime: Date) => timeHour.offset(endTime, -12),
|
||||||
|
},
|
||||||
|
'24h': {
|
||||||
|
label: '24 hours',
|
||||||
|
format: (timestamp: string) => hourWithMinutes(timestamp),
|
||||||
|
getOffset: (endTime: Date) => timeHour.offset(endTime, -24),
|
||||||
|
},
|
||||||
|
'1w': {
|
||||||
|
label: '1 week',
|
||||||
|
format: (timestamp: string) => formatDay(timestamp),
|
||||||
|
getOffset: (endTime: Date) => timeDay.offset(endTime, -7),
|
||||||
|
},
|
||||||
|
'30d': {
|
||||||
|
label: '30 days',
|
||||||
|
format: (timestamp: string) => formatDay(timestamp),
|
||||||
|
getOffset: (endTime: Date) => timeDay.offset(endTime, -30),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
2
site/src/types.d.ts
vendored
2
site/src/types.d.ts
vendored
@@ -75,3 +75,5 @@ export interface AlertRecord extends RecordModel {
|
|||||||
name: string
|
name: string
|
||||||
// user: string
|
// user: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChartTimes = '1h' | '12h' | '24h' | '1w' | '30d'
|
||||||
|
Reference in New Issue
Block a user