add biome and apply selected formatting / fixes

This commit is contained in:
henrygd
2025-09-08 13:03:07 -04:00
parent 270e59d9ea
commit f542bc70a1
37 changed files with 264 additions and 215 deletions

38
src/site/biome.json Normal file
View File

@@ -0,0 +1,38 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.3/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"indentWidth": 2,
"lineWidth": 120
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "asNeeded",
"trailingCommas": "es5"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}

Binary file not shown.

View File

@@ -8,7 +8,11 @@
"build": "lingui extract --overwrite && lingui compile && vite build",
"preview": "vite preview",
"sync": "lingui extract --overwrite && lingui compile",
"sync_and_purge": "lingui extract --overwrite --clean && lingui compile"
"sync_and_purge": "lingui extract --overwrite --clean && lingui compile",
"format": "biome format --write .",
"lint": "biome lint .",
"check": "biome check .",
"check:fix": "biome check --fix ."
},
"dependencies": {
"@henrygd/queue": "^1.0.7",
@@ -49,6 +53,7 @@
"valibot": "^0.42.1"
},
"devDependencies": {
"@biomejs/biome": "2.2.3",
"@lingui/cli": "^5.4.1",
"@lingui/swc-plugin": "^5.6.1",
"@lingui/vite-plugin": "^5.4.1",

View File

@@ -1,22 +1,22 @@
import { Suspense, memo, useEffect, useMemo } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"
import { $alerts, $allSystemsById } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import { GithubIcon } from "lucide-react"
import { Separator } from "../ui/separator"
import { AlertRecord } from "@/types"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { $router, Link } from "../router"
import { Plural, Trans, useLingui } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router"
import { alertInfo } from "@/lib/alerts"
import { GithubIcon } from "lucide-react"
import { memo, Suspense, useEffect, useMemo } from "react"
import { $router, Link } from "@/components/router"
import SystemsTable from "@/components/systems-table/systems-table"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
import { alertInfo } from "@/lib/alerts"
import { $alerts, $allSystemsById } from "@/lib/stores"
import type { AlertRecord } from "@/types"
export default memo(function () {
export default memo(() => {
const { t } = useLingui()
useEffect(() => {
document.title = t`Dashboard` + " / Beszel"
document.title = `${t`Dashboard`} / Beszel`
}, [t])
return useMemo(
@@ -32,6 +32,7 @@ export default memo(function () {
href="https://github.com/henrygd/beszel"
target="_blank"
className="flex items-center gap-0.5 text-muted-foreground hover:text-foreground duration-75"
rel="noopener"
>
<GithubIcon className="h-3 w-3" /> GitHub
</a>
@@ -40,6 +41,7 @@ export default memo(function () {
href="https://github.com/henrygd/beszel/releases"
target="_blank"
className="text-muted-foreground hover:text-foreground duration-75"
rel="noopener"
>
Beszel {globalThis.BESZEL.HUB_VERSION}
</a>
@@ -71,6 +73,7 @@ const ActiveAlerts = () => {
return { activeAlerts, alertsKey }
}, [alerts])
// biome-ignore lint/correctness/useExhaustiveDependencies: alertsKey is inclusive
return useMemo(() => {
if (activeAlerts.length === 0) {
return null

View File

@@ -1,24 +1,34 @@
import { t } from "@lingui/core/macro"
import { Plural, Trans } from "@lingui/react/macro"
import { Plural, Trans, useLingui } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router"
import { timeTicks } from "d3-time"
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from "lucide-react"
import { subscribeKeys } from "nanostores"
import React, { type JSX, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import AreaChartDefault from "@/components/charts/area-chart"
import ContainerChart from "@/components/charts/container-chart"
import DiskChart from "@/components/charts/disk-chart"
import GpuPowerChart from "@/components/charts/gpu-power-chart"
import { useContainerChartConfigs } from "@/components/charts/hooks"
import LoadAverageChart from "@/components/charts/load-average-chart"
import MemChart from "@/components/charts/mem-chart"
import SwapChart from "@/components/charts/swap-chart"
import TemperatureChart from "@/components/charts/temperature-chart"
import { getPbTimestamp, pb } from "@/lib/api"
import { ChartType, Os, SystemStatus, Unit } from "@/lib/enums"
import { batteryStateTranslations } from "@/lib/i18n"
import {
$systems,
$allSystemsByName,
$chartTime,
$containerFilter,
$userSettings,
$direction,
$maxValues,
$systems,
$temperatureFilter,
$allSystemsByName,
$userSettings,
} from "@/lib/stores"
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
import { useContainerChartConfigs } from "@/components/charts/hooks"
import { ChartType, Unit, Os, SystemStatus } from "@/lib/enums"
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react"
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
import { useStore } from "@nanostores/react"
import Spinner from "../spinner"
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from "lucide-react"
import ChartTimeSelect from "../charts/chart-time-select"
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
import {
chartTimeData,
cn,
@@ -30,34 +40,33 @@ import {
toFixedFloat,
useBrowserStorage,
} from "@/lib/utils"
import { getPbTimestamp, pb } from "@/lib/api"
import type { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
import ChartTimeSelect from "../charts/chart-time-select"
import { $router, navigate } from "../router"
import Spinner from "../spinner"
import { Button } from "../ui/button"
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { AppleIcon, ChartAverage, ChartMax, FreeBsdIcon, Rows, TuxIcon, WindowsIcon } from "../ui/icons"
import { Input } from "../ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
import { Separator } from "../ui/separator"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
import { Button } from "../ui/button"
import { Input } from "../ui/input"
import { ChartAverage, ChartMax, Rows, TuxIcon, WindowsIcon, AppleIcon, FreeBsdIcon } from "../ui/icons"
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
import { timeTicks } from "d3-time"
import { useLingui } from "@lingui/react/macro"
import { $router, navigate } from "../router"
import { getPagePath } from "@nanostores/router"
import { batteryStateTranslations } from "@/lib/i18n"
import AreaChartDefault from "@/components/charts/area-chart"
import ContainerChart from "@/components/charts/container-chart"
import MemChart from "@/components/charts/mem-chart"
import DiskChart from "@/components/charts/disk-chart"
import SwapChart from "@/components/charts/swap-chart"
import TemperatureChart from "@/components/charts/temperature-chart"
import GpuPowerChart from "@/components/charts/gpu-power-chart"
import LoadAverageChart from "@/components/charts/load-average-chart"
import { subscribeKeys } from "nanostores"
const cache = new Map<string, any>()
type ChartTimeData = {
time: number
data: {
ticks: number[]
domain: number[]
}
chartTime: ChartTimes
}
const cache = new Map<string, ChartTimeData | SystemStatsRecord[] | ContainerStatsRecord[]>()
// create ticks and domain for charts
function getTimeData(chartTime: ChartTimes, lastCreated: number) {
const cached = cache.get("td")
const cached = cache.get("td") as ChartTimeData | undefined
if (cached && cached.chartTime === chartTime) {
if (!lastCreated || cached.time >= lastCreated) {
return cached.data
@@ -79,7 +88,7 @@ function getTimeData(chartTime: ChartTimes, lastCreated: number) {
function addEmptyValues<T extends SystemStatsRecord | ContainerStatsRecord>(
prevRecords: T[],
newRecords: T[],
expectedInterval: number
expectedInterval: number,
) {
const modifiedRecords: T[] = []
let prevTime = (prevRecords.at(-1)?.created ?? 0) as number
@@ -90,7 +99,7 @@ function addEmptyValues<T extends SystemStatsRecord | ContainerStatsRecord>(
const interval = record.created - prevTime
// if interval is too large, add a null record
if (interval > expectedInterval / 2 + expectedInterval) {
// @ts-ignore
// @ts-expect-error
modifiedRecords.push({ created: null, stats: null })
}
}
@@ -100,8 +109,9 @@ function addEmptyValues<T extends SystemStatsRecord | ContainerStatsRecord>(
return modifiedRecords
}
async function getStats<T>(collection: string, system: SystemRecord, chartTime: ChartTimes): Promise<T[]> {
const lastCached = cache.get(`${system.id}_${chartTime}_${collection}`)?.at(-1)?.created as number
async function getStats<T extends SystemStatsRecord | ContainerStatsRecord>(collection: string, system: SystemRecord, chartTime: ChartTimes): Promise<T[]> {
const cachedStats = cache.get(`${system.id}_${chartTime}_${collection}`) as T[] | undefined
const lastCached = cachedStats?.at(-1)?.created as number
return await pb.collection<T>(collection).getFullList({
filter: pb.filter("system={:id} && created > {:created} && type={:type}", {
id: system.id,
@@ -137,6 +147,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
const [chartLoading, setChartLoading] = useState(true)
const isLongerChart = chartTime !== "1h"
const userSettings = $userSettings.get()
const chartWrapRef = useRef<HTMLDivElement>(null)
useEffect(() => {
document.title = `${name} / Beszel`
@@ -160,10 +171,11 @@ export default memo(function SystemDetail({ name }: { name: string }) {
})
}, [name])
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
const chartData: ChartData = useMemo(() => {
const lastCreated = Math.max(
(systemStats.at(-1)?.created as number) ?? 0,
(containerData.at(-1)?.created as number) ?? 0
(containerData.at(-1)?.created as number) ?? 0,
)
return {
systemStats,
@@ -178,8 +190,29 @@ export default memo(function SystemDetail({ name }: { name: string }) {
// Share chart config computation for all container charts
const containerChartConfigs = useContainerChartConfigs(containerData)
// make container stats for charts
const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => {
const containerData = [] as ChartData["containerData"]
for (let { created, stats } of containers) {
if (!created) {
// @ts-expect-error add null value for gaps
containerData.push({ created: null })
continue
}
created = new Date(created).getTime()
// @ts-expect-error not dealing with this rn
const containerStats: ChartData["containerData"][0] = { created }
for (const container of stats) {
containerStats[container.n] = container
}
containerData.push(containerStats)
}
setContainerData(containerData)
}, [])
// get stats
useEffect(() => {
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
useEffect(() => {
if (!system.id || !chartTime) {
return
}
@@ -223,25 +256,6 @@ export default memo(function SystemDetail({ name }: { name: string }) {
})
}, [system, chartTime])
// make container stats for charts
const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => {
const containerData = [] as ChartData["containerData"]
for (let { created, stats } of containers) {
if (!created) {
// @ts-ignore add null value for gaps
containerData.push({ created: null })
continue
}
created = new Date(created).getTime()
// @ts-ignore not dealing with this rn
let containerStats: ChartData["containerData"][0] = { created }
for (let container of stats) {
containerStats[container.n] = container
}
containerData.push(containerStats)
}
setContainerData(containerData)
}, [])
// values for system info bar
const systemInfo = useMemo(() => {
@@ -303,10 +317,10 @@ export default memo(function SystemDetail({ name }: { name: string }) {
] as {
value: string | number | undefined
label?: string
Icon: any
Icon: React.ElementType
hide?: boolean
}[]
}, [system.info, t])
}, [system, t])
/** Space for tooltip if more than 12 containers */
useEffect(() => {
@@ -315,12 +329,12 @@ export default memo(function SystemDetail({ name }: { name: string }) {
return
}
const tooltipHeight = (Object.keys(containerData[0]).length - 11) * 17.8 - 40
const wrapperEl = document.getElementById("chartwrap") as HTMLDivElement
const wrapperEl = chartWrapRef.current as HTMLDivElement
const wrapperRect = wrapperEl.getBoundingClientRect()
const chartRect = netCardRef.current.getBoundingClientRect()
const distanceToBottom = wrapperRect.bottom - chartRect.bottom
setBottomSpacing(tooltipHeight - distanceToBottom)
}, [netCardRef, containerData])
}, [containerData])
// keyboard navigation between systems
useEffect(() => {
@@ -343,15 +357,17 @@ export default memo(function SystemDetail({ name }: { name: string }) {
}
switch (e.key) {
case "ArrowLeft":
case "h":
case "h": {
const prevIndex = (currentIndex - 1 + systems.length) % systems.length
persistChartTime.current = true
return navigate(getPagePath($router, "system", { name: systems[prevIndex].name }))
}
case "ArrowRight":
case "l":
case "l": {
const nextIndex = (currentIndex + 1) % systems.length
persistChartTime.current = true
return navigate(getPagePath($router, "system", { name: systems[nextIndex].name }))
}
}
}
return listen(document, "keyup", handleKeyUp)
@@ -380,7 +396,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
return (
<>
<div id="chartwrap" className="grid gap-4 mb-14 overflow-x-clip">
<div ref={chartWrapRef} className="grid gap-4 mb-14 overflow-x-clip">
{/* system info */}
<Card>
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
@@ -406,7 +422,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
</span>
{translatedStatus}
</div>
{systemInfo.map(({ value, label, Icon, hide }, i) => {
{systemInfo.map(({ value, label, Icon, hide }) => {
if (hide || !value) {
return null
}
@@ -416,7 +432,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
</div>
)
return (
<div key={i} className="contents">
<div key={value} className="contents">
<Separator orientation="vertical" className="h-4 bg-primary/30" />
{label ? (
<TooltipProvider>
@@ -479,8 +495,8 @@ export default memo(function SystemDetail({ name }: { name: string }) {
opacity: 0.4,
},
]}
tickFormatter={(val) => toFixedFloat(val, 2) + "%"}
contentFormatter={({ value }) => decimalString(value) + "%"}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
/>
</ChartCard>
@@ -558,11 +574,11 @@ export default memo(function SystemDetail({ name }: { name: string }) {
]}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true)
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, true)
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
@@ -603,12 +619,12 @@ export default memo(function SystemDetail({ name }: { name: string }) {
},
]}
tickFormatter={(val) => {
let { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
const { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={(data) => {
const { value, unit } = formatBytes(data.value, true, userSettings.unitNet, false)
return decimalString(value, value >= 100 ? 1 : 2) + " " + unit
return `${decimalString(value, value >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
@@ -682,7 +698,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
description={`${t({
message: "Current state",
comment: "Context: Battery state",
})}: ${batteryStateTranslations[systemStats.at(-1)?.stats.bat![1] ?? 0]()}`}
})}: ${batteryStateTranslations[systemStats.at(-1)?.stats.bat?.[1] ?? 0]()}`}
>
<AreaChartDefault
chartData={chartData}
@@ -738,8 +754,8 @@ export default memo(function SystemDetail({ name }: { name: string }) {
opacity: 0.35,
},
]}
tickFormatter={(val) => toFixedFloat(val, 2) + "%"}
contentFormatter={({ value }) => decimalString(value) + "%"}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
/>
</ChartCard>
<ChartCard
@@ -761,11 +777,11 @@ export default memo(function SystemDetail({ name }: { name: string }) {
max={gpu.mt}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, false, Unit.Bytes, true)
return decimalString(convertedValue) + " " + unit
return `${decimalString(convertedValue)} ${unit}`
}}
/>
</ChartCard>
@@ -819,11 +835,11 @@ export default memo(function SystemDetail({ name }: { name: string }) {
maxToggled={maxValues}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true)
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, true)
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
@@ -846,7 +862,7 @@ function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilt
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
store.set(e.target.value)
}, [])
}, [store])
return (
<>

View File

@@ -1,8 +1,7 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import * as React from "react"
import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
const AlertDialog = AlertDialogPrimitive.Root

View File

@@ -1,5 +1,5 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import type * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,10 +1,8 @@
import type { JSX } from "react"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { chartTimeData, cn } from "@/lib/utils"
import { ChartData } from "@/types"
import type { JSX } from "react"
import type { ChartData } from "@/types"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
@@ -134,7 +132,7 @@ const ChartTooltipContent = React.forwardRef<
payload = payload?.filter((item) => (item.name as string)?.toLowerCase().includes(filter.toLowerCase()))
}
if (itemSorter) {
// @ts-ignore
// @ts-expect-error
payload?.sort(itemSorter)
}
}, [itemSorter, payload])
@@ -331,7 +329,7 @@ function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key:
}
let cachedAxis: JSX.Element
const xAxis = function ({ domain, ticks, chartTime }: ChartData) {
const xAxis = ({ domain, ticks, chartTime }: ChartData) => {
if (cachedAxis && domain[0] === cachedAxis.props.domain[0]) {
return cachedAxis
}

View File

@@ -1,8 +1,8 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,5 +1,5 @@
import * as React from "react"
import { ChevronDownIcon, HourglassIcon } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Button } from "./button"
@@ -17,11 +17,7 @@ export function Collapsible({ title, children, description, defaultOpen = false,
return (
<div className={cn("border rounded-lg", className)}>
<Button
variant="ghost"
className="w-full justify-between p-4 font-semibold"
onClick={() => setIsOpen(!isOpen)}
>
<Button variant="ghost" className="w-full justify-between p-4 font-semibold" onClick={() => setIsOpen(!isOpen)}>
<div className="flex items-center gap-2">
{icon}
{title}
@@ -32,18 +28,12 @@ export function Collapsible({ title, children, description, defaultOpen = false,
})}
/>
</Button>
{description && (
<div className="px-4 pb-2 text-sm text-muted-foreground">
{description}
</div>
)}
{description && <div className="px-4 pb-2 text-sm text-muted-foreground">{description}</div>}
{isOpen && (
<div className="px-4 pb-4">
<div className="grid gap-3">
{children}
</div>
<div className="grid gap-3">{children}</div>
</div>
)}
</div>
)
}
}

View File

@@ -1,9 +1,8 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import type * as React from "react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { cn } from "@/lib/utils"
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
return (

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,4 +1,4 @@
import { SVGProps } from "react"
import type { SVGProps } from "react"
// linux-logo-bold from https://github.com/phosphor-icons/core (MIT license)
export function TuxIcon(props: SVGProps<SVGSVGElement>) {

View File

@@ -1,9 +1,9 @@
import { copyToClipboard } from "@/lib/utils"
import { Input } from "./input"
import { Trans } from "@lingui/react/macro"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"
import { CopyIcon } from "lucide-react"
import { copyToClipboard } from "@/lib/utils"
import { Button } from "./button"
import { Input } from "./input"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"
export function InputCopy({ value, id, name }: { value: string; id: string; name: string }) {
return (

View File

@@ -1,9 +1,9 @@
import { XIcon } from "lucide-react"
import * as React from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
import { type InputProps } from "./input"
import { cn } from "@/lib/utils"
import type { InputProps } from "./input"
type InputTagsProps = Omit<InputProps, "value" | "onChange"> & {
value: string[]

View File

@@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { MinusIcon } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils"
@@ -78,8 +78,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)"
position === "popper" && "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)"
)}
>
{children}

View File

@@ -1,5 +1,5 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import type * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,5 +1,5 @@
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,5 +1,5 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,5 +1,5 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -6,18 +6,16 @@ export function Toaster() {
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
)
})}
{toasts.map(({ id, title, description, action, ...props }) => (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
))}
<ToastViewport />
</ToastProvider>
)

View File

@@ -1,5 +1,5 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import type * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -103,7 +103,7 @@ export const reducer = (state: State, action: Action): State => {
? {
...t,
open: false,
}
}
: t
),
}

View File

@@ -1,9 +1,9 @@
import type { AlertInfo, AlertRecord } from "@/types"
import type { RecordSubscription } from "pocketbase"
import { $alerts } from "@/lib/stores"
import { EthernetIcon } from "@/components/ui/icons"
import { ServerIcon, CpuIcon, MemoryStickIcon, HardDriveIcon, ThermometerIcon, HourglassIcon } from "lucide-react"
import { t } from "@lingui/core/macro"
import { CpuIcon, HardDriveIcon, HourglassIcon, MemoryStickIcon, ServerIcon, ThermometerIcon } from "lucide-react"
import type { RecordSubscription } from "pocketbase"
import { EthernetIcon } from "@/components/ui/icons"
import { $alerts } from "@/lib/stores"
import type { AlertInfo, AlertRecord } from "@/types"
import { pb } from "./api"
/** Alert info for each alert type */
@@ -14,7 +14,7 @@ export const alertInfo: Record<string, AlertInfo> = {
icon: ServerIcon,
desc: () => t`Triggers when status switches between up and down`,
/** "for x minutes" is appended to desc when only one value */
singleDesc: () => t`System` + " " + t`Down`,
singleDesc: () => `${t`System`} ${t`Down`}`,
},
CPU: {
name: () => t`CPU Usage`,
@@ -127,7 +127,7 @@ export const alertManager = (() => {
return (data: RecordSubscription<AlertRecord>) => {
const { record } = data
batch.set(`${record.system}${record.name}`, data)
clearTimeout(timeout!)
clearTimeout(timeout)
timeout = setTimeout(() => {
const groups = { create: [], update: [], delete: [] } as Record<string, AlertRecord[]>
for (const { action, record } of batch.values()) {

View File

@@ -1,10 +1,10 @@
import { ChartTimes, UserSettings } from "@/types"
import { $alerts, $allSystemsByName, $userSettings } from "./stores"
import { toast } from "@/components/ui/use-toast"
import { t } from "@lingui/core/macro"
import { chartTimeData } from "./utils"
import PocketBase from "pocketbase"
import { basePath } from "@/components/router"
import { toast } from "@/components/ui/use-toast"
import type { ChartTimes, UserSettings } from "@/types"
import { $alerts, $allSystemsByName, $userSettings } from "./stores"
import { chartTimeData } from "./utils"
/** PocketBase JS Client */
export const pb = new PocketBase(basePath)
@@ -46,7 +46,7 @@ export async function updateUserSettings() {
}
// create user settings if error fetching existing
try {
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id })
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record?.id })
$userSettings.set(createdSettings.settings)
} catch (e) {
console.error("create settings", e)

View File

@@ -1,11 +1,11 @@
import { $direction } from "./stores"
import { i18n } from "@lingui/core"
import type { Messages } from "@lingui/core"
import { i18n } from "@lingui/core"
import { t } from "@lingui/core/macro"
import { detect, fromNavigator, fromStorage } from "@lingui/detect-locale"
import languages from "@/lib/languages"
import { detect, fromStorage, fromNavigator } from "@lingui/detect-locale"
import { messages as enMessages } from "@/locales/en/en"
import { BatteryState } from "./enums"
import { t } from "@lingui/core/macro"
import { $direction } from "./stores"
// activates locale
function activateLocale(locale: string, messages: Messages = enMessages) {
@@ -18,7 +18,7 @@ function activateLocale(locale: string, messages: Messages = enMessages) {
// dynamically loads translations for the given locale
export async function dynamicActivate(locale: string) {
if (locale == "en") {
if (locale === "en") {
activateLocale(locale)
} else {
try {

View File

@@ -1,7 +1,7 @@
import { atom, computed, map, ReadableAtom } from "nanostores"
import { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
import { Unit } from "./enums"
import { atom, computed, map, type ReadableAtom } from "nanostores"
import type { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
import { pb } from "./api"
import { Unit } from "./enums"
/** Store if user is authenticated */
export const $authenticated = atom(pb.authStore.isValid)

View File

@@ -1,15 +1,15 @@
import { SystemRecord } from "@/types"
import { PreinitializedMapStore } from "nanostores"
import type { PreinitializedMapStore } from "nanostores"
import { pb, verifyAuth } from "@/lib/api"
import {
$allSystemsByName,
$upSystems,
$downSystems,
$pausedSystems,
$allSystemsById,
$allSystemsByName,
$downSystems,
$longestSystemNameLen,
$pausedSystems,
$upSystems,
} from "@/lib/stores"
import { updateFavicon, FAVICON_DEFAULT, FAVICON_GREEN, FAVICON_RED } from "@/lib/utils"
import { FAVICON_DEFAULT, FAVICON_GREEN, FAVICON_RED, updateFavicon } from "@/lib/utils"
import type { SystemRecord } from "@/types"
import { SystemStatus } from "./enums"
const COLLECTION = pb.collection<SystemRecord>("systems")

View File

@@ -1,13 +1,13 @@
import { t } from "@lingui/core/macro"
import { toast } from "@/components/ui/use-toast"
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { $copyContent, $userSettings } from "./stores"
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
import { timeDay, timeHour } from "d3-time"
import { useEffect, useState } from "react"
import { MeterState, Unit } from "./enums"
import { twMerge } from "tailwind-merge"
import { prependBasePath } from "@/components/router"
import { toast } from "@/components/ui/use-toast"
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
import { MeterState, Unit } from "./enums"
import { $copyContent, $userSettings } from "./stores"
export const FAVICON_DEFAULT = "favicon.svg"
export const FAVICON_GREEN = "favicon-green.svg"
@@ -31,7 +31,7 @@ export async function copyToClipboard(content: string) {
duration,
description: t`Copied to clipboard`,
})
} catch (e: any) {
} catch (e) {
$copyContent.set(content)
}
}
@@ -113,7 +113,7 @@ export function toFixedFloat(num: number, digits: number) {
return parseFloat((digits === 0 ? Math.ceil(num) : num).toFixed(digits))
}
let decimalFormatters: Map<number, Intl.NumberFormat> = new Map()
const decimalFormatters: Map<number, Intl.NumberFormat> = new Map()
/** Format number to x decimal places, maintaining trailing zeros */
export function decimalString(num: number, digits = 2) {
if (digits === 0) {
@@ -131,7 +131,7 @@ export function decimalString(num: number, digits = 2) {
}
/** Get value from local or session storage */
function getStorageValue(key: string, defaultValue: any, storageInterface: Storage = localStorage) {
function getStorageValue(key: string, defaultValue: unknown, storageInterface: Storage = localStorage) {
const saved = storageInterface?.getItem(key)
return saved ? JSON.parse(saved) : defaultValue
}
@@ -142,6 +142,7 @@ export function useBrowserStorage<T>(key: string, defaultValue: T, storageInterf
const [value, setValue] = useState(() => {
return getStorageValue(key, defaultValue, storageInterface)
})
// biome-ignore lint/correctness/useExhaustiveDependencies: storageInterface won't change
useEffect(() => {
storageInterface?.setItem(key, JSON.stringify(value))
}, [key, value])
@@ -155,7 +156,7 @@ export function formatTemperature(celsius: number, unit?: Unit): { value: number
unit = $userSettings.get().unitTemp || Unit.Celsius
}
// need loose equality check due to form data being strings
if (unit == Unit.Fahrenheit) {
if (unit === Unit.Fahrenheit) {
return {
value: celsius * 1.8 + 32,
unit: "°F",
@@ -178,7 +179,7 @@ export function formatBytes(
if (isMegabytes) size *= 1024 * 1024
// need loose equality check due to form data being strings
if (unit == Unit.Bits) {
if (unit === Unit.Bits) {
const bits = size * 8
const suffix = perSecond ? "ps" : ""
if (bits < 1000) return { value: bits, unit: `b${suffix}` }
@@ -314,6 +315,7 @@ export function getMeterState(value: number): MeterState {
return value >= colorCrit ? MeterState.Crit : value >= colorWarn ? MeterState.Warn : MeterState.Good
}
// biome-ignore lint/suspicious/noExplicitAny: any is used to allow any function to be passed in
export function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout>
return (...args: Parameters<T>) => {
@@ -323,8 +325,10 @@ export function debounce<T extends (...args: any[]) => any>(func: T, wait: numbe
}
// Cache for runOnce
// biome-ignore lint/complexity/noBannedTypes: Function is used to allow any function to be passed in
const runOnceCache = new WeakMap<Function, { done: boolean; result: unknown }>()
/** Run a function only once */
// biome-ignore lint/suspicious/noExplicitAny: any is used to allow any function to be passed in
export function runOnce<T extends (...args: any[]) => any>(fn: T): T {
return ((...args: Parameters<T>) => {
let state = runOnceCache.get(fn)

View File

@@ -1,21 +1,21 @@
import "./index.css"
// import { Suspense, lazy, useEffect, StrictMode } from "react"
import { Suspense, lazy, memo, useEffect } from "react"
import ReactDOM from "react-dom/client"
import { ThemeProvider } from "./components/theme-provider.tsx"
import { DirectionProvider } from "@radix-ui/react-direction"
import { $authenticated, $publicKey, $copyContent, $direction } from "./lib/stores.ts"
import { pb, updateUserSettings } from "./lib/api.ts"
import * as systemsManager from "./lib/systemsManager.ts"
import { useStore } from "@nanostores/react"
import { Toaster } from "./components/ui/toaster.tsx"
import { $router } from "./components/router.tsx"
import Navbar from "./components/navbar.tsx"
import { I18nProvider } from "@lingui/react"
import { i18n } from "@lingui/core"
import { getLocale, dynamicActivate } from "./lib/i18n"
import { alertManager } from "./lib/alerts"
import Settings from "./components/routes/settings/layout.tsx"
import { I18nProvider } from "@lingui/react"
import { useStore } from "@nanostores/react"
import { DirectionProvider } from "@radix-ui/react-direction"
// import { Suspense, lazy, useEffect, StrictMode } from "react"
import { lazy, memo, Suspense, useEffect } from "react"
import ReactDOM from "react-dom/client"
import Navbar from "@/components/navbar.tsx"
import { $router } from "@/components/router.tsx"
import Settings from "@/components/routes/settings/layout.tsx"
import { ThemeProvider } from "@/components/theme-provider.tsx"
import { Toaster } from "@/components/ui/toaster.tsx"
import { alertManager } from "@/lib/alerts"
import { pb, updateUserSettings } from "@/lib/api.ts"
import { dynamicActivate, getLocale } from "@/lib/i18n"
import { $authenticated, $copyContent, $direction, $publicKey } from "@/lib/stores.ts"
import * as systemsManager from "@/lib/systemsManager.ts"
const LoginPage = lazy(() => import("@/components/login/login.tsx"))
const Home = lazy(() => import("@/components/routes/home.tsx"))
@@ -114,7 +114,7 @@ const I18nApp = () => {
)
}
ReactDOM.createRoot(document.getElementById("app")!).render(
ReactDOM.createRoot(document.getElementById("app") as HTMLElement).render(
// strict mode in dev mounts / unmounts components twice
// and breaks the clipboard dialog
//<StrictMode>