updates to settings page and alerts

This commit is contained in:
Henry Dollman
2024-09-13 18:19:12 -04:00
parent 9710d0d2f1
commit f16e22e521
8 changed files with 75 additions and 68 deletions

View File

@@ -29,7 +29,7 @@ type AlertData struct {
LinkText string LinkText string
} }
type UserAlertSettings struct { type UserNotificationSettings struct {
Emails []string `json:"emails"` Emails []string `json:"emails"`
Webhooks []string `json:"webhooks"` Webhooks []string `json:"webhooks"`
} }
@@ -166,16 +166,16 @@ func (am *AlertManager) sendAlert(data AlertData) {
dbx.Params{"user": data.UserID}, dbx.Params{"user": data.UserID},
) )
if err != nil { if err != nil {
log.Println("Failed to get user settings", "err", err.Error()) am.app.Logger().Error("Failed to get user settings", "err", err.Error())
return return
} }
// unmarshal user settings // unmarshal user settings
userAlertSettings := UserAlertSettings{ userAlertSettings := UserNotificationSettings{
Emails: []string{}, Emails: []string{},
Webhooks: []string{}, Webhooks: []string{},
} }
if err := record.UnmarshalJSONField("settings", &userAlertSettings); err != nil { if err := record.UnmarshalJSONField("settings", &userAlertSettings); err != nil {
log.Println("Failed to unmarshal user settings", "err", err.Error()) am.app.Logger().Error("Failed to unmarshal user settings", "err", err.Error())
} }
// send alerts via webhooks // send alerts via webhooks
for _, webhook := range userAlertSettings.Webhooks { for _, webhook := range userAlertSettings.Webhooks {
@@ -186,13 +186,12 @@ func (am *AlertManager) sendAlert(data AlertData) {
} }
// send alerts via email // send alerts via email
if len(userAlertSettings.Emails) == 0 { if len(userAlertSettings.Emails) == 0 {
log.Println("No email addresses found") // log.Println("No email addresses found")
return return
} }
addresses := []mail.Address{} addresses := []mail.Address{}
for _, email := range userAlertSettings.Emails { for _, email := range userAlertSettings.Emails {
addresses = append(addresses, mail.Address{Address: email}) addresses = append(addresses, mail.Address{Address: email})
log.Println("Sending alert via email to", email)
} }
message := mailer.Message{ message := mailer.Message{
To: addresses, To: addresses,
@@ -211,6 +210,7 @@ func (am *AlertManager) sendAlert(data AlertData) {
} }
} }
// SendShoutrrrAlert sends an alert via a Shoutrrr URL
func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link, linkText string) error { func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link, linkText string) error {
// services that support title param // services that support title param
supportsTitle := []string{"bark", "discord", "gotify", "ifttt", "join", "matrix", "ntfy", "opsgenie", "pushbullet", "pushover", "slack", "teams", "telegram", "zulip"} supportsTitle := []string{"bark", "discord", "gotify", "ifttt", "join", "matrix", "ntfy", "opsgenie", "pushbullet", "pushover", "slack", "teams", "telegram", "zulip"}
@@ -259,7 +259,7 @@ func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link,
if err == nil { if err == nil {
am.app.Logger().Info("Sent shoutrrr alert", "title", title) am.app.Logger().Info("Sent shoutrrr alert", "title", title)
} else { } else {
am.app.Logger().Error("Error sending shoutrrr alert", "errs", err) am.app.Logger().Error("Error sending shoutrrr alert", "err", err.Error())
return err return err
} }
return nil return nil

View File

@@ -13,6 +13,7 @@ import { LoaderCircleIcon, SaveIcon } from 'lucide-react'
import { UserSettings } from '@/types' import { UserSettings } from '@/types'
import { saveSettings } from './layout' import { saveSettings } from './layout'
import { useState } from 'react' import { useState } from 'react'
// import { Input } from '@/components/ui/input'
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) { export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
@@ -30,17 +31,22 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
<div> <div>
<div> <div>
<h3 className="text-xl font-medium mb-2">General</h3> <h3 className="text-xl font-medium mb-2">General</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground leading-relaxed">
Set your preferred language and chart display options. Change general application options.
</p> </p>
</div> </div>
<Separator className="my-4" /> <Separator className="my-4" />
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} className="space-y-5">
{/* <Separator />
<div className="space-y-2"> <div className="space-y-2">
<div className="mb-4"> <div className="mb-4">
<h3 className="mb-1 text-lg font-medium">Language</h3> <h3 className="mb-1 text-lg font-medium">Language</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground leading-relaxed">
Additional language support coming soon. Internationalization will be added in a future release. Please see the{' '}
<a href="#" className="link" target="_blank">
discussion on GitHub
</a>{' '}
for more details.
</p> </p>
</div> </div>
<Label className="block" htmlFor="lang"> <Label className="block" htmlFor="lang">
@@ -54,12 +60,13 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
<SelectItem value="en">English</SelectItem> <SelectItem value="en">English</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div> */}
<Separator />
<div className="space-y-2"> <div className="space-y-2">
<div className="mb-4"> <div className="mb-4">
<h3 className="mb-1 text-lg font-medium">Chart options</h3> <h3 className="mb-1 text-lg font-medium">Chart options</h3>
<p className="text-sm text-muted-foreground">Adjust display options for charts.</p> <p className="text-sm text-muted-foreground leading-relaxed">
Adjust display options for charts.
</p>
</div> </div>
<Label className="block" htmlFor="chartTime"> <Label className="block" htmlFor="chartTime">
Default time period Default time period

