mirror of
https://github.com/fankes/beszel.git
synced 2025-10-18 17:29:28 +08:00
refactor (hub): add systemsManager
module
- Removed the `updateSystemList` function and replaced it with a more efficient system management approach using `systemsManager`. - Updated the `App` component to initialize and subscribe to system updates through the new `systemsManager`. - Refactored the `SystemsTable` and `SystemDetail` components to utilize the new state management for systems, improving performance and maintainability. - Enhanced the `ActiveAlerts` component to fetch system names directly from the new state structure.
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
import { Suspense, memo, useEffect, useMemo } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"
|
||||
import { $alerts, $systems } from "@/lib/stores"
|
||||
import { $alerts, $allSystemsById } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { GithubIcon } from "lucide-react"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { getSystemNameFromId } from "@/lib/utils"
|
||||
import { pb, updateRecordList, updateSystemList } from "@/lib/api"
|
||||
import { AlertRecord, SystemRecord } from "@/types"
|
||||
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"
|
||||
@@ -14,8 +12,6 @@ import { getPagePath } from "@nanostores/router"
|
||||
import { alertInfo } from "@/lib/alerts"
|
||||
import SystemsTable from "@/components/systems-table/systems-table"
|
||||
|
||||
// const SystemsTable = lazy(() => import("../systems-table/systems-table"))
|
||||
|
||||
export default memo(function () {
|
||||
const { t } = useLingui()
|
||||
|
||||
@@ -23,19 +19,6 @@ export default memo(function () {
|
||||
document.title = t`Dashboard` + " / Beszel"
|
||||
}, [t])
|
||||
|
||||
useEffect(() => {
|
||||
// make sure we have the latest list of systems
|
||||
updateSystemList()
|
||||
|
||||
// subscribe to real time updates for systems / alerts
|
||||
pb.collection<SystemRecord>("systems").subscribe("*", (e) => {
|
||||
updateRecordList(e, $systems)
|
||||
})
|
||||
return () => {
|
||||
pb.collection("systems").unsubscribe("*")
|
||||
}
|
||||
}, [])
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<>
|
||||
@@ -69,6 +52,7 @@ export default memo(function () {
|
||||
|
||||
const ActiveAlerts = () => {
|
||||
const alerts = useStore($alerts)
|
||||
const systems = useStore($allSystemsById)
|
||||
|
||||
const { activeAlerts, alertsKey } = useMemo(() => {
|
||||
const activeAlerts: AlertRecord[] = []
|
||||
@@ -112,7 +96,7 @@ const ActiveAlerts = () => {
|
||||
>
|
||||
<info.icon className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{getSystemNameFromId(alert.system)} {info.name().toLowerCase().replace("cpu", "CPU")}
|
||||
{systems[alert.system]?.name} {info.name().toLowerCase().replace("cpu", "CPU")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{alert.name === "Status" ? (
|
||||
@@ -125,7 +109,7 @@ const ActiveAlerts = () => {
|
||||
)}
|
||||
</AlertDescription>
|
||||
<Link
|
||||
href={getPagePath($router, "system", { name: getSystemNameFromId(alert.system) })}
|
||||
href={getPagePath($router, "system", { name: systems[alert.system]?.name })}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
aria-label="View system"
|
||||
></Link>
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
$direction,
|
||||
$maxValues,
|
||||
$temperatureFilter,
|
||||
$allSystemsByName,
|
||||
} from "@/lib/stores"
|
||||
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
|
||||
import { ChartType, Unit, Os, SystemStatus } from "@/lib/enums"
|
||||
@@ -49,6 +50,7 @@ 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>()
|
||||
|
||||
@@ -117,7 +119,7 @@ function dockerOrPodman(str: string, system: SystemRecord) {
|
||||
return str
|
||||
}
|
||||
|
||||
export default function SystemDetail({ name }: { name: string }) {
|
||||
export default memo(function SystemDetail({ name }: { name: string }) {
|
||||
const direction = useStore($direction)
|
||||
const { t } = useLingui()
|
||||
const systems = useStore($systems)
|
||||
@@ -149,36 +151,13 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
}
|
||||
}, [name])
|
||||
|
||||
// function resetCharts() {
|
||||
// setSystemStats([])
|
||||
// setContainerData([])
|
||||
// }
|
||||
|
||||
// useEffect(resetCharts, [chartTime])
|
||||
|
||||
// find matching system
|
||||
// find matching system and update when it changes
|
||||
useEffect(() => {
|
||||
if (system.id && system.name === name) {
|
||||
return
|
||||
}
|
||||
const matchingSystem = systems.find((s) => s.name === name) as SystemRecord
|
||||
if (matchingSystem) {
|
||||
setSystem(matchingSystem)
|
||||
}
|
||||
}, [name, system, systems])
|
||||
|
||||
// update system when new data is available
|
||||
useEffect(() => {
|
||||
if (!system.id) {
|
||||
return
|
||||
}
|
||||
pb.collection<SystemRecord>("systems").subscribe(system.id, (e) => {
|
||||
setSystem(e.record)
|
||||
return subscribeKeys($allSystemsByName, [name], (newSystems) => {
|
||||
const sys = newSystems[name]
|
||||
sys?.id && setSystem(sys)
|
||||
})
|
||||
return () => {
|
||||
pb.collection("systems").unsubscribe(system.id)
|
||||
}
|
||||
}, [system.id])
|
||||
}, [name])
|
||||
|
||||
const chartData: ChartData = useMemo(() => {
|
||||
const lastCreated = Math.max(
|
||||
@@ -835,7 +814,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
{bottomSpacing > 0 && <span className="block" style={{ height: bottomSpacing }} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
|
||||
const containerFilter = useStore(store)
|
||||
|
@@ -36,9 +36,9 @@ import {
|
||||
FilterIcon,
|
||||
} from "lucide-react"
|
||||
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { $systems } from "@/lib/stores"
|
||||
import { $pausedSystems, $downSystems, $upSystems, $systems } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { cn, runOnce, useLocalStorage } from "@/lib/utils"
|
||||
import { cn, runOnce, useBrowserStorage } from "@/lib/utils"
|
||||
import { $router, Link } from "../router"
|
||||
import { useLingui, Trans } from "@lingui/react/macro"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||
@@ -50,12 +50,15 @@ import { SystemStatus } from "@/lib/enums"
|
||||
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual"
|
||||
|
||||
type ViewMode = "table" | "grid"
|
||||
type StatusFilter = "all" | "up" | "down" | "paused"
|
||||
type StatusFilter = "all" | SystemRecord["status"]
|
||||
|
||||
const preloadSystemDetail = runOnce(() => import("@/components/routes/system.tsx"))
|
||||
|
||||
export default function SystemsTable() {
|
||||
const data = useStore($systems)
|
||||
const downSystems = $downSystems.get()
|
||||
const upSystems = $upSystems.get()
|
||||
const pausedSystems = $pausedSystems.get()
|
||||
const { i18n, t } = useLingui()
|
||||
const [filter, setFilter] = useState<string>()
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
|
||||
@@ -74,7 +77,13 @@ export default function SystemsTable() {
|
||||
if (statusFilter === "all") {
|
||||
return data
|
||||
}
|
||||
return data.filter((system) => system.status === statusFilter)
|
||||
if (statusFilter === SystemStatus.Up) {
|
||||
return Object.values(upSystems) ?? []
|
||||
}
|
||||
if (statusFilter === SystemStatus.Down) {
|
||||
return Object.values(downSystems) ?? []
|
||||
}
|
||||
return Object.values(pausedSystems) ?? []
|
||||
}, [data, statusFilter])
|
||||
|
||||
const [viewMode, setViewMode] = useBrowserStorage<ViewMode>(
|
||||
@@ -106,7 +115,6 @@ export default function SystemsTable() {
|
||||
columnVisibility,
|
||||
},
|
||||
defaultColumn: {
|
||||
// sortDescFirst: true,
|
||||
invertSorting: true,
|
||||
sortUndefined: "last",
|
||||
minSize: 0,
|
||||
@@ -118,18 +126,22 @@ export default function SystemsTable() {
|
||||
const rows = table.getRowModel().rows
|
||||
const columns = table.getAllColumns()
|
||||
const visibleColumns = table.getVisibleLeafColumns()
|
||||
|
||||
const [upSystemsLength, downSystemsLength, pausedSystemsLength] = useMemo(() => {
|
||||
return [Object.values(upSystems).length, Object.values(downSystems).length, Object.values(pausedSystems).length]
|
||||
}, [upSystems, downSystems, pausedSystems])
|
||||
|
||||
// TODO: hiding temp then gpu messes up table headers
|
||||
const CardHead = useMemo(() => {
|
||||
return (
|
||||
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
||||
<CardHeader className="pb-4.5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
||||
<div className="grid md:flex gap-5 w-full items-end">
|
||||
<div className="px-2 sm:px-1">
|
||||
<CardTitle className="mb-2.5">
|
||||
<CardTitle className="mb-2">
|
||||
<Trans>All Systems</Trans>
|
||||
</CardTitle>
|
||||
</CardTitle>
|
||||
<CardDescription className="flex">
|
||||
<Trans>Click on a system to view information - {runningRecords} / {totalRecords}</Trans>
|
||||
<p className={"ml-2 text-" + (runningRecords === totalRecords ? "emerald" : "red") + "-600"}>Online</p>
|
||||
<Trans>Click on a system to view more information.</Trans>
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
@@ -181,13 +193,13 @@ export default function SystemsTable() {
|
||||
<Trans>All Systems</Trans>
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="up" onSelect={(e) => e.preventDefault()}>
|
||||
<Trans>Up</Trans>
|
||||
<Trans>Up ({upSystemsLength})</Trans>
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="down" onSelect={(e) => e.preventDefault()}>
|
||||
<Trans>Down</Trans>
|
||||
<Trans>Down ({downSystemsLength})</Trans>
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="paused" onSelect={(e) => e.preventDefault()}>
|
||||
<Trans>Paused</Trans>
|
||||
<Trans>Paused ({pausedSystemsLength})</Trans>
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</div>
|
||||
@@ -258,7 +270,16 @@ export default function SystemsTable() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
)
|
||||
}, [visibleColumns.length, sorting, viewMode, locale, statusFilter, runningRecords, totalRecords])
|
||||
}, [
|
||||
visibleColumns.length,
|
||||
sorting,
|
||||
viewMode,
|
||||
locale,
|
||||
statusFilter,
|
||||
upSystemsLength,
|
||||
downSystemsLength,
|
||||
pausedSystemsLength,
|
||||
])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
@@ -1,10 +1,8 @@
|
||||
import { ChartTimes, SystemRecord, UserSettings } from "@/types"
|
||||
import { $alerts, $longestSystemNameLen, $systems, $userSettings } from "./stores"
|
||||
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 { WritableAtom } from "nanostores"
|
||||
import { RecordModel, RecordSubscription } from "pocketbase"
|
||||
import PocketBase from "pocketbase"
|
||||
import { basePath } from "@/components/router"
|
||||
|
||||
@@ -14,7 +12,7 @@ export const pb = new PocketBase(basePath)
|
||||
export const isAdmin = () => pb.authStore.record?.role === "admin"
|
||||
export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly"
|
||||
|
||||
const verifyAuth = () => {
|
||||
export const verifyAuth = () => {
|
||||
pb.collection("users")
|
||||
.authRefresh()
|
||||
.catch(() => {
|
||||
@@ -29,7 +27,7 @@ const verifyAuth = () => {
|
||||
|
||||
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
|
||||
export async function logOut() {
|
||||
$systems.set([])
|
||||
$allSystemsByName.set({})
|
||||
$alerts.set({})
|
||||
$userSettings.set({} as UserSettings)
|
||||
sessionStorage.setItem("lo", "t") // prevent auto login on logout
|
||||
@@ -54,74 +52,6 @@ export async function updateUserSettings() {
|
||||
console.error("create settings", e)
|
||||
}
|
||||
}
|
||||
/** Update systems / alerts list when records change */
|
||||
export function updateRecordList<T extends RecordModel>(e: RecordSubscription<T>, $store: WritableAtom<T[]>) {
|
||||
const curRecords = $store.get()
|
||||
const newRecords = []
|
||||
if (e.action === "delete") {
|
||||
for (const server of curRecords) {
|
||||
if (server.id !== e.record.id) {
|
||||
newRecords.push(server)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let found = 0
|
||||
for (const server of curRecords) {
|
||||
if (server.id === e.record.id) {
|
||||
found = newRecords.push(e.record)
|
||||
} else {
|
||||
newRecords.push(server)
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
newRecords.push(e.record)
|
||||
}
|
||||
}
|
||||
$store.set(newRecords)
|
||||
}
|
||||
/** Fetches updated system list from database */
|
||||
export const updateSystemList = (() => {
|
||||
let isFetchingSystems = false
|
||||
return async () => {
|
||||
if (isFetchingSystems) {
|
||||
return
|
||||
}
|
||||
isFetchingSystems = true
|
||||
try {
|
||||
let records = await pb
|
||||
.collection<SystemRecord>("systems")
|
||||
.getFullList({ sort: "+name", fields: "id,name,host,port,info,status" })
|
||||
|
||||
if (records.length) {
|
||||
// records = [
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ]
|
||||
// we need to loop once to get the longest name
|
||||
let longestName = $longestSystemNameLen.get()
|
||||
for (const { name } of records) {
|
||||
const nameLen = Math.min(20, name.length)
|
||||
if (nameLen > longestName) {
|
||||
$longestSystemNameLen.set(nameLen)
|
||||
longestName = nameLen
|
||||
}
|
||||
}
|
||||
$systems.set(records)
|
||||
} else {
|
||||
verifyAuth()
|
||||
}
|
||||
} finally {
|
||||
isFetchingSystems = false
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
|
||||
d ||= chartTimeData[timeString].getOffset(new Date())
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { atom, map } from "nanostores"
|
||||
import { atom, computed, map, ReadableAtom } from "nanostores"
|
||||
import { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
|
||||
import { Unit } from "./enums"
|
||||
import { pb } from "./api"
|
||||
@@ -6,8 +6,18 @@ import { pb } from "./api"
|
||||
/** Store if user is authenticated */
|
||||
export const $authenticated = atom(pb.authStore.isValid)
|
||||
|
||||
/** List of system records */
|
||||
export const $systems = atom<SystemRecord[]>([])
|
||||
/** Map of system records by name */
|
||||
export const $allSystemsByName = map<Record<string, SystemRecord>>({})
|
||||
/** Map of system records by id */
|
||||
export const $allSystemsById = map<Record<string, SystemRecord>>({})
|
||||
/** Map of up systems by id */
|
||||
export const $upSystems = map<Record<string, SystemRecord>>({})
|
||||
/** Map of down systems by id */
|
||||
export const $downSystems = map<Record<string, SystemRecord>>({})
|
||||
/** Map of paused systems by id */
|
||||
export const $pausedSystems = map<Record<string, SystemRecord>>({})
|
||||
/** List of all system records */
|
||||
export const $systems: ReadableAtom<SystemRecord[]> = computed($allSystemsByName, Object.values)
|
||||
|
||||
/** Map of alert records by system id and alert name */
|
||||
export const $alerts = map<AlertMap>({})
|
||||
|
161
beszel/site/src/lib/systemsManager.ts
Normal file
161
beszel/site/src/lib/systemsManager.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { SystemRecord } from "@/types"
|
||||
import { PreinitializedMapStore } from "nanostores"
|
||||
import { pb, verifyAuth } from "@/lib/api"
|
||||
import {
|
||||
$allSystemsByName,
|
||||
$upSystems,
|
||||
$downSystems,
|
||||
$pausedSystems,
|
||||
$allSystemsById,
|
||||
$longestSystemNameLen,
|
||||
} from "@/lib/stores"
|
||||
import { updateFavicon, FAVICON_DEFAULT, FAVICON_GREEN, FAVICON_RED } from "@/lib/utils"
|
||||
import { SystemStatus } from "./enums"
|
||||
|
||||
const COLLECTION = pb.collection<SystemRecord>("systems")
|
||||
const FIELDS_DEFAULT = "id,name,host,port,info,status"
|
||||
|
||||
/** Maximum system name length for display purposes */
|
||||
const MAX_SYSTEM_NAME_LENGTH = 20
|
||||
|
||||
let initialized = false
|
||||
let unsub: (() => void) | undefined | void
|
||||
|
||||
/** Initialize the systems manager and set up listeners */
|
||||
export function init() {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
initialized = true
|
||||
|
||||
// sync system stores on change
|
||||
$allSystemsByName.listen((newSystems, oldSystems, changedKey) => {
|
||||
const oldSystem = oldSystems[changedKey]
|
||||
const newSystem = newSystems[changedKey]
|
||||
|
||||
// if system is undefined (deleted), remove it from the stores
|
||||
if (oldSystem && !newSystem?.id) {
|
||||
removeFromStore(oldSystem, $upSystems)
|
||||
removeFromStore(oldSystem, $downSystems)
|
||||
removeFromStore(oldSystem, $pausedSystems)
|
||||
removeFromStore(oldSystem, $allSystemsById)
|
||||
}
|
||||
|
||||
if (!newSystem) {
|
||||
onSystemsChanged(newSystems, undefined)
|
||||
return
|
||||
}
|
||||
|
||||
const newStatus = newSystem.status
|
||||
if (newStatus === SystemStatus.Up) {
|
||||
$upSystems.setKey(newSystem.id, newSystem)
|
||||
removeFromStore(newSystem, $downSystems)
|
||||
removeFromStore(newSystem, $pausedSystems)
|
||||
} else if (newStatus === SystemStatus.Down) {
|
||||
$downSystems.setKey(newSystem.id, newSystem)
|
||||
removeFromStore(newSystem, $upSystems)
|
||||
removeFromStore(newSystem, $pausedSystems)
|
||||
} else if (newStatus === SystemStatus.Paused) {
|
||||
$pausedSystems.setKey(newSystem.id, newSystem)
|
||||
removeFromStore(newSystem, $upSystems)
|
||||
removeFromStore(newSystem, $downSystems)
|
||||
}
|
||||
|
||||
// run things that need to be done when systems change
|
||||
onSystemsChanged(newSystems, newSystem)
|
||||
})
|
||||
}
|
||||
|
||||
/** Update the longest system name length and favicon based on system status */
|
||||
function onSystemsChanged(_: Record<string, SystemRecord>, changedSystem: SystemRecord | undefined) {
|
||||
const upSystemsStore = $upSystems.get()
|
||||
const downSystemsStore = $downSystems.get()
|
||||
const upSystems = Object.values(upSystemsStore)
|
||||
const downSystems = Object.values(downSystemsStore)
|
||||
|
||||
// Update longest system name length
|
||||
const longestName = $longestSystemNameLen.get()
|
||||
const nameLen = Math.min(MAX_SYSTEM_NAME_LENGTH, changedSystem?.name.length || 0)
|
||||
if (nameLen > longestName) {
|
||||
$longestSystemNameLen.set(nameLen)
|
||||
}
|
||||
|
||||
// Update favicon based on system status
|
||||
if (downSystems.length > 0) {
|
||||
updateFavicon(FAVICON_RED)
|
||||
} else if (upSystems.length > 0) {
|
||||
updateFavicon(FAVICON_GREEN)
|
||||
} else {
|
||||
updateFavicon(FAVICON_DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch systems from collection */
|
||||
async function fetchSystems(): Promise<SystemRecord[]> {
|
||||
try {
|
||||
return await COLLECTION.getFullList({ sort: "+name", fields: FIELDS_DEFAULT })
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch systems:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Store management functions
|
||||
/** Add system to both name and ID stores */
|
||||
export function add(system: SystemRecord) {
|
||||
$allSystemsByName.setKey(system.name, system)
|
||||
$allSystemsById.setKey(system.id, system)
|
||||
}
|
||||
|
||||
/** Remove system from stores */
|
||||
export function remove(system: SystemRecord) {
|
||||
removeFromStore(system, $allSystemsByName)
|
||||
removeFromStore(system, $allSystemsById)
|
||||
removeFromStore(system, $upSystems)
|
||||
removeFromStore(system, $downSystems)
|
||||
removeFromStore(system, $pausedSystems)
|
||||
}
|
||||
|
||||
/** Remove system from specific store */
|
||||
function removeFromStore(system: SystemRecord, store: PreinitializedMapStore<Record<string, SystemRecord>>) {
|
||||
const key = store === $allSystemsByName ? system.name : system.id
|
||||
store.setKey(key, undefined as any)
|
||||
}
|
||||
|
||||
/** Action functions for subscription */
|
||||
const actionFns: Record<string, (system: SystemRecord) => void> = {
|
||||
create: add,
|
||||
update: add,
|
||||
delete: remove,
|
||||
}
|
||||
|
||||
/** Subscribe to real-time system updates from the collection */
|
||||
export async function subscribe() {
|
||||
try {
|
||||
unsub = await COLLECTION.subscribe("*", ({ action, record }) => actionFns[action]?.(record), {
|
||||
fields: FIELDS_DEFAULT,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to subscribe to systems collection:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Refresh all systems with latest data from the hub */
|
||||
export async function refresh() {
|
||||
try {
|
||||
const records = await fetchSystems()
|
||||
if (!records.length) {
|
||||
// No systems found, verify authentication
|
||||
verifyAuth()
|
||||
return
|
||||
}
|
||||
for (const record of records) {
|
||||
add(record)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh systems:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Unsubscribe from real-time system updates */
|
||||
export const unsubscribe = () => (unsub = unsub?.())
|
@@ -2,13 +2,17 @@ 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, $systems, $userSettings } from "./stores"
|
||||
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 { prependBasePath } from "@/components/router"
|
||||
|
||||
export const FAVICON_DEFAULT = "favicon.svg"
|
||||
export const FAVICON_GREEN = "favicon-green.svg"
|
||||
export const FAVICON_RED = "favicon-red.svg"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -344,19 +348,6 @@ export function debounce<T extends (...args: any[]) => any>(func: T, wait: numbe
|
||||
}
|
||||
}
|
||||
|
||||
/* returns the name of a system from its id */
|
||||
export const getSystemNameFromId = (() => {
|
||||
const cache = new Map<string, string>()
|
||||
return (systemId: string): string => {
|
||||
if (cache.has(systemId)) {
|
||||
return cache.get(systemId)!
|
||||
}
|
||||
const sysName = $systems.get().find((s) => s.id === systemId)?.name ?? ""
|
||||
cache.set(systemId, sysName)
|
||||
return sysName
|
||||
}
|
||||
})()
|
||||
|
||||
/** Run a function only once */
|
||||
export function runOnce<T extends (...args: any[]) => any>(fn: T): (...args: Parameters<T>) => ReturnType<T> {
|
||||
let done = false
|
||||
|
@@ -4,17 +4,16 @@ 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, $systems, $publicKey, $copyContent, $direction } from "./lib/stores.ts"
|
||||
import { pb, updateSystemList, updateUserSettings } from "./lib/api.ts"
|
||||
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 { updateFavicon } from "@/lib/utils"
|
||||
import Navbar from "./components/navbar.tsx"
|
||||
import { I18nProvider } from "@lingui/react"
|
||||
import { i18n } from "@lingui/core"
|
||||
import { getLocale, dynamicActivate } from "./lib/i18n"
|
||||
import { SystemStatus } from "./lib/enums"
|
||||
import { alertManager } from "./lib/alerts"
|
||||
import Settings from "./components/routes/settings/layout.tsx"
|
||||
|
||||
@@ -25,8 +24,6 @@ const CopyToClipboardDialog = lazy(() => import("@/components/copy-to-clipboard.
|
||||
|
||||
const App = memo(() => {
|
||||
const page = useStore($router)
|
||||
const authenticated = useStore($authenticated)
|
||||
const systems = useStore($systems)
|
||||
|
||||
useEffect(() => {
|
||||
// change auth store on auth change
|
||||
@@ -37,40 +34,26 @@ const App = memo(() => {
|
||||
pb.send("/api/beszel/getkey", {}).then((data) => {
|
||||
$publicKey.set(data.key)
|
||||
})
|
||||
// get servers / alerts / settings
|
||||
// get user settings
|
||||
updateUserSettings()
|
||||
// need to get system list before alerts
|
||||
updateSystemList()
|
||||
// get alerts
|
||||
systemsManager.init()
|
||||
systemsManager
|
||||
// get current systems list
|
||||
.refresh()
|
||||
// subscribe to new system updates
|
||||
.then(systemsManager.subscribe)
|
||||
// get current alerts
|
||||
.then(alertManager.refresh)
|
||||
// subscribe to new alert updates
|
||||
.then(alertManager.subscribe)
|
||||
|
||||
return () => {
|
||||
updateFavicon("favicon.svg")
|
||||
// updateFavicon("favicon.svg")
|
||||
alertManager.unsubscribe()
|
||||
systemsManager.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// update favicon
|
||||
useEffect(() => {
|
||||
if (!systems.length || !authenticated) {
|
||||
updateFavicon("favicon.svg")
|
||||
} else {
|
||||
let up = false
|
||||
for (const system of systems) {
|
||||
if (system.status === SystemStatus.Down) {
|
||||
updateFavicon("favicon-red.svg")
|
||||
return
|
||||
}
|
||||
if (system.status === SystemStatus.Up) {
|
||||
up = true
|
||||
}
|
||||
}
|
||||
updateFavicon(up ? "favicon-green.svg" : "favicon.svg")
|
||||
}
|
||||
}, [systems])
|
||||
|
||||
if (!page) {
|
||||
return <h1 className="text-3xl text-center my-14">404</h1>
|
||||
} else if (page.route === "home") {
|
||||
|
Reference in New Issue
Block a user