From 166d5633cc3f95a45bb99cf59ebff8706b6ce153 Mon Sep 17 00:00:00 2001 From: Nikan Zeyaei <72458440+NikanZeyaei@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:33:01 +0330 Subject: [PATCH] feat(frontend): confirm before stopping Xray and offer Start when it is stopped (#7104) Stop sat next to Restart on the dashboard and fired on the first click. A mis-click cut every proxy connection, and when the panel itself is reached through that proxy the only way back was SSH (#7103). Stop now opens a confirmation with Cancel focused. When Xray is stopped the first button reads Start instead of Restart and Stop is disabled; in the crashed state Stop stays enabled, because setting the manual-stop flag is what halts the crash-recovery restart loop. Closes #7103 --- .../src/pages/index/OverviewActionBar.tsx | 56 ++++++++---- .../src/test/overview-stop-confirm.test.tsx | 89 +++++++++++++++++++ internal/web/translation/ar-EG.json | 5 ++ internal/web/translation/en-US.json | 5 ++ internal/web/translation/es-ES.json | 5 ++ internal/web/translation/fa-IR.json | 5 ++ internal/web/translation/id-ID.json | 5 ++ internal/web/translation/ja-JP.json | 5 ++ internal/web/translation/pt-BR.json | 5 ++ internal/web/translation/ru-RU.json | 5 ++ internal/web/translation/sk-SK.json | 5 ++ internal/web/translation/tr-TR.json | 5 ++ internal/web/translation/uk-UA.json | 5 ++ internal/web/translation/vi-VN.json | 5 ++ internal/web/translation/zh-CN.json | 5 ++ internal/web/translation/zh-TW.json | 5 ++ 16 files changed, 198 insertions(+), 17 deletions(-) create mode 100644 frontend/src/test/overview-stop-confirm.test.tsx diff --git a/frontend/src/pages/index/OverviewActionBar.tsx b/frontend/src/pages/index/OverviewActionBar.tsx index 900ce1ead..9a5dbab7f 100644 --- a/frontend/src/pages/index/OverviewActionBar.tsx +++ b/frontend/src/pages/index/OverviewActionBar.tsx @@ -1,7 +1,7 @@ import { Fragment } from 'react'; import type { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Tag, Tooltip } from 'antd'; +import { Button, Modal, Tag, Tooltip } from 'antd'; import { ApiOutlined, ArrowUpOutlined, @@ -11,6 +11,7 @@ import { CloudServerOutlined, ControlOutlined, FileTextOutlined, + PlayCircleOutlined, PoweroffOutlined, ReloadOutlined, } from '@ant-design/icons'; @@ -26,7 +27,7 @@ interface OverviewActionBarProps { panelVersion: string; latestVersion: string; updateAvailable: boolean; - onStopXray: () => void; + onStopXray: () => void | Promise; onRestartXray: () => void; onOpenLogs: () => void; onOpenXrayLogs: () => void; @@ -45,6 +46,9 @@ interface BarAction { text: string; onClick: () => void; primary?: boolean; + danger?: boolean; + disabled?: boolean; + hint?: string; } const XRAY_STATE_KEYS: Record = { @@ -73,16 +77,18 @@ export default function OverviewActionBar({ onOpenVersionSwitch, }: OverviewActionBarProps) { const { t } = useTranslation(); + const [modal, modalContextHolder] = Modal.useModal(); const stateText = t(XRAY_STATE_KEYS[status.xray.state] ?? 'pages.index.xrayStatusUnknown'); const hasVersion = !!status.xray.version && status.xray.version !== 'Unknown'; const size = isMobile ? ('small' as const) : ('middle' as const); + const isStopped = status.xray.state === 'stop'; const actionGroups: BarAction[][] = [ [ { key: 'restart', - icon: , - text: t('pages.index.restartXray'), + icon: isStopped ? : , + text: t(isStopped ? 'pages.index.startXray' : 'pages.index.restartXray'), onClick: onRestartXray, primary: true, }, @@ -90,7 +96,19 @@ export default function OverviewActionBar({ key: 'stop', icon: , text: t('pages.index.stopXray'), - onClick: onStopXray, + danger: true, + disabled: isStopped, + hint: isStopped ? t('pages.index.stopXrayDisabledHint') : undefined, + onClick: () => + modal.confirm({ + title: t('pages.index.stopXrayConfirmTitle'), + content: t('pages.index.stopXrayConfirmContent'), + okText: t('pages.index.stopXrayConfirmOk'), + okType: 'danger', + cancelText: t('cancel'), + focusable: { autoFocusButton: 'cancel' }, + onOk: onStopXray, + }), }, ], [ @@ -165,6 +183,7 @@ export default function OverviewActionBar({ return (
+ {modalContextHolder} {status.xray.errorMsg ? ( {status.xray.errorMsg}}> {statePill} @@ -204,18 +223,21 @@ export default function OverviewActionBar({ {groupIndex > 0 && } {group.map((action) => ( - + + + ))} ))} diff --git a/frontend/src/test/overview-stop-confirm.test.tsx b/frontend/src/test/overview-stop-confirm.test.tsx new file mode 100644 index 000000000..105a8f6c3 --- /dev/null +++ b/frontend/src/test/overview-stop-confirm.test.tsx @@ -0,0 +1,89 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { Status } from '@/models/status'; +import OverviewActionBar from '@/pages/index/OverviewActionBar'; + +import { renderWithProviders } from './test-utils'; + +const noop = () => {}; + +function renderBar( + state: string, + { + onStopXray = noop, + onRestartXray = noop, + }: { onStopXray?: () => void; onRestartXray?: () => void } = {}, +) { + const status = new Status(); + status.xray.state = state; + renderWithProviders( + , + ); +} + +// Stop used to fire on the first click, so a mis-click killed every proxy connection (#7103). +describe('OverviewActionBar stop confirmation', () => { + it('does not stop Xray until the dialog is confirmed', async () => { + const onStopXray = vi.fn(); + renderBar('running', { onStopXray }); + + fireEvent.click(screen.getByRole('button', { name: 'Stop' })); + expect(await screen.findAllByText('Stop Xray?')).not.toHaveLength(0); + expect(onStopXray).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onStopXray).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole('button', { name: 'Stop' })); + await waitFor(() => + expect(screen.getAllByRole('button', { name: 'Stop Xray' })).not.toHaveLength(0), + ); + fireEvent.click(screen.getAllByRole('button', { name: 'Stop Xray' }).at(-1)!); + await waitFor(() => expect(onStopXray).toHaveBeenCalledOnce()); + }); +}); + +// Stop on a stopped core only errors and sets the manual-stop flag. In the crash +// state Stop is the one dashboard control that halts the restart loop. +describe('OverviewActionBar start / disabled stop', () => { + it('offers Start and disables Stop when the core is stopped', () => { + const onRestartXray = vi.fn(); + renderBar('stop', { onRestartXray }); + + expect(screen.queryByRole('button', { name: 'Restart' })).toBeNull(); + expect((screen.getByRole('button', { name: 'Stop' }) as HTMLButtonElement).disabled).toBe(true); + fireEvent.click(screen.getByRole('button', { name: 'Start' })); + expect(onRestartXray).toHaveBeenCalledOnce(); + }); + + it('keeps Restart and leaves Stop usable when the core errored', async () => { + const onStopXray = vi.fn(); + renderBar('error', { onStopXray }); + + expect(screen.getByRole('button', { name: 'Restart' })).toBeTruthy(); + const stop = screen.getByRole('button', { name: 'Stop' }) as HTMLButtonElement; + expect(stop.disabled).toBe(false); + fireEvent.click(stop); + fireEvent.click(await screen.findByRole('button', { name: 'Stop Xray' })); + await waitFor(() => expect(onStopXray).toHaveBeenCalledOnce()); + }); +}); diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index 08b8cbe64..a3eb4e3e3 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "إيقاف", "restartXray": "إعادة تشغيل", + "startXray": "تشغيل", + "stopXrayDisabledHint": "Xray غير قيد التشغيل", + "stopXrayConfirmTitle": "إيقاف Xray؟", + "stopXrayConfirmContent": "سيؤدي إيقاف Xray إلى قطع جميع اتصالات البروكسي فورًا. هل تريد المتابعة؟", + "stopXrayConfirmOk": "إيقاف Xray", "xraySwitch": "النسخة", "xrayUpdates": "تحديثات Xray", "xraySwitchClickDesk": "اختار بحذر، النسخ القديمة ممكن ما تتوافقش مع الإعدادات الحالية.", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 57a915a52..4b7d4cc1d 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "Stop", "restartXray": "Restart", + "startXray": "Start", + "stopXrayDisabledHint": "Xray is not running", + "stopXrayConfirmTitle": "Stop Xray?", + "stopXrayConfirmContent": "Stopping Xray will immediately interrupt all proxy connections. Are you sure you want to continue?", + "stopXrayConfirmOk": "Stop Xray", "xraySwitch": "Version", "xrayUpdates": "Xray Updates", "xraySwitchClickDesk": "Choose carefully, as older versions may not be compatible with current configurations.", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index c780eda85..8cc810704 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "Detener", "restartXray": "Reiniciar", + "startXray": "Iniciar", + "stopXrayDisabledHint": "Xray no se está ejecutando", + "stopXrayConfirmTitle": "¿Detener Xray?", + "stopXrayConfirmContent": "Detener Xray interrumpirá de inmediato todas las conexiones proxy. ¿Seguro que quieres continuar?", + "stopXrayConfirmOk": "Detener Xray", "xraySwitch": "Versión", "xrayUpdates": "Actualizaciones de Xray", "xraySwitchClickDesk": "Elige sabiamente, ya que las versiones anteriores pueden no ser compatibles con las configuraciones actuales.", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index 5922171e0..71e3e8c89 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "توقف", "restartXray": "راه‌اندازی مجدد", + "startXray": "شروع", + "stopXrayDisabledHint": "Xray در حال اجرا نیست", + "stopXrayConfirmTitle": "Xray متوقف شود؟", + "stopXrayConfirmContent": "توقف Xray بلافاصله همه اتصالات پراکسی را قطع می‌کند. آیا مطمئن هستید؟", + "stopXrayConfirmOk": "توقف Xray", "xraySwitch": "‌نسخه", "xrayUpdates": "به‌روزرسانی‌های Xray", "xraySwitchClickDesk": "لطفا بادقت انتخاب کنید. درصورت انتخاب نسخه قدیمی‌تر، امکان ناهماهنگی با پیکربندی فعلی وجود دارد", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 6102b7450..e21038caa 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "Hentikan", "restartXray": "Mulai ulang", + "startXray": "Mulai", + "stopXrayDisabledHint": "Xray tidak berjalan", + "stopXrayConfirmTitle": "Hentikan Xray?", + "stopXrayConfirmContent": "Menghentikan Xray akan langsung memutus semua koneksi proxy. Yakin ingin melanjutkan?", + "stopXrayConfirmOk": "Hentikan Xray", "xraySwitch": "Versi", "xrayUpdates": "Pembaruan Xray", "xraySwitchClickDesk": "Pilih dengan hati-hati, karena versi yang lebih lama mungkin tidak kompatibel dengan konfigurasi saat ini.", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 61ca87665..a3209c94e 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "停止", "restartXray": "再起動", + "startXray": "開始", + "stopXrayDisabledHint": "Xray は実行されていません", + "stopXrayConfirmTitle": "Xray を停止しますか?", + "stopXrayConfirmContent": "Xray を停止すると、すべてのプロキシ接続が直ちに切断されます。続行しますか?", + "stopXrayConfirmOk": "Xray を停止", "xraySwitch": "バージョン", "xrayUpdates": "Xrayの更新", "xraySwitchClickDesk": "慎重に選択してください。古いバージョンは現在の設定と互換性がない可能性があります。", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index 5172553e4..05e4aee69 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "Parar", "restartXray": "Reiniciar", + "startXray": "Iniciar", + "stopXrayDisabledHint": "O Xray não está em execução", + "stopXrayConfirmTitle": "Parar o Xray?", + "stopXrayConfirmContent": "Parar o Xray interromperá imediatamente todas as conexões de proxy. Tem certeza de que deseja continuar?", + "stopXrayConfirmOk": "Parar o Xray", "xraySwitch": "Versão", "xrayUpdates": "Atualizações do Xray", "xraySwitchClickDesk": "Escolha com cuidado, pois versões mais antigas podem não ser compatíveis com as configurações atuais.", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 350e8399a..2b228875b 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "Стоп", "restartXray": "Перезапуск", + "startXray": "Запуск", + "stopXrayDisabledHint": "Xray не запущен", + "stopXrayConfirmTitle": "Остановить Xray?", + "stopXrayConfirmContent": "Остановка Xray немедленно разорвёт все прокси-соединения. Продолжить?", + "stopXrayConfirmOk": "Остановить Xray", "xraySwitch": "Выбор версии", "xrayUpdates": "Обновления Xray", "xraySwitchClickDesk": "Важно: старые версии могут не поддерживать текущие настройки", diff --git a/internal/web/translation/sk-SK.json b/internal/web/translation/sk-SK.json index 07eaa676d..c936760b8 100644 --- a/internal/web/translation/sk-SK.json +++ b/internal/web/translation/sk-SK.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "Zastaviť", "restartXray": "Reštartovať", + "startXray": "Spustiť", + "stopXrayDisabledHint": "Xray nebeží", + "stopXrayConfirmTitle": "Zastaviť Xray?", + "stopXrayConfirmContent": "Zastavenie Xray okamžite preruší všetky proxy spojenia. Naozaj chcete pokračovať?", + "stopXrayConfirmOk": "Zastaviť Xray", "xraySwitch": "Verzia", "xrayUpdates": "Aktualizácie Xray", "xraySwitchClickDesk": "Verziu vyberajte opatrne. Staršie verzie nemusia podporovať aktuálnu konfiguráciu.", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index df2d995b9..92c82be09 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "Durdur", "restartXray": "Yeniden Başlat", + "startXray": "Başlat", + "stopXrayDisabledHint": "Xray çalışmıyor", + "stopXrayConfirmTitle": "Xray durdurulsun mu?", + "stopXrayConfirmContent": "Xray'i durdurmak tüm proxy bağlantılarını anında keser. Devam etmek istediğinizden emin misiniz?", + "stopXrayConfirmOk": "Xray'i durdur", "xraySwitch": "Sürüm", "xrayUpdates": "Xray Güncellemeleri", "xraySwitchClickDesk": "Dikkatli seçin, eski sürümler mevcut yapılandırmalarla uyumlu olmayabilir.", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 23dbefe4c..407785a7f 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "Стоп", "restartXray": "Перезапуск", + "startXray": "Запуск", + "stopXrayDisabledHint": "Xray не запущено", + "stopXrayConfirmTitle": "Зупинити Xray?", + "stopXrayConfirmContent": "Зупинка Xray негайно розірве всі проксі-з'єднання. Продовжити?", + "stopXrayConfirmOk": "Зупинити Xray", "xraySwitch": "Версія", "xrayUpdates": "Оновлення Xray", "xraySwitchClickDesk": "Вибирайте уважно, оскільки старіші версії можуть бути несумісними з поточними конфігураціями.", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 8693d7bf4..6dfb88600 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "Dừng", "restartXray": "Khởi động lại", + "startXray": "Khởi động", + "stopXrayDisabledHint": "Xray không chạy", + "stopXrayConfirmTitle": "Dừng Xray?", + "stopXrayConfirmContent": "Dừng Xray sẽ ngắt ngay lập tức mọi kết nối proxy. Bạn có chắc muốn tiếp tục?", + "stopXrayConfirmOk": "Dừng Xray", "xraySwitch": "Phiên bản", "xrayUpdates": "Cập nhật Xray", "xraySwitchClickDesk": "Hãy lựa chọn thận trọng, vì các phiên bản cũ có thể không tương thích với các cấu hình hiện tại.", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 56eedc32a..268d6eb62 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "停止", "restartXray": "重启", + "startXray": "启动", + "stopXrayDisabledHint": "Xray 未运行", + "stopXrayConfirmTitle": "停止 Xray?", + "stopXrayConfirmContent": "停止 Xray 将立即中断所有代理连接。确定要继续吗?", + "stopXrayConfirmOk": "停止 Xray", "xraySwitch": "版本", "xrayUpdates": "Xray 更新", "xraySwitchClickDesk": "请谨慎选择,因为较旧版本可能与当前配置不兼容", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index d3aff24d5..03945751d 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -151,6 +151,11 @@ "xrayStatus": "Xray", "stopXray": "停止", "restartXray": "重新啟動", + "startXray": "啟動", + "stopXrayDisabledHint": "Xray 未執行", + "stopXrayConfirmTitle": "停止 Xray?", + "stopXrayConfirmContent": "停止 Xray 將立即中斷所有代理連線。確定要繼續嗎?", + "stopXrayConfirmOk": "停止 Xray", "xraySwitch": "版本", "xrayUpdates": "Xray 更新", "xraySwitchClickDesk": "請謹慎選擇,因為較舊版本可能與當前配置不相容",