From e268376d0a00f02be0827e7a6955738ae7f9d3c5 Mon Sep 17 00:00:00 2001 From: Sinacket Date: Fri, 11 Apr 2025 13:54:23 +0330 Subject: [PATCH 1/3] feat: added download settings | enchaned tcp header settings in transport settings --- dashboard/public/statics/locales/en.json | 108 +++- dashboard/public/statics/locales/fa.json | 114 +++- dashboard/public/statics/locales/ru.json | 126 +++- dashboard/public/statics/locales/zh.json | 120 +++- .../src/components/dialogs/HostModal.tsx | 543 ++++++++++++++---- dashboard/src/components/hosts/HostForm.tsx | 278 --------- dashboard/src/components/hosts/Hosts.tsx | 274 ++++----- dashboard/src/index.css | 2 +- dashboard/src/pages/_dashboard.hosts.tsx | 31 +- path/to/components/HostModal.tsx | 1 + 10 files changed, 972 insertions(+), 625 deletions(-) delete mode 100644 dashboard/src/components/hosts/HostForm.tsx create mode 100644 path/to/components/HostModal.tsx diff --git a/dashboard/public/statics/locales/en.json b/dashboard/public/statics/locales/en.json index e041c5814..d64c36247 100644 --- a/dashboard/public/statics/locales/en.json +++ b/dashboard/public/statics/locales/en.json @@ -198,7 +198,10 @@ "cMaxReuseTimes": "Max Reuse Times", "cMaxLifetime": "Max Lifetime", "hMaxRequestTimes": "Max Request Times", - "hKeepAlivePeriod": "Keep Alive Period" + "hKeepAlivePeriod": "Keep Alive Period", + "downloadSettings": "Download Settings", + "downloadSettingsInfo": "Select a host to use for download settings", + "selectDownloadSettings": "Select download settings host" }, "grpc": { "multiMode": "Multi Mode", @@ -218,14 +221,27 @@ "writeBufferSize": "Write Buffer Size" }, "tcp": { - "header": "Header", - "request": "Request", - "version": "Version", - "headers": "Headers", - "method": "Method", - "response": "Response", - "status": "Status", - "reason": "Reason" + "title": "TCP Settings", + "header": "Header Type", + "request": { + "title": "Request Settings", + "version": "HTTP Version", + "method": "HTTP Method", + "headers": "Request Headers" + }, + "response": { + "title": "Response Settings", + "version": "HTTP Version", + "status": "Status Code", + "reason": "Status Reason", + "headers": "Response Headers" + }, + "headerName": "Header Name", + "headerValue": "Header Value", + "addHeader": "Add Header", + "removeHeader": "Remove Header", + "requestHeaders": "Request Headers", + "responseHeaders": "Response Headers" }, "websocket": { "heartbeatPeriod": "Heartbeat Period" @@ -314,7 +330,79 @@ "host.multiHost": "To set multiple addresses, separate them with ,. A random address will be selected each time.", "host.wildcard": "Use * to generate a random string (works for wildcard domains)", "sni.info": "SNI (Server Name Indication) is used to specify which hostname the client is trying to connect to. This is particularly useful when you have multiple domains pointing to the same IP address.", - "useSniAsHost": "Use SNI as Host" + "useSniAsHost": "Use SNI as Host", + "httpVersions": { + "1.0": "HTTP/1.0", + "1.1": "HTTP/1.1", + "2.0": "HTTP/2.0", + "3.0": "HTTP/3.0" + }, + "httpMethods": { + "GET": "GET", + "POST": "POST", + "PUT": "PUT", + "DELETE": "DELETE", + "HEAD": "HEAD", + "OPTIONS": "OPTIONS", + "PATCH": "PATCH", + "TRACE": "TRACE", + "CONNECT": "CONNECT" + }, + "httpReasons": { + "100": "Continue", + "101": "Switching Protocols", + "200": "OK", + "201": "Created", + "202": "Accepted", + "203": "Non-Authoritative Information", + "204": "No Content", + "205": "Reset Content", + "206": "Partial Content", + "300": "Multiple Choices", + "301": "Moved Permanently", + "302": "Found", + "303": "See Other", + "304": "Not Modified", + "305": "Use Proxy", + "307": "Temporary Redirect", + "308": "Permanent Redirect", + "400": "Bad Request", + "401": "Unauthorized", + "402": "Payment Required", + "403": "Forbidden", + "404": "Not Found", + "405": "Method Not Allowed", + "406": "Not Acceptable", + "407": "Proxy Authentication Required", + "408": "Request Timeout", + "409": "Conflict", + "410": "Gone", + "411": "Length Required", + "412": "Precondition Failed", + "413": "Payload Too Large", + "414": "URI Too Long", + "415": "Unsupported Media Type", + "416": "Range Not Satisfiable", + "417": "Expectation Failed", + "418": "I'm a teapot", + "421": "Misdirected Request", + "422": "Unprocessable Entity", + "423": "Locked", + "424": "Failed Dependency", + "425": "Too Early", + "426": "Upgrade Required", + "428": "Precondition Required", + "429": "Too Many Requests", + "431": "Request Header Fields Too Large", + "451": "Unavailable For Legal Reasons", + "500": "Internal Server Error", + "501": "Not Implemented", + "502": "Bad Gateway", + "503": "Service Unavailable", + "504": "Gateway Timeout", + "505": "HTTP Version Not Supported" + }, + "selectReason": "Select Reason" }, "inbound": "Inbound", "remark": "Host Name", diff --git a/dashboard/public/statics/locales/fa.json b/dashboard/public/statics/locales/fa.json index db4598435..b34bd3380 100644 --- a/dashboard/public/statics/locales/fa.json +++ b/dashboard/public/statics/locales/fa.json @@ -159,14 +159,17 @@ "scMaxEachPostBytes": "حداکثر بایت هر پست", "scMinPostsIntervalMs": "حداقل فاصله پست‌ها (میلی‌ثانیه)", "scMaxBufferedPosts": "حداکثر پست‌های بافر شده", - "scStreamUpServerSecs": "جریان بالا سرور (ثانیه)", + "scStreamUpServerSecs": "سرور استریم بالا (ثانیه)", "xmux": "تنظیمات XMux", - "maxConcurrency": "حداکثر هم‌زمانی", + "maxConcurrency": "حداکثر همزمانی", "maxConnections": "حداکثر اتصالات", "cMaxReuseTimes": "حداکثر دفعات استفاده مجدد", "cMaxLifetime": "حداکثر طول عمر", - "hMaxRequestTimes": "حداکثر زمان‌های درخواست", - "hKeepAlivePeriod": "دوره نگهداری اتصال" + "hMaxRequestTimes": "حداکثر دفعات درخواست", + "hKeepAlivePeriod": "دوره نگهداری اتصال", + "downloadSettings": "تنظیمات دانلود", + "downloadSettingsInfo": "انتخاب هاست برای تنظیمات دانلود", + "selectDownloadSettings": "انتخاب هاست تنظیمات دانلود" }, "grpc": { "multiMode": "حالت چندگانه", @@ -186,14 +189,27 @@ "writeBufferSize": "اندازه بافر نوشتن" }, "tcp": { - "header": "هدر", - "request": "درخواست", - "version": "نسخه", - "headers": "هدرها", - "method": "متد", - "response": "پاسخ", - "status": "وضعیت", - "reason": "دلیل" + "title": "تنظیمات TCP", + "header": "نوع هدر", + "request": { + "title": "تنظیمات درخواست", + "version": "نسخه HTTP", + "method": "متد HTTP", + "headers": "هدرهای درخواست" + }, + "response": { + "title": "تنظیمات پاسخ", + "version": "نسخه HTTP", + "status": "کد وضعیت", + "reason": "دلیل وضعیت", + "headers": "هدرهای پاسخ" + }, + "headerName": "نام هدر", + "headerValue": "مقدار هدر", + "addHeader": "افزودن هدر", + "removeHeader": "حذف هدر", + "requestHeaders": "هدرهای درخواست", + "responseHeaders": "هدرهای پاسخ" }, "websocket": { "heartbeatPeriod": "دوره ضربان قلب" @@ -274,7 +290,79 @@ "port.info": "به طور پیش‌فرض، هاست از پورت اعلام شده در ورودی استفاده می‌کند. اگر ترافیک از پورت دیگری هدایت می‌شود، می‌توانید پورت سفارشی تنظیم کنید. به عنوان مثال، سرور ممکن است ترافیک را از پورت 443 به پورت پیش‌فرض ورودی شما هدایت کند.", "proxyOutbound.info": "ترافیک خروجی اضافی (فقط در پیکربندی سفارشی v2ray)", "security.info": "اگر سرور واسط این هاست از لایه امنیتی متفاوتی نسبت به پیش‌فرض ورودی شما استفاده می‌کند، می‌توانید لایه امنیتی سفارشی را اینجا تنظیم کنید.", - "useSniAsHost": "استفاده از SNI به عنوان هاست" + "useSniAsHost": "استفاده از SNI به عنوان هاست", + "httpVersions": { + "1.0": "HTTP/1.0", + "1.1": "HTTP/1.1", + "2.0": "HTTP/2.0", + "3.0": "HTTP/3.0" + }, + "httpMethods": { + "GET": "GET", + "POST": "POST", + "PUT": "PUT", + "DELETE": "DELETE", + "HEAD": "HEAD", + "OPTIONS": "OPTIONS", + "PATCH": "PATCH", + "TRACE": "TRACE", + "CONNECT": "CONNECT" + }, + "httpReasons": { + "100": "ادامه", + "101": "تغییر پروتکل", + "200": "موفق", + "201": "ایجاد شد", + "202": "پذیرفته شد", + "203": "اطلاعات غیرمجاز", + "204": "بدون محتوا", + "205": "بازنشانی محتوا", + "206": "محتوی جزئی", + "300": "انتخاب چندگانه", + "301": "انتقال دائمی", + "302": "پیدا شد", + "303": "مشاهده مکان دیگر", + "304": "تغییر نکرده", + "305": "استفاده از پراکسی", + "307": "انتقال موقت", + "308": "انتقال دائمی", + "400": "درخواست نامعتبر", + "401": "غیرمجاز", + "402": "نیاز به پرداخت", + "403": "ممنوع", + "404": "پیدا نشد", + "405": "متد مجاز نیست", + "406": "قابل قبول نیست", + "407": "نیاز به احراز هویت پراکسی", + "408": "زمان درخواست به پایان رسید", + "409": "تضاد", + "410": "حذف شده", + "411": "نیاز به طول", + "412": "شرط اولیه برقرار نیست", + "413": "بار درخواست خیلی بزرگ است", + "414": "URI خیلی طولانی است", + "415": "نوع رسانه پشتیبانی نمی‌شود", + "416": "محدوده درخواست قابل اجرا نیست", + "417": "انتظار برآورده نشد", + "418": "من یک قوری هستم", + "421": "درخواست اشتباه", + "422": "موجودیت غیرقابل پردازش", + "423": "قفل شده", + "424": "وابستگی شکست خورده", + "425": "خیلی زود", + "426": "نیاز به ارتقا", + "428": "نیاز به شرط اولیه", + "429": "درخواست‌های خیلی زیاد", + "431": "فیلدهای هدر درخواست خیلی بزرگ هستند", + "451": "به دلایل قانونی در دسترس نیست", + "500": "خطای داخلی سرور", + "501": "پیاده‌سازی نشده", + "502": "درگاه نامعتبر", + "503": "سرویس در دسترس نیست", + "504": "زمان درگاه به پایان رسید", + "505": "نسخه HTTP پشتیبانی نمی‌شود" + }, + "selectReason": "انتخاب دلیل" }, "inbound": "ورودی", "remark": "اسم هاست", diff --git a/dashboard/public/statics/locales/ru.json b/dashboard/public/statics/locales/ru.json index af317147c..6fc80f5b6 100644 --- a/dashboard/public/statics/locales/ru.json +++ b/dashboard/public/statics/locales/ru.json @@ -373,17 +373,20 @@ "mode": "Режим", "noGrpcHeader": "Без заголовка gRPC", "xPaddingBytes": "X-Padding байты", - "scMaxEachPostBytes": "Макс. байт на пост", - "scMinPostsIntervalMs": "Мин. интервал постов (мс)", - "scMaxBufferedPosts": "Макс. буферизованных постов", - "scStreamUpServerSecs": "Поток сервера (сек)", + "scMaxEachPostBytes": "Максимум байт на пост", + "scMinPostsIntervalMs": "Минимальный интервал постов (мс)", + "scMaxBufferedPosts": "Максимум буферизованных постов", + "scStreamUpServerSecs": "Сервер стриминга (сек)", "xmux": "Настройки XMux", - "maxConcurrency": "Макс. параллелизм", - "maxConnections": "Макс. подключений", - "cMaxReuseTimes": "Макс. повторных использований", - "cMaxLifetime": "Макс. время жизни", - "hMaxRequestTimes": "Макс. запросов", - "hKeepAlivePeriod": "Период поддержания связи" + "maxConcurrency": "Максимум параллельных", + "maxConnections": "Максимум соединений", + "cMaxReuseTimes": "Максимум повторных использований", + "cMaxLifetime": "Максимальный срок службы", + "hMaxRequestTimes": "Максимум запросов", + "hKeepAlivePeriod": "Период поддержания соединения", + "downloadSettings": "Настройки загрузки", + "downloadSettingsInfo": "Выберите хост для настроек загрузки", + "selectDownloadSettings": "Выберите хост настроек загрузки" }, "grpc": { "multiMode": "Мульти режим", @@ -403,14 +406,27 @@ "writeBufferSize": "Размер буфера записи" }, "tcp": { - "header": "Заголовок", - "request": "Запрос", - "version": "Версия", - "headers": "Заголовки", - "method": "Метод", - "response": "Ответ", - "status": "Статус", - "reason": "Причина" + "title": "Настройки TCP", + "header": "Тип заголовка", + "request": { + "title": "Настройки запроса", + "version": "Версия HTTP", + "method": "Метод HTTP", + "headers": "Заголовки запроса" + }, + "response": { + "title": "Настройки ответа", + "version": "Версия HTTP", + "status": "Код состояния", + "reason": "Причина состояния", + "headers": "Заголовки ответа" + }, + "headerName": "Имя заголовка", + "headerValue": "Значение заголовка", + "addHeader": "Добавить заголовок", + "removeHeader": "Удалить заголовок", + "requestHeaders": "Заголовки запроса", + "responseHeaders": "Заголовки ответа" }, "websocket": { "heartbeatPeriod": "Период сердцебиения" @@ -457,7 +473,79 @@ "status_emoji": "Статус пользователя в виде эмодзи (✅,⌛️,🪫,❌,🔌)", "protocol": "Протокол прокси (например, VMess)", "transport": "Метод транспорта прокси (например, ws)" - } + }, + "httpVersions": { + "1.0": "HTTP/1.0", + "1.1": "HTTP/1.1", + "2.0": "HTTP/2.0", + "3.0": "HTTP/3.0" + }, + "httpMethods": { + "GET": "GET", + "POST": "POST", + "PUT": "PUT", + "DELETE": "DELETE", + "HEAD": "HEAD", + "OPTIONS": "OPTIONS", + "PATCH": "PATCH", + "TRACE": "TRACE", + "CONNECT": "CONNECT" + }, + "httpReasons": { + "100": "Продолжить", + "101": "Переключение протоколов", + "200": "OK", + "201": "Создано", + "202": "Принято", + "203": "Неавторитетная информация", + "204": "Нет содержимого", + "205": "Сбросить содержимое", + "206": "Частичное содержимое", + "300": "Множественный выбор", + "301": "Перемещено навсегда", + "302": "Найдено", + "303": "Смотреть другое", + "304": "Не изменялось", + "305": "Использовать прокси", + "307": "Временное перенаправление", + "308": "Постоянное перенаправление", + "400": "Неверный запрос", + "401": "Не авторизован", + "402": "Необходима оплата", + "403": "Запрещено", + "404": "Не найдено", + "405": "Метод не разрешен", + "406": "Неприемлемо", + "407": "Требуется аутентификация прокси", + "408": "Тайм-аут запроса", + "409": "Конфликт", + "410": "Удалено", + "411": "Требуется длина", + "412": "Предварительное условие не выполнено", + "413": "Полезная нагрузка слишком велика", + "414": "URI слишком длинный", + "415": "Неподдерживаемый тип носителя", + "416": "Диапазон не выполним", + "417": "Ожидание не выполнено", + "418": "Я чайник", + "421": "Неправильно направленный запрос", + "422": "Необрабатываемая сущность", + "423": "Заблокировано", + "424": "Невыполненная зависимость", + "425": "Слишком рано", + "426": "Требуется обновление", + "428": "Требуется предварительное условие", + "429": "Слишком много запросов", + "431": "Поля заголовка запроса слишком большие", + "451": "Недоступно по юридическим причинам", + "500": "Внутренняя ошибка сервера", + "501": "Не реализовано", + "502": "Ошибочный шлюз", + "503": "Служба недоступна", + "504": "Тайм-аут шлюза", + "505": "Версия HTTP не поддерживается" + }, + "selectReason": "Выбрать причину" }, "enable": "Включить", "host": { diff --git a/dashboard/public/statics/locales/zh.json b/dashboard/public/statics/locales/zh.json index b73684aa2..ea57139cf 100644 --- a/dashboard/public/statics/locales/zh.json +++ b/dashboard/public/statics/locales/zh.json @@ -370,19 +370,22 @@ "transportSettingsAccordion": "传输设置", "xhttp": { "mode": "模式", - "noGrpcHeader": "无 gRPC 标头", + "noGrpcHeader": "无 gRPC 头", "xPaddingBytes": "X-Padding 字节", - "scMaxEachPostBytes": "每帖最大字节", - "scMinPostsIntervalMs": "帖子最小间隔 (毫秒)", - "scMaxBufferedPosts": "最大缓冲帖子", - "scStreamUpServerSecs": "服务器上行流 (秒)", + "scMaxEachPostBytes": "每个帖子最大字节数", + "scMinPostsIntervalMs": "帖子最小间隔(毫秒)", + "scMaxBufferedPosts": "最大缓冲帖子数", + "scStreamUpServerSecs": "流上传服务器(秒)", "xmux": "XMux 设置", "maxConcurrency": "最大并发数", "maxConnections": "最大连接数", - "cMaxReuseTimes": "最大复用次数", + "cMaxReuseTimes": "最大重用次数", "cMaxLifetime": "最大生命周期", "hMaxRequestTimes": "最大请求次数", - "hKeepAlivePeriod": "保持活动周期" + "hKeepAlivePeriod": "保活周期", + "downloadSettings": "下载设置", + "downloadSettingsInfo": "选择用于下载设置的主机", + "selectDownloadSettings": "选择下载设置主机" }, "grpc": { "multiMode": "多模式", @@ -402,14 +405,27 @@ "writeBufferSize": "写缓冲区大小" }, "tcp": { - "header": "头部", - "request": "请求", - "version": "版本", - "headers": "标头", - "method": "方法", - "response": "响应", - "status": "状态", - "reason": "原因" + "title": "TCP 设置", + "header": "头部类型", + "request": { + "title": "请求设置", + "version": "HTTP 版本", + "method": "HTTP 方法", + "headers": "请求头" + }, + "response": { + "title": "响应设置", + "version": "HTTP 版本", + "status": "状态码", + "reason": "状态原因", + "headers": "响应头" + }, + "headerName": "头部名称", + "headerValue": "头部值", + "addHeader": "添加头部", + "removeHeader": "删除头部", + "requestHeaders": "请求头", + "responseHeaders": "响应头" }, "websocket": { "heartbeatPeriod": "心跳周期" @@ -456,7 +472,79 @@ "status_emoji": "用户状态表情符号 (✅,⌛️,🪫,❌,🔌)", "protocol": "代理协议(如 VMess)", "transport": "代理传输方式(如 ws)" - } + }, + "httpVersions": { + "1.0": "HTTP/1.0", + "1.1": "HTTP/1.1", + "2.0": "HTTP/2.0", + "3.0": "HTTP/3.0" + }, + "httpMethods": { + "GET": "GET", + "POST": "POST", + "PUT": "PUT", + "DELETE": "DELETE", + "HEAD": "HEAD", + "OPTIONS": "OPTIONS", + "PATCH": "PATCH", + "TRACE": "TRACE", + "CONNECT": "CONNECT" + }, + "httpReasons": { + "100": "继续", + "101": "切换协议", + "200": "成功", + "201": "已创建", + "202": "已接受", + "203": "非授权信息", + "204": "无内容", + "205": "重置内容", + "206": "部分内容", + "300": "多种选择", + "301": "永久移动", + "302": "临时移动", + "303": "查看其他位置", + "304": "未修改", + "305": "使用代理", + "307": "临时重定向", + "308": "永久重定向", + "400": "错误请求", + "401": "未授权", + "402": "需要支付", + "403": "禁止", + "404": "未找到", + "405": "方法禁用", + "406": "不接受", + "407": "需要代理授权", + "408": "请求超时", + "409": "冲突", + "410": "已删除", + "411": "需要有效长度", + "412": "未满足前提条件", + "413": "请求实体过大", + "414": "请求的 URI 过长", + "415": "不支持的媒体类型", + "416": "请求范围不符合要求", + "417": "未满足期望值", + "418": "我是茶壶", + "421": "错误的请求", + "422": "不可处理的实体", + "423": "已锁定", + "424": "失败的依赖", + "425": "太早", + "426": "需要升级", + "428": "需要先决条件", + "429": "太多请求", + "431": "请求头字段太大", + "451": "因法律原因不可用", + "500": "服务器内部错误", + "501": "尚未实施", + "502": "错误网关", + "503": "服务不可用", + "504": "网关超时", + "505": "HTTP 版本不受支持" + }, + "selectReason": "选择原因" }, "enable": "启用", "host": { diff --git a/dashboard/src/components/dialogs/HostModal.tsx b/dashboard/src/components/dialogs/HostModal.tsx index da3a7065e..5d4fb28e7 100644 --- a/dashboard/src/components/dialogs/HostModal.tsx +++ b/dashboard/src/components/dialogs/HostModal.tsx @@ -2,7 +2,7 @@ import { UseFormReturn } from "react-hook-form" import { HostFormValues } from "../hosts/Hosts" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" import { Input } from "@/components/ui/input" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectGroup, SelectLabel } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { Checkbox } from "@/components/ui/checkbox" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" @@ -12,7 +12,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { useState } from "react" import { useTranslation } from "react-i18next" import { useQuery } from "@tanstack/react-query" -import { getInbounds, UserStatus } from "@/service/api" +import { getInbounds, UserStatus, getHosts } from "@/service/api" import { Cable, ChevronsLeftRightEllipsis, GlobeLock, Lock, Plus, Trash2, Network, Info } from "lucide-react" import { toast } from "@/hooks/use-toast" import useDirDetection from "@/hooks/use-dir-detection" @@ -45,6 +45,7 @@ const HostModal: React.FC = ({ form }) => { const [openSection, setOpenSection] = useState(undefined); + const [isTransportOpen, setIsTransportOpen] = useState(false); const { t } = useTranslation(); const dir = useDirDetection(); @@ -98,7 +99,17 @@ const HostModal: React.FC = ({ queryFn: () => getInbounds(), }); + const { data: hosts = [] } = useQuery({ + queryKey: ["getHostsQueryKey"], + queryFn: () => getHosts(), + enabled: isTransportOpen, + select: (data) => data.filter((host) => host.id != null), // Filter out hosts with null IDs + }); + const handleAccordionChange = (value: string) => { + if (value === "transport") { + setIsTransportOpen(true); + } setOpenSection((prevSection) => (prevSection === value ? undefined : value)); }; @@ -443,7 +454,7 @@ const HostModal: React.FC = ({ - + @@ -1079,16 +1090,16 @@ const HostModal: React.FC = ({
- - XHTTP - gRPC - KCP - TCP - WebSocket + + XHTTP + gRPC + KCP + TCP + WebSocket {/* XHTTP Settings */} - +
= ({ control={form.control} name="transport_settings.xhttp_settings.xmux.max_concurrency" render={({ field }) => ( - + {t("hostsDialog.xhttp.maxConcurrency")} @@ -1223,7 +1234,7 @@ const HostModal: React.FC = ({ control={form.control} name="transport_settings.xhttp_settings.xmux.max_connections" render={({ field }) => ( - + {t("hostsDialog.xhttp.maxConnections")} @@ -1237,7 +1248,7 @@ const HostModal: React.FC = ({ control={form.control} name="transport_settings.xhttp_settings.xmux.c_max_reuse_times" render={({ field }) => ( - + {t("hostsDialog.xhttp.cMaxReuseTimes")} @@ -1251,7 +1262,7 @@ const HostModal: React.FC = ({ control={form.control} name="transport_settings.xhttp_settings.xmux.c_max_lifetime" render={({ field }) => ( - + {t("hostsDialog.xhttp.cMaxLifetime")} @@ -1265,7 +1276,7 @@ const HostModal: React.FC = ({ control={form.control} name="transport_settings.xhttp_settings.xmux.h_max_request_times" render={({ field }) => ( - + {t("hostsDialog.xhttp.hMaxRequestTimes")} @@ -1279,7 +1290,7 @@ const HostModal: React.FC = ({ control={form.control} name="transport_settings.xhttp_settings.xmux.h_keep_alive_period" render={({ field }) => ( - + {t("hostsDialog.xhttp.hKeepAlivePeriod")} @@ -1288,12 +1299,60 @@ const HostModal: React.FC = ({ )} /> + ( + +
+ {t("hostsDialog.xhttp.downloadSettings")} + + + + + +

+ {t("hostsDialog.xhttp.downloadSettingsInfo")} +

+
+
+
+ + +
+ )} + />
+ {/* gRPC Settings */} - +
= ({ {/* KCP Settings */} - +
= ({ {/* TCP Settings */} - +
( - + {t("hostsDialog.tcp.header")} - - - - )} - /> + {form.watch("transport_settings.tcp_settings.header") === "http" && ( + <> +
+

{t("hostsDialog.tcp.request.title")}

+
+ ( + + {t("hostsDialog.tcp.request.version")} + + + + )} + /> - ( - - {t("hostsDialog.tcp.method")} - - - - )} - /> -
-
+ ( + + {t("hostsDialog.tcp.request.method")} + + + + )} + /> +
-
-

{t("hostsDialog.tcp.response")}

-
- ( - - {t("hostsDialog.tcp.version")} - - - - - - )} - /> + {/* Request Headers */} +
+
+

{t("hostsDialog.tcp.requestHeaders")}

+ +
- ( - - {t("hostsDialog.tcp.status")} - - - - - - )} - /> + {/* Render request headers */} + {Object.entries(form.watch("transport_settings.tcp_settings.request.headers") || {}).map(([key, values]) => ( +
+ { + if (e.target.value !== key) { + const currentHeaders = { ...form.getValues("transport_settings.tcp_settings.request.headers") }; + const oldValues = currentHeaders[key]; + delete currentHeaders[key]; + currentHeaders[e.target.value] = oldValues; + form.setValue("transport_settings.tcp_settings.request.headers", currentHeaders, { + shouldDirty: true, + shouldTouch: true + }); + } + }} + /> + { + const currentHeaders = { ...form.getValues("transport_settings.tcp_settings.request.headers") }; + currentHeaders[key] = e.target.value.split(",").map(v => v.trim()); + form.setValue("transport_settings.tcp_settings.request.headers", currentHeaders, { + shouldDirty: true, + shouldTouch: true + }); + }} + /> + +
+ ))} +
+
- ( - - {t("hostsDialog.tcp.reason")} - - - - - - )} - /> -
-
+
+

{t("hostsDialog.tcp.response.title")}

+
+ ( + + {t("hostsDialog.tcp.response.version")} + + + + )} + /> + + ( + + {t("hostsDialog.tcp.response.status")} + + + + + + )} + /> + + ( + + {t("hostsDialog.tcp.response.reason")} + + + + )} + /> +
+ + {/* Response Headers */} +
+
+

{t("hostsDialog.tcp.responseHeaders")}

+ +
+ + {/* Render response headers */} + {Object.entries(form.watch("transport_settings.tcp_settings.response.headers") || {}).map(([key, values]) => ( +
+ { + if (e.target.value !== key) { + const currentHeaders = { ...form.getValues("transport_settings.tcp_settings.response.headers") }; + const oldValues = currentHeaders[key]; + delete currentHeaders[key]; + currentHeaders[e.target.value] = oldValues; + form.setValue("transport_settings.tcp_settings.response.headers", currentHeaders, { + shouldDirty: true, + shouldTouch: true + }); + } + }} + /> + { + const currentHeaders = { ...form.getValues("transport_settings.tcp_settings.response.headers") }; + currentHeaders[key] = e.target.value.split(",").map(v => v.trim()); + form.setValue("transport_settings.tcp_settings.response.headers", currentHeaders, { + shouldDirty: true, + shouldTouch: true + }); + }} + /> + +
+ ))} +
+
+ + )}
{/* WebSocket Settings */} @@ -1793,7 +2117,7 @@ const HostModal: React.FC = ({ {t("hostsDialog.protocol")} field.onChange(value === "null" ? undefined : value)} value={field.value ?? "null"} > @@ -1951,8 +2275,7 @@ const HostModal: React.FC = ({ - {t("none")} - h2 + {t("none")} smux yamux h2mux diff --git a/dashboard/src/components/hosts/HostForm.tsx b/dashboard/src/components/hosts/HostForm.tsx deleted file mode 100644 index b441d0855..000000000 --- a/dashboard/src/components/hosts/HostForm.tsx +++ /dev/null @@ -1,278 +0,0 @@ -"use client" -import { useState } from "react" -import { useForm } from "react-hook-form" -import { z } from "zod" -import { zodResolver } from "@hookform/resolvers/zod" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" -import { toast } from "@/components/ui/use-toast" -import { useTranslation } from "react-i18next" -import { useQueryClient } from "react-query" - -const hostFormSchema = z.object({ - inbound_tag: z.string().min(1, "Inbound is required"), - remark: z.string().min(1, "Remark is required"), - address: z.string().ip("Invalid IP address"), - port: z - .string() - .regex(/^\d+$/, "Port must be a number") - .transform(Number) - .refine((n) => n >= 1 && n <= 65535, "Port must be between 1 and 65535") - .optional(), - sni: z.string().optional(), - host: z.string().optional(), - path: z.string().optional(), - security: z.string().optional(), - alpn: z.string().optional(), - fingerprint: z.string().optional(), - allowinsecure: z.boolean().optional(), - is_disabled: z.boolean().optional(), - mux_enable: z.boolean().optional(), - fragment_setting: z.string().optional(), - random_user_agent: z.boolean().optional(), - noise_setting: z.string().optional(), - use_sni_as_host: z.boolean().optional(), -}) - -type HostFormValues = z.infer - -interface HostFormProps { - host?: HostFormValues -} - -export function HostForm({ host }: HostFormProps) { - const [openSection, setOpenSection] = useState(undefined) - const { t } = useTranslation() - const queryClient = useQueryClient() - - const form = useForm({ - resolver: zodResolver(hostFormSchema), - defaultValues: host || { - inbound_tag: "", - remark: "", - address: "", - port: undefined, // Since port is a number, use `undefined` instead of an empty string - sni: "", - host: "", - path: "", - security: "", - alpn: "", - fingerprint: "", - allowinsecure: false, - is_disabled: false, - mux_enable: false, - fragment_setting: "", - random_user_agent: false, - noise_setting: "", - use_sni_as_host: false, - }, - }) - - const handleAccordionChange = (value: string) => { - setOpenSection((prevSection) => (prevSection === value ? undefined : value)) - } - - const onSubmit = async (data: HostFormValues) => { - try { - const response = await onSubmit(data); - if (response.status >= 400) { - throw new Error(`Operation failed with status: ${response.status}`); - } - // Only show success toast and close modal if the operation was successful - toast({ - dir, - description: t(editingHost - ? "hostsDialog.editSuccess" - : "hostsDialog.createSuccess", - { name: data.remark } - ), - }); - // Close the modal - handleModalOpenChange(false); - // The form reset is handled by the parent component - // Invalidate hosts query to refresh the list - queryClient.invalidateQueries({ - queryKey: ["getGetHostsQueryKey"], - }); - } catch (error) { - // Show error toast if the operation failed - toast({ - dir, - variant: "destructive", - description: t(editingHost - ? "hostsDialog.editFailed" - : "hostsDialog.createFailed", - { name: data.remark } - ), - }); - // Don't close the modal or reset the form on error - } - }; - - return ( -
- -
- ( - - Inbound - - - - )} - /> - - ( - - Remark - - - - - - )} - /> - -
- ( - - Address - - - - - - )} - /> - ( - - Port - - - - - - )} - /> -
-
- - - - Network Settings - -
- ( - - SNI - - - - - - )} - /> - ( - - Request Host - - - - - - )} - /> - ( - - Path - - - - - - )} - /> -
-
-
- - - Transport Settings - -
{/* Add transport settings fields */}
-
-
- - - Security Settings - -
{/* Add security settings fields */}
-
-
- - - Camouflage Settings - -
{/* Add camouflage settings fields */}
-
-
- - - Mux Settings - -
{/* Add mux settings fields */}
-
-
-
- -
- - -
-
- - ) -} - diff --git a/dashboard/src/components/hosts/Hosts.tsx b/dashboard/src/components/hosts/Hosts.tsx index 6f84d9848..79571be82 100644 --- a/dashboard/src/components/hosts/Hosts.tsx +++ b/dashboard/src/components/hosts/Hosts.tsx @@ -25,7 +25,7 @@ interface XrayMuxSettings { } interface SingBoxMuxSettings { - protocol: string | null; + protocol: string | null | undefined; max_connections: number | null; max_streams: number | null; min_streams: number | null; @@ -34,7 +34,7 @@ interface SingBoxMuxSettings { } interface ClashMuxSettings { - protocol: string | null; + protocol: string | null | undefined; max_connections: number | null; max_streams: number | null; min_streams: number | null; @@ -93,6 +93,7 @@ export interface HostFormValues { sc_min_posts_interval_ms?: string; sc_max_buffered_posts?: string; sc_stream_up_server_secs?: string; + download_settings?: number; xmux?: { max_concurrency?: string; max_connections?: string; @@ -142,61 +143,90 @@ export interface HostFormValues { // Update the transport settings schema const transportSettingsSchema = z.object({ xhttp_settings: z.object({ - mode: z.enum(["auto", "packet-up", "stream-up", "stream-one"]).nullish(), - no_grpc_header: z.boolean().nullish(), - x_padding_bytes: z.string().nullish(), - sc_max_each_post_bytes: z.string().nullish(), - sc_min_posts_interval_ms: z.string().nullish(), - sc_max_buffered_posts: z.string().nullish(), - sc_stream_up_server_secs: z.string().nullish() - }).nullish(), + mode: z.enum(["auto", "packet-up", "stream-up", "stream-one"]).nullish().optional(), + no_grpc_header: z.boolean().nullish().optional(), + x_padding_bytes: z.string().nullish().optional(), + sc_max_each_post_bytes: z.string().nullish().optional(), + sc_min_posts_interval_ms: z.string().nullish().optional(), + sc_max_buffered_posts: z.string().nullish().optional(), + sc_stream_up_server_secs: z.string().nullish().optional(), + download_settings: z.number().nullish().optional(), + xmux: z.object({ + max_concurrency: z.string().nullish().optional(), + max_connections: z.string().nullish().optional(), + c_max_reuse_times: z.string().nullish().optional(), + c_max_lifetime: z.string().nullish().optional(), + h_max_request_times: z.string().nullish().optional(), + h_keep_alive_period: z.string().nullish().optional() + }).nullish().optional() + }).nullish().optional(), grpc_settings: z.object({ - multi_mode: z.boolean().nullish(), - idle_timeout: z.number().nullish(), - health_check_timeout: z.number().nullish(), - permit_without_stream: z.number().nullish(), - initial_windows_size: z.number().nullish() - }).nullish(), + multi_mode: z.boolean().nullish().optional(), + idle_timeout: z.number().nullish().optional(), + health_check_timeout: z.number().nullish().optional(), + permit_without_stream: z.number().nullish().optional(), + initial_windows_size: z.number().nullish().optional() + }).nullish().optional(), kcp_settings: z.object({ - header: z.string().nullish(), - mtu: z.number().nullish(), - tti: z.number().nullish(), - uplink_capacity: z.number().nullish(), - downlink_capacity: z.number().nullish(), - congestion: z.number().nullish(), - read_buffer_size: z.number().nullish(), - write_buffer_size: z.number().nullish() - }).nullish(), + header: z.string().nullish().optional(), + mtu: z.number().nullish().optional(), + tti: z.number().nullish().optional(), + uplink_capacity: z.number().nullish().optional(), + downlink_capacity: z.number().nullish().optional(), + congestion: z.number().nullish().optional(), + read_buffer_size: z.number().nullish().optional(), + write_buffer_size: z.number().nullish().optional() + }).nullish().optional(), tcp_settings: z.object({ - header: z.string().nullish(), + header: z.enum(['none', 'http']).nullish().optional(), request: z.object({ - version: z.string().nullish(), - method: z.string().nullish(), - headers: z.record(z.array(z.string())).nullish() - }).nullish(), + version: z.enum(['1.0', '1.1', '2.0', '3.0']).nullish().optional(), + method: z.enum([ + 'GET', 'POST', 'PUT', 'DELETE', 'HEAD', + 'OPTIONS', 'PATCH', 'TRACE', 'CONNECT' + ]).nullish().optional(), + headers: z.record(z.array(z.string())).nullish().optional() + }).nullish().optional(), response: z.object({ - version: z.string().nullish(), - status: z.string().nullish(), - reason: z.string().nullish(), - headers: z.record(z.array(z.string())).nullish() - }).nullish() - }).nullish(), + version: z.enum(['1.0', '1.1', '2.0', '3.0']).nullish().optional(), + status: z.string().regex(/^[1-5]\d{2}$/).nullish().optional(), + reason: z.enum([ + 'Continue', 'Switching Protocols', 'OK', 'Created', 'Accepted', + 'Non-Authoritative Information', 'No Content', 'Reset Content', + 'Partial Content', 'Multiple Choices', 'Moved Permanently', + 'Found', 'See Other', 'Not Modified', 'Use Proxy', + 'Temporary Redirect', 'Permanent Redirect', 'Bad Request', + 'Unauthorized', 'Payment Required', 'Forbidden', 'Not Found', + 'Method Not Allowed', 'Not Acceptable', 'Proxy Authentication Required', + 'Request Timeout', 'Conflict', 'Gone', 'Length Required', + 'Precondition Failed', 'Payload Too Large', 'URI Too Long', + 'Unsupported Media Type', 'Range Not Satisfiable', 'Expectation Failed', + 'I\'m a teapot', 'Misdirected Request', 'Unprocessable Entity', + 'Locked', 'Failed Dependency', 'Too Early', 'Upgrade Required', + 'Precondition Required', 'Too Many Requests', + 'Request Header Fields Too Large', 'Unavailable For Legal Reasons', + 'Internal Server Error', 'Not Implemented', 'Bad Gateway', + 'Service Unavailable', 'Gateway Timeout', 'HTTP Version Not Supported' + ]).nullish().optional(), + headers: z.record(z.array(z.string())).nullish().optional() + }).nullish().optional(), + }).nullish().optional(), websocket_settings: z.object({ - heartbeatPeriod: z.number().nullish() - }).nullish() -}).nullish(); + heartbeatPeriod: z.number().nullish().optional() + }).nullish().optional() +}).nullish().optional(); export const HostFormSchema = z.object({ remark: z.string().min(1, "Remark is required"), address: z.string().min(1, "Address is required"), port: z.number().min(1, "Port must be at least 1").max(65535, "Port must be at most 65535"), - inbound_tag: z.string().optional(), + inbound_tag: z.string().min(1, "Inbound tag is required"), status: z.array(z.string()).default([]), host: z.string().default(""), sni: z.string().default(""), path: z.string().default(""), http_headers: z.record(z.string()).default({}), - security: z.enum(["inbound_default", "tls", "reality"]).default("inbound_default"), + security: z.enum(["inbound_default", "tls", "none"]).default("inbound_default"), alpn: z.string().default(""), fingerprint: z.string().default(""), allowinsecure: z.boolean().default(false), @@ -213,40 +243,40 @@ export const HostFormSchema = z.object({ }).optional(), noise_settings: z.object({ xray: z.array(z.object({ - type: z.string().regex(/^(?:rand|str|base64|hex)$/), - packet: z.string(), - delay: z.string().regex(/^\d{1,16}(-\d{1,16})?$/) + type: z.string().regex(/^(?:rand|str|base64|hex)$/).optional(), + packet: z.string().optional(), + delay: z.string().regex(/^\d{1,16}(-\d{1,16})?$/).optional() })).optional() }).optional(), mux_settings: z.object({ xray: z.object({ - concurrency: z.number().nullable(), - xudp_concurrency: z.number().nullable(), - xudp_proxy_443: z.string() + concurrency: z.number().nullable().optional(), + xudp_concurrency: z.number().nullable().optional(), + xudp_proxy_443: z.enum(["reject", "proxy"]).nullable().optional() }).optional(), sing_box: z.object({ - protocol: z.enum(["null", "h2mux", "smux", "yamux"]).nullable(), - max_connections: z.number().nullable(), - max_streams: z.number().nullable(), - min_streams: z.number().nullable(), - padding: z.boolean().nullable(), + protocol: z.enum(["none", "smux", "yamux", "h2mux"]).optional(), + max_connections: z.number().nullable().optional(), + max_streams: z.number().nullable().optional(), + min_streams: z.number().nullable().optional(), + padding: z.boolean().nullable().optional(), brutal: z.object({ - up_mbps: z.number(), - down_mbps: z.number() - }).nullable() + up_mbps: z.number().nullable().optional(), + down_mbps: z.number().nullable().optional() + }).nullable().optional() }).optional(), clash: z.object({ - protocol: z.enum(["null", "h2", "smux", "yamux", "h2mux"]).nullable(), - max_connections: z.number().nullable(), - max_streams: z.number().nullable(), - min_streams: z.number().nullable(), - padding: z.boolean().nullable(), + protocol: z.enum(["none", "smux", "yamux", "h2mux"]).optional(), + max_connections: z.number().nullable().optional(), + max_streams: z.number().nullable().optional(), + min_streams: z.number().nullable().optional(), + padding: z.boolean().nullable().optional(), brutal: z.object({ - up_mbps: z.number(), - down_mbps: z.number() - }).nullable(), - statistic: z.boolean().nullable(), - only_tcp: z.boolean().nullable() + up_mbps: z.number().nullable().optional(), + down_mbps: z.number().nullable().optional() + }).nullable().optional(), + statistic: z.boolean().nullable().optional(), + only_tcp: z.boolean().nullable().optional() }).optional() }).optional(), transport_settings: transportSettingsSchema @@ -271,88 +301,7 @@ const initialDefaultValues: HostFormValues = { random_user_agent: false, use_sni_as_host: false, priority: 0, - fragment_settings: undefined, - noise_settings: { - xray: [] - }, - mux_settings: { - xray: { - concurrency: null, - xudp_concurrency: null, - xudp_proxy_443: "reject" - }, - sing_box: { - protocol: null, - max_connections: 0, - max_streams: 0, - min_streams: 0, - padding: false, - brutal: null - }, - clash: { - protocol: null, - max_connections: 0, - max_streams: 0, - min_streams: 0, - padding: false, - brutal: null, - statistic: false, - only_tcp: false - } - }, - transport_settings: { - xhttp_settings: { - mode: "auto", - no_grpc_header: false, - x_padding_bytes: "", - sc_max_each_post_bytes: "", - sc_min_posts_interval_ms: "", - sc_max_buffered_posts: "", - sc_stream_up_server_secs: "", - xmux: { - max_concurrency: "", - max_connections: "", - c_max_reuse_times: "", - c_max_lifetime: "", - h_max_request_times: "", - h_keep_alive_period: "" - } - }, - grpc_settings: { - multi_mode: false, - idle_timeout: 0, - health_check_timeout: 0, - permit_without_stream: 0, - initial_windows_size: 0 - }, - kcp_settings: { - header: "none", - mtu: 0, - tti: 0, - uplink_capacity: 0, - downlink_capacity: 0, - congestion: 0, - read_buffer_size: 0, - write_buffer_size: 0 - }, - tcp_settings: { - header: "", - request: { - version: "", - headers: {}, - method: "" - }, - response: { - version: "", - headers: {}, - status: "", - reason: "" - } - }, - websocket_settings: { - heartbeatPeriod: 0 - } - } + fragment_settings: undefined }; export interface HostsProps { @@ -443,7 +392,8 @@ export default function Hosts({ data, onAddHost, isDialogOpen, onSubmit }: Hosts sc_max_each_post_bytes: host.transport_settings.xhttp_settings.sc_max_each_post_bytes ?? undefined, sc_min_posts_interval_ms: host.transport_settings.xhttp_settings.sc_min_posts_interval_ms ?? undefined, sc_max_buffered_posts: host.transport_settings.xhttp_settings.sc_max_buffered_posts ?? undefined, - sc_stream_up_server_secs: host.transport_settings.xhttp_settings.sc_stream_up_server_secs ?? undefined + sc_stream_up_server_secs: host.transport_settings.xhttp_settings.sc_stream_up_server_secs ?? undefined, + download_settings: host.transport_settings.xhttp_settings.download_settings ?? undefined } : undefined, grpc_settings: host.transport_settings.grpc_settings ? { multi_mode: host.transport_settings.grpc_settings.multi_mode === null ? undefined : !!host.transport_settings.grpc_settings.multi_mode, @@ -594,27 +544,19 @@ export default function Hosts({ data, onAddHost, isDialogOpen, onSubmit }: Hosts xudp_concurrency: data.mux_settings.xray.xudp_concurrency ?? null, xudp_proxy_443: data.mux_settings.xray.xudp_proxy_443 ?? "reject" } : undefined, - sing_box: data.mux_settings.sing_box ? { - protocol: data.mux_settings.sing_box.protocol === "null" ? null : data.mux_settings.sing_box.protocol, - max_connections: data.mux_settings.sing_box.max_connections ?? null, - max_streams: data.mux_settings.sing_box.max_streams ?? null, - min_streams: data.mux_settings.sing_box.min_streams ?? null, - padding: data.mux_settings.sing_box.padding ?? null, - brutal: data.mux_settings.sing_box.brutal ?? null - } : undefined, - clash: data.mux_settings.clash ? { - protocol: data.mux_settings.clash.protocol === "null" ? null : data.mux_settings.clash.protocol, - max_connections: data.mux_settings.clash.max_connections ?? null, - max_streams: data.mux_settings.clash.max_streams ?? null, - min_streams: data.mux_settings.clash.min_streams ?? null, - padding: data.mux_settings.clash.padding ?? null, - brutal: data.mux_settings.clash.brutal ?? null, - statistic: data.mux_settings.clash.statistic ?? null, - only_tcp: data.mux_settings.clash.only_tcp ?? null - } : undefined + sing_box: data.mux_settings.sing_box && data.mux_settings.sing_box.protocol !== "none" ? data.mux_settings.sing_box : undefined, + clash: data.mux_settings.clash && data.mux_settings.clash.protocol !== "none" ? data.mux_settings.clash : undefined } : undefined }; + // Remove mux_settings if it's empty + if (cleanedData.mux_settings && + !cleanedData.mux_settings.xray && + !cleanedData.mux_settings.sing_box && + !cleanedData.mux_settings.clash) { + delete cleanedData.mux_settings; + } + const response = await onSubmit(cleanedData); return response; } catch (error) { diff --git a/dashboard/src/index.css b/dashboard/src/index.css index c099d58db..d184522d1 100644 --- a/dashboard/src/index.css +++ b/dashboard/src/index.css @@ -101,7 +101,7 @@ } ::-webkit-scrollbar { - @apply w-2; + @apply sm:w-2; } ::-webkit-scrollbar-track { diff --git a/dashboard/src/pages/_dashboard.hosts.tsx b/dashboard/src/pages/_dashboard.hosts.tsx index af0e62dbb..85ee4ead1 100644 --- a/dashboard/src/pages/_dashboard.hosts.tsx +++ b/dashboard/src/pages/_dashboard.hosts.tsx @@ -3,7 +3,7 @@ import { Plus } from 'lucide-react' import MainSection from '@/components/hosts/Hosts' import { useState } from 'react' import { useQuery } from '@tanstack/react-query' -import { getHosts, addHost, modifyHost, CreateHost, ProxyHostALPN, ProxyHostFingerprint, MultiplexProtocol } from '@/service/api' +import { getHosts, addHost, modifyHost, CreateHost, ProxyHostALPN, ProxyHostFingerprint, MultiplexProtocol, Xudp } from '@/service/api' import { HostFormValues } from '@/components/hosts/Hosts' export default function HostsPage() { @@ -27,15 +27,21 @@ export default function HostsPage() { const handleSubmit = async (formData: HostFormValues) => { try { + // Check if all protocols are set to none + const allProtocolsNone = formData.mux_settings && + (!formData.mux_settings.sing_box?.protocol || formData.mux_settings.sing_box.protocol === 'none') && + (!formData.mux_settings.clash?.protocol || formData.mux_settings.clash.protocol === 'none') && + (!formData.mux_settings.xray?.concurrency); + // Convert HostFormValues to CreateHost type const hostData: CreateHost = { ...formData, alpn: formData.alpn as ProxyHostALPN | undefined, fingerprint: formData.fingerprint as ProxyHostFingerprint | undefined, - mux_settings: formData.mux_settings ? { + mux_settings: allProtocolsNone ? undefined : (formData.mux_settings ? { ...formData.mux_settings, sing_box: formData.mux_settings.sing_box ? { - protocol: formData.mux_settings.sing_box.protocol === 'null' ? undefined : formData.mux_settings.sing_box.protocol as MultiplexProtocol, + protocol: formData.mux_settings.sing_box.protocol === 'none' ? undefined : formData.mux_settings.sing_box.protocol as MultiplexProtocol, max_connections: formData.mux_settings.sing_box.max_connections || undefined, max_streams: formData.mux_settings.sing_box.max_streams || undefined, min_streams: formData.mux_settings.sing_box.min_streams || undefined, @@ -43,15 +49,21 @@ export default function HostsPage() { brutal: formData.mux_settings.sing_box.brutal || undefined } : undefined, clash: formData.mux_settings.clash ? { - protocol: formData.mux_settings.clash.protocol === 'null' ? undefined : formData.mux_settings.clash.protocol as MultiplexProtocol, + protocol: formData.mux_settings.clash.protocol === 'none' ? undefined : formData.mux_settings.clash.protocol as MultiplexProtocol, max_connections: formData.mux_settings.clash.max_connections || undefined, max_streams: formData.mux_settings.clash.max_streams || undefined, min_streams: formData.mux_settings.clash.min_streams || undefined, padding: formData.mux_settings.clash.padding || undefined, - brutal: formData.mux_settings.clash.brutal || undefined + brutal: formData.mux_settings.clash.brutal || undefined, + statistic: formData.mux_settings.clash.statistic || undefined, + only_tcp: formData.mux_settings.clash.only_tcp || undefined } : undefined, - xray: formData.mux_settings.xray || undefined - } : undefined, + xray: formData.mux_settings.xray ? { + concurrency: formData.mux_settings.xray.concurrency || undefined, + xudp_concurrency: formData.mux_settings.xray.xudp_concurrency || undefined, + xudp_proxy_443: formData.mux_settings.xray.xudp_proxy_443 as Xudp || undefined + } : undefined + } : undefined), fragment_settings: formData.fragment_settings ? { xray: formData.fragment_settings.xray ? { packets: formData.fragment_settings.xray.packets || '', @@ -69,11 +81,6 @@ export default function HostsPage() { if (existingHost?.id) { // This is an edit operation - // Preserve existing mux settings if not provided in the form - if (!hostData.mux_settings && existingHost.mux_settings) { - hostData.mux_settings = existingHost.mux_settings; - } - const response = await modifyHost(existingHost.id, hostData); return { status: 200 }; } else { diff --git a/path/to/components/HostModal.tsx b/path/to/components/HostModal.tsx new file mode 100644 index 000000000..0519ecba6 --- /dev/null +++ b/path/to/components/HostModal.tsx @@ -0,0 +1 @@ + \ No newline at end of file From fcfec9cb5aae1fcdeb27d6a841e5fb15f61c245e Mon Sep 17 00:00:00 2001 From: Sinacket Date: Fri, 11 Apr 2025 14:04:03 +0330 Subject: [PATCH 2/3] fix: xudp443 selectbox fixed | order on edit changes bug fixed --- dashboard/public/statics/locales/en.json | 6 +++++- dashboard/public/statics/locales/fa.json | 16 ++++++++------ dashboard/public/statics/locales/ru.json | 8 +++++-- dashboard/public/statics/locales/zh.json | 3 +++ .../src/components/dialogs/HostModal.tsx | 21 ++++++++++--------- dashboard/src/components/hosts/Hosts.tsx | 4 ++-- dashboard/src/pages/_dashboard.hosts.tsx | 2 +- 7 files changed, 38 insertions(+), 22 deletions(-) diff --git a/dashboard/public/statics/locales/en.json b/dashboard/public/statics/locales/en.json index d64c36247..7ed645f5c 100644 --- a/dashboard/public/statics/locales/en.json +++ b/dashboard/public/statics/locales/en.json @@ -529,7 +529,11 @@ "disableSuccess": "Host «{{name}}» has been disabled successfully", "disableFailed": "Failed to disable host «{{name}}»", "duplicateSuccess": "Host «{{name}}» has been duplicated successfully", - "duplicateFailed": "Failed to duplicate host «{{name}}»" + "duplicateFailed": "Failed to duplicate host «{{name}}»", + "xudp_proxy_443": "XUDP Proxy 443", + "reject": "Reject", + "allow": "Allow", + "skip": "Skip" }, "usersTable.sortByExpire": "Sort by expiry time", "dateInfo.day": " day", diff --git a/dashboard/public/statics/locales/fa.json b/dashboard/public/statics/locales/fa.json index b34bd3380..936b658a6 100644 --- a/dashboard/public/statics/locales/fa.json +++ b/dashboard/public/statics/locales/fa.json @@ -487,12 +487,16 @@ "enable": "فعالسازی", "editHost.title": "ویرایش هاست", "host": { - "enableSuccess": "هاست «{{name}}» با موفقیت فعال شد", - "enableFailed": "فعال‌سازی هاست «{{name}}» با خطا مواجه شد", - "disableSuccess": "هاست «{{name}}» با موفقیت غیرفعال شد", - "disableFailed": "غیرفعال‌سازی هاست «{{name}}» با خطا مواجه شد", - "duplicateSuccess": "هاست «{{name}}» با موفقیت کپی شد", - "duplicateFailed": "کپی‌سازی هاست «{{name}}» با خطا مواجه شد" + "enableSuccess": "میزبان «{{name}}» با موفقیت فعال شد", + "enableFailed": "فعال کردن میزبان «{{name}}» با شکست مواجه شد", + "disableSuccess": "میزبان «{{name}}» با موفقیت غیرفعال شد", + "disableFailed": "غیرفعال کردن میزبان «{{name}}» با شکست مواجه شد", + "duplicateSuccess": "میزبان «{{name}}» با موفقیت کپی شد", + "duplicateFailed": "کپی کردن میزبان «{{name}}» با شکست مواجه شد", + "xudp_proxy_443": "پراکسی XUDP 443", + "reject": "رد کردن", + "allow": "اجازه دادن", + "skip": "رد شدن" }, "usersTable.sortByExpire": "مرتب‌سازی بر اساس زمان انقضا", "group": { diff --git a/dashboard/public/statics/locales/ru.json b/dashboard/public/statics/locales/ru.json index 6fc80f5b6..4e2239da7 100644 --- a/dashboard/public/statics/locales/ru.json +++ b/dashboard/public/statics/locales/ru.json @@ -553,8 +553,12 @@ "enableFailed": "Не удалось включить хост «{{name}}»", "disableSuccess": "Хост «{{name}}» успешно отключен", "disableFailed": "Не удалось отключить хост «{{name}}»", - "duplicateSuccess": "Хост «{{name}}» успешно скопирован", - "duplicateFailed": "Не удалось скопировать хост «{{name}}»" + "duplicateSuccess": "Хост «{{name}}» успешно дублирован", + "duplicateFailed": "Не удалось дублировать хост «{{name}}»", + "xudp_proxy_443": "XUDP Прокси 443", + "reject": "Отклонить", + "allow": "Разрешить", + "skip": "Пропустить" }, "marzban": "Marzban", "group": { diff --git a/dashboard/public/statics/locales/zh.json b/dashboard/public/statics/locales/zh.json index ea57139cf..b7cb23492 100644 --- a/dashboard/public/statics/locales/zh.json +++ b/dashboard/public/statics/locales/zh.json @@ -548,6 +548,9 @@ }, "enable": "启用", "host": { + "xudp_proxy_443": "XUDP 代理 443", + "reject": "拒绝", + "allow": "允许", "enableSuccess": "主机「{{name}}」已成功启用", "enableFailed": "启用主机「{{name}}」失败", "disableSuccess": "主机「{{name}}」已成功禁用", diff --git a/dashboard/src/components/dialogs/HostModal.tsx b/dashboard/src/components/dialogs/HostModal.tsx index 5d4fb28e7..b13b05647 100644 --- a/dashboard/src/components/dialogs/HostModal.tsx +++ b/dashboard/src/components/dialogs/HostModal.tsx @@ -2082,21 +2082,22 @@ const HostModal: React.FC = ({ ( + render={() => ( {t("hostsDialog.xudpProxy443")} diff --git a/dashboard/src/components/hosts/Hosts.tsx b/dashboard/src/components/hosts/Hosts.tsx index 79571be82..b54819a01 100644 --- a/dashboard/src/components/hosts/Hosts.tsx +++ b/dashboard/src/components/hosts/Hosts.tsx @@ -252,7 +252,7 @@ export const HostFormSchema = z.object({ xray: z.object({ concurrency: z.number().nullable().optional(), xudp_concurrency: z.number().nullable().optional(), - xudp_proxy_443: z.enum(["reject", "proxy"]).nullable().optional() + xudp_proxy_443: z.enum(["reject", "allow", "skip"]).nullable().optional() }).optional(), sing_box: z.object({ protocol: z.enum(["none", "smux", "yamux", "h2mux"]).optional(), @@ -351,7 +351,7 @@ export default function Hosts({ data, onAddHost, isDialogOpen, onSubmit }: Hosts allowinsecure: host.allowinsecure || false, random_user_agent: host.random_user_agent || false, use_sni_as_host: host.use_sni_as_host || false, - priority: host.priority ? Number(host.priority) : (hosts?.length ?? 0), + priority: host.priority || 0, is_disabled: host.is_disabled || false, fragment_settings: host.fragment_settings ? { xray: host.fragment_settings.xray ?? undefined diff --git a/dashboard/src/pages/_dashboard.hosts.tsx b/dashboard/src/pages/_dashboard.hosts.tsx index 85ee4ead1..52804d3d4 100644 --- a/dashboard/src/pages/_dashboard.hosts.tsx +++ b/dashboard/src/pages/_dashboard.hosts.tsx @@ -61,7 +61,7 @@ export default function HostsPage() { xray: formData.mux_settings.xray ? { concurrency: formData.mux_settings.xray.concurrency || undefined, xudp_concurrency: formData.mux_settings.xray.xudp_concurrency || undefined, - xudp_proxy_443: formData.mux_settings.xray.xudp_proxy_443 as Xudp || undefined + xudp_proxy_443: formData.mux_settings.xray.xudp_proxy_443 === 'none' ? undefined : formData.mux_settings.xray.xudp_proxy_443 as Xudp } : undefined } : undefined), fragment_settings: formData.fragment_settings ? { From 62f68d17927cbfad1e3a9f89bad3de893271ff69 Mon Sep 17 00:00:00 2001 From: Sinacket Date: Fri, 11 Apr 2025 17:08:10 +0330 Subject: [PATCH 3/3] fix: users pagination control overflow fixed --- dashboard/src/components/users-table/filters.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboard/src/components/users-table/filters.tsx b/dashboard/src/components/users-table/filters.tsx index 7848504aa..21338c8a6 100644 --- a/dashboard/src/components/users-table/filters.tsx +++ b/dashboard/src/components/users-table/filters.tsx @@ -103,7 +103,7 @@ export const PaginationControls = () => { const paginationRange = getPaginationRange(currentPage, totalPages) return ( -
+