import React, { useEffect, useMemo, useState } from 'react';
import { api } from '../api.js';
import {
autoServer,
filterServers,
groupServers,
SERVER_RESULT_WINDOW,
} from '../utils/serverPicker.js';
const FAVORITES_KEY = 'harbor-server-favorites';
const RECENT_KEY = 'harbor-server-recent';
const AUTO_KEY = 'harbor-server-auto';
const SIMPLE_SERVER_LIMIT = 5;
function readList(key) {
try {
const value = JSON.parse(localStorage.getItem(key) || '[]');
return Array.isArray(value) ? value.map(String) : [];
} catch {
return [];
}
}
function write(key, value) {
try {
localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
} catch {
// Preferences remain available for this session.
}
}
function readAuto() {
try {
return localStorage.getItem(AUTO_KEY) === 'true';
} catch {
return false;
}
}
function serverHealthText(ping) {
if (ping?.error) return 'Проверка недоступна';
if (ping?.ok) return `${ping.latency} мс`;
return ping ? 'Недоступен' : null;
}
function ServerHealth({ ping, fallback }) {
const health = fallback || serverHealthText(ping);
if (!health && !ping?.checking) return null;
return
{health}
Проверяем…
;
}
function ServerCheckButton({ checking, disabled, onClick }) {
return ;
}
function ServerRow({ server, selected, favorite, ping, disabled, index, onSelect, onFavorite }) {
const health = ping?.checking ? 'Проверяем пинг' : serverHealthText(ping);
return
{onFavorite && }
;
}
export function ServerPicker({
servers,
selectedServerId,
disabled,
prompt,
leaving,
revealVersion,
onSelect,
}) {
const [query, setQuery] = useState('');
const [advanced, setAdvanced] = useState(false);
const [view, setView] = useState('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 [collapsed, setCollapsed] = useState([]);
const [pings, setPings] = useState({});
const [checking, setChecking] = useState(false);
const serverKey = servers.map(({ id }) => id).join('|');
useEffect(() => {
setPage(0);
}, [query, view, serverKey]);
const selected = servers.find(({ id }) => id === selectedServerId);
const filtered = useMemo(() => {
const found = filterServers(servers, query);
if (view === 'favorites') return found.filter(({ id }) => favorites.includes(id));
if (view === 'recent') return recent.flatMap((id) => found.find((server) => server.id === id) || []);
return found;
}, [servers, query, view, favorites, recent]);
const results = filtered.filter(({ id }) => id !== selectedServerId);
const pageCount = Math.max(1, Math.ceil(results.length / SERVER_RESULT_WINDOW));
const visible = results.slice(page * SERVER_RESULT_WINDOW, (page + 1) * SERVER_RESULT_WINDOW);
const grouped = servers.length >= 10;
useEffect(() => {
setPage((current) => Math.min(current, pageCount - 1));
}, [pageCount]);
function toggleFavorite(id) {
setFavorites((current) => {
const next = current.includes(id) ? current.filter((item) => item !== id) : [id, ...current];
write(FAVORITES_KEY, next);
return next;
});
}
function select(id, automatic = false) {
setAutoActive(automatic);
write(AUTO_KEY, String(automatic));
if (!automatic) {
setRecent((current) => {
const next = [id, ...current.filter((item) => item !== id)].slice(0, 5);
write(RECENT_KEY, next);
return next;
});
}
onSelect(id);
}
async function checkVisible() {
const ids = [...new Set([selectedServerId, ...visible.map(({ id }) => id)].filter(Boolean))].slice(0, 30);
if (!ids.length) return;
const startedAt = performance.now();
setChecking(true);
setPings((current) => ({
...current,
...Object.fromEntries(ids.map((id) => [id, { ...current[id], checking: true }])),
}));
try {
const data = await api.servers.ping(ids);
setPings((current) => ({
...current,
...Object.fromEntries((data.results || []).map((result) => [result.id, { ...result, checking: true }])),
}));
} catch {
setPings((current) => ({
...current,
...Object.fromEntries(ids.map((id) => [id, { error: true, checking: true, checkedAt: new Date().toISOString() }])),
}));
} finally {
await new Promise((resolve) => setTimeout(resolve, Math.max(0, 700 - (performance.now() - startedAt))));
setPings((current) => ({
...current,
...Object.fromEntries(ids.map((id) => [id, { ...current[id], checking: false }])),
}));
setChecking(false);
}
}
if (servers.length === 1) {
return
{prompt && Выберите сервер}
;
}
const renderRows = (items, offset = 0) => items.map((server, index) => (
));
const simpleServers = [
...(selected ? [selected] : []),
...servers.filter(({ id }) => id !== selectedServerId),
].slice(0, SIMPLE_SERVER_LIMIT);
return
{prompt && Выберите сервер}
{simpleServers.map((server, index) => )}
{servers.length > simpleServers.length &&
Ещё {servers.length - simpleServers.length} — доступны через поиск
}
{selected && }
{!visible.length &&
Серверы не найдены
}
{grouped ? groupServers(visible).map(([group, items]) => {
const isCollapsed = collapsed.includes(group);
return
{!isCollapsed && {renderRows(items)}
}
;
}) :
{renderRows(visible)}
}
{pageCount > 1 &&
}
;
}