Refactor VPN proxy components and update related behavior
Build and Deploy Gateway / build-and-push (push) Successful in 20s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-11 01:27:46 +03:00
parent c89e56942a
commit aa9c959368
58 changed files with 4234 additions and 2755 deletions
+21 -21
View File
@@ -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>
+21 -13
View File
@@ -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