- Added `NotifyOnStatusChange` property to the `Node` class and related DTOs to allow individual node configuration for status change notifications. - Updated `NodeHealthCheckService` to send Telegram notifications to admins when a node's status changes, based on the new property. - Enhanced the `ITelegramNotifier` interface with a method for notifying admins about node status changes. - Modified the frontend to include a checkbox for `NotifyOnStatusChange` in the node editing dialog, allowing admins to easily configure this setting. - Updated API documentation to reflect the new `notifyOnStatusChange` parameter in the node update endpoint. - Added tests to ensure the correct behavior of the new feature and its integration with existing functionality.
118 lines
4.7 KiB
TypeScript
118 lines
4.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { toast } from '@/shared/ui/toast-store'
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/ui/dialog'
|
|
import { Button } from '@/shared/ui/button'
|
|
import { Input } from '@/shared/ui/input'
|
|
import { Label } from '@/shared/ui/label'
|
|
import { HttpError } from '@/shared/api/client'
|
|
import type { NodeDto } from '@/shared/api/types'
|
|
import { updateNode } from './api'
|
|
|
|
export function EditNodeDialog({ node, open, onOpenChange }: { node: NodeDto; open: boolean; onOpenChange: (open: boolean) => void }) {
|
|
const { t } = useTranslation()
|
|
const queryClient = useQueryClient()
|
|
const [name, setName] = useState(node.name)
|
|
const [baseAddress, setBaseAddress] = useState(node.baseAddress)
|
|
const [location, setLocation] = useState(node.location ?? '')
|
|
const [isEnabled, setIsEnabled] = useState(node.isEnabled)
|
|
const [notifyOnStatusChange, setNotifyOnStatusChange] = useState(node.notifyOnStatusChange)
|
|
const [username, setUsername] = useState(node.username)
|
|
const [password, setPassword] = useState('')
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: () =>
|
|
updateNode(
|
|
node.id,
|
|
name.trim(),
|
|
baseAddress.trim(),
|
|
location.trim() || undefined,
|
|
isEnabled,
|
|
notifyOnStatusChange,
|
|
username.trim() || undefined,
|
|
password || undefined,
|
|
),
|
|
onSuccess: async () => {
|
|
toast.success(t('admin.nodes.updated'))
|
|
await queryClient.invalidateQueries({ queryKey: ['admin-nodes'] })
|
|
onOpenChange(false)
|
|
},
|
|
onError: (error) => toast.error(error instanceof HttpError ? error.detail : t('auth.genericError')),
|
|
})
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
|
|
<DialogHeader>
|
|
<DialogTitle>{node.name}</DialogTitle>
|
|
</DialogHeader>
|
|
<form
|
|
className="flex flex-col gap-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault()
|
|
mutation.mutate()
|
|
}}
|
|
>
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="editName">{t('admin.nodes.name')}</Label>
|
|
<Input id="editName" value={name} onChange={(e) => setName(e.target.value)} required />
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="editBaseAddress">{t('admin.nodes.baseAddress')}</Label>
|
|
<Input
|
|
id="editBaseAddress"
|
|
placeholder="https://panel.example.com:2053"
|
|
value={baseAddress}
|
|
onChange={(e) => setBaseAddress(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="editLocation">{t('admin.nodes.location')}</Label>
|
|
<Input id="editLocation" value={location} onChange={(e) => setLocation(e.target.value)} />
|
|
</div>
|
|
<label className="flex items-center gap-2 text-sm">
|
|
<input type="checkbox" checked={isEnabled} onChange={(e) => setIsEnabled(e.target.checked)} />
|
|
{t('admin.nodes.enabled')}
|
|
</label>
|
|
<label className="flex items-center gap-2 text-sm">
|
|
<input
|
|
type="checkbox"
|
|
checked={notifyOnStatusChange}
|
|
onChange={(e) => setNotifyOnStatusChange(e.target.checked)}
|
|
/>
|
|
{t('admin.nodes.notifyOnStatusChange')}
|
|
</label>
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="editUsername">
|
|
{t('admin.nodes.username')} ({t('admin.nodes.optional')})
|
|
</Label>
|
|
<Input id="editUsername" value={username} onChange={(e) => setUsername(e.target.value)} placeholder={t('admin.nodes.username')} />
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="editPassword">
|
|
{t('admin.nodes.password')} ({t('admin.nodes.passwordKeepUnchanged')})
|
|
</Label>
|
|
<Input
|
|
id="editPassword"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
data-lpignore="true"
|
|
data-1p-ignore
|
|
data-bwignore
|
|
data-form-type="other"
|
|
placeholder="••••••••"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
/>
|
|
</div>
|
|
<Button type="submit" disabled={!name.trim() || !baseAddress.trim() || mutation.isPending}>
|
|
{t('admin.roles.save')}
|
|
</Button>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|