From 7dcc8889a0eed41ff61fecbdbb3aa5cf97bb128b Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 2 Jul 2026 17:06:53 +0300 Subject: [PATCH] Enhance dialogs and configuration handling in the admin interface - Updated `EditNodeDialog` and `RegisterNodeDialog` to prevent interaction outside the dialog, improving user experience. - Added data attributes to password fields in both dialogs for better password management. - Refactored `CreateConfigDialog` to accept inbounds as a prop, improving data handling and user feedback when no inbounds are available. - Introduced a warning banner in the root layout to inform users about the necessity of linking their Telegram account for notifications and password recovery. - Updated translations for improved clarity regarding available inbounds and Telegram linking requirements. --- .../features/admin/nodes/EditNodeDialog.tsx | 6 +++- .../admin/nodes/RegisterNodeDialog.tsx | 6 +++- .../features/configs/CreateConfigDialog.tsx | 14 +++----- .../telegram/TelegramLinkWarningBanner.tsx | 32 +++++++++++++++++++ frontend/src/routes/__root.tsx | 3 ++ frontend/src/routes/dashboard.tsx | 11 +++++-- frontend/src/shared/lib/i18n.ts | 16 ++++++++-- 7 files changed, 72 insertions(+), 16 deletions(-) create mode 100644 frontend/src/features/telegram/TelegramLinkWarningBanner.tsx diff --git a/frontend/src/features/admin/nodes/EditNodeDialog.tsx b/frontend/src/features/admin/nodes/EditNodeDialog.tsx index ded3a79..4eebc90 100644 --- a/frontend/src/features/admin/nodes/EditNodeDialog.tsx +++ b/frontend/src/features/admin/nodes/EditNodeDialog.tsx @@ -31,7 +31,7 @@ export function EditNodeDialog({ node, open, onOpenChange }: { node: NodeDto; op return ( - + e.preventDefault()}> {node.name} @@ -68,6 +68,10 @@ export function EditNodeDialog({ node, open, onOpenChange }: { node: NodeDto; op id="editPassword" type="password" autoComplete="new-password" + data-lpignore="true" + data-1p-ignore + data-bwignore + data-form-type="other" value={password} onChange={(e) => setPassword(e.target.value)} /> diff --git a/frontend/src/features/admin/nodes/RegisterNodeDialog.tsx b/frontend/src/features/admin/nodes/RegisterNodeDialog.tsx index 1ede827..e27a17b 100644 --- a/frontend/src/features/admin/nodes/RegisterNodeDialog.tsx +++ b/frontend/src/features/admin/nodes/RegisterNodeDialog.tsx @@ -41,7 +41,7 @@ export function RegisterNodeDialog() { - + e.preventDefault()}> {t('admin.nodes.create')} @@ -76,6 +76,10 @@ export function RegisterNodeDialog() { id="nodePassword" type="password" autoComplete="new-password" + data-lpignore="true" + data-1p-ignore + data-bwignore + data-form-type="other" value={password} onChange={(e) => setPassword(e.target.value)} required diff --git a/frontend/src/features/configs/CreateConfigDialog.tsx b/frontend/src/features/configs/CreateConfigDialog.tsx index ef15977..f251d7c 100644 --- a/frontend/src/features/configs/CreateConfigDialog.tsx +++ b/frontend/src/features/configs/CreateConfigDialog.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { Plus } from 'lucide-react' import { toast } from '@/shared/ui/toast-store' @@ -9,9 +9,10 @@ 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 { createConfig, listAvailableInbounds } from './api' +import type { AvailableInboundDto } from '@/shared/api/types' +import { createConfig } from './api' -export function CreateConfigDialog() { +export function CreateConfigDialog({ inbounds }: { inbounds: AvailableInboundDto[] }) { const { t } = useTranslation() const queryClient = useQueryClient() const [open, setOpen] = useState(false) @@ -19,8 +20,6 @@ export function CreateConfigDialog() { const [label, setLabel] = useState('') const [deviceLimit, setDeviceLimit] = useState('') - const inboundsQuery = useQuery({ queryKey: ['available-inbounds'], queryFn: listAvailableInbounds, enabled: open }) - const createMutation = useMutation({ mutationFn: () => createConfig(inboundId, label.trim() || undefined, deviceLimit ? Number(deviceLimit) : undefined), onSuccess: async () => { @@ -64,16 +63,13 @@ export function CreateConfigDialog() { - {inboundsQuery.data?.map((inbound) => ( + {inbounds.map((inbound) => ( {inbound.displayName} ({inbound.protocol}) ))} - {inboundsQuery.data?.length === 0 && ( -

{t('configs.noInboundsAvailable')}

- )}
diff --git a/frontend/src/features/telegram/TelegramLinkWarningBanner.tsx b/frontend/src/features/telegram/TelegramLinkWarningBanner.tsx new file mode 100644 index 0000000..066127a --- /dev/null +++ b/frontend/src/features/telegram/TelegramLinkWarningBanner.tsx @@ -0,0 +1,32 @@ +import { useState } from 'react' +import { Link } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' +import { X } from 'lucide-react' +import { useAuthStore } from '@/features/auth/store' + +export function TelegramLinkWarningBanner() { + const { t } = useTranslation() + const { user, isBootstrapping } = useAuthStore() + const [dismissed, setDismissed] = useState(false) + + if (isBootstrapping || !user || user.telegramLinked || dismissed) return null + + return ( +
+

+ {t('telegramLinkWarning.message')}{' '} + + {t('telegramLinkWarning.action')} + +

+ +
+ ) +} diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 916932f..bee8109 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -7,6 +7,7 @@ import { Toaster } from '@/shared/ui/toaster' import { Button } from '@/shared/ui/button' import { useAuthStore } from '@/features/auth/store' import { bootstrapSession, clearSession, logout } from '@/features/auth/api' +import { TelegramLinkWarningBanner } from '@/features/telegram/TelegramLinkWarningBanner' export const Route = createRootRoute({ component: RootLayout }) @@ -87,6 +88,8 @@ function RootLayout() { + +
diff --git a/frontend/src/routes/dashboard.tsx b/frontend/src/routes/dashboard.tsx index e2deecd..982932c 100644 --- a/frontend/src/routes/dashboard.tsx +++ b/frontend/src/routes/dashboard.tsx @@ -6,7 +6,7 @@ import { ActivationGate } from '@/features/activation/ActivationGate' import { ConfigCard } from '@/features/configs/ConfigCard' import { CreateConfigDialog } from '@/features/configs/CreateConfigDialog' import { SubscriptionCard } from '@/features/configs/SubscriptionCard' -import { getMyConfigs } from '@/features/configs/api' +import { getMyConfigs, listAvailableInbounds } from '@/features/configs/api' export const Route = createFileRoute('/dashboard')({ component: DashboardPage }) @@ -25,10 +25,11 @@ function DashboardPage() { function ConfigsList() { const { t } = useTranslation() const { data, isLoading } = useQuery({ queryKey: ['my-configs'], queryFn: getMyConfigs }) + const inboundsQuery = useQuery({ queryKey: ['available-inbounds'], queryFn: listAvailableInbounds }) return (
-
+

{t('configs.title')}

{data && ( @@ -39,7 +40,11 @@ function ConfigsList() {

)}
- + {inboundsQuery.data?.length === 0 ? ( +

{t('configs.noInboundsNotice')}

+ ) : ( + inboundsQuery.data && + )}
{!isLoading && data && data.configs.length > 0 && } diff --git a/frontend/src/shared/lib/i18n.ts b/frontend/src/shared/lib/i18n.ts index 7ff05a2..b347d30 100644 --- a/frontend/src/shared/lib/i18n.ts +++ b/frontend/src/shared/lib/i18n.ts @@ -36,6 +36,12 @@ const resources = { telegramLoginExpired: 'Время ожидания истекло, попробуйте снова.', }, + telegramLinkWarning: { + message: 'Вы не привязали Telegram — без этого вы не будете получать уведомления и не сможете самостоятельно восстановить пароль.', + action: 'Привязать Telegram', + dismiss: 'Скрыть', + }, + nav: { dashboard: 'Мои конфиги', instructions: 'Инструкции', @@ -66,7 +72,7 @@ const resources = { label: 'Метка (необязательно)', deviceLimitLabel: 'Лимит устройств (необязательно)', deviceLimitPlaceholder: 'Без лимита', - noInboundsAvailable: 'Нет доступных локаций для вашей роли.', + noInboundsNotice: 'Пока нет доступных локаций для создания конфига. Обратитесь к администратору — необходимо, чтобы он добавил сервер.', created: 'Конфиг создан.', quotaExceeded: 'Достигнут лимит конфигов для вашей роли.', showLink: 'Ссылка / QR', @@ -297,6 +303,12 @@ const resources = { telegramLoginExpired: 'The request expired, please try again.', }, + telegramLinkWarning: { + message: "You haven't linked Telegram — without it you won't receive notifications and won't be able to recover your password yourself.", + action: 'Link Telegram', + dismiss: 'Dismiss', + }, + nav: { dashboard: 'My configs', instructions: 'Instructions', @@ -327,7 +339,7 @@ const resources = { label: 'Label (optional)', deviceLimitLabel: 'Device limit (optional)', deviceLimitPlaceholder: 'Unlimited', - noInboundsAvailable: 'No locations available for your role.', + noInboundsNotice: 'No locations are available for creating a config yet. Please contact the administrator — a server needs to be added.', created: 'Config created.', quotaExceeded: 'Config quota reached for your role.', showLink: 'Link / QR',