mirror of
https://github.com/fankes/beszel.git
synced 2025-10-20 10:19:27 +08:00
add toggle for chart grid layout
This commit is contained in:
Binary file not shown.
@@ -21,6 +21,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.1.0",
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"@tanstack/react-table": "^8.20.1",
|
"@tanstack/react-table": "^8.20.1",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
@@ -21,7 +21,7 @@ export default function ChartTimeSelect({ className }: { className?: string }) {
|
|||||||
onValueChange={(value: ChartTimes) => $chartTime.set(value)}
|
onValueChange={(value: ChartTimes) => $chartTime.set(value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className={cn(className, 'relative pl-10 pr-5')}>
|
<SelectTrigger className={cn(className, 'relative pl-10 pr-5')}>
|
||||||
<HistoryIcon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-80" />
|
<HistoryIcon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-85" />
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
@@ -4,12 +4,20 @@ import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } fro
|
|||||||
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 } from 'lucide-react'
|
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon } from 'lucide-react'
|
||||||
import ChartTimeSelect from '../charts/chart-time-select'
|
import ChartTimeSelect from '../charts/chart-time-select'
|
||||||
import { chartTimeData, cn, getPbTimestamp, useClampedIsInViewport } from '@/lib/utils'
|
import {
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
getPbTimestamp,
|
||||||
|
useClampedIsInViewport,
|
||||||
|
useLocalStorage,
|
||||||
|
} from '@/lib/utils'
|
||||||
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 { buttonVariants } from '../ui/button'
|
||||||
|
|
||||||
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'))
|
||||||
@@ -25,6 +33,7 @@ const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
|||||||
export default function SystemDetail({ name }: { name: string }) {
|
export default function SystemDetail({ name }: { name: string }) {
|
||||||
const systems = useStore($systems)
|
const systems = useStore($systems)
|
||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
|
const [grid, setGrid] = useLocalStorage('grid', true)
|
||||||
const [ticks, setTicks] = useState([] as number[])
|
const [ticks, setTicks] = useState([] as number[])
|
||||||
const [system, setSystem] = useState({} as SystemRecord)
|
const [system, setSystem] = useState({} as SystemRecord)
|
||||||
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
||||||
@@ -244,57 +253,96 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChartTimeSelect className="w-full lg:w-40 xl:w-52 ml-auto max-sm:-mb-1" />
|
<div className="lg:ml-auto flex items-center gap-2 max-sm:-mb-1">
|
||||||
|
<ChartTimeSelect className="w-full lg:w-40" />
|
||||||
|
<TooltipProvider delayDuration={100}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild className="hidden lg:block opacity-85">
|
||||||
|
<span>
|
||||||
|
<Toggle
|
||||||
|
aria-label="Toggle grid"
|
||||||
|
className={cn(
|
||||||
|
'p-0 border border-card',
|
||||||
|
buttonVariants({ variant: 'ghost', size: 'icon' })
|
||||||
|
)}
|
||||||
|
pressed={grid}
|
||||||
|
onPressedChange={setGrid}
|
||||||
|
>
|
||||||
|
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||||
|
</Toggle>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Toggle grid</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<ChartCard title="Total CPU Usage" description="Average system-wide CPU utilization">
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Total CPU Usage"
|
||||||
|
description="Average system-wide CPU utilization"
|
||||||
|
>
|
||||||
<CpuChart ticks={ticks} systemData={systemStats} />
|
<CpuChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && (
|
{hasDockerStats && (
|
||||||
<ChartCard title="Docker CPU Usage" description="CPU utilization of docker containers">
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Docker CPU Usage"
|
||||||
|
description="CPU utilization of docker containers"
|
||||||
|
>
|
||||||
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ChartCard title="Total Memory Usage" description="Precise utilization at the recorded time">
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Total Memory Usage"
|
||||||
|
description="Precise utilization at the recorded time"
|
||||||
|
>
|
||||||
<MemChart ticks={ticks} systemData={systemStats} />
|
<MemChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && (
|
{hasDockerStats && (
|
||||||
<ChartCard title="Docker Memory Usage" description="Memory usage of docker containers">
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Docker Memory Usage"
|
||||||
|
description="Memory usage of docker containers"
|
||||||
|
>
|
||||||
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(systemStats.at(-1)?.stats.s ?? 0) > 0 && (
|
{(systemStats.at(-1)?.stats.s ?? 0) > 0 && (
|
||||||
<ChartCard title="Swap Usage" description="Swap space used by the system">
|
<ChartCard grid={grid} title="Swap Usage" description="Swap space used by the system">
|
||||||
<SwapChart ticks={ticks} systemData={systemStats} />
|
<SwapChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ChartCard grid={grid} title="Disk Space" description="Usage of root partition">
|
||||||
|
<DiskChart ticks={ticks} systemData={systemStats} />
|
||||||
|
</ChartCard>
|
||||||
|
|
||||||
{systemStats.at(-1)?.stats.t && (
|
{systemStats.at(-1)?.stats.t && (
|
||||||
<ChartCard 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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ChartCard title="Disk Usage" description="Space usage of root partition">
|
<ChartCard grid={grid} title="Disk I/O" description="Throughput of root filesystem">
|
||||||
<DiskChart ticks={ticks} systemData={systemStats} />
|
|
||||||
</ChartCard>
|
|
||||||
|
|
||||||
<ChartCard title="Disk I/O" description="Throughput of root filesystem">
|
|
||||||
<DiskIoChart ticks={ticks} systemData={systemStats} />
|
<DiskIoChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
<ChartCard title="Bandwidth" description="Network traffic of public interfaces">
|
<ChartCard grid={grid} title="Bandwidth" description="Network traffic of public interfaces">
|
||||||
<BandwidthChart ticks={ticks} systemData={systemStats} />
|
<BandwidthChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && dockerNetChartData.length > 0 && (
|
{hasDockerStats && dockerNetChartData.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<ChartCard
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
title="Docker Network I/O"
|
title="Docker Network I/O"
|
||||||
description="Includes traffic between internal services"
|
description="Includes traffic between internal services"
|
||||||
>
|
>
|
||||||
@@ -319,21 +367,23 @@ function ChartCard({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
children,
|
children,
|
||||||
|
grid,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
grid: 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 className="pb-2 sm:pb-4 even:last-of-type:col-span-full" ref={wrappedTargetRef}>
|
<Card
|
||||||
|
className={cn('pb-2 sm:pb-4 even:last-of-type:col-span-full', { 'col-span-full': !grid })}
|
||||||
|
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>
|
||||||
{/* <div className="w-full pt-1 sm:w-40 hidden sm:block absolute top-1.5 right-3.5">
|
|
||||||
<ChartTimeSelect />
|
|
||||||
</div> */}
|
|
||||||
</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 />}
|
||||||
|
@@ -135,7 +135,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
|||||||
<Button
|
<Button
|
||||||
data-nolink
|
data-nolink
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
className="text-foreground/90 h-7 px-1.5 gap-1.5"
|
className="text-primary/90 h-7 px-1.5 gap-1.5"
|
||||||
onClick={() => copyToClipboard(info.getValue() as string)}
|
onClick={() => copyToClipboard(info.getValue() as string)}
|
||||||
>
|
>
|
||||||
{info.getValue() as string}
|
{info.getValue() as string}
|
||||||
|
@@ -44,7 +44,7 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
|
|||||||
<tr
|
<tr
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
'border-b transition-colors hover:bg-muted/40 dark:hover:bg-muted/30 data-[state=selected]:bg-muted',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
43
beszel/site/src/components/ui/toggle.tsx
Normal file
43
beszel/site/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const toggleVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-3",
|
||||||
|
sm: "h-9 px-2.5",
|
||||||
|
lg: "h-11 px-5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toggle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>
|
||||||
|
>(({ className, variant, size, ...props }, ref) => (
|
||||||
|
<TogglePrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Toggle, toggleVariants }
|
@@ -241,3 +241,22 @@ export function toFixedWithoutTrailingZeros(num: number, digits: number) {
|
|||||||
export function toFixedFloat(num: number, digits: number) {
|
export function toFixedFloat(num: number, digits: number) {
|
||||||
return parseFloat(num.toFixed(digits))
|
return parseFloat(num.toFixed(digits))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get value from local storage */
|
||||||
|
function getStorageValue(key: string, defaultValue: any) {
|
||||||
|
const saved = localStorage?.getItem(key)
|
||||||
|
return saved ? JSON.parse(saved) : defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hook to sync value in local storage */
|
||||||
|
export const useLocalStorage = (key: string, defaultValue: any) => {
|
||||||
|
key = `besz-${key}`
|
||||||
|
const [value, setValue] = useState(() => {
|
||||||
|
return getStorageValue(key, defaultValue)
|
||||||
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage?.setItem(key, JSON.stringify(value))
|
||||||
|
}, [key, value])
|
||||||
|
|
||||||
|
return [value, setValue]
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user