Skip to content

Commit 089f85d

Browse files
committed
feat(core-editor): add outbound latency testing dialog
- Add outbound latency test dialog component with node selection and timeout configuration - Add latency testing UI strings to all supported locales (en, fa, ru, zh) - Update xray-outbounds-section to integrate latency test menu action - Update xray-balancers-section with latency testing support - Update core-editor-data-table and row-actions-menu to support latency operations - Remove deprecated observation summary and short label strings from locales - Update default Xray core config to support latency testing infrastructure - Enables users to test outbound connectivity and measure response times from connected nodes
1 parent cdbd246 commit 089f85d

10 files changed

Lines changed: 661 additions & 182 deletions

File tree

dashboard/public/statics/locales/en.json

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2377,6 +2377,32 @@
23772377
"uriImportFailed": "Could not parse that share link.",
23782378
"jsonInvalid": "Fix JSON errors before leaving the JSON tab.",
23792379
"formatJson": "Format",
2380+
"latency": {
2381+
"menu": "Test latency",
2382+
"testAll": "Test all",
2383+
"title": "Test outbound latency",
2384+
"untagged": "Untagged outbound",
2385+
"allOutbounds": "All outbounds",
2386+
"outbound": "Outbound",
2387+
"node": "Connected node",
2388+
"selectNode": "Select a connected node",
2389+
"timeout": "Timeout",
2390+
"test": "Test",
2391+
"total": "Total",
2392+
"alive": "Alive",
2393+
"average": "Average delay",
2394+
"unreachable": "Unreachable",
2395+
"lastTry": "Last try",
2396+
"lastSeen": "Last seen",
2397+
"loading": "Testing outbound...",
2398+
"loadingAll": "Testing outbounds...",
2399+
"empty": "Select a connected node and run the test.",
2400+
"emptyAll": "Select a connected node and test all outbounds.",
2401+
"errorDescription": "Unable to retrieve outbound latency for this node.",
2402+
"noConnectedNodes": "No connected nodes are available.",
2403+
"noConnectedNodesForCore": "No connected nodes are using this core.",
2404+
"saveCoreFirst": "Save this core and connect a node to it before testing outbound latency."
2405+
},
23802406
"freedom": {
23812407
"blurb": "Freedom forwards traffic as-is. Optional: domain strategy, redirect, PROXY protocol, TCP fragment, UDP noises, and final rules (see Xray docs). Use the JSON tab for uncommon shapes.",
23822408
"domainStrategy": "Domain strategy",
@@ -2564,10 +2590,6 @@
25642590
"finishCurrentDescription": "Add it to the list, or close the dialog and discard the draft, before starting another balancer.",
25652591
"observationRequiredWithBalancers": "At least one observation source must stay enabled while this profile has balancers.",
25662592
"observatoryRequiresLeastPing": "Observatory needs at least one balancer with the leastPing strategy. Use Burst observatory until then.",
2567-
"observationActiveSummary": "Balancer probe sources are configured.",
2568-
"observationInactiveSummary": "No probe source is configured.",
2569-
"observatoryShort": "Observatory",
2570-
"burstShort": "Burst",
25712593
"observationSources": "Observation sources",
25722594
"selectorEmpty": "Add outbound tags to participate in this balancer.",
25732595
"selectorPlaceholder": "Select outbound tags…",
@@ -2585,9 +2607,6 @@
25852607
"subjectSelectorPlaceholder": "Select outbound tag prefixes...",
25862608
"noObservationSources": "Enable an observation source here when using leastPing or leastLoad, or when random/roundRobin should filter unavailable outbounds.",
25872609
"observation": {
2588-
"enabled": "On",
2589-
"disabled": "Off",
2590-
"configure": "Configure",
25912610
"probeURL": "Probe URL",
25922611
"probeInterval": "Probe interval",
25932612
"enableConcurrency": "Enable concurrency",

dashboard/public/statics/locales/fa.json

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2385,6 +2385,32 @@
23852385
"xhttpSettings": "تنظیمات XHTTP",
23862386
"uriImportFailed": "تجزیهٔ لینک اشتراک‌گذاری ناموفق بود.",
23872387
"jsonInvalid": "قبل از ترک تب JSON، خطاها را برطرف کنید.",
2388+
"latency": {
2389+
"testAll": "تست همه",
2390+
"allOutbounds": "همه خروجی‌ها",
2391+
"loadingAll": "در حال تست خروجی‌ها...",
2392+
"emptyAll": "یک گره متصل را انتخاب کنید و همه خروجی‌ها را تست کنید.",
2393+
"menu": "تست تاخیر",
2394+
"title": "تست تاخیر خروجی",
2395+
"untagged": "خروجی بدون تگ",
2396+
"outbound": "خروجی",
2397+
"node": "گره متصل",
2398+
"selectNode": "یک گره متصل را انتخاب کنید",
2399+
"timeout": "مهلت",
2400+
"test": "تست",
2401+
"total": "کل",
2402+
"alive": "فعال",
2403+
"average": "میانگین تاخیر",
2404+
"unreachable": "غیرقابل دسترسی",
2405+
"lastTry": "آخرین تلاش",
2406+
"lastSeen": "آخرین مشاهده",
2407+
"loading": "در حال تست خروجی...",
2408+
"empty": "یک گره متصل را انتخاب و تست را اجرا کنید.",
2409+
"errorDescription": "دریافت تاخیر خروجی برای این گره ممکن نیست.",
2410+
"noConnectedNodes": "هیچ گره متصلی در دسترس نیست.",
2411+
"noConnectedNodesForCore": "هیچ گره متصلی از این هسته استفاده نمی‌کند.",
2412+
"saveCoreFirst": "برای تست تاخیر خروجی، ابتدا این هسته را ذخیره و یک گره را به آن متصل کنید."
2413+
},
23882414
"muxSection": "چندگانه‌سازی (Mux)",
23892415
"mux": {
23902416
"enabled": "فعال‌سازی Mux",
@@ -2476,10 +2502,6 @@
24762502
"finishCurrentDescription": "آن را به فهرست اضافه کنید، یا دیالوگ را ببندید و پیش‌نویس را دور بیندازید، قبل از شروع متعادل‌کننده دیگر.",
24772503
"observationRequiredWithBalancers": "تا وقتی این پروفایل متعادل‌کننده دارد، حداقل یک منبع مشاهده باید فعال بماند.",
24782504
"observatoryRequiresLeastPing": "مشاهده‌گر به حداقل یک متعادل‌کننده با استراتژی leastPing نیاز دارد. تا آن زمان از مشاهده‌گر Burst استفاده کنید.",
2479-
"observationActiveSummary": "منابع پایش متعادل‌کننده پیکربندی شده‌اند.",
2480-
"observationInactiveSummary": "هیچ منبع پایشی پیکربندی نشده است.",
2481-
"observatoryShort": "مشاهده‌گر",
2482-
"burstShort": "جهشی",
24832505
"observationSources": "منابع مشاهده",
24842506
"selectorEmpty": "برچسب‌های خروجی را اضافه کنید تا در این متعادل‌کننده شرکت کنند.",
24852507
"selectorPlaceholder": "برچسب‌های خروجی را انتخاب کنید…",
@@ -2497,9 +2519,6 @@
24972519
"subjectSelectorPlaceholder": "پیشوندهای برچسب خروجی را انتخاب کنید...",
24982520
"noObservationSources": "هنگام استفاده از leastPing یا leastLoad، یا وقتی random/roundRobin باید خروجی‌های در دسترس نبودنی را فیلتر کند، یک منبع مشاهده را اینجا فعال کنید.",
24992521
"observation": {
2500-
"enabled": "فعال",
2501-
"disabled": "غیرفعال",
2502-
"configure": "پیکربندی",
25032522
"probeURL": "آدرس پایش",
25042523
"probeInterval": "بازه پایش",
25052524
"enableConcurrency": "فعال‌سازی هم‌زمانی",

dashboard/public/statics/locales/ru.json

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2361,6 +2361,32 @@
23612361
"xhttpSettings": "Настройки XHTTP",
23622362
"uriImportFailed": "Не удалось разобрать ссылку.",
23632363
"jsonInvalid": "Исправьте ошибки JSON перед выходом из вкладки JSON.",
2364+
"latency": {
2365+
"testAll": "Проверить все",
2366+
"allOutbounds": "Все исходящие",
2367+
"loadingAll": "Проверка исходящих...",
2368+
"emptyAll": "Выберите подключенный узел и проверьте все исходящие.",
2369+
"menu": "Проверить задержку",
2370+
"title": "Проверка задержки исходящего",
2371+
"untagged": "Исходящий без тега",
2372+
"outbound": "Исходящий",
2373+
"node": "Подключенный узел",
2374+
"selectNode": "Выберите подключенный узел",
2375+
"timeout": "Тайм-аут",
2376+
"test": "Проверить",
2377+
"total": "Всего",
2378+
"alive": "Доступно",
2379+
"average": "Средняя задержка",
2380+
"unreachable": "Недоступно",
2381+
"lastTry": "Последняя попытка",
2382+
"lastSeen": "Последняя активность",
2383+
"loading": "Проверка исходящего...",
2384+
"empty": "Выберите подключенный узел и запустите проверку.",
2385+
"errorDescription": "Не удалось получить задержку исходящего для этого узла.",
2386+
"noConnectedNodes": "Нет доступных подключенных узлов.",
2387+
"noConnectedNodesForCore": "Нет подключенных узлов, использующих это ядро.",
2388+
"saveCoreFirst": "Сохраните это ядро и подключите к нему узел перед проверкой задержки исходящего."
2389+
},
23642390
"muxSection": "Мультиплексирование (Mux)",
23652391
"mux": {
23662392
"enabled": "Включить Mux",
@@ -2452,10 +2478,6 @@
24522478
"finishCurrentDescription": "Добавьте его в список или закройте диалог и отмените черновик перед созданием другого балансировщика.",
24532479
"observationRequiredWithBalancers": "Пока в профиле есть балансировщики, должен оставаться включённым хотя бы один источник наблюдений.",
24542480
"observatoryRequiresLeastPing": "Наблюдатель требует хотя бы один балансировщик со стратегией leastPing. До этого используйте Burst-наблюдатель.",
2455-
"observationActiveSummary": "Источники проверок балансировщика настроены.",
2456-
"observationInactiveSummary": "Источник проверок не настроен.",
2457-
"observatoryShort": "Наблюдатель",
2458-
"burstShort": "Пакетный",
24592481
"observationSources": "Источники наблюдений",
24602482
"selectorEmpty": "Добавьте теги исходящих подключений для участия в этом балансировщике.",
24612483
"selectorPlaceholder": "Выберите теги исходящих подключений…",
@@ -2473,9 +2495,6 @@
24732495
"subjectSelectorPlaceholder": "Выберите префиксы тегов исходящих подключений...",
24742496
"noObservationSources": "Включите здесь источник наблюдений при использовании leastPing или leastLoad, либо когда random/roundRobin должны отфильтровывать недоступные исходящие подключения.",
24752497
"observation": {
2476-
"enabled": "Вкл.",
2477-
"disabled": "Выкл.",
2478-
"configure": "Настроить",
24792498
"probeURL": "URL проверки",
24802499
"probeInterval": "Интервал проверки",
24812500
"enableConcurrency": "Включить параллельность",

dashboard/public/statics/locales/zh.json

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2432,6 +2432,32 @@
24322432
"xhttpSettings": "XHTTP 设置",
24332433
"uriImportFailed": "无法解析该分享链接。",
24342434
"jsonInvalid": "离开 JSON 标签页前请修复错误。",
2435+
"latency": {
2436+
"testAll": "测试全部",
2437+
"allOutbounds": "所有出站",
2438+
"loadingAll": "正在测试出站...",
2439+
"emptyAll": "请选择已连接节点并测试所有出站。",
2440+
"menu": "测试延迟",
2441+
"title": "测试出站延迟",
2442+
"untagged": "未标记的出站",
2443+
"outbound": "出站",
2444+
"node": "已连接节点",
2445+
"selectNode": "选择一个已连接节点",
2446+
"timeout": "超时",
2447+
"test": "测试",
2448+
"total": "总数",
2449+
"alive": "可用",
2450+
"average": "平均延迟",
2451+
"unreachable": "不可达",
2452+
"lastTry": "上次尝试",
2453+
"lastSeen": "上次可用",
2454+
"loading": "正在测试出站...",
2455+
"empty": "请选择已连接节点并运行测试。",
2456+
"errorDescription": "无法获取此节点的出站延迟。",
2457+
"noConnectedNodes": "没有可用的已连接节点。",
2458+
"noConnectedNodesForCore": "没有已连接节点正在使用此核心。",
2459+
"saveCoreFirst": "请先保存此核心并连接节点,然后再测试出站延迟。"
2460+
},
24352461
"muxSection": "多路复用 (Mux)",
24362462
"mux": {
24372463
"enabled": "启用 Mux",
@@ -2523,10 +2549,6 @@
25232549
"finishCurrentDescription": "请先加入列表,或关闭对话框并丢弃草稿,再开始另一个负载均衡。",
25242550
"observationRequiredWithBalancers": "当此配置文件包含负载均衡时,至少必须保持一个观测源启用。",
25252551
"observatoryRequiresLeastPing": "观测器至少需要一个使用 leastPing 策略的负载均衡。在此之前请使用突发观测器。",
2526-
"observationActiveSummary": "已配置负载均衡探测源。",
2527-
"observationInactiveSummary": "未配置探测源。",
2528-
"observatoryShort": "观测器",
2529-
"burstShort": "突发",
25302552
"observationSources": "观测源",
25312553
"selectorEmpty": "添加出站标签以参与此负载均衡。",
25322554
"selectorPlaceholder": "选择出站标签…",
@@ -2544,9 +2566,6 @@
25442566
"subjectSelectorPlaceholder": "选择出站标签前缀...",
25452567
"noObservationSources": "使用 leastPing 或 leastLoad 时,或 random/roundRobin 需要过滤不可用出站时,请在此启用一个观测源。",
25462568
"observation": {
2547-
"enabled": "开启",
2548-
"disabled": "关闭",
2549-
"configure": "配置",
25502569
"probeURL": "探测 URL",
25512570
"probeInterval": "探测间隔",
25522571
"enableConcurrency": "启用并发",

dashboard/src/features/core-editor/components/shared/core-editor-data-table.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-tabl
2828
import { cn } from '@/lib/utils'
2929
import { CoreEditorListItemCard } from '@/features/core-editor/components/shared/core-editor-list-item-card'
3030
import { CoreEditorSortableGridCard } from '@/features/core-editor/components/shared/core-editor-sortable-grid-card'
31-
import { CoreEditorRowActionsMenu } from '@/features/core-editor/components/shared/core-editor-row-actions-menu'
31+
import { CoreEditorRowActionsMenu, type CoreEditorRowAction } from '@/features/core-editor/components/shared/core-editor-row-actions-menu'
3232
import { Search, X } from 'lucide-react'
3333
import { useCallback, useEffect, useMemo, useState } from 'react'
3434
import { useTranslation } from 'react-i18next'
@@ -122,6 +122,7 @@ export interface CoreEditorDataTableProps<TData> {
122122
searchPlaceholder?: string
123123
/** Optional controls rendered next to the grid/list view toggle. */
124124
toolbarActions?: ReactNode
125+
getRowActions?: (row: TData, index: number) => CoreEditorRowAction[]
125126
}
126127

127128
export function CoreEditorDataTable<TData>({
@@ -143,6 +144,7 @@ export function CoreEditorDataTable<TData>({
143144
getSearchableText,
144145
searchPlaceholder,
145146
toolbarActions,
147+
getRowActions,
146148
}: CoreEditorDataTableProps<TData>) {
147149
const { t, i18n } = useTranslation()
148150
const dir = useDirDetection()
@@ -298,13 +300,14 @@ export function CoreEditorDataTable<TData>({
298300
<CoreEditorRowActionsMenu
299301
onEdit={() => onRowClick?.(item, originalIdx)}
300302
onRemove={() => onRemoveRow(originalIdx)}
303+
extraActions={getRowActions?.(item, originalIdx)}
301304
removeDisabled={removeDisabled}
302305
/>
303306
)
304307
},
305308
},
306309
]
307-
}, [listColumnsSansMenu, displayData, originalIndices, onRowClick, onRemoveRow, removeDisabled])
310+
}, [listColumnsSansMenu, displayData, originalIndices, onRowClick, onRemoveRow, getRowActions, removeDisabled])
308311

309312
const empty = emptyLabel ?? t('noResults', { defaultValue: 'No results' })
310313
const emptyDisplay =
@@ -399,6 +402,7 @@ export function CoreEditorDataTable<TData>({
399402
<CoreEditorRowActionsMenu
400403
onEdit={() => onRowClick?.(item, originalIndex)}
401404
onRemove={() => onRemoveRow(originalIndex)}
405+
extraActions={getRowActions?.(item, originalIndex)}
402406
removeDisabled={removeDisabled}
403407
/>
404408
)

dashboard/src/features/core-editor/components/shared/core-editor-row-actions-menu.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
import { Button } from '@/components/ui/button'
2-
import {
3-
DropdownMenu,
4-
DropdownMenuContent,
5-
DropdownMenuItem,
6-
DropdownMenuSeparator,
7-
DropdownMenuTrigger,
8-
} from '@/components/ui/dropdown-menu'
2+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
93
import { useTranslation } from 'react-i18next'
104
import { MoreVertical, Pencil, Trash2 } from 'lucide-react'
5+
import type { ReactNode } from 'react'
6+
7+
export interface CoreEditorRowAction {
8+
key: string
9+
label: ReactNode
10+
icon?: ReactNode
11+
onSelect: () => void
12+
disabled?: boolean
13+
}
1114

1215
export interface CoreEditorRowActionsMenuProps {
1316
/** Edit opens the detailed editor (same as row click). */
1417
onEdit: () => void
1518
onRemove: () => void
19+
extraActions?: CoreEditorRowAction[]
1620
/** When true, delete is shown but not actionable (e.g. minimum list size). */
1721
removeDisabled?: boolean
1822
className?: string
1923
}
2024

21-
export function CoreEditorRowActionsMenu({ onEdit, onRemove, removeDisabled, className }: CoreEditorRowActionsMenuProps) {
25+
export function CoreEditorRowActionsMenu({ onEdit, onRemove, extraActions = [], removeDisabled, className }: CoreEditorRowActionsMenuProps) {
2226
const { t } = useTranslation()
2327

2428
const handleEditSelect = (event: Event) => {
@@ -45,6 +49,20 @@ export function CoreEditorRowActionsMenu({ onEdit, onRemove, removeDisabled, cla
4549
<Pencil className="size-4 shrink-0" />
4650
{t('edit')}
4751
</DropdownMenuItem>
52+
{extraActions.map(action => (
53+
<DropdownMenuItem
54+
key={action.key}
55+
className="gap-2"
56+
disabled={action.disabled}
57+
onSelect={event => {
58+
event.stopPropagation()
59+
if (!action.disabled) action.onSelect()
60+
}}
61+
>
62+
{action.icon}
63+
{action.label}
64+
</DropdownMenuItem>
65+
))}
4866
<DropdownMenuSeparator />
4967
<DropdownMenuItem
5068
disabled={removeDisabled}

0 commit comments

Comments
 (0)