/* 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, Edit, Trash2, Save } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { getBgColorClass } from '@/lib/colors' 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Switch } from '@/components/ui/switch' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { StatusBadge } from '@/components/status-badge' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' type ApiInfo = { id: number url: string route: string description: string color: string } type ApiInfoSectionProps = { enabled: boolean data: string } const createApiInfoSchema = (t: (key: string) => string) => z.object({ url: z.string().url(t('Must be a valid URL')), route: z.string().min(1, t('Route is required')), description: z.string().min(1, t('Description is required')), color: z.string().min(1, t('Color is required')), }) type ApiInfoFormValues = z.infer> const colorOptions = [ { value: 'blue', label: 'Blue' }, { value: 'green', label: 'Green' }, { value: 'cyan', label: 'Cyan' }, { value: 'purple', label: 'Purple' }, { value: 'pink', label: 'Pink' }, { value: 'red', label: 'Red' }, { value: 'orange', label: 'Orange' }, { value: 'amber', label: 'Amber' }, { value: 'yellow', label: 'Yellow' }, { value: 'lime', label: 'Lime' }, { value: 'teal', label: 'Teal' }, { value: 'indigo', label: 'Indigo' }, { value: 'violet', label: 'Violet' }, { value: 'slate', label: 'Slate' }, ] export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { const { t } = useTranslation() const updateOption = useUpdateOption() const apiInfoSchema = createApiInfoSchema(t) const [apiInfoList, setApiInfoList] = 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 [editingApiInfo, setEditingApiInfo] = useState(null) const [deleteTarget, setDeleteTarget] = useState<'single' | 'batch'>('single') const form = useForm({ resolver: zodResolver(apiInfoSchema), defaultValues: { url: '', route: '', description: '', color: 'blue', }, }) useEffect(() => { try { const parsed = JSON.parse(data || '[]') if (Array.isArray(parsed)) { setApiInfoList( parsed.map((item, idx) => ({ ...item, id: item.id || idx + 1, })) ) } } catch { setApiInfoList([]) } }, [data]) useEffect(() => { setIsEnabled(enabled) }, [enabled]) const handleToggleEnabled = async (checked: boolean) => { try { await updateOption.mutateAsync({ key: 'console_setting.api_info_enabled', value: checked, }) setIsEnabled(checked) toast.success(t('Setting saved')) } catch { toast.error(t('Failed to update setting')) } } const handleAdd = () => { setEditingApiInfo(null) form.reset({ url: '', route: '', description: '', color: 'blue', }) setShowDialog(true) } const handleEdit = (apiInfo: ApiInfo) => { setEditingApiInfo(apiInfo) form.reset({ url: apiInfo.url, route: apiInfo.route, description: apiInfo.description, color: apiInfo.color, }) setShowDialog(true) } const handleDelete = (apiInfo: ApiInfo) => { setEditingApiInfo(apiInfo) 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' && editingApiInfo) { setApiInfoList((prev) => prev.filter((item) => item.id !== editingApiInfo.id) ) setHasChanges(true) toast.success(t('API info deleted. Click "Save Settings" to apply.')) } else if (deleteTarget === 'batch') { setApiInfoList((prev) => prev.filter((item) => !selectedIds.includes(item.id)) ) setSelectedIds([]) setHasChanges(true) toast.success( t('{{count}} API entries deleted. Click "Save Settings" to apply.', { count: selectedIds.length, }) ) } setShowDeleteDialog(false) setEditingApiInfo(null) } const handleSubmitForm = (values: ApiInfoFormValues) => { if (editingApiInfo) { setApiInfoList((prev) => prev.map((item) => item.id === editingApiInfo.id ? { ...item, ...values } : item ) ) toast.success(t('API info updated. Click "Save Settings" to apply.')) } else { const newId = Math.max(...apiInfoList.map((item) => item.id), 0) + 1 setApiInfoList((prev) => [...prev, { id: newId, ...values }]) toast.success(t('API info added. Click "Save Settings" to apply.')) } setHasChanges(true) setShowDialog(false) } const handleSaveAll = async () => { try { const result = await updateOption.mutateAsync({ key: 'console_setting.api_info', value: JSON.stringify(apiInfoList), }) if (result.success) { setHasChanges(false) } } catch { toast.error(t('Failed to save API info')) } } const toggleSelectAll = (checked: boolean) => { setSelectedIds(checked ? apiInfoList.map((item) => item.id) : []) } const toggleSelectOne = (id: number, checked: boolean) => { setSelectedIds((prev) => checked ? [...prev, id] : prev.filter((item) => item !== id) ) } const getColorClass = (color: string) => getBgColorClass(color) return ( {t('Add API')} {t('Delete (')} {selectedIds.length}) {updateOption.isPending ? t('Saving...') : t('Save Settings')} {t('Enabled')} 0 } onCheckedChange={toggleSelectAll} /> {t('URL')} {t('Route')} {t('Description')} {t('Color')} {t('Actions')} {apiInfoList.length === 0 ? ( {t('No API Domains yet. Click "Add API" to create one.')} ) : ( apiInfoList.map((apiInfo) => ( toggleSelectOne(apiInfo.id, checked as boolean) } /> {apiInfo.description} {apiInfo.color} handleEdit(apiInfo)} size='sm' variant='ghost' > handleDelete(apiInfo)} size='sm' variant='ghost' > )) )} {editingApiInfo ? t('Edit API Shortcut') : t('Add API Shortcut')} {t('Configure API documentation links for the dashboard')} ( {t('API URL')} )} /> ( {t('Route Description')} )} /> ( {t('Description')} )} /> ( {t('Badge Color')} ({ value: option.value, label: ( {option.label} ), })), ]} onValueChange={field.onChange} value={field.value} > {colorOptions.map((option) => ( {option.label} ))} {t('Visual indicator color for the API card')} )} /> setShowDialog(false)} > {t('Cancel')} {editingApiInfo ? t('Update') : t('Add')} {t('Are you sure?')} {deleteTarget === 'single' ? 'This API shortcut will be removed from the list.' : `${selectedIds.length} API shortcuts will be removed from the list.`} {t('Cancel')} {t('Delete')} ) }