mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-14 00:55:01 +03:00
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
This commit is contained in:
@@ -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<void>;
|
||||
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<string, string> = {
|
||||
@@ -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: <ReloadOutlined aria-hidden />,
|
||||
text: t('pages.index.restartXray'),
|
||||
icon: isStopped ? <PlayCircleOutlined aria-hidden /> : <ReloadOutlined aria-hidden />,
|
||||
text: t(isStopped ? 'pages.index.startXray' : 'pages.index.restartXray'),
|
||||
onClick: onRestartXray,
|
||||
primary: true,
|
||||
},
|
||||
@@ -90,7 +96,19 @@ export default function OverviewActionBar({
|
||||
key: 'stop',
|
||||
icon: <PoweroffOutlined aria-hidden />,
|
||||
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 (
|
||||
<div className="ov-bar">
|
||||
{modalContextHolder}
|
||||
{status.xray.errorMsg ? (
|
||||
<Tooltip title={<span className="ov-error-detail">{status.xray.errorMsg}</span>}>
|
||||
{statePill}
|
||||
@@ -204,18 +223,21 @@ export default function OverviewActionBar({
|
||||
<Fragment key={group[0].key}>
|
||||
{groupIndex > 0 && <span className="ov-bar-sep" />}
|
||||
{group.map((action) => (
|
||||
<Button
|
||||
key={action.key}
|
||||
type={action.primary ? undefined : 'text'}
|
||||
color={action.primary ? 'primary' : undefined}
|
||||
variant={action.primary ? 'outlined' : undefined}
|
||||
size={size}
|
||||
icon={action.icon}
|
||||
aria-label={action.text}
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{isMobile ? undefined : action.text}
|
||||
</Button>
|
||||
<Tooltip key={action.key} title={action.hint}>
|
||||
<Button
|
||||
type={action.primary ? undefined : 'text'}
|
||||
color={action.primary ? 'primary' : undefined}
|
||||
variant={action.primary ? 'outlined' : undefined}
|
||||
size={size}
|
||||
danger={action.danger}
|
||||
disabled={action.disabled}
|
||||
icon={action.icon}
|
||||
aria-label={action.text}
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{isMobile ? undefined : action.text}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
@@ -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(
|
||||
<OverviewActionBar
|
||||
status={status}
|
||||
isMobile={false}
|
||||
accessLogEnable={false}
|
||||
panelVersion="v3.10.1"
|
||||
latestVersion="v3.10.1"
|
||||
updateAvailable={false}
|
||||
onStopXray={onStopXray}
|
||||
onRestartXray={onRestartXray}
|
||||
onOpenLogs={noop}
|
||||
onOpenXrayLogs={noop}
|
||||
onOpenAmneziaWGLogs={noop}
|
||||
onOpenConfig={noop}
|
||||
onOpenBackup={noop}
|
||||
onOpenSystemHistory={noop}
|
||||
onOpenXrayMetrics={noop}
|
||||
onOpenPanelUpdate={noop}
|
||||
onOpenVersionSwitch={noop}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
// 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());
|
||||
});
|
||||
});
|
||||
@@ -151,6 +151,11 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "إيقاف",
|
||||
"restartXray": "إعادة تشغيل",
|
||||
"startXray": "تشغيل",
|
||||
"stopXrayDisabledHint": "Xray غير قيد التشغيل",
|
||||
"stopXrayConfirmTitle": "إيقاف Xray؟",
|
||||
"stopXrayConfirmContent": "سيؤدي إيقاف Xray إلى قطع جميع اتصالات البروكسي فورًا. هل تريد المتابعة؟",
|
||||
"stopXrayConfirmOk": "إيقاف Xray",
|
||||
"xraySwitch": "النسخة",
|
||||
"xrayUpdates": "تحديثات Xray",
|
||||
"xraySwitchClickDesk": "اختار بحذر، النسخ القديمة ممكن ما تتوافقش مع الإعدادات الحالية.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -151,6 +151,11 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "توقف",
|
||||
"restartXray": "راهاندازی مجدد",
|
||||
"startXray": "شروع",
|
||||
"stopXrayDisabledHint": "Xray در حال اجرا نیست",
|
||||
"stopXrayConfirmTitle": "Xray متوقف شود؟",
|
||||
"stopXrayConfirmContent": "توقف Xray بلافاصله همه اتصالات پراکسی را قطع میکند. آیا مطمئن هستید؟",
|
||||
"stopXrayConfirmOk": "توقف Xray",
|
||||
"xraySwitch": "نسخه",
|
||||
"xrayUpdates": "بهروزرسانیهای Xray",
|
||||
"xraySwitchClickDesk": "لطفا بادقت انتخاب کنید. درصورت انتخاب نسخه قدیمیتر، امکان ناهماهنگی با پیکربندی فعلی وجود دارد",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -151,6 +151,11 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "停止",
|
||||
"restartXray": "再起動",
|
||||
"startXray": "開始",
|
||||
"stopXrayDisabledHint": "Xray は実行されていません",
|
||||
"stopXrayConfirmTitle": "Xray を停止しますか?",
|
||||
"stopXrayConfirmContent": "Xray を停止すると、すべてのプロキシ接続が直ちに切断されます。続行しますか?",
|
||||
"stopXrayConfirmOk": "Xray を停止",
|
||||
"xraySwitch": "バージョン",
|
||||
"xrayUpdates": "Xrayの更新",
|
||||
"xraySwitchClickDesk": "慎重に選択してください。古いバージョンは現在の設定と互換性がない可能性があります。",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -151,6 +151,11 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "Стоп",
|
||||
"restartXray": "Перезапуск",
|
||||
"startXray": "Запуск",
|
||||
"stopXrayDisabledHint": "Xray не запущен",
|
||||
"stopXrayConfirmTitle": "Остановить Xray?",
|
||||
"stopXrayConfirmContent": "Остановка Xray немедленно разорвёт все прокси-соединения. Продолжить?",
|
||||
"stopXrayConfirmOk": "Остановить Xray",
|
||||
"xraySwitch": "Выбор версии",
|
||||
"xrayUpdates": "Обновления Xray",
|
||||
"xraySwitchClickDesk": "Важно: старые версии могут не поддерживать текущие настройки",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -151,6 +151,11 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "Стоп",
|
||||
"restartXray": "Перезапуск",
|
||||
"startXray": "Запуск",
|
||||
"stopXrayDisabledHint": "Xray не запущено",
|
||||
"stopXrayConfirmTitle": "Зупинити Xray?",
|
||||
"stopXrayConfirmContent": "Зупинка Xray негайно розірве всі проксі-з'єднання. Продовжити?",
|
||||
"stopXrayConfirmOk": "Зупинити Xray",
|
||||
"xraySwitch": "Версія",
|
||||
"xrayUpdates": "Оновлення Xray",
|
||||
"xraySwitchClickDesk": "Вибирайте уважно, оскільки старіші версії можуть бути несумісними з поточними конфігураціями.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -151,6 +151,11 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "停止",
|
||||
"restartXray": "重启",
|
||||
"startXray": "启动",
|
||||
"stopXrayDisabledHint": "Xray 未运行",
|
||||
"stopXrayConfirmTitle": "停止 Xray?",
|
||||
"stopXrayConfirmContent": "停止 Xray 将立即中断所有代理连接。确定要继续吗?",
|
||||
"stopXrayConfirmOk": "停止 Xray",
|
||||
"xraySwitch": "版本",
|
||||
"xrayUpdates": "Xray 更新",
|
||||
"xraySwitchClickDesk": "请谨慎选择,因为较旧版本可能与当前配置不兼容",
|
||||
|
||||
@@ -151,6 +151,11 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "停止",
|
||||
"restartXray": "重新啟動",
|
||||
"startXray": "啟動",
|
||||
"stopXrayDisabledHint": "Xray 未執行",
|
||||
"stopXrayConfirmTitle": "停止 Xray?",
|
||||
"stopXrayConfirmContent": "停止 Xray 將立即中斷所有代理連線。確定要繼續嗎?",
|
||||
"stopXrayConfirmOk": "停止 Xray",
|
||||
"xraySwitch": "版本",
|
||||
"xrayUpdates": "Xray 更新",
|
||||
"xraySwitchClickDesk": "請謹慎選擇,因為較舊版本可能與當前配置不相容",
|
||||
|
||||
Reference in New Issue
Block a user