mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 09:49:28 +08:00
updates to settings page and alerts
This commit is contained in:
@@ -29,7 +29,7 @@ type AlertData struct {
|
||||
LinkText string
|
||||
}
|
||||
|
||||
type UserAlertSettings struct {
|
||||
type UserNotificationSettings struct {
|
||||
Emails []string `json:"emails"`
|
||||
Webhooks []string `json:"webhooks"`
|
||||
}
|
||||
@@ -166,16 +166,16 @@ func (am *AlertManager) sendAlert(data AlertData) {
|
||||
dbx.Params{"user": data.UserID},
|
||||
)
|
||||
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
|
||||
}
|
||||
// unmarshal user settings
|
||||
userAlertSettings := UserAlertSettings{
|
||||
userAlertSettings := UserNotificationSettings{
|
||||
Emails: []string{},
|
||||
Webhooks: []string{},
|
||||
}
|
||||
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
|
||||
for _, webhook := range userAlertSettings.Webhooks {
|
||||
@@ -186,13 +186,12 @@ func (am *AlertManager) sendAlert(data AlertData) {
|
||||
}
|
||||
// send alerts via email
|
||||
if len(userAlertSettings.Emails) == 0 {
|
||||
log.Println("No email addresses found")
|
||||
// log.Println("No email addresses found")
|
||||
return
|
||||
}
|
||||
addresses := []mail.Address{}
|
||||
for _, email := range userAlertSettings.Emails {
|
||||
addresses = append(addresses, mail.Address{Address: email})
|
||||
log.Println("Sending alert via email to", email)
|
||||
}
|
||||
message := mailer.Message{
|
||||
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 {
|
||||
// services that support title param
|
||||
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 {
|
||||
am.app.Logger().Info("Sent shoutrrr alert", "title", title)
|
||||
} 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 nil
|
||||
|
@@ -13,6 +13,7 @@ import { LoaderCircleIcon, SaveIcon } from 'lucide-react'
|
||||
import { UserSettings } from '@/types'
|
||||
import { saveSettings } from './layout'
|
||||
import { useState } from 'react'
|
||||
// import { Input } from '@/components/ui/input'
|
||||
|
||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -30,17 +31,22 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">General</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Set your preferred language and chart display options.
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
Change general application options.
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* <Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">Language</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Additional language support coming soon.
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
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>
|
||||
</div>
|
||||
<Label className="block" htmlFor="lang">
|
||||
@@ -54,12 +60,13 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Separator />
|
||||
</div> */}
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<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>
|
||||
<Label className="block" htmlFor="chartTime">
|
||||
Default time period
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Suspense, lazy, useEffect } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { Separator } from '../../ui/separator'
|
||||
import { SidebarNav } from './sidebar-nav.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 { toast } from '@/components/ui/use-toast.ts'
|
||||
import { UserSettings } from '@/types.js'
|
||||
|
||||
const General = lazy(() => import('./general.tsx'))
|
||||
const Notifications = lazy(() => import('./notifications.tsx'))
|
||||
import General from './general.tsx'
|
||||
import Notifications from './notifications.tsx'
|
||||
|
||||
const sidebarNavItems = [
|
||||
{
|
||||
@@ -27,31 +26,28 @@ const sidebarNavItems = [
|
||||
]
|
||||
|
||||
export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
// console.log('Updating settings:', newSettings)
|
||||
try {
|
||||
// get fresh copy of settings
|
||||
const req = await pb.collection('user_settings').getFirstListItem('', {
|
||||
fields: 'id,settings',
|
||||
})
|
||||
// make new user settings
|
||||
const mergedSettings = {
|
||||
...req.settings,
|
||||
...newSettings,
|
||||
}
|
||||
// update user settings
|
||||
const updatedSettings = await pb.collection('user_settings').update(req.id, {
|
||||
settings: mergedSettings,
|
||||
settings: {
|
||||
...req.settings,
|
||||
...newSettings,
|
||||
},
|
||||
})
|
||||
$userSettings.set(updatedSettings.settings)
|
||||
toast({
|
||||
title: 'Settings saved',
|
||||
description: 'Your notification settings have been updated.',
|
||||
description: 'Your user settings have been updated.',
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('update settings', e)
|
||||
// console.error('update settings', e)
|
||||
toast({
|
||||
title: 'Failed to save settings',
|
||||
description: 'Please check logs for more details.',
|
||||
description: 'Check logs for more details.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -72,19 +68,17 @@ export default function SettingsLayout() {
|
||||
<Card className="pt-5 px-4 pb-8 sm:pt-6 sm:px-7">
|
||||
<CardHeader className="p-0">
|
||||
<CardTitle className="mb-1">Settings</CardTitle>
|
||||
<CardDescription>Manage notification and display preferences.</CardDescription>
|
||||
<CardDescription>Manage display and notification preferences.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Separator className="my-3 md:my-5" />
|
||||
<div className="flex flex-col gap-3 md:flex-row md:gap-5 lg:gap-10">
|
||||
<Separator className="hidden md:block my-5" />
|
||||
<div className="flex flex-col gap-3.5 md:flex-row md:gap-5 lg:gap-10">
|
||||
<aside className="md:w-48 w-full">
|
||||
<SidebarNav items={sidebarNavItems} />
|
||||
</aside>
|
||||
<div className="flex-1">
|
||||
<Suspense>
|
||||
{/* @ts-ignore */}
|
||||
<SettingsContent name={page?.params?.name ?? 'general'} />
|
||||
</Suspense>
|
||||
{/* @ts-ignore */}
|
||||
<SettingsContent name={page?.params?.name ?? 'general'} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -101,5 +95,4 @@ function SettingsContent({ name }: { name: string }) {
|
||||
case 'notifications':
|
||||
return <Notifications userSettings={userSettings} />
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ import { InputTags } from '@/components/ui/input-tags'
|
||||
import { UserSettings } from '@/types'
|
||||
import { saveSettings } from './layout'
|
||||
import * as v from 'valibot'
|
||||
import { isAdmin } from '@/lib/utils'
|
||||
|
||||
interface ShoutrrrUrlCardProps {
|
||||
url: string
|
||||
@@ -41,7 +42,6 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const parsedData = v.parse(NotificationSchema, { emails, webhooks })
|
||||
console.log('parsedData', parsedData)
|
||||
await saveSettings(parsedData)
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
@@ -57,12 +57,12 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
<div>
|
||||
<div>
|
||||
<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.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1.5">
|
||||
Looking for where to create system alerts? Click the bell icons{' '}
|
||||
<BellIcon className="inline h-4 w-4" /> in the dashboard table.
|
||||
<p className="text-sm text-muted-foreground mt-1.5 leading-relaxed">
|
||||
Looking instead for where to create system alerts? Click the bell{' '}
|
||||
<BellIcon className="inline h-4 w-4" /> icons in the systems table.
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
@@ -70,26 +70,36 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">Email notifications</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Leave blank to disable email notifications.
|
||||
</p>
|
||||
{isAdmin() && (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
<Label className="block">To email(s)</Label>
|
||||
<Label className="block" htmlFor="email">
|
||||
To email(s)
|
||||
</Label>
|
||||
<InputTags
|
||||
value={emails}
|
||||
onChange={setEmails}
|
||||
placeholder="Enter email address..."
|
||||
className="w-full"
|
||||
type="email"
|
||||
id="email"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<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{' '}
|
||||
<a
|
||||
href="https://containrrr.dev/shoutrrr/services/overview/"
|
||||
@@ -102,7 +112,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
</p>
|
||||
</div>
|
||||
{webhooks.length > 0 && (
|
||||
<div className="grid gap-3">
|
||||
<div className="grid gap-2.5">
|
||||
{webhooks.map((webhook, index) => (
|
||||
<ShoutrrrUrlCard
|
||||
key={index}
|
||||
|
@@ -27,8 +27,8 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||
<>
|
||||
{/* Mobile View */}
|
||||
<div className="md:hidden">
|
||||
<Select onValueChange={(value: string) => navigate(value)} defaultValue={page?.path}>
|
||||
<SelectTrigger className="w-full mb-3">
|
||||
<Select onValueChange={(value: string) => navigate(value)} value={page?.path}>
|
||||
<SelectTrigger className="w-full my-3.5">
|
||||
<SelectValue placeholder="Select a page" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
@@ -260,8 +260,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<Tooltip>
|
||||
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex gap-1.5">
|
||||
<ClockArrowUp className="h-4 w-4 mt-[1px]" /> {uptime}
|
||||
<div className="flex gap-1.5 items-center">
|
||||
<ClockArrowUp className="h-4 w-4" /> {uptime}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Uptime</TooltipContent>
|
||||
@@ -271,8 +271,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
{system.info?.m && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
||||
<div className="flex gap-1.5">
|
||||
<CpuIcon className="h-4 w-4 mt-[1px]" />
|
||||
<div className="flex gap-1.5 items-center">
|
||||
<CpuIcon className="h-4 w-4" />
|
||||
{system.info.m} ({system.info.c}c / {system.info.t}t)
|
||||
</div>
|
||||
</>
|
||||
|
@@ -324,7 +324,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
||||
? 'auto'
|
||||
: 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())}
|
||||
</TableCell>
|
||||
|
@@ -15,6 +15,7 @@ import { Switch } from '@/components/ui/switch'
|
||||
import { AlertRecord, SystemRecord } from '@/types'
|
||||
import { lazy, Suspense, useMemo, useState } from 'react'
|
||||
import { toast } from './ui/use-toast'
|
||||
import { Link } from './router'
|
||||
|
||||
const Slider = lazy(() => import('./ui/slider'))
|
||||
|
||||
@@ -49,17 +50,13 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-full overflow-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-1">Alerts for {system.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isAdmin() && (
|
||||
<span>
|
||||
Please{' '}
|
||||
<a href="/_/#/settings/mail" className="link">
|
||||
configure an SMTP server
|
||||
</a>{' '}
|
||||
to ensure alerts are delivered.{' '}
|
||||
</span>
|
||||
)}
|
||||
<DialogTitle className="text-xl">{system.name} alerts</DialogTitle>
|
||||
<DialogDescription className="mb-1">
|
||||
See{' '}
|
||||
<Link href="/settings/notifications" className="link">
|
||||
notification settings
|
||||
</Link>{' '}
|
||||
to configure how you receive alerts.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-3">
|
||||
@@ -83,7 +80,7 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
|
||||
alerts={systemAlerts}
|
||||
name="Disk"
|
||||
title="Disk Usage"
|
||||
description="Triggers when disk usage exceeds a threshold."
|
||||
description="Triggers when root usage exceeds a threshold."
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
Reference in New Issue
Block a user