mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 09:49:28 +08:00
fix: error adding alerts to 50+ systems (#664)
- Fixed PB SDK autocancelling requests. - Refactored SystemAlert and SystemAlertGlobal components. - Introduced a batchWrapper function for handling batch operations on alerts.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { memo, useState } from "react"
|
import { memo, useMemo, useState } from "react"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { $alerts, $systems } from "@/lib/stores"
|
import { $alerts } from "@/lib/stores"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -23,98 +23,109 @@ export default memo(function AlertsButton({ system }: { system: SystemRecord })
|
|||||||
const alerts = useStore($alerts)
|
const alerts = useStore($alerts)
|
||||||
const [opened, setOpened] = useState(false)
|
const [opened, setOpened] = useState(false)
|
||||||
|
|
||||||
const systemAlerts = alerts.filter((alert) => alert.system === system.id) as AlertRecord[]
|
const hasAlert = alerts.some((alert) => alert.system === system.id)
|
||||||
const active = systemAlerts.length > 0
|
|
||||||
|
|
||||||
return (
|
return useMemo(
|
||||||
<Dialog>
|
() => (
|
||||||
<DialogTrigger asChild>
|
<Dialog>
|
||||||
<Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>
|
<DialogTrigger asChild>
|
||||||
<BellIcon
|
<Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>
|
||||||
className={cn("h-[1.2em] w-[1.2em] pointer-events-none", {
|
<BellIcon
|
||||||
"fill-primary": active,
|
className={cn("h-[1.2em] w-[1.2em] pointer-events-none", {
|
||||||
})}
|
"fill-primary": hasAlert,
|
||||||
/>
|
})}
|
||||||
</Button>
|
/>
|
||||||
</DialogTrigger>
|
</Button>
|
||||||
<DialogContent className="max-h-full overflow-auto max-w-[35rem]">
|
</DialogTrigger>
|
||||||
{opened && <TheContent data={{ system, alerts, systemAlerts }} />}
|
<DialogContent className="max-h-full overflow-auto max-w-[35rem]">
|
||||||
</DialogContent>
|
{opened && <AlertDialogContent system={system} />}
|
||||||
</Dialog>
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
),
|
||||||
|
[opened, hasAlert]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function TheContent({
|
function AlertDialogContent({ system }: { system: SystemRecord }) {
|
||||||
data: { system, alerts, systemAlerts },
|
const alerts = useStore($alerts)
|
||||||
}: {
|
|
||||||
data: { system: SystemRecord; alerts: AlertRecord[]; systemAlerts: AlertRecord[] }
|
|
||||||
}) {
|
|
||||||
const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
|
const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
|
||||||
const systems = $systems.get()
|
|
||||||
|
|
||||||
const data = Object.keys(alertInfo).map((key) => {
|
// alertsSignature changes only when alerts for this system change
|
||||||
const alert = alertInfo[key as keyof typeof alertInfo]
|
let alertsSignature = ""
|
||||||
return {
|
const systemAlerts = alerts.filter((alert) => {
|
||||||
key: key as keyof typeof alertInfo,
|
if (alert.system === system.id) {
|
||||||
alert,
|
alertsSignature += alert.name + alert.min + alert.value
|
||||||
system,
|
return true
|
||||||
}
|
}
|
||||||
})
|
return false
|
||||||
|
}) as AlertRecord[]
|
||||||
|
|
||||||
return (
|
return useMemo(() => {
|
||||||
<>
|
// console.log("render modal", system.name, alertsSignature)
|
||||||
<DialogHeader>
|
const data = Object.keys(alertInfo).map((name) => {
|
||||||
<DialogTitle className="text-xl">
|
const alert = alertInfo[name as keyof typeof alertInfo]
|
||||||
<Trans>Alerts</Trans>
|
return {
|
||||||
</DialogTitle>
|
name: name as keyof typeof alertInfo,
|
||||||
<DialogDescription>
|
alert,
|
||||||
<Trans>
|
system,
|
||||||
See{" "}
|
}
|
||||||
<Link href="/settings/notifications" className="link">
|
})
|
||||||
notification settings
|
|
||||||
</Link>{" "}
|
return (
|
||||||
to configure how you receive alerts.
|
<>
|
||||||
</Trans>
|
<DialogHeader>
|
||||||
</DialogDescription>
|
<DialogTitle className="text-xl">
|
||||||
</DialogHeader>
|
<Trans>Alerts</Trans>
|
||||||
<Tabs defaultValue="system">
|
</DialogTitle>
|
||||||
<TabsList className="mb-1 -mt-0.5">
|
<DialogDescription>
|
||||||
<TabsTrigger value="system">
|
<Trans>
|
||||||
<ServerIcon className="me-2 h-3.5 w-3.5" />
|
See{" "}
|
||||||
{system.name}
|
<Link href="/settings/notifications" className="link">
|
||||||
</TabsTrigger>
|
notification settings
|
||||||
<TabsTrigger value="global">
|
</Link>{" "}
|
||||||
<GlobeIcon className="me-1.5 h-3.5 w-3.5" />
|
to configure how you receive alerts.
|
||||||
<Trans>All Systems</Trans>
|
</Trans>
|
||||||
</TabsTrigger>
|
</DialogDescription>
|
||||||
</TabsList>
|
</DialogHeader>
|
||||||
<TabsContent value="system">
|
<Tabs defaultValue="system">
|
||||||
<div className="grid gap-3">
|
<TabsList className="mb-1 -mt-0.5">
|
||||||
{data.map((d) => (
|
<TabsTrigger value="system">
|
||||||
<SystemAlert key={d.key} system={system} data={d} systemAlerts={systemAlerts} />
|
<ServerIcon className="me-2 h-3.5 w-3.5" />
|
||||||
))}
|
{system.name}
|
||||||
</div>
|
</TabsTrigger>
|
||||||
</TabsContent>
|
<TabsTrigger value="global">
|
||||||
<TabsContent value="global">
|
<GlobeIcon className="me-1.5 h-3.5 w-3.5" />
|
||||||
<label
|
<Trans>All Systems</Trans>
|
||||||
htmlFor="ovw"
|
</TabsTrigger>
|
||||||
className="mb-3 flex gap-2 items-center justify-center cursor-pointer border rounded-sm py-3 px-4 border-destructive text-destructive font-semibold text-sm"
|
</TabsList>
|
||||||
>
|
<TabsContent value="system">
|
||||||
<Checkbox
|
<div className="grid gap-3">
|
||||||
id="ovw"
|
{data.map((d) => (
|
||||||
className="text-destructive border-destructive data-[state=checked]:bg-destructive"
|
<SystemAlert key={d.name} system={system} data={d} systemAlerts={systemAlerts} />
|
||||||
checked={overwriteExisting}
|
))}
|
||||||
onCheckedChange={setOverwriteExisting}
|
</div>
|
||||||
/>
|
</TabsContent>
|
||||||
<Trans>Overwrite existing alerts</Trans>
|
<TabsContent value="global">
|
||||||
</label>
|
<label
|
||||||
<div className="grid gap-3">
|
htmlFor="ovw"
|
||||||
{data.map((d) => (
|
className="mb-3 flex gap-2 items-center justify-center cursor-pointer border rounded-sm py-3 px-4 border-destructive text-destructive font-semibold text-sm"
|
||||||
<SystemAlertGlobal key={d.key} data={d} overwrite={overwriteExisting} alerts={alerts} systems={systems} />
|
>
|
||||||
))}
|
<Checkbox
|
||||||
</div>
|
id="ovw"
|
||||||
</TabsContent>
|
className="text-destructive border-destructive data-[state=checked]:bg-destructive"
|
||||||
</Tabs>
|
checked={overwriteExisting}
|
||||||
</>
|
onCheckedChange={setOverwriteExisting}
|
||||||
)
|
/>
|
||||||
|
<Trans>Overwrite existing alerts</Trans>
|
||||||
|
</label>
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{data.map((d) => (
|
||||||
|
<SystemAlertGlobal key={d.name} data={d} overwrite={overwriteExisting} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}, [alertsSignature, overwriteExisting])
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,19 @@
|
|||||||
import { pb } from "@/lib/stores"
|
import { $alerts, $systems, pb } from "@/lib/stores"
|
||||||
import { alertInfo, cn } from "@/lib/utils"
|
import { alertInfo, cn } from "@/lib/utils"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { AlertInfo, AlertRecord, SystemRecord } from "@/types"
|
import { AlertInfo, AlertRecord, SystemRecord } from "@/types"
|
||||||
import { lazy, Suspense, useRef, useState } from "react"
|
import { lazy, Suspense, useMemo, useState } from "react"
|
||||||
import { toast } from "../ui/use-toast"
|
import { toast } from "../ui/use-toast"
|
||||||
import { RecordOptions } from "pocketbase"
|
import { BatchService } from "pocketbase"
|
||||||
import { Trans, t, Plural } from "@lingui/macro"
|
import { Trans, t, Plural } from "@lingui/macro"
|
||||||
|
import { getSemaphore } from "@henrygd/semaphore"
|
||||||
|
|
||||||
interface AlertData {
|
interface AlertData {
|
||||||
checked?: boolean
|
checked?: boolean
|
||||||
val?: number
|
val?: number
|
||||||
min?: number
|
min?: number
|
||||||
updateAlert?: (checked: boolean, value: number, min: number) => void
|
updateAlert?: (checked: boolean, value: number, min: number) => void
|
||||||
key: keyof typeof alertInfo
|
name: keyof typeof alertInfo
|
||||||
alert: AlertInfo
|
alert: AlertInfo
|
||||||
system: SystemRecord
|
system: SystemRecord
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@ export function SystemAlert({
|
|||||||
systemAlerts: AlertRecord[]
|
systemAlerts: AlertRecord[]
|
||||||
data: AlertData
|
data: AlertData
|
||||||
}) {
|
}) {
|
||||||
const alert = systemAlerts.find((alert) => alert.name === data.key)
|
const alert = systemAlerts.find((alert) => alert.name === data.name)
|
||||||
|
|
||||||
data.updateAlert = async (checked: boolean, value: number, min: number) => {
|
data.updateAlert = async (checked: boolean, value: number, min: number) => {
|
||||||
try {
|
try {
|
||||||
@@ -47,7 +48,7 @@ export function SystemAlert({
|
|||||||
pb.collection("alerts").create({
|
pb.collection("alerts").create({
|
||||||
system: system.id,
|
system: system.id,
|
||||||
user: pb.authStore.record!.id,
|
user: pb.authStore.record!.id,
|
||||||
name: data.key,
|
name: data.name,
|
||||||
value: value,
|
value: value,
|
||||||
min: min,
|
min: min,
|
||||||
})
|
})
|
||||||
@@ -66,99 +67,150 @@ export function SystemAlert({
|
|||||||
return <AlertContent data={data} />
|
return <AlertContent data={data} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SystemAlertGlobal({
|
export const SystemAlertGlobal = ({ data, overwrite }: { data: AlertData; overwrite: boolean | "indeterminate" }) => {
|
||||||
data,
|
|
||||||
overwrite,
|
|
||||||
alerts,
|
|
||||||
systems,
|
|
||||||
}: {
|
|
||||||
data: AlertData
|
|
||||||
overwrite: boolean | "indeterminate"
|
|
||||||
alerts: AlertRecord[]
|
|
||||||
systems: SystemRecord[]
|
|
||||||
}) {
|
|
||||||
const systemsWithExistingAlerts = useRef<{ set: Set<string>; populatedSet: boolean }>({
|
|
||||||
set: new Set(),
|
|
||||||
populatedSet: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
data.checked = false
|
data.checked = false
|
||||||
data.val = data.min = 0
|
data.val = data.min = 0
|
||||||
|
|
||||||
|
// set of system ids that have an alert for this name when the component is mounted
|
||||||
|
const existingAlertsSystems = useMemo(() => {
|
||||||
|
const map = new Set<string>()
|
||||||
|
const alerts = $alerts.get()
|
||||||
|
for (const alert of alerts) {
|
||||||
|
if (alert.name === data.name) {
|
||||||
|
map.add(alert.system)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [])
|
||||||
|
|
||||||
data.updateAlert = async (checked: boolean, value: number, min: number) => {
|
data.updateAlert = async (checked: boolean, value: number, min: number) => {
|
||||||
const { set, populatedSet } = systemsWithExistingAlerts.current
|
const sem = getSemaphore("alerts")
|
||||||
|
await sem.acquire()
|
||||||
|
try {
|
||||||
|
// if another update is waiting behind, don't start this one
|
||||||
|
if (sem.size() > 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// if overwrite checked, make sure all alerts will be overwritten
|
const recordData: Partial<AlertRecord> = {
|
||||||
if (overwrite) {
|
value,
|
||||||
set.clear()
|
min,
|
||||||
}
|
triggered: false,
|
||||||
|
}
|
||||||
|
|
||||||
const recordData: Partial<AlertRecord> = {
|
const batch = batchWrapper("alerts", 25)
|
||||||
value,
|
const systems = $systems.get()
|
||||||
min,
|
const currentAlerts = $alerts.get()
|
||||||
triggered: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// we can only send 50 in one batch
|
// map of current alerts with this name right now by system id
|
||||||
let done = 0
|
const currentAlertsSystems = new Map<string, AlertRecord>()
|
||||||
|
for (const alert of currentAlerts) {
|
||||||
while (done < systems.length) {
|
if (alert.name === data.name) {
|
||||||
const batch = pb.createBatch()
|
currentAlertsSystems.set(alert.system, alert)
|
||||||
let batchSize = 0
|
|
||||||
|
|
||||||
for (let i = done; i < Math.min(done + 50, systems.length); i++) {
|
|
||||||
const system = systems[i]
|
|
||||||
// if overwrite is false and system is in set (alert existed), skip
|
|
||||||
if (!overwrite && set.has(system.id)) {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
// find matching existing alert
|
}
|
||||||
const existingAlert = alerts.find((alert) => alert.system === system.id && data.key === alert.name)
|
|
||||||
// if first run, add system to set (alert already existed when global panel was opened)
|
if (overwrite) {
|
||||||
if (existingAlert && !populatedSet && !overwrite) {
|
existingAlertsSystems.clear()
|
||||||
set.add(system.id)
|
}
|
||||||
continue
|
|
||||||
}
|
const processSystem = async (system: SystemRecord): Promise<void> => {
|
||||||
batchSize++
|
const existingAlert = existingAlertsSystems.has(system.id)
|
||||||
const requestOptions: RecordOptions = {
|
|
||||||
requestKey: system.id,
|
if (!overwrite && existingAlert) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// checked - make sure alert is created or updated
|
const currentAlert = currentAlertsSystems.get(system.id)
|
||||||
|
|
||||||
|
// delete existing alert if unchecked
|
||||||
|
if (!checked && currentAlert) {
|
||||||
|
return batch.remove(currentAlert.id)
|
||||||
|
}
|
||||||
|
if (checked && currentAlert) {
|
||||||
|
// update existing alert if checked
|
||||||
|
return batch.update(currentAlert.id, recordData)
|
||||||
|
}
|
||||||
if (checked) {
|
if (checked) {
|
||||||
if (existingAlert) {
|
// create new alert if checked and not existing
|
||||||
batch.collection("alerts").update(existingAlert.id, recordData, requestOptions)
|
return batch.create({
|
||||||
} else {
|
system: system.id,
|
||||||
batch.collection("alerts").create(
|
user: pb.authStore.record!.id,
|
||||||
{
|
name: data.name,
|
||||||
system: system.id,
|
...recordData,
|
||||||
user: pb.authStore.record!.id,
|
})
|
||||||
name: data.key,
|
|
||||||
...recordData,
|
|
||||||
},
|
|
||||||
requestOptions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (existingAlert) {
|
|
||||||
batch.collection("alerts").delete(existingAlert.id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
batchSize && batch.send()
|
// make sure current system is updated in the first batch
|
||||||
} catch (e) {
|
await processSystem(data.system)
|
||||||
failedUpdateToast()
|
for (const system of systems) {
|
||||||
} finally {
|
if (system.id === data.system.id) {
|
||||||
done += 50
|
continue
|
||||||
|
}
|
||||||
|
if (sem.size() > 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await processSystem(system)
|
||||||
}
|
}
|
||||||
|
await batch.send()
|
||||||
|
} finally {
|
||||||
|
sem.release()
|
||||||
}
|
}
|
||||||
systemsWithExistingAlerts.current.populatedSet = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <AlertContent data={data} />
|
return <AlertContent data={data} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a wrapper for performing batch operations on a specified collection.
|
||||||
|
*/
|
||||||
|
function batchWrapper(collection: string, batchSize: number) {
|
||||||
|
let batch: BatchService | undefined
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
const create = async <T extends Record<string, any>>(options: T) => {
|
||||||
|
batch ||= pb.createBatch()
|
||||||
|
batch.collection(collection).create(options)
|
||||||
|
if (++count >= batchSize) {
|
||||||
|
await send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = async <T extends Record<string, any>>(id: string, data: T) => {
|
||||||
|
batch ||= pb.createBatch()
|
||||||
|
batch.collection(collection).update(id, data)
|
||||||
|
if (++count >= batchSize) {
|
||||||
|
await send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const remove = async (id: string) => {
|
||||||
|
batch ||= pb.createBatch()
|
||||||
|
batch.collection(collection).delete(id)
|
||||||
|
if (++count >= batchSize) {
|
||||||
|
await send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const send = async () => {
|
||||||
|
if (count) {
|
||||||
|
await batch?.send({ requestKey: null })
|
||||||
|
batch = undefined
|
||||||
|
count = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
update,
|
||||||
|
remove,
|
||||||
|
send,
|
||||||
|
create,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function AlertContent({ data }: { data: AlertData }) {
|
function AlertContent({ data }: { data: AlertData }) {
|
||||||
const { key } = data
|
const { name } = data
|
||||||
|
|
||||||
const singleDescription = data.alert.singleDesc?.()
|
const singleDescription = data.alert.singleDesc?.()
|
||||||
|
|
||||||
@@ -166,17 +218,12 @@ function AlertContent({ data }: { data: AlertData }) {
|
|||||||
const [min, setMin] = useState(data.min || 10)
|
const [min, setMin] = useState(data.min || 10)
|
||||||
const [value, setValue] = useState(data.val || (singleDescription ? 0 : 80))
|
const [value, setValue] = useState(data.val || (singleDescription ? 0 : 80))
|
||||||
|
|
||||||
const newMin = useRef(min)
|
const Icon = alertInfo[name].icon
|
||||||
const newValue = useRef(value)
|
|
||||||
|
|
||||||
const Icon = alertInfo[key].icon
|
|
||||||
|
|
||||||
const updateAlert = (c?: boolean) => data.updateAlert?.(c ?? checked, newValue.current, newMin.current)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
|
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
|
||||||
<label
|
<label
|
||||||
htmlFor={`s${key}`}
|
htmlFor={`s${name}`}
|
||||||
className={cn("flex flex-row items-center justify-between gap-4 cursor-pointer p-4", {
|
className={cn("flex flex-row items-center justify-between gap-4 cursor-pointer p-4", {
|
||||||
"pb-0": checked,
|
"pb-0": checked,
|
||||||
})}
|
})}
|
||||||
@@ -188,44 +235,51 @@ function AlertContent({ data }: { data: AlertData }) {
|
|||||||
{!checked && <span className="block text-sm text-muted-foreground">{data.alert.desc()}</span>}
|
{!checked && <span className="block text-sm text-muted-foreground">{data.alert.desc()}</span>}
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
id={`s${key}`}
|
id={`s${name}`}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(newChecked) => {
|
||||||
setChecked(checked)
|
setChecked(newChecked)
|
||||||
updateAlert(checked)
|
data.updateAlert?.(newChecked, value, min)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{checked && (
|
{checked && (
|
||||||
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground">
|
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground">
|
||||||
<Suspense fallback={<div className="h-10" />}>
|
<Suspense fallback={<div className="h-10" />}>
|
||||||
{!singleDescription && (
|
{!singleDescription && (
|
||||||
<div>
|
<div>
|
||||||
<p id={`v${key}`} className="text-sm block h-8">
|
<p id={`v${name}`} className="text-sm block h-8">
|
||||||
<Trans>
|
<Trans>
|
||||||
Average exceeds{" "}
|
Average exceeds{" "}
|
||||||
<strong className="text-foreground">
|
<strong className="text-foreground">
|
||||||
{value}
|
{value}
|
||||||
{data.alert.unit}
|
{data.alert.unit}
|
||||||
</strong>
|
</strong>
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Slider
|
<Slider
|
||||||
aria-labelledby={`v${key}`}
|
aria-labelledby={`v${name}`}
|
||||||
defaultValue={[value]}
|
defaultValue={[value]}
|
||||||
onValueCommit={(val) => (newValue.current = val[0]) && updateAlert()}
|
onValueCommit={(val) => {
|
||||||
onValueChange={(val) => setValue(val[0])}
|
data.updateAlert?.(true, val[0], min)
|
||||||
min={1}
|
}}
|
||||||
max={alertInfo[key].max ?? 99}
|
onValueChange={(val) => {
|
||||||
/>
|
setValue(val[0])
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
|
max={alertInfo[name].max ?? 99}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
<div className={cn(singleDescription && "col-span-full lowercase")}>
|
<div className={cn(singleDescription && "col-span-full lowercase")}>
|
||||||
<p id={`t${key}`} className="text-sm block h-8 first-letter:uppercase">
|
<p id={`t${name}`} className="text-sm block h-8 first-letter:uppercase">
|
||||||
{singleDescription && (
|
{singleDescription && (
|
||||||
<>{singleDescription}{` `}</>
|
<>
|
||||||
|
{singleDescription}
|
||||||
|
{` `}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Trans>
|
<Trans>
|
||||||
For <strong className="text-foreground">{min}</strong>{" "}
|
For <strong className="text-foreground">{min}</strong>{" "}
|
||||||
@@ -234,10 +288,14 @@ function AlertContent({ data }: { data: AlertData }) {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Slider
|
<Slider
|
||||||
aria-labelledby={`v${key}`}
|
aria-labelledby={`v${name}`}
|
||||||
defaultValue={[min]}
|
defaultValue={[min]}
|
||||||
onValueCommit={(val) => (newMin.current = val[0]) && updateAlert()}
|
onValueCommit={(min) => {
|
||||||
onValueChange={(val) => setMin(val[0])}
|
data.updateAlert?.(true, value, min[0])
|
||||||
|
}}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setMin(val[0])
|
||||||
|
}}
|
||||||
min={1}
|
min={1}
|
||||||
max={60}
|
max={60}
|
||||||
/>
|
/>
|
||||||
|
Reference in New Issue
Block a user