From 6696e1c7497895be51ea734ff02262294e321d71 Mon Sep 17 00:00:00 2001 From: Henry Dollman Date: Mon, 15 Jul 2024 15:49:00 -0400 Subject: [PATCH] alert updates --- migrations/1720568457_collections_snapshot.go | 6 +- site/src/components/command-palette.tsx | 4 +- site/src/components/routes/server.tsx | 6 +- .../components/server-table/systems-table.tsx | 66 +--------- site/src/components/table-alerts.tsx | 120 ++++++++++++++++++ site/src/lib/stores.ts | 12 +- site/src/lib/utils.ts | 46 ++++++- site/src/main.tsx | 72 +++++------ site/src/types.d.ts | 7 + 9 files changed, 224 insertions(+), 115 deletions(-) create mode 100644 site/src/components/table-alerts.tsx diff --git a/migrations/1720568457_collections_snapshot.go b/migrations/1720568457_collections_snapshot.go index 508d5cd..446688b 100644 --- a/migrations/1720568457_collections_snapshot.go +++ b/migrations/1720568457_collections_snapshot.go @@ -255,7 +255,7 @@ func init() { { "id": "elngm8x1l60zi2v", "created": "2024-07-15 01:16:04.044Z", - "updated": "2024-07-15 01:19:11.639Z", + "updated": "2024-07-15 18:48:55.881Z", "name": "alerts", "type": "base", "system": false, @@ -270,7 +270,7 @@ func init() { "unique": false, "options": { "collectionId": "_pb_users_auth_", - "cascadeDelete": false, + "cascadeDelete": true, "minSelect": null, "maxSelect": 1, "displayFields": null @@ -286,7 +286,7 @@ func init() { "unique": false, "options": { "collectionId": "2hz5ncl8tizk5nx", - "cascadeDelete": false, + "cascadeDelete": true, "minSelect": null, "maxSelect": 1, "displayFields": null diff --git a/site/src/components/command-palette.tsx b/site/src/components/command-palette.tsx index 925c993..3b6432d 100644 --- a/site/src/components/command-palette.tsx +++ b/site/src/components/command-palette.tsx @@ -21,12 +21,12 @@ import { } from '@/components/ui/command' import { useEffect, useState } from 'react' import { useStore } from '@nanostores/react' -import { $servers, navigate } from '@/lib/stores' +import { $systems, navigate } from '@/lib/stores' import { isAdmin } from '@/lib/utils' export default function CommandPalette() { const [open, setOpen] = useState(false) - const servers = useStore($servers) + const servers = useStore($systems) useEffect(() => { const down = (e: KeyboardEvent) => { diff --git a/site/src/components/routes/server.tsx b/site/src/components/routes/server.tsx index 097e941..da75dfd 100644 --- a/site/src/components/routes/server.tsx +++ b/site/src/components/routes/server.tsx @@ -1,4 +1,4 @@ -import { $servers, pb } from '@/lib/stores' +import { $systems, pb } from '@/lib/stores' import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types' import { Suspense, lazy, useEffect, useState } from 'react' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card' @@ -39,7 +39,7 @@ function timestampToBrowserTime(timestamp: string) { // } export default function ServerDetail({ name }: { name: string }) { - const servers = useStore($servers) + const servers = useStore($systems) const [server, setServer] = useState({} as SystemRecord) const [containers, setContainers] = useState([] as ContainerStatsRecord[]) @@ -107,7 +107,7 @@ export default function ServerDetail({ name }: { name: string }) { }, [serverStats]) useEffect(() => { - if ($servers.get().length === 0) { + if ($systems.get().length === 0) { // console.log('skipping') return } diff --git a/site/src/components/server-table/systems-table.tsx b/site/src/components/server-table/systems-table.tsx index d8f3e62..149aed9 100644 --- a/site/src/components/server-table/systems-table.tsx +++ b/site/src/components/server-table/systems-table.tsx @@ -55,23 +55,13 @@ import { PauseCircleIcon, PlayCircleIcon, Trash2Icon, - BellIcon, } from 'lucide-react' -import { useEffect, useMemo, useState } from 'react' -import { $servers, pb, navigate } from '@/lib/stores' +import { useMemo, useState } from 'react' +import { $systems, pb, navigate } from '@/lib/stores' import { useStore } from '@nanostores/react' import { AddServerButton } from '../add-server' import { cn, copyToClipboard, isAdmin } from '@/lib/utils' -import { - Dialog, - DialogContent, - DialogTrigger, - DialogDescription, - DialogTitle, - DialogHeader, -} from '@/components/ui/dialog' -import { Switch } from '@/components/ui/switch' -import { Separator } from '../ui/separator' +import AlertsButton from '../table-alerts' function CellFormatter(info: CellContext) { const val = info.getValue() as number @@ -106,14 +96,10 @@ function sortableHeader(column: Column, name: string, Ico } export default function SystemsTable() { - const data = useStore($servers) + const data = useStore($systems) const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) - // useEffect(() => { - // console.log('servers', data) - // }, [data]) - const columns: ColumnDef[] = useMemo(() => { return [ { @@ -171,49 +157,7 @@ export default function SystemsTable() { const { id, name, status, host } = row.original return (
- - - - - - - Alerts for {name} - {isAdmin() && ( - - Please{' '} - - configure an SMTP server - {' '} - to ensure alerts are delivered. - - )} - - -
-
- -

- Triggers when system status switches between up and down. -

-
- -
-
-
-
+ diff --git a/site/src/components/table-alerts.tsx b/site/src/components/table-alerts.tsx new file mode 100644 index 0000000..376697f --- /dev/null +++ b/site/src/components/table-alerts.tsx @@ -0,0 +1,120 @@ +import { $alerts, pb } from '@/lib/stores' +import { useStore } from '@nanostores/react' +import { + Dialog, + DialogTrigger, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { BellIcon } from 'lucide-react' +import { cn, isAdmin } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { Switch } from '@/components/ui/switch' +import { AlertRecord, SystemRecord } from '@/types' +import { useMemo, useState } from 'react' +import { toast } from './ui/use-toast' + +export default function AlertsButton({ system }: { system: SystemRecord }) { + const alerts = useStore($alerts) + + const active = useMemo(() => { + return alerts.find((alert) => alert.system === system.id) + }, [alerts, system]) + + const systemAlerts = useMemo(() => { + return alerts.filter((alert) => alert.system === system.id) as AlertRecord[] + }, [alerts, system]) + + return ( + + + + + + + Alerts for {system.name} + + {isAdmin() && ( + + Please{' '} + + configure an SMTP server + {' '} + to ensure alerts are delivered.{' '} + + )} + Webhook delivery and more alert options will be added in the future. + + + + + + ) +} + +function Alert({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[] }) { + const [pendingChange, setPendingChange] = useState(false) + + const alert = useMemo(() => { + return alerts.find((alert) => alert.name === 'status') + }, [alerts]) + + return ( + + ) +} diff --git a/site/src/lib/stores.ts b/site/src/lib/stores.ts index 1f9c877..59a8cc6 100644 --- a/site/src/lib/stores.ts +++ b/site/src/lib/stores.ts @@ -1,8 +1,9 @@ import PocketBase from 'pocketbase' import { atom } from 'nanostores' -import { SystemRecord } from '@/types' +import { AlertRecord, SystemRecord } from '@/types' import { createRouter } from '@nanostores/router' +/** PocketBase JS Client */ export const pb = new PocketBase('/') export const $router = createRouter( @@ -13,12 +14,19 @@ export const $router = createRouter( { links: false } ) +/** Navigate to url using router */ export const navigate = (urlString: string) => { $router.open(urlString) } +/** Store if user is authenticated */ export const $authenticated = atom(pb.authStore.isValid) -export const $servers = atom([] as SystemRecord[]) +/** List of system records */ +export const $systems = atom([] as SystemRecord[]) +/** List of alert records */ +export const $alerts = atom([] as AlertRecord[]) + +/** SSH public key */ export const $publicKey = atom('') diff --git a/site/src/lib/utils.ts b/site/src/lib/utils.ts index bc5e085..dcd8875 100644 --- a/site/src/lib/utils.ts +++ b/site/src/lib/utils.ts @@ -1,8 +1,10 @@ import { toast } from '@/components/ui/use-toast' import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' -import { $servers, pb } from './stores' -import { SystemRecord } from '@/types' +import { $alerts, $systems, pb } from './stores' +import { AlertRecord, SystemRecord } from '@/types' +import { RecordModel, RecordSubscription } from 'pocketbase' +import { WritableAtom } from 'nanostores' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) @@ -28,7 +30,15 @@ export const updateServerList = () => { pb.collection('systems') .getFullList({ sort: '+name' }) .then((records) => { - $servers.set(records) + $systems.set(records) + }) +} + +export const updateAlerts = () => { + pb.collection('alerts') + .getFullList({ fields: 'id,name,system' }) + .then((records) => { + $alerts.set(records) }) } @@ -56,3 +66,33 @@ export const updateFavicon = (newIconUrl: string) => ((document.querySelector("link[rel='icon']") as HTMLLinkElement).href = newIconUrl) export const isAdmin = () => pb.authStore.model?.admin + +/** Update systems / alerts list when records change */ +export function updateRecordList( + e: RecordSubscription, + $store: WritableAtom +) { + const curRecords = $store.get() + const newRecords = [] + // console.log('e', e) + 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) +} diff --git a/site/src/main.tsx b/site/src/main.tsx index 50678d8..b1de0e2 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -3,11 +3,18 @@ import React, { Suspense, lazy, useEffect } from 'react' import ReactDOM from 'react-dom/client' import Home from './components/routes/home.tsx' import { ThemeProvider } from './components/theme-provider.tsx' -import { $authenticated, $router, $servers, navigate, pb } from './lib/stores.ts' +import { $alerts, $authenticated, $router, $systems, navigate, pb } from './lib/stores.ts' import { ModeToggle } from './components/mode-toggle.tsx' -import { cn, isAdmin, updateFavicon, updateServerList } from './lib/utils.ts' +import { + cn, + isAdmin, + updateAlerts, + updateFavicon, + updateRecordList, + updateServerList, +} from './lib/utils.ts' import { buttonVariants } from './components/ui/button.tsx' -import { DatabaseBackupIcon, Github, LogOutIcon, LogsIcon, MailIcon, UserIcon } from 'lucide-react' +import { DatabaseBackupIcon, Github, LogOutIcon, LogsIcon, UserIcon } from 'lucide-react' import { useStore } from '@nanostores/react' import { Toaster } from './components/ui/toaster.tsx' import { Logo } from './components/logo.tsx' @@ -24,7 +31,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from './components/ui/dropdown-menu.tsx' -import { SystemRecord } from './types' +import { AlertRecord, SystemRecord } from './types' const ServerDetail = lazy(() => import('./components/routes/server.tsx')) const CommandPalette = lazy(() => import('./components/command-palette.tsx')) @@ -33,45 +40,26 @@ const LoginPage = lazy(() => import('./components/login.tsx')) const App = () => { const page = useStore($router) const authenticated = useStore($authenticated) - const servers = useStore($servers) + const servers = useStore($systems) useEffect(() => { - // get servers - updateServerList() // change auth store on auth change pb.authStore.onChange(() => { $authenticated.set(pb.authStore.isValid) }) - }, []) - - useEffect(() => { + // get servers / alerts + updateServerList() + updateAlerts() + // subscribe to real time updates for systems / alerts pb.collection('systems').subscribe('*', (e) => { - const curServers = $servers.get() - const newServers = [] - // console.log('e', e) - if (e.action === 'delete') { - for (const server of curServers) { - if (server.id !== e.record.id) { - newServers.push(server) - } - } - } else { - let found = 0 - for (const server of curServers) { - if (server.id === e.record.id) { - found = newServers.push(e.record) - } else { - newServers.push(server) - } - } - if (!found) { - newServers.push(e.record) - } - } - $servers.set(newServers) + updateRecordList(e, $systems) + }) + pb.collection('alerts').subscribe('*', (e) => { + updateRecordList(e, $alerts) }) return () => { pb.collection('systems').unsubscribe('*') + pb.collection('alerts').unsubscribe('*') } }, []) @@ -114,7 +102,11 @@ const Layout = () => { const authenticated = useStore($authenticated) if (!authenticated) { - return + return ( + + + + ) } return ( @@ -191,7 +183,9 @@ const Layout = () => {
- + + +
) @@ -200,12 +194,8 @@ const Layout = () => { ReactDOM.createRoot(document.getElementById('app')!).render( - - - - - - + + ) diff --git a/site/src/types.d.ts b/site/src/types.d.ts index 65c5d4d..7966e77 100644 --- a/site/src/types.d.ts +++ b/site/src/types.d.ts @@ -68,3 +68,10 @@ export interface SystemStatsRecord extends RecordModel { system: string info: SystemStats } + +export interface AlertRecord extends RecordModel { + id: string + system: string + name: string + // user: string +}