import { useState } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { Plus } from 'lucide-react' import { toast } from '@/shared/ui/toast-store' import { Button } from '@/shared/ui/button' import { Input } from '@/shared/ui/input' import { Label } from '@/shared/ui/label' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/shared/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select' import { HttpError } from '@/shared/api/client' import type { AvailableInboundDto } from '@/shared/api/types' import { CountryFlag } from '@/shared/ui/country-flag' import { cn } from '@/shared/lib/cn' import { createConfig } from './api' // Пороги «светофора» загрузки ноды, % от ёмкости. const LOAD_MEDIUM = 50 const LOAD_HIGH = 80 function NodeLoad({ inbound }: { inbound: AvailableInboundDto }) { const { t } = useTranslation() if (inbound.loadPercent !== null) { const level = inbound.loadPercent >= LOAD_HIGH ? 'high' : inbound.loadPercent >= LOAD_MEDIUM ? 'medium' : 'low' return ( {t(`configs.load.${level}`)} · {inbound.loadPercent}% ) } if (inbound.nodeConfigCount !== null) { return ( {t('configs.nodeConfigs', { count: inbound.nodeConfigCount })} ) } return null } export function CreateConfigDialog({ inbounds }: { inbounds: AvailableInboundDto[] }) { const { t } = useTranslation() const queryClient = useQueryClient() const navigate = useNavigate() const [open, setOpen] = useState(false) const [inboundId, setInboundId] = useState('') const [label, setLabel] = useState('') const createMutation = useMutation({ mutationFn: () => createConfig(inboundId, label.trim() || undefined), onSuccess: async () => { toast.success(t('configs.created')) await queryClient.invalidateQueries({ queryKey: ['my-configs'] }) setOpen(false) setInboundId('') setLabel('') }, onError: (error) => { // Safety-net: дашборд уже скрывает эту кнопку при истёкшей оплате (см. dashboard.tsx), но // между открытием страницы и отправкой формы биллинг мог протухнуть — сервер всё равно // проверяет (ConfigErrors.BillingRequired) и на всякий случай отправляем на оплату. if (error instanceof HttpError && error.title === 'Configs.BillingRequired') { toast.error(t('configs.billingRequiredNotice')) setOpen(false) void navigate({ to: '/billing' }) return } const message = error instanceof HttpError && error.status === 409 ? t('configs.quotaExceeded') : t('auth.genericError') toast.error(message) }, }) return ( {t('configs.create')} {t('configs.create')} { e.preventDefault() if (inboundId) createMutation.mutate() }} > {t('configs.location')} {inbounds.map((inbound) => ( {inbound.displayName} ({inbound.protocol}) ))} {t('configs.label')} setLabel(e.target.value)} /> {t('configs.create')} ) }