mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
extra fs charts and filter bar for container charts
This commit is contained in:
@@ -9,7 +9,7 @@ import { useMemo, useRef } from 'react'
|
|||||||
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
|
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime, $containerFilter } from '@/lib/stores'
|
||||||
|
|
||||||
export default function ContainerCpuChart({
|
export default function ContainerCpuChart({
|
||||||
chartData,
|
chartData,
|
||||||
@@ -21,6 +21,7 @@ export default function ContainerCpuChart({
|
|||||||
const chartRef = useRef<HTMLDivElement>(null)
|
const chartRef = useRef<HTMLDivElement>(null)
|
||||||
const yAxisWidth = useYaxisWidth(chartRef)
|
const yAxisWidth = useYaxisWidth(chartRef)
|
||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
|
const filter = useStore($containerFilter)
|
||||||
|
|
||||||
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
@@ -109,23 +110,27 @@ export default function ContainerCpuChart({
|
|||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
itemSorter={(a, b) => b.value - a.value}
|
itemSorter={(a, b) => b.value - a.value}
|
||||||
content={<ChartTooltipContent unit="%" indicator="line" />}
|
content={<ChartTooltipContent filter={filter} unit="%" indicator="line" />}
|
||||||
/>
|
/>
|
||||||
{Object.keys(chartConfig).map((key) => (
|
{Object.keys(chartConfig).map((key) => {
|
||||||
|
const filtered = filter && !key.includes(filter)
|
||||||
|
let fillOpacity = filtered ? 0.05 : 0.4
|
||||||
|
let strokeOpacity = filtered ? 0.1 : 1
|
||||||
|
return (
|
||||||
<Area
|
<Area
|
||||||
key={key}
|
key={key}
|
||||||
// isAnimationActive={chartData.length < 20}
|
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
// animateNewValues={false}
|
|
||||||
// animationDuration={1200}
|
|
||||||
dataKey={key}
|
dataKey={key}
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill={chartConfig[key].color}
|
fill={chartConfig[key].color}
|
||||||
fillOpacity={0.4}
|
fillOpacity={fillOpacity}
|
||||||
stroke={chartConfig[key].color}
|
stroke={chartConfig[key].color}
|
||||||
|
strokeOpacity={strokeOpacity}
|
||||||
|
activeDot={{ opacity: filtered ? 0 : 1 }}
|
||||||
stackId="a"
|
stackId="a"
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime, $containerFilter } from '@/lib/stores'
|
||||||
|
|
||||||
export default function ContainerMemChart({
|
export default function ContainerMemChart({
|
||||||
chartData,
|
chartData,
|
||||||
@@ -27,6 +27,7 @@ export default function ContainerMemChart({
|
|||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
const chartRef = useRef<HTMLDivElement>(null)
|
const chartRef = useRef<HTMLDivElement>(null)
|
||||||
const yAxisWidth = useYaxisWidth(chartRef)
|
const yAxisWidth = useYaxisWidth(chartRef)
|
||||||
|
const filter = useStore($containerFilter)
|
||||||
|
|
||||||
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
@@ -114,21 +115,27 @@ export default function ContainerMemChart({
|
|||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
itemSorter={(a, b) => b.value - a.value}
|
itemSorter={(a, b) => b.value - a.value}
|
||||||
content={<ChartTooltipContent unit=" MB" indicator="line" />}
|
content={<ChartTooltipContent filter={filter} unit=" MB" indicator="line" />}
|
||||||
/>
|
/>
|
||||||
{Object.keys(chartConfig).map((key) => (
|
{Object.keys(chartConfig).map((key) => {
|
||||||
|
const filtered = filter && !key.includes(filter)
|
||||||
|
let fillOpacity = filtered ? 0.05 : 0.4
|
||||||
|
let strokeOpacity = filtered ? 0.1 : 1
|
||||||
|
return (
|
||||||
<Area
|
<Area
|
||||||
key={key}
|
key={key}
|
||||||
// animationDuration={1200}
|
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
dataKey={key}
|
dataKey={key}
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill={chartConfig[key].color}
|
fill={chartConfig[key].color}
|
||||||
fillOpacity={0.4}
|
strokeOpacity={strokeOpacity}
|
||||||
|
fillOpacity={fillOpacity}
|
||||||
stroke={chartConfig[key].color}
|
stroke={chartConfig[key].color}
|
||||||
|
activeDot={filtered ? false : {}}
|
||||||
stackId="a"
|
stackId="a"
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime, $containerFilter } from '@/lib/stores'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
|
||||||
export default function ContainerCpuChart({
|
export default function ContainerCpuChart({
|
||||||
@@ -28,6 +28,7 @@ export default function ContainerCpuChart({
|
|||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
const chartRef = useRef<HTMLDivElement>(null)
|
const chartRef = useRef<HTMLDivElement>(null)
|
||||||
const yAxisWidth = useYaxisWidth(chartRef)
|
const yAxisWidth = useYaxisWidth(chartRef)
|
||||||
|
const filter = useStore($containerFilter)
|
||||||
|
|
||||||
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
@@ -116,6 +117,7 @@ export default function ContainerCpuChart({
|
|||||||
itemSorter={(a, b) => b.value - a.value}
|
itemSorter={(a, b) => b.value - a.value}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
|
filter={filter}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
contentFormatter={(item, key) => {
|
contentFormatter={(item, key) => {
|
||||||
try {
|
try {
|
||||||
@@ -136,7 +138,11 @@ export default function ContainerCpuChart({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{Object.keys(chartConfig).map((key) => (
|
{Object.keys(chartConfig).map((key) => {
|
||||||
|
const filtered = filter && !key.includes(filter)
|
||||||
|
let fillOpacity = filtered ? 0.05 : 0.4
|
||||||
|
let strokeOpacity = filtered ? 0.1 : 1
|
||||||
|
return (
|
||||||
<Area
|
<Area
|
||||||
key={key}
|
key={key}
|
||||||
name={key}
|
name={key}
|
||||||
@@ -145,11 +151,14 @@ export default function ContainerCpuChart({
|
|||||||
dataKey={(data) => data?.[key]?.[2] ?? 0}
|
dataKey={(data) => data?.[key]?.[2] ?? 0}
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill={chartConfig[key].color}
|
fill={chartConfig[key].color}
|
||||||
fillOpacity={0.4}
|
fillOpacity={fillOpacity}
|
||||||
stroke={chartConfig[key].color}
|
stroke={chartConfig[key].color}
|
||||||
|
strokeOpacity={strokeOpacity}
|
||||||
|
activeDot={{ opacity: filtered ? 0 : 1 }}
|
||||||
stackId="a"
|
stackId="a"
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -22,22 +22,9 @@ export default function DiskChart({
|
|||||||
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
const diskSize = useMemo(() => {
|
const diskSize = useMemo(() => {
|
||||||
return Math.round(systemData[0]?.stats.d)
|
return Math.round(systemData.at(-1)?.stats.d ?? NaN)
|
||||||
}, [systemData])
|
}, [systemData])
|
||||||
|
|
||||||
// const ticks = useMemo(() => {
|
|
||||||
// let ticks = [0]
|
|
||||||
// for (let i = 1; i < diskSize; i += diskSize / 5) {
|
|
||||||
// ticks.push(Math.trunc(i))
|
|
||||||
// }
|
|
||||||
// ticks.push(diskSize)
|
|
||||||
// return ticks
|
|
||||||
// }, [diskSize])
|
|
||||||
|
|
||||||
// if (!systemData.length || !ticks.length) {
|
|
||||||
// return <Spinner />
|
|
||||||
// }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={chartRef}>
|
<div ref={chartRef}>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
@@ -63,6 +50,7 @@ export default function DiskChart({
|
|||||||
width={yAxisWidth}
|
width={yAxisWidth}
|
||||||
domain={[0, diskSize]}
|
domain={[0, diskSize]}
|
||||||
tickCount={9}
|
tickCount={9}
|
||||||
|
minTickGap={6}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
unit={' GB'}
|
unit={' GB'}
|
||||||
|
92
beszel/site/src/components/charts/extra-fs-disk-chart.tsx
Normal file
92
beszel/site/src/components/charts/extra-fs-disk-chart.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
|
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
|
||||||
|
import { useMemo, useRef } from 'react'
|
||||||
|
// import Spinner from '../spinner'
|
||||||
|
import { useStore } from '@nanostores/react'
|
||||||
|
import { $chartTime } from '@/lib/stores'
|
||||||
|
import { SystemStatsRecord } from '@/types'
|
||||||
|
|
||||||
|
export default function ExFsDiskChart({
|
||||||
|
ticks,
|
||||||
|
systemData,
|
||||||
|
fs,
|
||||||
|
}: {
|
||||||
|
ticks: number[]
|
||||||
|
systemData: SystemStatsRecord[]
|
||||||
|
fs: string
|
||||||
|
}) {
|
||||||
|
const chartTime = useStore($chartTime)
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null)
|
||||||
|
const yAxisWidth = useYaxisWidth(chartRef)
|
||||||
|
|
||||||
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
|
const diskSize = useMemo(() => {
|
||||||
|
const size = systemData.at(-1)?.stats.efs?.[fs].d ?? 0
|
||||||
|
return size > 10 ? Math.round(size) : size
|
||||||
|
}, [systemData])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={chartRef}>
|
||||||
|
<ChartContainer
|
||||||
|
config={{}}
|
||||||
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
|
'opacity-100': yAxisSet,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AreaChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={systemData}
|
||||||
|
margin={{
|
||||||
|
top: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<YAxis
|
||||||
|
className="tracking-tighter"
|
||||||
|
width={yAxisWidth}
|
||||||
|
domain={[0, diskSize]}
|
||||||
|
tickCount={9}
|
||||||
|
minTickGap={6}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
unit={' GB'}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="created"
|
||||||
|
domain={[ticks[0], ticks.at(-1)!]}
|
||||||
|
ticks={ticks}
|
||||||
|
type="number"
|
||||||
|
scale={'time'}
|
||||||
|
minTickGap={35}
|
||||||
|
tickMargin={8}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={chartTimeData[chartTime].format}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
animationEasing="ease-out"
|
||||||
|
animationDuration={150}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
unit=" GB"
|
||||||
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
indicator="line"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey={`stats.efs.${fs}.du`}
|
||||||
|
name="Disk Usage"
|
||||||
|
type="monotoneX"
|
||||||
|
fill="hsl(var(--chart-4))"
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke="hsl(var(--chart-4))"
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
111
beszel/site/src/components/charts/extra-fs-disk-io-chart.tsx
Normal file
111
beszel/site/src/components/charts/extra-fs-disk-io-chart.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
|
import {
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
toFixedWithoutTrailingZeros,
|
||||||
|
useYaxisWidth,
|
||||||
|
} from '@/lib/utils'
|
||||||
|
// import Spinner from '../spinner'
|
||||||
|
import { useStore } from '@nanostores/react'
|
||||||
|
import { $chartTime } from '@/lib/stores'
|
||||||
|
import { SystemStatsRecord } from '@/types'
|
||||||
|
import { useMemo, useRef } from 'react'
|
||||||
|
|
||||||
|
export default function ExFsDiskIoChart({
|
||||||
|
ticks,
|
||||||
|
systemData,
|
||||||
|
fs,
|
||||||
|
}: {
|
||||||
|
ticks: number[]
|
||||||
|
systemData: SystemStatsRecord[]
|
||||||
|
fs: string
|
||||||
|
}) {
|
||||||
|
const chartTime = useStore($chartTime)
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null)
|
||||||
|
const yAxisWidth = useYaxisWidth(chartRef)
|
||||||
|
|
||||||
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
|
// if (!systemData.length || !ticks.length) {
|
||||||
|
// return <Spinner />
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={chartRef}>
|
||||||
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
|
<ChartContainer
|
||||||
|
config={{}}
|
||||||
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
|
'opacity-100': yAxisSet,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AreaChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={systemData}
|
||||||
|
margin={{
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 10,
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<YAxis
|
||||||
|
className="tracking-tighter"
|
||||||
|
width={yAxisWidth}
|
||||||
|
// domain={[0, (max: number) => (max <= 0.4 ? 0.4 : Math.ceil(max))]}
|
||||||
|
tickFormatter={(value) => toFixedWithoutTrailingZeros(value, 2)}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
unit={' MB/s'}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="created"
|
||||||
|
domain={[ticks[0], ticks.at(-1)!]}
|
||||||
|
ticks={ticks}
|
||||||
|
type="number"
|
||||||
|
scale={'time'}
|
||||||
|
minTickGap={35}
|
||||||
|
tickMargin={8}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={chartTimeData[chartTime].format}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
animationEasing="ease-out"
|
||||||
|
animationDuration={150}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
unit=" MB/s"
|
||||||
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
indicator="line"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey={`stats.efs.${fs}.w`}
|
||||||
|
name="Write"
|
||||||
|
type="monotoneX"
|
||||||
|
fill="hsl(var(--chart-3))"
|
||||||
|
fillOpacity={0.3}
|
||||||
|
stroke="hsl(var(--chart-3))"
|
||||||
|
// animationDuration={1200}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey={`stats.efs.${fs}.r`}
|
||||||
|
name="Read"
|
||||||
|
type="monotoneX"
|
||||||
|
fill="hsl(var(--chart-1))"
|
||||||
|
fillOpacity={0.3}
|
||||||
|
stroke="hsl(var(--chart-1))"
|
||||||
|
// animationDuration={1200}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@@ -51,7 +51,7 @@ export default function () {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Filter..."
|
placeholder="Filter..."
|
||||||
onChange={(e) => setFilter(e.target.value)}
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
className="w-full md:w-56 lg:w-80 ml-auto pl-4"
|
className="w-full md:w-56 lg:w-80 ml-auto px-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
@@ -1,10 +1,17 @@
|
|||||||
import { $systems, pb, $chartTime } from '@/lib/stores'
|
import { $systems, pb, $chartTime, $containerFilter } from '@/lib/stores'
|
||||||
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
|
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
|
||||||
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon } from 'lucide-react'
|
import {
|
||||||
|
ClockArrowUp,
|
||||||
|
CpuIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
LayoutGridIcon,
|
||||||
|
StretchHorizontalIcon,
|
||||||
|
XIcon,
|
||||||
|
} from 'lucide-react'
|
||||||
import ChartTimeSelect from '../charts/chart-time-select'
|
import ChartTimeSelect from '../charts/chart-time-select'
|
||||||
import {
|
import {
|
||||||
chartTimeData,
|
chartTimeData,
|
||||||
@@ -16,8 +23,8 @@ import {
|
|||||||
import { Separator } from '../ui/separator'
|
import { Separator } from '../ui/separator'
|
||||||
import { scaleTime } from 'd3-scale'
|
import { scaleTime } from 'd3-scale'
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
||||||
import { Toggle } from '../ui/toggle'
|
import { Button, buttonVariants } from '../ui/button'
|
||||||
import { buttonVariants } from '../ui/button'
|
import { Input } from '../ui/input'
|
||||||
|
|
||||||
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'))
|
||||||
@@ -29,6 +36,8 @@ const BandwidthChart = lazy(() => import('../charts/bandwidth-chart'))
|
|||||||
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
|
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
|
||||||
const SwapChart = lazy(() => import('../charts/swap-chart'))
|
const SwapChart = lazy(() => import('../charts/swap-chart'))
|
||||||
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
||||||
|
const ExFsDiskChart = lazy(() => import('../charts/extra-fs-disk-chart'))
|
||||||
|
const ExFsDiskIoChart = lazy(() => import('../charts/extra-fs-disk-io-chart'))
|
||||||
|
|
||||||
export default function SystemDetail({ name }: { name: string }) {
|
export default function SystemDetail({ name }: { name: string }) {
|
||||||
const systems = useStore($systems)
|
const systems = useStore($systems)
|
||||||
@@ -38,6 +47,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
const [system, setSystem] = useState({} as SystemRecord)
|
const [system, setSystem] = useState({} as SystemRecord)
|
||||||
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
||||||
const [hasDockerStats, setHasDocker] = useState(false)
|
const [hasDockerStats, setHasDocker] = useState(false)
|
||||||
|
const netCardRef = useRef<HTMLDivElement>(null)
|
||||||
const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>(
|
const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@@ -62,6 +72,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
setDockerCpuChartData([])
|
setDockerCpuChartData([])
|
||||||
setDockerMemChartData([])
|
setDockerMemChartData([])
|
||||||
setDockerNetChartData([])
|
setDockerNetChartData([])
|
||||||
|
$containerFilter.set('')
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(resetCharts, [chartTime])
|
useEffect(resetCharts, [chartTime])
|
||||||
@@ -114,7 +125,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
if (prevTime) {
|
if (prevTime) {
|
||||||
const interval = record.created - prevTime
|
const interval = record.created - prevTime
|
||||||
// if interval is too large, add a null record
|
// if interval is too large, add a null record
|
||||||
if (interval - interval * 0.5 > expectedInterval) {
|
if (interval > expectedInterval / 2 + expectedInterval) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
modifiedRecords.push({ created: null, stats: null })
|
modifiedRecords.push({ created: null, stats: null })
|
||||||
}
|
}
|
||||||
@@ -195,13 +206,28 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
return `${Math.trunc(system.info?.u / 86400)} days`
|
return `${Math.trunc(system.info?.u / 86400)} days`
|
||||||
}, [system.info?.u])
|
}, [system.info?.u])
|
||||||
|
|
||||||
|
/** Space for tooltip if more than 12 containers */
|
||||||
|
const bottomSpacing = useMemo(() => {
|
||||||
|
if (!netCardRef.current || !dockerNetChartData.length) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const tooltipHeight = (Object.keys(dockerNetChartData[0]).length - 11) * 17.8 - 40
|
||||||
|
const wrapperEl = document.getElementById('chartwrap') as HTMLDivElement
|
||||||
|
const wrapperRect = wrapperEl.getBoundingClientRect()
|
||||||
|
const chartRect = netCardRef.current.getBoundingClientRect()
|
||||||
|
const distanceToBottom = wrapperRect.bottom - chartRect.bottom
|
||||||
|
return tooltipHeight - distanceToBottom
|
||||||
|
}, [netCardRef.current, dockerNetChartData])
|
||||||
|
|
||||||
if (!system.id) {
|
if (!system.id) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid lg:grid-cols-2 gap-4 mb-10">
|
<>
|
||||||
<Card className="col-span-full">
|
<div id="chartwrap" className="grid gap-4 mb-10">
|
||||||
|
{/* system info */}
|
||||||
|
<Card>
|
||||||
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
||||||
@@ -226,8 +252,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
{system.status}
|
{system.status}
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5 items-center">
|
||||||
<GlobeIcon className="h-4 w-4 mt-[1px]" /> {system.host}
|
<GlobeIcon className="h-4 w-4" /> {system.host}
|
||||||
</div>
|
</div>
|
||||||
{system.info?.u && (
|
{system.info?.u && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -257,20 +283,21 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
<ChartTimeSelect className="w-full lg:w-40" />
|
<ChartTimeSelect className="w-full lg:w-40" />
|
||||||
<TooltipProvider delayDuration={100}>
|
<TooltipProvider delayDuration={100}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild className="hidden lg:block opacity-85">
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<Button
|
||||||
<Toggle
|
|
||||||
aria-label="Toggle grid"
|
aria-label="Toggle grid"
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-0 border border-card',
|
buttonVariants({ variant: 'outline', size: 'icon' }),
|
||||||
buttonVariants({ variant: 'ghost', size: 'icon' })
|
'hidden lg:flex p-0 text-primary'
|
||||||
)}
|
)}
|
||||||
pressed={grid}
|
onClick={() => setGrid(!grid)}
|
||||||
onPressedChange={setGrid}
|
|
||||||
>
|
>
|
||||||
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem]" />
|
{grid ? (
|
||||||
</Toggle>
|
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-85" />
|
||||||
</span>
|
) : (
|
||||||
|
<StretchHorizontalIcon className="h-[1.2rem] w-[1.2rem] opacity-85" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Toggle grid</TooltipContent>
|
<TooltipContent>Toggle grid</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -279,6 +306,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* main charts */}
|
||||||
|
<div className="grid lg:grid-cols-2 gap-4">
|
||||||
<ChartCard
|
<ChartCard
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title="Total CPU Usage"
|
title="Total CPU Usage"
|
||||||
@@ -292,6 +321,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
grid={grid}
|
grid={grid}
|
||||||
title="Docker CPU Usage"
|
title="Docker CPU Usage"
|
||||||
description="CPU utilization of docker containers"
|
description="CPU utilization of docker containers"
|
||||||
|
isContainerChart={true}
|
||||||
>
|
>
|
||||||
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
@@ -310,6 +340,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
grid={grid}
|
grid={grid}
|
||||||
title="Docker Memory Usage"
|
title="Docker Memory Usage"
|
||||||
description="Memory usage of docker containers"
|
description="Memory usage of docker containers"
|
||||||
|
isContainerChart={true}
|
||||||
>
|
>
|
||||||
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
@@ -325,39 +356,101 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
<DiskChart ticks={ticks} systemData={systemStats} />
|
<DiskChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
|
<ChartCard grid={grid} title="Disk I/O" description="Throughput of root filesystem">
|
||||||
|
<DiskIoChart ticks={ticks} systemData={systemStats} />
|
||||||
|
</ChartCard>
|
||||||
|
|
||||||
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Bandwidth"
|
||||||
|
description="Network traffic of public interfaces"
|
||||||
|
>
|
||||||
|
<BandwidthChart ticks={ticks} systemData={systemStats} />
|
||||||
|
</ChartCard>
|
||||||
|
|
||||||
|
{hasDockerStats && dockerNetChartData.length > 0 && (
|
||||||
|
<div
|
||||||
|
ref={netCardRef}
|
||||||
|
className={cn({
|
||||||
|
'col-span-full': !grid,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ChartCard
|
||||||
|
title="Docker Network I/O"
|
||||||
|
description="Includes traffic between internal services"
|
||||||
|
isContainerChart={true}
|
||||||
|
>
|
||||||
|
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
|
||||||
|
</ChartCard>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{systemStats.at(-1)?.stats.t && (
|
{systemStats.at(-1)?.stats.t && (
|
||||||
<ChartCard grid={grid} title="Temperature" description="Temperatures of system sensors">
|
<ChartCard grid={grid} title="Temperature" description="Temperatures of system sensors">
|
||||||
<TemperatureChart ticks={ticks} systemData={systemStats} />
|
<TemperatureChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<ChartCard grid={grid} title="Disk I/O" description="Throughput of root filesystem">
|
{/* extra filesystem charts */}
|
||||||
<DiskIoChart ticks={ticks} systemData={systemStats} />
|
{Object.keys(systemStats.at(-1)?.stats.efs ?? {}).length > 0 && (
|
||||||
</ChartCard>
|
<div className="grid lg:grid-cols-2 gap-4">
|
||||||
|
{Object.keys(systemStats.at(-1)?.stats.efs ?? {}).map((extraFsName) => {
|
||||||
<ChartCard grid={grid} title="Bandwidth" description="Network traffic of public interfaces">
|
return (
|
||||||
<BandwidthChart ticks={ticks} systemData={systemStats} />
|
<div key={extraFsName} className="contents">
|
||||||
</ChartCard>
|
|
||||||
|
|
||||||
{hasDockerStats && dockerNetChartData.length > 0 && (
|
|
||||||
<>
|
|
||||||
<ChartCard
|
<ChartCard
|
||||||
grid={grid}
|
grid={true}
|
||||||
title="Docker Network I/O"
|
title={`${extraFsName} Usage`}
|
||||||
description="Includes traffic between internal services"
|
description={`Disk usage of ${extraFsName}`}
|
||||||
>
|
>
|
||||||
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
|
<ExFsDiskChart ticks={ticks} systemData={systemStats} fs={extraFsName} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
{/* add space for tooltip if more than 12 containers */}
|
<ChartCard
|
||||||
{Object.keys(dockerNetChartData[0]).length > 12 && (
|
grid={true}
|
||||||
<span
|
title={`${extraFsName} I/O`}
|
||||||
className="block"
|
description={`Throughput of of ${extraFsName}`}
|
||||||
style={{
|
>
|
||||||
height: (Object.keys(dockerNetChartData[0]).length - 13) * 18,
|
<ExFsDiskIoChart ticks={ticks} systemData={systemStats} fs={extraFsName} />
|
||||||
}}
|
</ChartCard>
|
||||||
/>
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* add space for tooltip if more than 12 containers */}
|
||||||
|
{bottomSpacing > 0 && <span className="block" style={{ height: bottomSpacing }} />}
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContainerFilterBar() {
|
||||||
|
const containerFilter = useStore($containerFilter)
|
||||||
|
|
||||||
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
$containerFilter.set(e.target.value)
|
||||||
|
}, []) // Use an empty dependency array to prevent re-creation
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter..."
|
||||||
|
className="pl-4 pr-8"
|
||||||
|
value={containerFilter}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
{containerFilter && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Clear"
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
|
||||||
|
onClick={() => $containerFilter.set('')}
|
||||||
|
>
|
||||||
|
<XIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -368,22 +461,26 @@ function ChartCard({
|
|||||||
description,
|
description,
|
||||||
children,
|
children,
|
||||||
grid,
|
grid,
|
||||||
|
isContainerChart,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
grid: boolean
|
grid?: boolean
|
||||||
|
isContainerChart?: boolean
|
||||||
}) {
|
}) {
|
||||||
const target = useRef<HTMLDivElement>(null)
|
const target = useRef<HTMLDivElement>(null)
|
||||||
const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target })
|
const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={cn('pb-2 sm:pb-4 even:last-of-type:col-span-full', { 'col-span-full': !grid })}
|
className={cn('pb-2 sm:pb-4 odd:last-of-type:col-span-full', { 'col-span-full': !grid })}
|
||||||
ref={wrappedTargetRef}
|
ref={wrappedTargetRef}
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
|
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
|
||||||
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
||||||
<CardDescription>{description}</CardDescription>
|
<CardDescription>{description}</CardDescription>
|
||||||
|
{isContainerChart && <ContainerFilterBar />}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
<CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
||||||
{<Spinner />}
|
{<Spinner />}
|
||||||
|
@@ -72,7 +72,7 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute inset-0 w-full h-full origin-left',
|
'absolute inset-0 w-full h-full origin-left',
|
||||||
(val < 60 && 'bg-green-500') || (val < 90 && 'bg-yellow-500') || 'bg-red-600'
|
(val < 65 && 'bg-green-500') || (val < 90 && 'bg-yellow-500') || 'bg-red-600'
|
||||||
)}
|
)}
|
||||||
style={{ transform: `scalex(${val}%)` }}
|
style={{ transform: `scalex(${val}%)` }}
|
||||||
></span>
|
></span>
|
||||||
|
@@ -100,6 +100,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
nameKey?: string
|
nameKey?: string
|
||||||
labelKey?: string
|
labelKey?: string
|
||||||
unit?: string
|
unit?: string
|
||||||
|
filter?: string
|
||||||
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
@@ -119,6 +120,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
nameKey,
|
nameKey,
|
||||||
labelKey,
|
labelKey,
|
||||||
unit,
|
unit,
|
||||||
|
filter,
|
||||||
itemSorter,
|
itemSorter,
|
||||||
contentFormatter: content = undefined,
|
contentFormatter: content = undefined,
|
||||||
},
|
},
|
||||||
@@ -127,6 +129,9 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
const { config } = useChart()
|
const { config } = useChart()
|
||||||
|
|
||||||
React.useMemo(() => {
|
React.useMemo(() => {
|
||||||
|
if (filter) {
|
||||||
|
payload = payload?.filter((item) => (item.name as string)?.includes(filter))
|
||||||
|
}
|
||||||
if (itemSorter) {
|
if (itemSorter) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
payload?.sort(itemSorter)
|
payload?.sort(itemSorter)
|
||||||
|
@@ -22,3 +22,6 @@ export const $hubVersion = atom('')
|
|||||||
|
|
||||||
/** Chart time period */
|
/** Chart time period */
|
||||||
export const $chartTime = atom('1h') as WritableAtom<ChartTimes>
|
export const $chartTime = atom('1h') as WritableAtom<ChartTimes>
|
||||||
|
|
||||||
|
/** Container chart filter */
|
||||||
|
export const $containerFilter = atom('')
|
||||||
|
13
beszel/site/src/types.d.ts
vendored
13
beszel/site/src/types.d.ts
vendored
@@ -59,6 +59,19 @@ export interface SystemStats {
|
|||||||
nr: number
|
nr: number
|
||||||
/** temperatures */
|
/** temperatures */
|
||||||
t?: Record<string, number>
|
t?: Record<string, number>
|
||||||
|
/** extra filesystems */
|
||||||
|
efs?: Record<string, ExtraFsStats>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtraFsStats {
|
||||||
|
/** disk size (gb) */
|
||||||
|
d: number
|
||||||
|
/** disk used (gb) */
|
||||||
|
du: number
|
||||||
|
/** total read (mb) */
|
||||||
|
r: number
|
||||||
|
/** total write (mb) */
|
||||||
|
w: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContainerStatsRecord extends RecordModel {
|
export interface ContainerStatsRecord extends RecordModel {
|
||||||
|
Reference in New Issue
Block a user