/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useEffect, useState } from 'react' import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { Plus, Trash2, Save } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { StaticDataTable } from '@/components/data-table/static/static-data-table' import { StaticRowActions } from '@/components/data-table/static/static-row-actions' import { Dialog } from '@/components/dialog' import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' type UptimeKumaGroup = { id: number categoryName: string url: string slug: string } type UptimeKumaSectionProps = { enabled: boolean data: string } const createUptimeKumaSchema = (t: (key: string) => string) => z.object({ categoryName: z .string() .min(1, { error: t('Category name is required') }) .max(50, { error: t('Category name must be less than 50 characters') }), url: z.string().url({ error: t('Must be a valid URL') }), slug: z .string() .min(1, { error: t('Slug is required') }) .max(100, { error: t('Slug must be less than 100 characters') }) .regex(/^[a-zA-Z0-9_-]+$/, { error: t( 'Slug can only contain letters, numbers, hyphens, and underscores' ), }), }) type UptimeKumaFormValues = z.infer> const UPTIME_KUMA_FORM_ID = 'uptime-kuma-form' export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) { const { t } = useTranslation() const updateOption = useUpdateOption() const uptimeKumaSchema = createUptimeKumaSchema(t) const [groups, setGroups] = useState([]) const [isEnabled, setIsEnabled] = useState(enabled) const [hasChanges, setHasChanges] = useState(false) const [selectedIds, setSelectedIds] = useState([]) const [showDialog, setShowDialog] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [editingGroup, setEditingGroup] = useState(null) const [deleteTarget, setDeleteTarget] = useState<'single' | 'batch'>('single') const form = useForm({ resolver: zodResolver(uptimeKumaSchema), defaultValues: { categoryName: '', url: '', slug: '', }, }) useEffect(() => { try { const parsed = JSON.parse(data || '[]') if (Array.isArray(parsed)) { setGroups( parsed.map((item, idx) => ({ ...item, id: item.id || idx + 1, })) ) } } catch { setGroups([]) } }, [data]) useEffect(() => { setIsEnabled(enabled) }, [enabled]) const handleToggleEnabled = async (checked: boolean) => { try { await updateOption.mutateAsync({ key: 'console_setting.uptime_kuma_enabled', value: checked, }) setIsEnabled(checked) toast.success(t('Setting saved')) } catch { toast.error(t('Failed to update setting')) } } const handleAdd = () => { setEditingGroup(null) form.reset({ categoryName: '', url: '', slug: '', }) setShowDialog(true) } const handleEdit = (group: UptimeKumaGroup) => { setEditingGroup(group) form.reset({ categoryName: group.categoryName, url: group.url, slug: group.slug, }) setShowDialog(true) } const handleDelete = (group: UptimeKumaGroup) => { setEditingGroup(group) setDeleteTarget('single') setShowDeleteDialog(true) } const handleBatchDelete = () => { if (selectedIds.length === 0) { toast.error(t('Please select items to delete')) return } setDeleteTarget('batch') setShowDeleteDialog(true) } const confirmDelete = () => { if (deleteTarget === 'single' && editingGroup) { setGroups((prev) => prev.filter((item) => item.id !== editingGroup.id)) setHasChanges(true) toast.success(t('Group deleted. Click "Save Settings" to apply.')) } else if (deleteTarget === 'batch') { setGroups((prev) => prev.filter((item) => !selectedIds.includes(item.id))) setSelectedIds([]) setHasChanges(true) toast.success( t('{{count}} groups deleted. Click "Save Settings" to apply.', { count: selectedIds.length, }) ) } setShowDeleteDialog(false) setEditingGroup(null) } const handleSubmitForm = (values: UptimeKumaFormValues) => { if (editingGroup) { setGroups((prev) => prev.map((item) => item.id === editingGroup.id ? { ...item, ...values } : item ) ) toast.success(t('Group updated. Click "Save Settings" to apply.')) } else { const newId = Math.max(...groups.map((item) => item.id), 0) + 1 setGroups((prev) => [...prev, { id: newId, ...values }]) toast.success(t('Group added. Click "Save Settings" to apply.')) } setHasChanges(true) setShowDialog(false) } const handleSaveAll = async () => { try { await updateOption.mutateAsync({ key: 'console_setting.uptime_kuma_groups', value: JSON.stringify(groups), }) setHasChanges(false) toast.success(t('Uptime Kuma groups saved successfully')) } catch { toast.error(t('Failed to save Uptime Kuma groups')) } } const toggleSelectAll = (checked: boolean) => { setSelectedIds(checked ? groups.map((item) => item.id) : []) } const toggleSelectOne = (id: number, checked: boolean) => { setSelectedIds((prev) => checked ? [...prev, id] : prev.filter((item) => item !== id) ) } return (
group.id} emptyContent={t( 'No Uptime Kuma groups yet. Click "Add Group" to create one.' )} columns={[ { id: 'select', header: ( 0 } onCheckedChange={toggleSelectAll} /> ), className: 'w-12', cell: (group) => ( toggleSelectOne(group.id, checked as boolean) } /> ), }, { id: 'category', header: t('Category Name'), cellClassName: 'font-medium', cell: (group) => group.categoryName, }, { id: 'url', header: t('Uptime Kuma URL'), cellClassName: 'text-primary max-w-xs truncate font-mono text-sm', cell: (group) => group.url, }, { id: 'slug', header: t('Status Page Slug'), cellClassName: 'text-muted-foreground font-mono text-sm', cell: (group) => group.slug, }, { id: 'actions', header: t('Actions'), cell: (group) => ( handleEdit(group)} onDelete={() => handleDelete(group)} /> ), }, ]} />
} >
( {t('Category Name')} {t( 'Display name for this monitoring group (max 50 characters)' )} )} /> ( {t('Uptime Kuma URL')} {t('Base URL of your Uptime Kuma instance')} )} /> ( {t('Status Page Slug')} {t('The slug is appended to the URL:')} {'{url}'} {t('/status/')} {'{slug}'} )} />
{t('Are you sure?')} {deleteTarget === 'single' ? t('This Uptime Kuma group will be removed from the list.') : t( '{{count}} Uptime Kuma groups will be removed from the list.', { count: selectedIds.length } )} {t('Cancel')} {t('Delete')}
) }