Add stop confirmation to client power action
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 17s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-07-15 15:25:10 +03:00
parent b4adcce26a
commit c6d3fd39fb
3 changed files with 34 additions and 4 deletions

View File

@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.8.10',
gatewayClient: '0.8.9',
macClient: '0.8.11',
gatewayClient: '0.8.10',
gatewayBackend: '0.8.0',
});

View File

@@ -644,6 +644,7 @@ export function ClientOverviewPage({
const [localRulesRevision, setLocalRulesRevision] = useState(state?.route?.localRulesRevision || 0);
const [confirmingLocalRulesClose, setConfirmingLocalRulesClose] = useState(false);
const [confirmingDelete, setConfirmingDelete] = useState(false);
const [confirmingStop, setConfirmingStop] = useState(false);
const [openInstructionId, setOpenInstructionId] = useState('');
const subscriptionInputRef = useRef(null);
const subscriptionRef = useRef(null);
@@ -899,13 +900,21 @@ export function ClientOverviewPage({
return () => document.removeEventListener('pointerdown', closeInstructionsOutside);
}, [instructionsOpen]);
async function toggleConnection() {
function toggleConnection() {
const action = connectionAction({ connected, selectedServerId, configExists: state?.configExists });
if (action?.type === 'stop') return onStop();
if (action?.type === 'stop') {
setConfirmingStop(true);
return;
}
if (action?.type === 'apply') return onApply(action.serverId);
if (action?.type === 'restart') return onRestart();
}
async function stopConnection() {
if (!await onStop()) return;
setConfirmingStop(false);
}
function selectServer(serverId) {
setPendingServerId(serverId);
if (connected && serverId) onApply(serverId);
@@ -1443,6 +1452,18 @@ export function ClientOverviewPage({
onClose={requestCloseLocalRules}
onSave={saveLocalRules}
/>}
<ConfirmationPopup
open={confirmingStop}
id="stop-connection"
kicker="Защита от случайного отключения"
title="Отключить VPN?"
description="Harbor остановит текущее VPN-подключение. Локальный прокси перестанет передавать трафик до повторного включения."
cancelLabel="Оставить включённым"
confirmLabel="Отключить VPN"
busy={connectionBlocked}
onCancel={() => setConfirmingStop(false)}
onConfirm={stopConnection}
/>
<ConfirmationPopup
open={confirmingLocalRulesClose}
id="discard-local-rules"

View File

@@ -40,6 +40,7 @@ test('rule editor add latency stays constant and dirty exits are guarded', () =>
});
test('critical confirmations share one accessible blocking popup', () => {
assert.match(component, /id="stop-connection"/);
assert.match(component, /id="discard-local-rules"/);
assert.match(component, /id="delete-subscription"/);
assert.doesNotMatch(component, /client-local-rules-discard|client-delete-confirmation/);
@@ -47,9 +48,17 @@ test('critical confirmations share one accessible blocking popup', () => {
assert.match(popup, /aria-modal="true"/);
assert.match(popup, /querySelectorAll\(FOCUSABLE\)/);
assert.match(popup, /element\.inert = true/);
assert.match(popup, /requestAnimationFrame\(\(\) => cancelRef\.current\?\.focus\(\)\)/);
assert.match(styles, /\.client-confirmation-popup\.is-open[\s\S]*backdrop-filter: blur\(18px\)/);
});
test('power click and native Enter cannot stop VPN without a separate confirmation', () => {
assert.match(component, /className="client-power"[\s\S]*type="button"[\s\S]*onClick=\{toggleConnection\}/);
assert.match(component, /if \(action\?\.type === 'stop'\) \{[\s\S]*setConfirmingStop\(true\);[\s\S]*return;[\s\S]*\}/);
assert.doesNotMatch(component, /if \(action\?\.type === 'stop'\) return onStop\(\)/);
assert.match(component, /id="stop-connection"[\s\S]*cancelLabel="Оставить включённым"[\s\S]*confirmLabel="Отключить VPN"[\s\S]*onConfirm=\{stopConnection\}/);
});
test('copy feedback, drawers and Gateway access actions expose complete semantics', () => {
assert.match(component, /className="client-live-region" role="status" aria-live="polite" aria-atomic="true"/);
assert.match(component, /Не удалось скопировать/);