/* 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 { Plus, Search } from 'lucide-react' import { useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { StaticDataTable } from '@/components/data-table/static/static-data-table' import { StaticRowActions } from '@/components/data-table/static/static-row-actions' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { safeJsonParseWithValidation } from '../utils/json-parser' import { isObjectRecord } from '../utils/json-validators' import { RateLimitDialog, type RateLimitEntryData } from './rate-limit-dialog' type RateLimitVisualEditorProps = { value: string onChange: (value: string) => void } type RateLimitEntry = RateLimitEntryData export function RateLimitVisualEditor({ value, onChange, }: RateLimitVisualEditorProps) { const { t } = useTranslation() const [searchText, setSearchText] = useState('') const [dialogOpen, setDialogOpen] = useState(false) const [editData, setEditData] = useState(null) const rateLimits = useMemo(() => { if (!value || value.trim() === '') return [] const parsed = safeJsonParseWithValidation>(value, { fallback: {}, validator: isObjectRecord, validatorMessage: 'Rate limits must be a JSON object', context: 'rate limits', }) return Object.entries(parsed) .map(([groupName, limits]) => { if ( Array.isArray(limits) && limits.length === 2 && typeof limits[0] === 'number' && typeof limits[1] === 'number' ) { return { groupName, maxRequests: limits[0], maxSuccess: limits[1], } } return null }) .filter((item): item is RateLimitEntry => item !== null) }, [value]) const filteredRateLimits = useMemo(() => { if (!searchText) return rateLimits const lowerSearch = searchText.toLowerCase() return rateLimits.filter((limit) => limit.groupName.toLowerCase().includes(lowerSearch) ) }, [rateLimits, searchText]) const handleSave = (data: RateLimitEntryData) => { const parsed = safeJsonParseWithValidation>(value, { fallback: {}, validator: isObjectRecord, silent: true, }) if (editData && editData.groupName !== data.groupName) { delete parsed[editData.groupName] } parsed[data.groupName] = [data.maxRequests, data.maxSuccess] onChange(JSON.stringify(parsed, null, 2)) } const handleDelete = (groupName: string) => { const parsed = safeJsonParseWithValidation>(value, { fallback: {}, validator: isObjectRecord, silent: true, }) delete parsed[groupName] onChange(JSON.stringify(parsed, null, 2)) } const handleEdit = (limit: RateLimitEntry) => { setEditData(limit) setDialogOpen(true) } const handleAdd = () => { setEditData(null) setDialogOpen(true) } return (
setSearchText(e.target.value)} className='pl-9' />
limit.groupName} emptyContent={ searchText ? t('No groups match your search') : t( 'No group-based rate limits configured. Click "Add group" to get started.' ) } columns={[ { id: 'group', header: t('Group Name'), cellClassName: 'font-medium', cell: (limit) => limit.groupName, }, { id: 'max-requests', header: t('Max Requests (incl. failures)'), className: 'text-right', cellClassName: 'text-right', cell: (limit) => ( {limit.maxRequests === 0 ? t('Unlimited') : limit.maxRequests.toLocaleString()} ), }, { id: 'max-success', header: t('Max Success'), className: 'text-right', cellClassName: 'text-right', cell: (limit) => ( {limit.maxSuccess.toLocaleString()} ), }, { id: 'actions', header: t('Actions'), className: 'text-right', cellClassName: 'text-right', cell: (limit) => ( handleEdit(limit)} onDelete={() => handleDelete(limit.groupName)} /> ), }, ]} />
) }