diff --git a/src/shared/versions.js b/src/shared/versions.js index 8e17553..c5e8ef3 100644 --- a/src/shared/versions.js +++ b/src/shared/versions.js @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.17.8', - gatewayClient: '0.18.8', + macClient: '0.17.10', + gatewayClient: '0.18.10', gatewayBackend: '0.18.1', }); diff --git a/src/web/components/ConnectivityDiagnosticsPanel.jsx b/src/web/components/ConnectivityDiagnosticsPanel.jsx index 76826f9..f86f7fb 100644 --- a/src/web/components/ConnectivityDiagnosticsPanel.jsx +++ b/src/web/components/ConnectivityDiagnosticsPanel.jsx @@ -1,23 +1,14 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { flushSync } from 'react-dom'; import { api } from '../api.js'; import { - assessConnectivity, CONNECTIVITY_IP_SOURCES, CONNECTIVITY_SITES, MAX_CUSTOM_DIAGNOSTIC_SERVICES, } from '../../shared/connectivityDiagnostics.js'; const CUSTOM_SERVICES_KEY = 'harbor-diagnostic-services'; -const SUMMARY_COPY = { - available: ['is-good', 'Оба маршрута работают.'], - 'likely-direct-restriction': ['is-warning', 'Прямой маршрут, похоже, ограничен.'], - 'vpn-problem': ['is-error', 'VPN-маршрут требует проверки.'], - 'direct-offline': ['is-error', 'Прямой маршрут недоступен.'], - offline: ['is-error', 'Оба маршрута недоступны.'], - 'same-ip': ['is-warning', 'Внешний IP не изменился.'], - 'vpn-off': ['is-muted', 'VPN выключен.'], - inconclusive: ['is-muted', 'Недостаточно данных.'], -}; +const HIDDEN_SERVICES_KEY = 'harbor-hidden-diagnostic-services'; function readCustomServices() { try { @@ -36,6 +27,17 @@ function readCustomServices() { } } +function readHiddenServices() { + try { + const value = JSON.parse(localStorage.getItem(HIDDEN_SERVICES_KEY) || '[]'); + return Array.isArray(value) + ? value.filter((id) => CONNECTIVITY_SITES.some((service) => service.id === id)) + : []; + } catch { + return []; + } +} + function resultStatus(site, pending, available = true) { if (!available) return ['is-muted', '—']; if (pending) return ['is-running', 'Тестируем']; @@ -101,7 +103,7 @@ function mergePath(previous, incoming) { function mergeResult(previous, incoming) { const direct = mergePath(previous?.direct, incoming.direct); const vpn = mergePath(previous?.vpn, incoming.vpn); - return { ...incoming, direct, vpn, assessment: assessConnectivity(direct, vpn) }; + return { ...incoming, direct, vpn }; } export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeRef, onClose }) { @@ -110,7 +112,9 @@ export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeR const [activeTarget, setActiveTarget] = useState(null); const [error, setError] = useState(null); const [customServices, setCustomServices] = useState(readCustomServices); + const [hiddenServiceIds, setHiddenServiceIds] = useState(readHiddenServices); const [adding, setAdding] = useState(false); + const [removingServiceId, setRemovingServiceId] = useState(''); const [serviceName, setServiceName] = useState(''); const [serviceUrl, setServiceUrl] = useState(''); const [formError, setFormError] = useState(''); @@ -126,6 +130,14 @@ export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeR } }, [customServices]); + useEffect(() => { + try { + localStorage.setItem(HIDDEN_SERVICES_KEY, JSON.stringify(hiddenServiceIds)); + } catch { + // The service list still works for this session when browser storage is unavailable. + } + }, [hiddenServiceIds]); + useLayoutEffect(() => { const sheet = sheetRef.current; const runner = runnerRef.current; @@ -157,7 +169,7 @@ export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeR let next = result; const targets = [ ...CONNECTIVITY_IP_SOURCES.map(({ id }) => `ip:${id}`), - ...[...CONNECTIVITY_SITES, ...customServices].map(({ id }) => `site:${id}`), + ...sites.map(({ id }) => `site:${id}`), ]; for (const target of targets) { setActiveTarget(target); @@ -191,17 +203,49 @@ export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeR setServiceUrl(''); setFormError(''); setAdding(false); + setResult(null); } catch (validationError) { setFormError(validationError.message || 'Проверьте адрес.'); } } + function removeService(serviceId) { + if (matchMedia('(prefers-reduced-motion: reduce)').matches) { + finishRemoveService(serviceId); + return; + } + setRemovingServiceId(serviceId); + } + + function finishRemoveService(serviceId) { + const update = () => flushSync(() => { + if (serviceId === 'draft') { + setAdding(false); + setServiceName(''); + setServiceUrl(''); + setFormError(''); + } else if (serviceId.startsWith('custom-')) { + setCustomServices((services) => services.filter((service) => service.id !== serviceId)); + } else { + setHiddenServiceIds((ids) => [...new Set([...ids, serviceId])]); + } + setRemovingServiceId(''); + setResult(null); + }); + if (!document.startViewTransition || matchMedia('(prefers-reduced-motion: reduce)').matches) { + update(); + return; + } + document.startViewTransition(update); + } + const pending = status === 'running'; - const sites = [...CONNECTIVITY_SITES, ...customServices]; - const summary = SUMMARY_COPY[result?.assessment?.summary] || SUMMARY_COPY.inconclusive; - const checkedAt = result?.checkedAt - ? new Date(result.checkedAt).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) - : null; + const sites = [ + ...CONNECTIVITY_SITES.filter(({ id }) => !hiddenServiceIds.includes(id)), + ...customServices, + ]; + const serviceEditorBlocked = pending || Boolean(removingServiceId); + const addHint = formError || (adding ? 'Введите адрес и нажмите «Добавить»' : ''); return (