View File

@@ -1,4 +1,4 @@
import { Suspense, lazy, useEffect } from 'react' import { useEffect } from 'react'
import { Separator } from '../../ui/separator' import { Separator } from '../../ui/separator'
import { SidebarNav } from './sidebar-nav.tsx' import { SidebarNav } from './sidebar-nav.tsx'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card.tsx' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card.tsx'
@@ -9,9 +9,8 @@ import { BellIcon, SettingsIcon } from 'lucide-react'
import { $userSettings, pb } from '@/lib/stores.ts' import { $userSettings, pb } from '@/lib/stores.ts'
import { toast } from '@/components/ui/use-toast.ts' import { toast } from '@/components/ui/use-toast.ts'
import { UserSettings } from '@/types.js' import { UserSettings } from '@/types.js'
import General from './general.tsx'
const General = lazy(() => import('./general.tsx')) import Notifications from './notifications.tsx'
const Notifications = lazy(() => import('./notifications.tsx'))
const sidebarNavItems = [ const sidebarNavItems = [
{ {
@@ -27,31 +26,28 @@ const sidebarNavItems = [
] ]
export async function saveSettings(newSettings: Partial<UserSettings>) { export async function saveSettings(newSettings: Partial<UserSettings>) {
// console.log('Updating settings:', newSettings)
try { try {
// get fresh copy of settings // get fresh copy of settings
const req = await pb.collection('user_settings').getFirstListItem('', { const req = await pb.collection('user_settings').getFirstListItem('', {
fields: 'id,settings', fields: 'id,settings',
}) })
// make new user settings
const mergedSettings = {
...req.settings,
...newSettings,
}
// update user settings // update user settings
const updatedSettings = await pb.collection('user_settings').update(req.id, { const updatedSettings = await pb.collection('user_settings').update(req.id, {
settings: mergedSettings, settings: {
...req.settings,
...newSettings,
},
}) })
$userSettings.set(updatedSettings.settings) $userSettings.set(updatedSettings.settings)
toast({ toast({
title: 'Settings saved', title: 'Settings saved',
description: 'Your notification settings have been updated.', description: 'Your user settings have been updated.',
}) })
} catch (e) { } catch (e) {
console.log('update settings', e) // console.error('update settings', e)
toast({ toast({
title: 'Failed to save settings', title: 'Failed to save settings',
description: 'Please check logs for more details.', description: 'Check logs for more details.',
variant: 'destructive', variant: 'destructive',
}) })
} }
@@ -72,19 +68,17 @@ export default function SettingsLayout() {
<Card className="pt-5 px-4 pb-8 sm:pt-6 sm:px-7"> <Card className="pt-5 px-4 pb-8 sm:pt-6 sm:px-7">
<CardHeader className="p-0"> <CardHeader className="p-0">
<CardTitle className="mb-1">Settings</CardTitle> <CardTitle className="mb-1">Settings</CardTitle>
<CardDescription>Manage notification and display preferences.</CardDescription> <CardDescription>Manage display and notification preferences.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
<Separator className="my-3 md:my-5" /> <Separator className="hidden md:block my-5" />
<div className="flex flex-col gap-3 md:flex-row md:gap-5 lg:gap-10"> <div className="flex flex-col gap-3.5 md:flex-row md:gap-5 lg:gap-10">
<aside className="md:w-48 w-full"> <aside className="md:w-48 w-full">
<SidebarNav items={sidebarNavItems} /> <SidebarNav items={sidebarNavItems} />
</aside> </aside>
<div className="flex-1"> <div className="flex-1">
<Suspense>
{/* @ts-ignore */} {/* @ts-ignore */}
<SettingsContent name={page?.params?.name ?? 'general'} /> <SettingsContent name={page?.params?.name ?? 'general'} />
</Suspense>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -101,5 +95,4 @@ function SettingsContent({ name }: { name: string }) {
case 'notifications': case 'notifications':
return <Notifications userSettings={userSettings} /> return <Notifications userSettings={userSettings} />
} }
return ''
} }

View File

@@ -11,6 +11,7 @@ import { InputTags } from '@/components/ui/input-tags'
import { UserSettings } from '@/types' import { UserSettings } from '@/types'
import { saveSettings } from './layout' import { saveSettings } from './layout'
import * as v from 'valibot' import * as v from 'valibot'
import { isAdmin } from '@/lib/utils'
interface ShoutrrrUrlCardProps { interface ShoutrrrUrlCardProps {
url: string url: string
@@ -41,7 +42,6 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
setIsLoading(true) setIsLoading(true)
try { try {
const parsedData = v.parse(NotificationSchema, { emails, webhooks }) const parsedData = v.parse(NotificationSchema, { emails, webhooks })
console.log('parsedData', parsedData)
await saveSettings(parsedData) await saveSettings(parsedData)
} catch (e: any) { } catch (e: any) {
toast({ toast({
@@ -57,12 +57,12 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
<div> <div>
<div> <div>
<h3 className="text-xl font-medium mb-2">Notifications</h3> <h3 className="text-xl font-medium mb-2">Notifications</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground leading-relaxed">
Configure how you receive alert notifications. Configure how you receive alert notifications.
</p> </p>
<p className="text-sm text-muted-foreground mt-1.5"> <p className="text-sm text-muted-foreground mt-1.5 leading-relaxed">
Looking for where to create system alerts? Click the bell icons{' '} Looking instead for where to create system alerts? Click the bell{' '}
<BellIcon className="inline h-4 w-4" /> in the dashboard table. <BellIcon className="inline h-4 w-4" /> icons in the systems table.
</p> </p>
</div> </div>
<Separator className="my-4" /> <Separator className="my-4" />
@@ -70,26 +70,36 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
<div className="space-y-2"> <div className="space-y-2">
<div className="mb-4"> <div className="mb-4">
<h3 className="mb-1 text-lg font-medium">Email notifications</h3> <h3 className="mb-1 text-lg font-medium">Email notifications</h3>
<p className="text-sm text-muted-foreground"> {isAdmin() && (
Leave blank to disable email notifications. <p className="text-sm text-muted-foreground leading-relaxed">
Please{' '}
<a href="/_/#/settings/mail" className="link" target="_blank">
configure an SMTP server
</a>{' '}
to ensure alerts are delivered.{' '}
</p> </p>
)}
</div> </div>
<Label className="block">To email(s)</Label> <Label className="block" htmlFor="email">
To email(s)
</Label>
<InputTags <InputTags
value={emails} value={emails}
onChange={setEmails} onChange={setEmails}
placeholder="Enter email address..." placeholder="Enter email address..."
className="w-full" className="w-full"
type="email"
id="email"
/> />
<p className="text-[0.8rem] text-muted-foreground"> <p className="text-[0.8rem] text-muted-foreground">
Save address using enter key or comma. Save address using enter key or comma. Leave blank to disable email notifications.
</p> </p>
</div> </div>
<Separator /> <Separator />
<div className="space-y-4"> <div className="space-y-3">
<div> <div>
<h3 className="mb-1 text-lg font-medium">Webhook / Push notifications</h3> <h3 className="mb-1 text-lg font-medium">Webhook / Push notifications</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground leading-relaxed">
Beszel uses{' '} Beszel uses{' '}
<a <a
href="https://containrrr.dev/shoutrrr/services/overview/" href="https://containrrr.dev/shoutrrr/services/overview/"
@@ -102,7 +112,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
</p> </p>
</div> </div>
{webhooks.length > 0 && ( {webhooks.length > 0 && (
<div className="grid gap-3"> <div className="grid gap-2.5">
{webhooks.map((webhook, index) => ( {webhooks.map((webhook, index) => (
<ShoutrrrUrlCard <ShoutrrrUrlCard
key={index} key={index}

View File

@@ -27,8 +27,8 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
<> <>
{/* Mobile View */} {/* Mobile View */}
<div className="md:hidden"> <div className="md:hidden">
<Select onValueChange={(value: string) => navigate(value)} defaultValue={page?.path}> <Select onValueChange={(value: string) => navigate(value)} value={page?.path}>
<SelectTrigger className="w-full mb-3"> <SelectTrigger className="w-full my-3.5">
<SelectValue placeholder="Select a page" /> <SelectValue placeholder="Select a page" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@@ -260,8 +260,8 @@ export default function SystemDetail({ name }: { name: string }) {
<Tooltip> <Tooltip>
<Separator orientation="vertical" className="h-4 bg-primary/30" /> <Separator orientation="vertical" className="h-4 bg-primary/30" />
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="flex gap-1.5"> <div className="flex gap-1.5 items-center">
<ClockArrowUp className="h-4 w-4 mt-[1px]" /> {uptime} <ClockArrowUp className="h-4 w-4" /> {uptime}
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Uptime</TooltipContent> <TooltipContent>Uptime</TooltipContent>
@@ -271,8 +271,8 @@ export default function SystemDetail({ name }: { name: string }) {
{system.info?.m && ( {system.info?.m && (
<> <>
<Separator orientation="vertical" className="h-4 bg-primary/30" /> <Separator orientation="vertical" className="h-4 bg-primary/30" />
<div className="flex gap-1.5"> <div className="flex gap-1.5 items-center">
<CpuIcon className="h-4 w-4 mt-[1px]" /> <CpuIcon className="h-4 w-4" />
{system.info.m} ({system.info.c}c / {system.info.t}t) {system.info.m} ({system.info.c}c / {system.info.t}t)
</div> </div>
</> </>

View File

@@ -324,7 +324,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
? 'auto' ? 'auto'
: cell.column.getSize(), : cell.column.getSize(),
}} }}
className={'overflow-hidden relative py-2.5'} className={cn('overflow-hidden relative', data.length > 10 ? 'py-2' : 'py-2.5')}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>

View File

@@ -15,6 +15,7 @@ import { Switch } from '@/components/ui/switch'
import { AlertRecord, SystemRecord } from '@/types' import { AlertRecord, SystemRecord } from '@/types'
import { lazy, Suspense, useMemo, useState } from 'react' import { lazy, Suspense, useMemo, useState } from 'react'
import { toast } from './ui/use-toast' import { toast } from './ui/use-toast'
import { Link } from './router'
const Slider = lazy(() => import('./ui/slider')) const Slider = lazy(() => import('./ui/slider'))
@@ -49,17 +50,13 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-full overflow-auto"> <DialogContent className="max-h-full overflow-auto">
<DialogHeader> <DialogHeader>
<DialogTitle className="mb-1">Alerts for {system.name}</DialogTitle> <DialogTitle className="text-xl">{system.name} alerts</DialogTitle>
<DialogDescription> <DialogDescription className="mb-1">
{isAdmin() && ( See{' '}
<span> <Link href="/settings/notifications" className="link">
Please{' '} notification settings
<a href="/_/#/settings/mail" className="link"> </Link>{' '}
configure an SMTP server to configure how you receive alerts.
</a>{' '}
to ensure alerts are delivered.{' '}
</span>
)}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-3"> <div className="grid gap-3">
@@ -83,7 +80,7 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
alerts={systemAlerts} alerts={systemAlerts}
name="Disk" name="Disk"
title="Disk Usage" title="Disk Usage"
description="Triggers when disk usage exceeds a threshold." description="Triggers when root usage exceeds a threshold."
/> />
</div> </div>
</DialogContent> </DialogContent>