Add stop confirmation to client power action
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.8.10',
|
macClient: '0.8.11',
|
||||||
gatewayClient: '0.8.9',
|
gatewayClient: '0.8.10',
|
||||||
gatewayBackend: '0.8.0',
|
gatewayBackend: '0.8.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -644,6 +644,7 @@ export function ClientOverviewPage({
|
|||||||
const [localRulesRevision, setLocalRulesRevision] = useState(state?.route?.localRulesRevision || 0);
|
const [localRulesRevision, setLocalRulesRevision] = useState(state?.route?.localRulesRevision || 0);
|
||||||
const [confirmingLocalRulesClose, setConfirmingLocalRulesClose] = useState(false);
|
const [confirmingLocalRulesClose, setConfirmingLocalRulesClose] = useState(false);
|
||||||
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
||||||
|
const [confirmingStop, setConfirmingStop] = useState(false);
|
||||||
const [openInstructionId, setOpenInstructionId] = useState('');
|
const [openInstructionId, setOpenInstructionId] = useState('');
|
||||||
const subscriptionInputRef = useRef(null);
|
const subscriptionInputRef = useRef(null);
|
||||||
const subscriptionRef = useRef(null);
|
const subscriptionRef = useRef(null);
|
||||||
@@ -899,13 +900,21 @@ export function ClientOverviewPage({
|
|||||||
return () => document.removeEventListener('pointerdown', closeInstructionsOutside);
|
return () => document.removeEventListener('pointerdown', closeInstructionsOutside);
|
||||||
}, [instructionsOpen]);
|
}, [instructionsOpen]);
|
||||||
|
|
||||||
async function toggleConnection() {
|
function toggleConnection() {
|
||||||
const action = connectionAction({ connected, selectedServerId, configExists: state?.configExists });
|
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 === 'apply') return onApply(action.serverId);
|
||||||
if (action?.type === 'restart') return onRestart();
|
if (action?.type === 'restart') return onRestart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function stopConnection() {
|
||||||
|
if (!await onStop()) return;
|
||||||
|
setConfirmingStop(false);
|
||||||
|
}
|
||||||
|
|
||||||
function selectServer(serverId) {
|
function selectServer(serverId) {
|
||||||
setPendingServerId(serverId);
|
setPendingServerId(serverId);
|
||||||
if (connected && serverId) onApply(serverId);
|
if (connected && serverId) onApply(serverId);
|
||||||
@@ -1443,6 +1452,18 @@ export function ClientOverviewPage({
|
|||||||
onClose={requestCloseLocalRules}
|
onClose={requestCloseLocalRules}
|
||||||
onSave={saveLocalRules}
|
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
|
<ConfirmationPopup
|
||||||
open={confirmingLocalRulesClose}
|
open={confirmingLocalRulesClose}
|
||||||
id="discard-local-rules"
|
id="discard-local-rules"
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ test('rule editor add latency stays constant and dirty exits are guarded', () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('critical confirmations share one accessible blocking popup', () => {
|
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="discard-local-rules"/);
|
||||||
assert.match(component, /id="delete-subscription"/);
|
assert.match(component, /id="delete-subscription"/);
|
||||||
assert.doesNotMatch(component, /client-local-rules-discard|client-delete-confirmation/);
|
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, /aria-modal="true"/);
|
||||||
assert.match(popup, /querySelectorAll\(FOCUSABLE\)/);
|
assert.match(popup, /querySelectorAll\(FOCUSABLE\)/);
|
||||||
assert.match(popup, /element\.inert = true/);
|
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\)/);
|
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', () => {
|
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, /className="client-live-region" role="status" aria-live="polite" aria-atomic="true"/);
|
||||||
assert.match(component, /Не удалось скопировать/);
|
assert.match(component, /Не удалось скопировать/);
|
||||||
|
|||||||
Reference in New Issue
Block a user