mirror of
https://github.com/fankes/beszel.git
synced 2025-10-20 02:09:28 +08:00
alerts for cpu, memory, and disk
This commit is contained in:
142
hub/alerts.go
Normal file
142
hub/alerts.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/mail"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleSystemAlerts(newStatus string, newRecord *models.Record, oldRecord *models.Record) {
|
||||||
|
alertRecords, err := app.Dao().FindRecordsByExpr("alerts",
|
||||||
|
dbx.NewExp("system = {:system}", dbx.Params{"system": oldRecord.Get("id")}),
|
||||||
|
)
|
||||||
|
if err != nil || len(alertRecords) == 0 {
|
||||||
|
// log.Println("no alerts found for system")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// log.Println("found alerts", len(alertRecords))
|
||||||
|
var systemInfo *SystemInfo
|
||||||
|
for _, alertRecord := range alertRecords {
|
||||||
|
name := alertRecord.Get("name").(string)
|
||||||
|
switch name {
|
||||||
|
case "Status":
|
||||||
|
handleStatusAlerts(newStatus, oldRecord, alertRecord)
|
||||||
|
case "CPU", "Memory", "Disk":
|
||||||
|
if newStatus != "up" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if systemInfo == nil {
|
||||||
|
systemInfo = getSystemInfo(newRecord)
|
||||||
|
}
|
||||||
|
if name == "CPU" {
|
||||||
|
handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.Cpu)
|
||||||
|
} else if name == "Memory" {
|
||||||
|
handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.MemPct)
|
||||||
|
} else if name == "Disk" {
|
||||||
|
handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.DiskPct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSystemInfo(record *models.Record) *SystemInfo {
|
||||||
|
var SystemInfo SystemInfo
|
||||||
|
json.Unmarshal([]byte(record.Get("info").(types.JsonRaw)), &SystemInfo)
|
||||||
|
return &SystemInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSlidingValueAlert(newRecord *models.Record, alertRecord *models.Record, name string, curValue float64) {
|
||||||
|
triggered := alertRecord.Get("triggered").(bool)
|
||||||
|
threshold := alertRecord.Get("value").(float64)
|
||||||
|
// fmt.Println(name, curValue, "threshold", threshold, "triggered", triggered)
|
||||||
|
var subject string
|
||||||
|
var body string
|
||||||
|
if !triggered && curValue > threshold {
|
||||||
|
alertRecord.Set("triggered", true)
|
||||||
|
systemName := newRecord.Get("name").(string)
|
||||||
|
subject = fmt.Sprintf("%s usage threshold exceeded on %s", name, systemName)
|
||||||
|
body = fmt.Sprintf("%s usage on %s is %.1f%%.\n\n- Beszel", name, systemName, curValue)
|
||||||
|
} else if triggered && curValue <= threshold {
|
||||||
|
alertRecord.Set("triggered", false)
|
||||||
|
systemName := newRecord.Get("name").(string)
|
||||||
|
subject = fmt.Sprintf("%s usage returned below threshold on %s", name, systemName)
|
||||||
|
body = fmt.Sprintf("%s usage on %s is below threshold at %.1f%%.\n\n%s\n\n- Beszel", name, systemName, curValue, app.Settings().Meta.AppUrl+"/system/"+systemName)
|
||||||
|
} else {
|
||||||
|
// fmt.Println(name, "not triggered")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := app.Dao().SaveRecord(alertRecord); err != nil {
|
||||||
|
// app.Logger().Error("failed to save alert record", "err", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// expand the user relation and send the alert
|
||||||
|
if errs := app.Dao().ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
|
||||||
|
// app.Logger().Error("failed to expand user relation", "errs", errs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user := alertRecord.ExpandedOne("user"); user != nil {
|
||||||
|
sendAlert(EmailData{
|
||||||
|
to: user.Get("email").(string),
|
||||||
|
subj: subject,
|
||||||
|
body: body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStatusAlerts(newStatus string, oldRecord *models.Record, alertRecord *models.Record) error {
|
||||||
|
var alertStatus string
|
||||||
|
switch newStatus {
|
||||||
|
case "up":
|
||||||
|
if oldRecord.Get("status") == "down" {
|
||||||
|
alertStatus = "up"
|
||||||
|
}
|
||||||
|
case "down":
|
||||||
|
if oldRecord.Get("status") == "up" {
|
||||||
|
alertStatus = "down"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if alertStatus == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// expand the user relation
|
||||||
|
if errs := app.Dao().ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
|
||||||
|
return fmt.Errorf("failed to expand: %v", errs)
|
||||||
|
}
|
||||||
|
user := alertRecord.ExpandedOne("user")
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
emoji := "\U0001F534"
|
||||||
|
if alertStatus == "up" {
|
||||||
|
emoji = "\u2705"
|
||||||
|
}
|
||||||
|
// send alert
|
||||||
|
systemName := oldRecord.Get("name").(string)
|
||||||
|
sendAlert(EmailData{
|
||||||
|
to: user.Get("email").(string),
|
||||||
|
subj: fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji),
|
||||||
|
body: fmt.Sprintf("Connection to %s is %s\n\n- Beszel", systemName, alertStatus),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendAlert(data EmailData) {
|
||||||
|
// fmt.Println("sending alert", "to", data.to, "subj", data.subj, "body", data.body)
|
||||||
|
message := &mailer.Message{
|
||||||
|
From: mail.Address{
|
||||||
|
Address: app.Settings().Meta.SenderAddress,
|
||||||
|
Name: app.Settings().Meta.SenderName,
|
||||||
|
},
|
||||||
|
To: []mail.Address{{Address: data.to}},
|
||||||
|
Subject: data.subj,
|
||||||
|
Text: data.body,
|
||||||
|
}
|
||||||
|
if err := app.NewMailClient().Send(message); err != nil {
|
||||||
|
app.Logger().Error("Failed to send alert: ", "err", err.Error())
|
||||||
|
}
|
||||||
|
}
|
67
hub/main.go
67
hub/main.go
@@ -12,7 +12,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/mail"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -26,7 +25,6 @@ import (
|
|||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||||
"github.com/pocketbase/pocketbase/tools/cron"
|
"github.com/pocketbase/pocketbase/tools/cron"
|
||||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
@@ -175,7 +173,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// alerts
|
// alerts
|
||||||
handleStatusAlerts(newStatus, oldRecord)
|
handleSystemAlerts(newStatus, newRecord, oldRecord)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -378,69 +376,6 @@ func requestJson(server *Server) (SystemData, error) {
|
|||||||
return systemData, nil
|
return systemData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendAlert(data EmailData) {
|
|
||||||
message := &mailer.Message{
|
|
||||||
From: mail.Address{
|
|
||||||
Address: app.Settings().Meta.SenderAddress,
|
|
||||||
Name: app.Settings().Meta.SenderName,
|
|
||||||
},
|
|
||||||
To: []mail.Address{{Address: data.to}},
|
|
||||||
Subject: data.subj,
|
|
||||||
Text: data.body,
|
|
||||||
}
|
|
||||||
if err := app.NewMailClient().Send(message); err != nil {
|
|
||||||
app.Logger().Error("Failed to send alert: ", "err", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleStatusAlerts(newStatus string, oldRecord *models.Record) error {
|
|
||||||
var alertStatus string
|
|
||||||
switch newStatus {
|
|
||||||
case "up":
|
|
||||||
if oldRecord.Get("status") == "down" {
|
|
||||||
alertStatus = "up"
|
|
||||||
}
|
|
||||||
case "down":
|
|
||||||
if oldRecord.Get("status") == "up" {
|
|
||||||
alertStatus = "down"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if alertStatus == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
alerts, err := app.Dao().FindRecordsByFilter("alerts", "name = 'status' && system = {:system}", "-created", -1, 0, dbx.Params{
|
|
||||||
"system": oldRecord.Get("id")})
|
|
||||||
if err != nil {
|
|
||||||
log.Println("failed to get users", "err", err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(alerts) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// expand the user relation
|
|
||||||
if errs := app.Dao().ExpandRecords(alerts, []string{"user"}, nil); len(errs) > 0 {
|
|
||||||
return fmt.Errorf("failed to expand: %v", errs)
|
|
||||||
}
|
|
||||||
systemName := oldRecord.Get("name").(string)
|
|
||||||
emoji := "\U0001F534"
|
|
||||||
if alertStatus == "up" {
|
|
||||||
emoji = "\u2705"
|
|
||||||
}
|
|
||||||
for _, alert := range alerts {
|
|
||||||
user := alert.ExpandedOne("user")
|
|
||||||
if user == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// send alert
|
|
||||||
sendAlert(EmailData{
|
|
||||||
to: user.Get("email").(string),
|
|
||||||
subj: fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji),
|
|
||||||
body: fmt.Sprintf("Connection to %s is %s\n\n- Beszel", systemName, alertStatus),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSSHKey() ([]byte, error) {
|
func getSSHKey() ([]byte, error) {
|
||||||
dataDir := app.DataDir()
|
dataDir := app.DataDir()
|
||||||
// check if the key pair already exists
|
// check if the key pair already exists
|
||||||
|
@@ -15,7 +15,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
"id": "2hz5ncl8tizk5nx",
|
"id": "2hz5ncl8tizk5nx",
|
||||||
"created": "2024-07-07 16:08:20.979Z",
|
"created": "2024-07-07 16:08:20.979Z",
|
||||||
"updated": "2024-07-17 15:27:00.429Z",
|
"updated": "2024-07-22 19:39:17.434Z",
|
||||||
"name": "systems",
|
"name": "systems",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -102,7 +102,7 @@ func init() {
|
|||||||
"unique": false,
|
"unique": false,
|
||||||
"options": {
|
"options": {
|
||||||
"collectionId": "_pb_users_auth_",
|
"collectionId": "_pb_users_auth_",
|
||||||
"cascadeDelete": false,
|
"cascadeDelete": true,
|
||||||
"minSelect": null,
|
"minSelect": null,
|
||||||
"maxSelect": null,
|
"maxSelect": null,
|
||||||
"displayFields": null
|
"displayFields": null
|
||||||
@@ -250,7 +250,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
"id": "_pb_users_auth_",
|
"id": "_pb_users_auth_",
|
||||||
"created": "2024-07-14 16:25:18.226Z",
|
"created": "2024-07-14 16:25:18.226Z",
|
||||||
"updated": "2024-07-20 00:55:02.071Z",
|
"updated": "2024-07-22 20:10:20.670Z",
|
||||||
"name": "users",
|
"name": "users",
|
||||||
"type": "auth",
|
"type": "auth",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -304,7 +304,7 @@ func init() {
|
|||||||
"options": {
|
"options": {
|
||||||
"allowEmailAuth": true,
|
"allowEmailAuth": true,
|
||||||
"allowOAuth2Auth": true,
|
"allowOAuth2Auth": true,
|
||||||
"allowUsernameAuth": true,
|
"allowUsernameAuth": false,
|
||||||
"exceptEmailDomains": null,
|
"exceptEmailDomains": null,
|
||||||
"manageRule": null,
|
"manageRule": null,
|
||||||
"minPasswordLength": 8,
|
"minPasswordLength": 8,
|
||||||
@@ -316,7 +316,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
"id": "elngm8x1l60zi2v",
|
"id": "elngm8x1l60zi2v",
|
||||||
"created": "2024-07-15 01:16:04.044Z",
|
"created": "2024-07-15 01:16:04.044Z",
|
||||||
"updated": "2024-07-15 22:44:12.297Z",
|
"updated": "2024-07-22 19:13:16.498Z",
|
||||||
"name": "alerts",
|
"name": "alerts",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -364,16 +364,43 @@ func init() {
|
|||||||
"options": {
|
"options": {
|
||||||
"maxSelect": 1,
|
"maxSelect": 1,
|
||||||
"values": [
|
"values": [
|
||||||
"status"
|
"Status",
|
||||||
|
"CPU",
|
||||||
|
"Memory",
|
||||||
|
"Disk"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "o2ablxvn",
|
||||||
|
"name": "value",
|
||||||
|
"type": "number",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"noDecimal": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "6hgdf6hs",
|
||||||
|
"name": "triggered",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||||
"viewRule": "",
|
"viewRule": "",
|
||||||
"createRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
"createRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||||
"updateRule": null,
|
"updateRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||||
"deleteRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
"deleteRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||||
"options": {}
|
"options": {}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
@@ -17,6 +17,7 @@
|
|||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
|
"@radix-ui/react-slider": "^1.2.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.1.0",
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
|
@@ -13,9 +13,18 @@ import { cn, isAdmin } from '@/lib/utils'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { AlertRecord, SystemRecord } from '@/types'
|
import { AlertRecord, SystemRecord } from '@/types'
|
||||||
import { useMemo, useState } from 'react'
|
import { lazy, Suspense, useMemo, useState } from 'react'
|
||||||
import { toast } from './ui/use-toast'
|
import { toast } from './ui/use-toast'
|
||||||
|
|
||||||
|
const Slider = lazy(() => import('./ui/slider'))
|
||||||
|
|
||||||
|
const failedUpdateToast = () =>
|
||||||
|
toast({
|
||||||
|
title: 'Failed to update alert',
|
||||||
|
description: 'Please check logs for more details.',
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
|
||||||
export default function AlertsButton({ system }: { system: SystemRecord }) {
|
export default function AlertsButton({ system }: { system: SystemRecord }) {
|
||||||
const alerts = useStore($alerts)
|
const alerts = useStore($alerts)
|
||||||
|
|
||||||
@@ -38,7 +47,7 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
|
|||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent className="max-h-full overflow-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="mb-1">Alerts for {system.name}</DialogTitle>
|
<DialogTitle className="mb-1">Alerts for {system.name}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -54,38 +63,57 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
|
|||||||
to ensure alerts are delivered.{' '}
|
to ensure alerts are delivered.{' '}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
Webhook delivery and more alert options will be added in the future.
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Alert system={system} alerts={systemAlerts} />
|
<div className="grid gap-3">
|
||||||
|
<AlertStatus system={system} alerts={systemAlerts} />
|
||||||
|
<AlertWithSlider
|
||||||
|
system={system}
|
||||||
|
alerts={systemAlerts}
|
||||||
|
name="CPU"
|
||||||
|
title="CPU usage"
|
||||||
|
description="Triggers when CPU usage exceeds a threshold."
|
||||||
|
/>
|
||||||
|
<AlertWithSlider
|
||||||
|
system={system}
|
||||||
|
alerts={systemAlerts}
|
||||||
|
name="Memory"
|
||||||
|
title="Memory usage"
|
||||||
|
description="Triggers when memory usage exceeds a threshold."
|
||||||
|
/>
|
||||||
|
<AlertWithSlider
|
||||||
|
system={system}
|
||||||
|
alerts={systemAlerts}
|
||||||
|
name="Disk"
|
||||||
|
title="Disk usage"
|
||||||
|
description="Triggers when disk usage exceeds a threshold."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Alert({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[] }) {
|
function AlertStatus({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[] }) {
|
||||||
const [pendingChange, setPendingChange] = useState(false)
|
const [pendingChange, setPendingChange] = useState(false)
|
||||||
|
|
||||||
const alert = useMemo(() => {
|
const alert = useMemo(() => {
|
||||||
return alerts.find((alert) => alert.name === 'status')
|
return alerts.find((alert) => alert.name === 'Status')
|
||||||
}, [alerts])
|
}, [alerts])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
htmlFor="status"
|
htmlFor="alert-status"
|
||||||
className="space-y-2 flex flex-row items-center justify-between rounded-lg border p-4 cursor-pointer"
|
className="space-y-2 flex flex-row items-center justify-between rounded-lg border p-4 cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className="grid gap-0.5 select-none">
|
<div className="grid gap-0.5 select-none">
|
||||||
<p className="font-medium text-base">System status</p>
|
<p className="font-medium text-[1.05em]">System status</p>
|
||||||
<span
|
<span className="block text-[0.85em] text-foreground opacity-80">
|
||||||
id=":r3m:-form-item-description"
|
|
||||||
className="block text-[0.8rem] text-foreground opacity-80"
|
|
||||||
>
|
|
||||||
Triggers when status switches between up and down.
|
Triggers when status switches between up and down.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
id="status"
|
id="alert-status"
|
||||||
className={cn('transition-opacity', pendingChange && 'opacity-40')}
|
className={cn('transition-opacity', pendingChange && 'opacity-40')}
|
||||||
checked={!!alert}
|
checked={!!alert}
|
||||||
value={!!alert ? 'on' : 'off'}
|
value={!!alert ? 'on' : 'off'}
|
||||||
@@ -101,15 +129,11 @@ function Alert({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[]
|
|||||||
pb.collection('alerts').create({
|
pb.collection('alerts').create({
|
||||||
system: system.id,
|
system: system.id,
|
||||||
user: pb.authStore.model!.id,
|
user: pb.authStore.model!.id,
|
||||||
name: 'status',
|
name: 'Status',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
failedUpdateToast()
|
||||||
title: 'Failed to update alert',
|
|
||||||
description: 'Please check logs for more details.',
|
|
||||||
variant: 'destructive',
|
|
||||||
})
|
|
||||||
} finally {
|
} finally {
|
||||||
setPendingChange(false)
|
setPendingChange(false)
|
||||||
}
|
}
|
||||||
@@ -118,3 +142,93 @@ function Alert({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[]
|
|||||||
</label>
|
</label>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AlertWithSlider({
|
||||||
|
system,
|
||||||
|
alerts,
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
system: SystemRecord
|
||||||
|
alerts: AlertRecord[]
|
||||||
|
name: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}) {
|
||||||
|
const [pendingChange, setPendingChange] = useState(false)
|
||||||
|
const [liveValue, setLiveValue] = useState(50)
|
||||||
|
|
||||||
|
const alert = useMemo(() => {
|
||||||
|
const alert = alerts.find((alert) => alert.name === name)
|
||||||
|
if (alert) {
|
||||||
|
setLiveValue(alert.value)
|
||||||
|
}
|
||||||
|
return alert
|
||||||
|
}, [alerts])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border">
|
||||||
|
<label
|
||||||
|
htmlFor={`alert-${name}`}
|
||||||
|
className={cn('space-y-2 flex flex-row items-center justify-between cursor-pointer p-4', {
|
||||||
|
'pb-0': !!alert,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="grid gap-0.5 select-none">
|
||||||
|
<p className="font-medium text-[1.05em]">{title}</p>
|
||||||
|
<span className="block text-[0.85em] text-foreground opacity-80">{description}</span>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id={`alert-${name}`}
|
||||||
|
className={cn('transition-opacity', pendingChange && 'opacity-40')}
|
||||||
|
checked={!!alert}
|
||||||
|
value={!!alert ? 'on' : 'off'}
|
||||||
|
onCheckedChange={async (active) => {
|
||||||
|
if (pendingChange) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPendingChange(true)
|
||||||
|
try {
|
||||||
|
if (!active && alert) {
|
||||||
|
await pb.collection('alerts').delete(alert.id)
|
||||||
|
} else if (active) {
|
||||||
|
pb.collection('alerts').create({
|
||||||
|
system: system.id,
|
||||||
|
user: pb.authStore.model!.id,
|
||||||
|
name,
|
||||||
|
value: liveValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
failedUpdateToast()
|
||||||
|
} finally {
|
||||||
|
setPendingChange(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{alert && (
|
||||||
|
<div className="flex mt-2 mb-3 gap-3 px-4">
|
||||||
|
<Suspense>
|
||||||
|
<Slider
|
||||||
|
defaultValue={[liveValue]}
|
||||||
|
onValueCommit={(val) => {
|
||||||
|
pb.collection('alerts').update(alert.id, {
|
||||||
|
value: val[0],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setLiveValue(val[0])
|
||||||
|
}}
|
||||||
|
min={10}
|
||||||
|
max={99}
|
||||||
|
// step={1}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
<span className="tabular-nums tracking-tighter text-[.92em]">{liveValue}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
23
hub/site/src/components/ui/slider.tsx
Normal file
23
hub/site/src/components/ui/slider.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Slider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
))
|
||||||
|
Slider.displayName = SliderPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export default Slider
|
@@ -45,7 +45,7 @@ export const updateSystemList = async () => {
|
|||||||
|
|
||||||
export const updateAlerts = () => {
|
export const updateAlerts = () => {
|
||||||
pb.collection('alerts')
|
pb.collection('alerts')
|
||||||
.getFullList<AlertRecord>({ fields: 'id,name,system' })
|
.getFullList<AlertRecord>({ fields: 'id,name,system,value' })
|
||||||
.then((records) => {
|
.then((records) => {
|
||||||
$alerts.set(records)
|
$alerts.set(records)
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user