alert / settings page updates

This commit is contained in:
Henry Dollman
2024-09-11 17:47:36 -04:00
parent ce6e887d1b
commit 2889d151ea
4 changed files with 149 additions and 76 deletions

View File

@@ -5,7 +5,6 @@ import (
"beszel/internal/entities/system" "beszel/internal/entities/system"
"fmt" "fmt"
"log" "log"
"net/http"
"net/mail" "net/mail"
"net/url" "net/url"
"os" "os"
@@ -254,13 +253,13 @@ func (am *AlertManager) SendTestNotification(c echo.Context) error {
return apis.NewForbiddenError("Forbidden", nil) return apis.NewForbiddenError("Forbidden", nil)
} }
url := c.QueryParam("url") url := c.QueryParam("url")
log.Println("url", url) // log.Println("url", url)
if url == "" { if url == "" {
return c.JSON(http.StatusOK, map[string]string{"err": "URL is required"}) return c.JSON(200, map[string]string{"err": "URL is required"})
} }
err := am.SendShoutrrrAlert(url, "Test Alert", "This is a notification from Beszel.", am.app.Settings().Meta.AppUrl, "View Beszel") err := am.SendShoutrrrAlert(url, "Test Alert", "This is a notification from Beszel.", am.app.Settings().Meta.AppUrl, "View Beszel")
if err != nil { if err != nil {
return c.JSON(http.StatusOK, map[string]string{"err": err.Error()}) return c.JSON(200, map[string]string{"err": err.Error()})
} }
return c.JSON(http.StatusOK, map[string]bool{"err": false}) return c.JSON(200, map[string]bool{"err": false})
} }

View File

