Refactor VPN proxy components and update related behavior
This commit is contained in:
@@ -26,7 +26,6 @@ interface ConnectionPanelProps {
|
||||
connected: boolean;
|
||||
gatewayDirect: boolean;
|
||||
selectedServerId: string;
|
||||
configured: boolean;
|
||||
startedAt?: string | null;
|
||||
gatewayAddress: string;
|
||||
gatewayUiOrigin?: string | null;
|
||||
@@ -41,7 +40,6 @@ interface ConnectionPanelProps {
|
||||
statusSlot?: ReactNode;
|
||||
onCopyProxy: (kind: CopyKind) => unknown;
|
||||
onApply: (serverId: string) => unknown;
|
||||
onRestart: () => unknown;
|
||||
onStop: () => unknown;
|
||||
}
|
||||
|
||||
@@ -67,7 +65,6 @@ export function ConnectionPanel({
|
||||
connected,
|
||||
gatewayDirect,
|
||||
selectedServerId,
|
||||
configured,
|
||||
startedAt,
|
||||
gatewayAddress,
|
||||
gatewayUiOrigin,
|
||||
@@ -82,7 +79,6 @@ export function ConnectionPanel({
|
||||
statusSlot,
|
||||
onCopyProxy,
|
||||
onApply,
|
||||
onRestart,
|
||||
onStop,
|
||||
}: ConnectionPanelProps) {
|
||||
const [durationMode, setDurationMode] = useState(() => {
|
||||
@@ -93,8 +89,9 @@ export function ConnectionPanel({
|
||||
}
|
||||
});
|
||||
const [confirmingStop, setConfirmingStop] = useState(false);
|
||||
const canStart = Boolean(selectedServerId || configured);
|
||||
const powerUnavailable = isGateway && !connected && !canStart;
|
||||
const remoteOwned = !isGateway && gatewayDirect;
|
||||
const canStart = Boolean(selectedServerId);
|
||||
const powerUnavailable = remoteOwned || (!connected && !canStart);
|
||||
const proxyUrls = localProxyUrls(proxyPort, gatewayAddress);
|
||||
const duration = connectionDurationParts(startedAt, now);
|
||||
const clockUnits: Array<[string, DurationUnit]> = [
|
||||
@@ -104,9 +101,9 @@ export function ConnectionPanel({
|
||||
];
|
||||
const wordClockDuration = clockUnits
|
||||
.filter(([name, part]) => duration.days.value || part.value || name === 'seconds');
|
||||
const connectionTitle = connected
|
||||
? gatewayDirect ? 'Gateway подключён' : 'VPN включён'
|
||||
: 'Подключение выключено';
|
||||
const connectionTitle = remoteOwned
|
||||
? 'Gateway подключён'
|
||||
: connected ? 'VPN включён' : 'Подключение выключено';
|
||||
const proxyKinds: Array<[CopyKind, string]> = isGateway
|
||||
? [
|
||||
['gateway', 'GATEWAY'],
|
||||
@@ -119,13 +116,12 @@ export function ConnectionPanel({
|
||||
];
|
||||
|
||||
function toggleConnection() {
|
||||
const action = connectionAction({ connected, selectedServerId, configExists: configured });
|
||||
const action = connectionAction({ connected, selectedServerId });
|
||||
if (action?.type === 'stop') {
|
||||
setConfirmingStop(true);
|
||||
return;
|
||||
}
|
||||
if (action?.type === 'apply') return onApply(action.serverId);
|
||||
if (action?.type === 'restart') return onRestart();
|
||||
}
|
||||
|
||||
async function stopConnection() {
|
||||
@@ -149,12 +145,14 @@ export function ConnectionPanel({
|
||||
className="client-power"
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={connected}
|
||||
aria-label={isGateway
|
||||
aria-checked={connected || remoteOwned}
|
||||
aria-label={remoteOwned
|
||||
? 'Подключением управляет Harbor Gateway'
|
||||
: isGateway
|
||||
? connected ? 'Остановить VPN' : 'Запустить VPN'
|
||||
: connected ? 'Остановить Harbor Connect' : 'Запустить Harbor Connect'}
|
||||
aria-describedby={powerUnavailable ? 'gateway-power-unavailable' : undefined}
|
||||
disabled={blocked || (!connected && !canStart)}
|
||||
disabled={blocked || powerUnavailable}
|
||||
onClick={toggleConnection}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
@@ -165,7 +163,7 @@ export function ConnectionPanel({
|
||||
return <>
|
||||
{visible ? <section className={`client-power-section${isGateway ? ' is-gateway' : ''}`} aria-labelledby="connection-title">
|
||||
<div
|
||||
className={`client-power-control${isGateway ? ' client-tooltip-anchor' : ''}`}
|
||||
className={`client-power-control${powerUnavailable ? ' client-tooltip-anchor' : ''}`}
|
||||
tabIndex={powerUnavailable ? 0 : undefined}
|
||||
aria-label={powerUnavailable ? 'VPN недоступен' : undefined}
|
||||
aria-describedby={powerUnavailable ? 'gateway-power-unavailable' : undefined}
|
||||
@@ -173,11 +171,11 @@ export function ConnectionPanel({
|
||||
{brandSlot}
|
||||
{powerButton}
|
||||
{powerUnavailable && <span className="client-tooltip" id="gateway-power-unavailable" role="tooltip">
|
||||
Сначала добавьте подписку и выберите сервер
|
||||
{remoteOwned ? 'Подключением управляет Harbor Gateway' : 'Сначала добавьте подписку и выберите сервер'}
|
||||
</span>}
|
||||
</div>
|
||||
<div className="client-state-detail">
|
||||
{connected ? (
|
||||
{connected && !remoteOwned ? (
|
||||
<button
|
||||
className={`client-duration-toggle client-tooltip-anchor${durationMode === 'words' ? ' is-words' : ''}`}
|
||||
type="button"
|
||||
@@ -224,16 +222,18 @@ export function ConnectionPanel({
|
||||
</button>
|
||||
) : (
|
||||
<p key="hint">
|
||||
{canStart ? 'Нажмите, чтобы включить' : 'Добавьте ссылку и выберите сервер'}
|
||||
{remoteOwned
|
||||
? 'Управляется Harbor Gateway'
|
||||
: canStart ? 'Нажмите, чтобы включить' : 'Добавьте ссылку и выберите сервер'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{routingSlot}
|
||||
<div className="client-state-copy" aria-live="polite">
|
||||
<h2 id="connection-title" className="client-connection-title" aria-label={connectionTitle}>
|
||||
<span className={!connected ? 'is-active' : ''} aria-hidden="true">Подключение выключено</span>
|
||||
<span className={connected && !gatewayDirect ? 'is-active' : ''} aria-hidden="true">VPN включён</span>
|
||||
<span className={connected && gatewayDirect ? 'is-active' : ''} aria-hidden="true">Gateway подключён</span>
|
||||
<span className={!connected && !remoteOwned ? 'is-active' : ''} aria-hidden="true">Подключение выключено</span>
|
||||
<span className={connected && !remoteOwned ? 'is-active' : ''} aria-hidden="true">VPN включён</span>
|
||||
<span className={remoteOwned ? 'is-active' : ''} aria-hidden="true">Gateway подключён</span>
|
||||
</h2>
|
||||
{serverSlot}
|
||||
</div>
|
||||
|
||||
@@ -53,9 +53,9 @@ function write(key: string, value: string | string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function readAuto() {
|
||||
function readAuto(key: string) {
|
||||
try {
|
||||
return localStorage.getItem(AUTO_KEY) === 'true';
|
||||
return localStorage.getItem(key) === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -153,7 +153,8 @@ function ServerRow({
|
||||
}
|
||||
|
||||
interface ServerPickerProps {
|
||||
pingServers: (ids: string[]) => Promise<unknown>;
|
||||
profileId: string;
|
||||
pingServers: (profileId: string, ids: string[]) => Promise<unknown>;
|
||||
servers: PickerServer[];
|
||||
selectedServerId: string;
|
||||
disabled: boolean;
|
||||
@@ -164,6 +165,7 @@ interface ServerPickerProps {
|
||||
}
|
||||
|
||||
export function ServerPicker({
|
||||
profileId,
|
||||
pingServers,
|
||||
servers,
|
||||
selectedServerId,
|
||||
@@ -173,13 +175,16 @@ export function ServerPicker({
|
||||
revealVersion,
|
||||
onSelect,
|
||||
}: ServerPickerProps) {
|
||||
const favoritesKey = `${FAVORITES_KEY}:${profileId}`;
|
||||
const recentKey = `${RECENT_KEY}:${profileId}`;
|
||||
const autoKey = `${AUTO_KEY}:${profileId}`;
|
||||
const [query, setQuery] = useState('');
|
||||
const [advanced, setAdvanced] = useState(false);
|
||||
const [view, setView] = useState<'all' | 'favorites' | 'recent'>('all');
|
||||
const [page, setPage] = useState(0);
|
||||
const [favorites, setFavorites] = useState(() => readList(FAVORITES_KEY));
|
||||
const [recent, setRecent] = useState(() => readList(RECENT_KEY));
|
||||
const [autoActive, setAutoActive] = useState(readAuto);
|
||||
const [favorites, setFavorites] = useState(() => readList(favoritesKey));
|
||||
const [recent, setRecent] = useState(() => readList(recentKey));
|
||||
const [autoActive, setAutoActive] = useState(() => readAuto(autoKey));
|
||||
const [collapsed, setCollapsed] = useState<string[]>([]);
|
||||
const [pings, setPings] = useState<PingState>({});
|
||||
const [checking, setChecking] = useState(false);
|
||||
@@ -208,18 +213,18 @@ export function ServerPicker({
|
||||
function toggleFavorite(id: string) {
|
||||
setFavorites((current) => {
|
||||
const next = current.includes(id) ? current.filter((item) => item !== id) : [id, ...current];
|
||||
write(FAVORITES_KEY, next);
|
||||
write(favoritesKey, next);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function select(id: string, automatic = false) {
|
||||
setAutoActive(automatic);
|
||||
write(AUTO_KEY, String(automatic));
|
||||
write(autoKey, String(automatic));
|
||||
if (!automatic) {
|
||||
setRecent((current) => {
|
||||
const next = [id, ...current.filter((item) => item !== id)].slice(0, 5);
|
||||
write(RECENT_KEY, next);
|
||||
write(recentKey, next);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
@@ -236,7 +241,7 @@ export function ServerPicker({
|
||||
...Object.fromEntries(ids.map((id) => [id, { ...current[id], checking: true }])),
|
||||
}));
|
||||
try {
|
||||
const results = parseServerPingResults(await pingServers(ids)) as PingResult[];
|
||||
const results = parseServerPingResults(await pingServers(profileId, ids)) as PingResult[];
|
||||
setPings((current) => ({
|
||||
...current,
|
||||
...Object.fromEntries(results.map((result) => [result.id, { ...result, checking: true }])),
|
||||
@@ -251,7 +256,10 @@ export function ServerPicker({
|
||||
}])),
|
||||
}));
|
||||
} finally {
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.max(0, 900 - (performance.now() - startedAt))));
|
||||
const elapsed = performance.now() - startedAt;
|
||||
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const completeAt = reduced ? elapsed : Math.max(900, Math.ceil(elapsed / 900) * 900);
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.max(0, completeAt - elapsed)));
|
||||
setPings((current) => ({
|
||||
...current,
|
||||
...Object.fromEntries(ids.map((id) => [id, { ...current[id], checking: false }])),
|
||||
@@ -324,7 +332,7 @@ export function ServerPicker({
|
||||
inert={advanced ? true : undefined}
|
||||
>
|
||||
<div className="client-server-mode-panel-inner">
|
||||
<div className={`client-server-scroll${leaving ? ' is-leaving' : ''}`} key={`simple:${serverKey}:${revealVersion}`}>
|
||||
<div className={`client-server-scroll${leaving ? ' is-leaving' : ''}`} key={`simple:${revealVersion}`}>
|
||||
<div className="client-server-grid">
|
||||
{simpleServers.map((server, index) => <ServerRow
|
||||
key={server.id}
|
||||
@@ -401,7 +409,7 @@ export function ServerPicker({
|
||||
/>}
|
||||
</div>
|
||||
|
||||
<div className={`client-server-scroll${leaving ? ' is-leaving' : ''}`} key={`advanced:${serverKey}:${revealVersion}`}>
|
||||
<div className={`client-server-scroll${leaving ? ' is-leaving' : ''}`} key={`advanced:${revealVersion}`}>
|
||||
{!visible.length && <p className="client-server-empty">Серверы не найдены</p>}
|
||||
{grouped ? groupServers(visible).map(([group, items]) => {
|
||||
const isCollapsed = collapsed.includes(group);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user