@@ -41,9 +41,9 @@ export default function SettingsLayout() {
<CardDescription>Manage notification and display preferences.</CardDescription> <CardDescription>Manage notification and display preferences.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
<Separator className="my-5" /> <Separator className="my-3 md:my-5" />
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> <div className="flex flex-col gap-3 md:flex-row md:gap-5 lg:gap-10">
<aside className="lg:w-48 w-full overflow-auto"> <aside className="md:w-48 w-full">
<SidebarNav items={sidebarNavItems} /> <SidebarNav items={sidebarNavItems} />
</aside> </aside>
<div className="flex-1"> <div className="flex-1">

View File

@@ -4,12 +4,46 @@ import { Label } from '@/components/ui/label'
import { pb } from '@/lib/stores' import { pb } from '@/lib/stores'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Card } from '@/components/ui/card' import { Card } from '@/components/ui/card'
// import { Switch } from '@/components/ui/switch'
import { LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from 'lucide-react' import { LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { toast } from '@/components/ui/use-toast' import { toast } from '@/components/ui/use-toast'
export default function SettingsNotificationsPage() { interface UserSettings {
webhooks: string[]
}
interface ShoutrrrUrlCardProps {
url: string
onUrlChange: (value: string) => void
onRemove: () => void
}
const userSettings: UserSettings = {
webhooks: ['generic://webhook.site/xxx'],
}
const SettingsNotificationsPage = () => {
const [email, setEmail] = useState(pb.authStore.model?.email || '')
const [webhooks, setWebhooks] = useState(userSettings.webhooks ?? [])
const addWebhook = () => setWebhooks([...webhooks, ''])
const removeWebhook = (index: number) => setWebhooks(webhooks.filter((_, i) => i !== index))
const updateWebhook = (index: number, value: string) => {
const newWebhooks = [...webhooks]
newWebhooks[index] = value
setWebhooks(newWebhooks)
}
const saveSettings = async () => {
// TODO: Implement actual saving logic
console.log('Saving settings:', { email, webhooks })
toast({
title: 'Settings saved',
description: 'Your notification settings have been updated.',
})
}
return ( return (
<div> <div>
{/* <div> {/* <div>
@@ -22,11 +56,15 @@ export default function SettingsNotificationsPage() {
<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"> <p className="text-sm text-muted-foreground">
Leave the emails field to disable email notifications. Leave blank to disable email notifications.
</p> </p>
</div> </div>
<Label className="block">To email(s)</Label> <Label className="block">To email(s)</Label>
<Input placeholder="name@example.com" defaultValue={pb.authStore.model?.email} /> <Input
placeholder="name@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<p className="text-[0.8rem] text-muted-foreground"> <p className="text-[0.8rem] text-muted-foreground">
Separate multiple emails with commas. Separate multiple emails with commas.
</p> </p>
@@ -47,20 +85,31 @@ export default function SettingsNotificationsPage() {
to integrate with popular notification services. to integrate with popular notification services.
</p> </p>
</div> </div>
<ShoutrrrUrlCard /> {webhooks.length > 0 && (
<div className="grid gap-3">
{webhooks.map((webhook, index) => (
<ShoutrrrUrlCard
key={index}
url={webhook}
onUrlChange={(value: string) => updateWebhook(index, value)}
onRemove={() => removeWebhook(index)}
/>
))}
</div>
)}
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
className="mt-2 flex items-center gap-1" className="mt-2 flex items-center gap-1"
// onClick={() => append({ value: '' })} onClick={addWebhook}
> >
<PlusIcon className="h-4 w-4 -ml-0.5" /> <PlusIcon className="h-4 w-4 -ml-0.5" />
Add URL Add URL
</Button> </Button>
</div> </div>
<Separator /> <Separator />
<Button type="submit" className="flex items-center gap-1.5"> <Button type="button" className="flex items-center gap-1.5" onClick={saveSettings}>
<SaveIcon className="h-4 w-4" /> <SaveIcon className="h-4 w-4" />
Save settings Save settings
</Button> </Button>
@@ -69,68 +118,65 @@ export default function SettingsNotificationsPage() {
) )
} }
async function sendTestNotification(url: string) { const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) => {
const res = await pb.send('/api/beszel/send-test-notification', { url })
if ('err' in res && !res.err) {
toast({
title: 'Test notification sent',
description: 'Check your notification service',
})
} else {
toast({
title: 'Error',
description: res.err ?? 'Failed to send test notification',
variant: 'destructive',
})
}
}
// todo unique ids
function ShoutrrrUrlCard() {
const [url, setUrl] = useState('')
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const sendTestNotification = async () => {
setIsLoading(true)
const res = await pb.send('/api/beszel/send-test-notification', { url })
if ('err' in res && !res.err) {
toast({
title: 'Test notification sent',
description: 'Check your notification service',
})
} else {
toast({
title: 'Error',
description: res.err ?? 'Failed to send test notification',
variant: 'destructive',
})
}
setIsLoading(false)
}
return ( return (
<Card className="bg-muted/30 p-3.5"> <Card className="bg-muted/30 p-2 md:p-3">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Label htmlFor="name" className="sr-only">
URL
</Label>
<Input <Input
id="name"
name="name"
className="light:bg-card" className="light:bg-card"
required required
placeholder="generic://webhook.site/xxxxxx" placeholder="generic://webhook.site/xxxxxx"
onChange={(e) => setUrl(e.target.value)} value={url}
onChange={(e) => onUrlChange(e.target.value)}
/> />
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
className="w-28" className="w-20 md:w-28"
disabled={isLoading || url === ''} disabled={isLoading || url === ''}
onClick={async () => { onClick={sendTestNotification}
setIsLoading(true)
await sendTestNotification(url)
setIsLoading(false)
}}
> >
{isLoading ? <LoaderCircleIcon className="sh-4 w-4 animate-spin" /> : 'Test URL'} {isLoading ? (
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
) : (
<span>
Test <span className="hidden md:inline">URL</span>
</span>
)}
</Button> </Button>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="icon" size="icon"
className="shrink-0" className="shrink-0"
// onClick={() => append({ value: '' })} aria-label="Delete"
onClick={onRemove}
> >
<Trash2Icon className="sh-4 w-4" /> <Trash2Icon className="h-4 w-4" />
</Button> </Button>
{/* <Label htmlFor="enabled-01" className="sr-only">
Enabled
</Label>
<Switch defaultChecked id="enabled-01" className="ml-2" /> */}
</div> </div>
</Card> </Card>
) )
} }
export default SettingsNotificationsPage

View File

@@ -1,8 +1,16 @@
import React from 'react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { buttonVariants } from '../../ui/button' import { buttonVariants } from '../../ui/button'
import { $router, Link } from '../../router' import { $router, Link, navigate } from '../../router'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import React from 'react' import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> { interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
items: { items: {
@@ -16,25 +24,45 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
const page = useStore($router) const page = useStore($router)
return ( return (
<nav <>
className={cn('flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1', className)} {/* Mobile View */}
{...props} <div className="md:hidden">
> <Select onValueChange={(value: string) => navigate(value)} defaultValue={page?.path}>
{items.map((item) => ( <SelectTrigger className="w-full mb-3">
<Link <SelectValue placeholder="Select a page" />
key={item.href} </SelectTrigger>
href={item.href} <SelectContent>
className={cn( {items.map((item) => (
buttonVariants({ variant: 'ghost' }), <SelectItem key={item.href} value={item.href}>
'flex items-center gap-3', <span className="flex items-center gap-2">
page?.path === item.href ? 'bg-muted hover:bg-muted' : 'hover:bg-muted/50', {item.icon && <item.icon className="h-4 w-4" />}
'justify-start' {item.title}
)} </span>
> </SelectItem>
{item.icon && <item.icon className="h-4 w-4" />} ))}
{item.title} </SelectContent>
</Link> </Select>
))} <Separator />
</nav> </div>
{/* Desktop View */}
<nav className={cn('hidden md:grid gap-1', className)} {...props}>
{items.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
buttonVariants({ variant: 'ghost' }),
'flex items-center gap-3',
page?.path === item.href ? 'bg-muted hover:bg-muted' : 'hover:bg-muted/50',
'justify-start'
)}
>
{item.icon && <item.icon className="h-4 w-4" />}
{item.title}
</Link>
))}
</nav>
</>
) )
} }