diff --git a/frontend/src/FilterManager.jsx b/frontend/src/FilterManager.jsx
index 5d03a43..7a8138f 100644
--- a/frontend/src/FilterManager.jsx
+++ b/frontend/src/FilterManager.jsx
@@ -70,6 +70,207 @@ const countryToFlag = (code) => {
return String.fromCodePoint(...codePoints);
};
+// Компонент автокомплита для выбора Gateway
+function GatewayAutocomplete({ label, value, onChange, gateways = [], required = false, placeholder = 'Введите или выберите gateway...' }) {
+ const [inputValue, setInputValue] = useState(value || '');
+ const [isOpen, setIsOpen] = useState(false);
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
+ const containerRef = useRef(null);
+ const inputRef = useRef(null);
+
+ // Синхронизируем inputValue с value при изменении извне
+ useEffect(() => {
+ setInputValue(value || '');
+ }, [value]);
+
+ // Фильтруем gateways по введённому тексту
+ const filteredGateways = gateways.filter(gw => {
+ const search = inputValue.toLowerCase();
+ return (
+ gw.name.toLowerCase().includes(search) ||
+ (gw.ip && gw.ip.toLowerCase().includes(search)) ||
+ (gw.serverDns && gw.serverDns.toLowerCase().includes(search)) ||
+ (gw.comment && gw.comment.toLowerCase().includes(search))
+ );
+ });
+
+ // Закрытие при клике вне компонента
+ useEffect(() => {
+ const handleClickOutside = (e) => {
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
+ setIsOpen(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const handleInputChange = (e) => {
+ const val = e.target.value;
+ setInputValue(val);
+ onChange(val);
+ setIsOpen(true);
+ setHighlightedIndex(-1);
+ };
+
+ const handleSelect = (gw) => {
+ setInputValue(gw.name);
+ onChange(gw.name);
+ setIsOpen(false);
+ setHighlightedIndex(-1);
+ };
+
+ const handleKeyDown = (e) => {
+ if (!isOpen) {
+ if (e.key === 'ArrowDown' || e.key === 'Enter') {
+ setIsOpen(true);
+ e.preventDefault();
+ }
+ return;
+ }
+
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setHighlightedIndex(prev => Math.min(prev + 1, filteredGateways.length - 1));
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setHighlightedIndex(prev => Math.max(prev - 1, 0));
+ break;
+ case 'Enter':
+ e.preventDefault();
+ if (highlightedIndex >= 0 && filteredGateways[highlightedIndex]) {
+ handleSelect(filteredGateways[highlightedIndex]);
+ }
+ break;
+ case 'Escape':
+ setIsOpen(false);
+ setHighlightedIndex(-1);
+ break;
+ }
+ };
+
+ const selectedGateway = gateways.find(g => g.name === value);
+
+ return (
+
+
+
+
+
+
+
+ setIsOpen(true)}
+ onKeyDown={handleKeyDown}
+ placeholder={placeholder}
+ required={required}
+ autoComplete="off"
+ />
+ setIsOpen(!isOpen)}
+ >
+
+
+
+
+ {/* Dropdown */}
+ {isOpen && (
+
+ {gateways.length === 0 ? (
+
+
+ Нет доступных gateway
+
+ ) : filteredGateways.length === 0 ? (
+
+ Ничего не найдено. Нажмите Enter чтобы использовать "{inputValue}"
+
+ ) : (
+ <>
+ {inputValue && !gateways.some(g => g.name === inputValue) && (
+
Без привязки
+ )}
+ {filteredGateways.map((gw, idx) => (
+
handleSelect(gw)}
+ onMouseEnter={() => setHighlightedIndex(idx)}
+ >
+
+
+ {gw.country ? countryToFlag(gw.country) : }
+
+
+
+ {gw.name}
+ {gw.ip && ({gw.ip})}
+ {gw.serverDns && — {gw.serverDns}}
+
+
+ {[gw.country, gw.comment].filter(Boolean).join(' · ') || 'Шлюз'}
+
+
+ {gw.primary &&
Основной}
+
+
+ ))}
+ >
+ )}
+
+ )}
+
+
+ {/* Показываем выбранный gateway */}
+ {value && selectedGateway && !isOpen && (
+
+
+
+ {selectedGateway.country ? countryToFlag(selectedGateway.country) : }
+
+
+
{selectedGateway.name}
+
+ {[selectedGateway.ip, selectedGateway.serverDns, selectedGateway.comment].filter(Boolean).join(' · ')}
+
+
+ {selectedGateway.primary &&
Основной}
+
+
+ )}
+
+ {/* Предупреждение если gateway не в списке */}
+ {value && !selectedGateway && !isOpen && (
+
+
+
+
Gateway "{value}" — свободный ввод
+
+
+ )}
+
+ );
+}
+
function FilterManager() {
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
@@ -149,37 +350,70 @@ function FilterManager() {
// Inventory servers (from /servers endpoint)
const [inventoryServers, setInventoryServers] = useState([]);
- // Собираем все gateways из всех серверов для выбора
- const allGateways = (() => {
+ // Собираем gateways только для выбранного сервера
+ const selectedServerGateways = (() => {
+ if (!selectedServer) return [];
+
+ // Ищем соответствующий сервер в inventoryServers по имени
+ const matchedServer = inventoryServers.find(srv =>
+ String(srv.dns || '').trim() === selectedServer.name ||
+ String(srv.hostName || '').trim() === selectedServer.name ||
+ String(srv.ip || '').trim() === selectedServer.name
+ );
+
const gws = [];
- inventoryServers.forEach(srv => {
- if (Array.isArray(srv.gateways)) {
- srv.gateways.forEach(gw => {
- if (gw.name && !gws.some(g => g.name === gw.name)) {
+
+ if (matchedServer) {
+ // Добавляем gateways из найденного сервера
+ if (Array.isArray(matchedServer.gateways)) {
+ matchedServer.gateways.forEach(gw => {
+ if (gw.name) {
gws.push({
name: gw.name,
ip: gw.ip || '',
comment: gw.comment || '',
- serverDns: srv.dns || srv.hostName || '',
- country: srv.country || '',
+ serverDns: matchedServer.dns || matchedServer.hostName || '',
+ country: matchedServer.country || '',
primary: gw.primary
});
}
});
}
// Также добавляем старое поле gateway если есть
- if (srv.gateway && !gws.some(g => g.name === srv.gateway)) {
+ if (matchedServer.gateway && !gws.some(g => g.name === matchedServer.gateway)) {
gws.push({
- name: srv.gateway,
+ name: matchedServer.gateway,
ip: '',
comment: 'Legacy gateway',
- serverDns: srv.dns || srv.hostName || '',
- country: srv.country || '',
+ serverDns: matchedServer.dns || matchedServer.hostName || '',
+ country: matchedServer.country || '',
primary: true
});
}
+ }
+
+ // Также проверяем gateways из selectedServer (server-configs)
+ if (Array.isArray(selectedServer.gateways)) {
+ selectedServer.gateways.forEach(gw => {
+ if (gw.name && !gws.some(g => g.name === gw.name)) {
+ gws.push({
+ name: gw.name,
+ ip: gw.ip || '',
+ comment: gw.comment || '',
+ serverDns: selectedServer.name || '',
+ country: matchedServer?.country || '',
+ primary: gw.primary
+ });
+ }
+ });
+ }
+
+ return gws.sort((a, b) => {
+ // Primary первым
+ if (a.primary && !b.primary) return -1;
+ if (!a.primary && b.primary) return 1;
+ return a.name.localeCompare(b.name);
});
- return gws.sort((a, b) => a.name.localeCompare(b.name));
})();
useEffect(() => {
@@ -1357,50 +1591,13 @@ function FilterManager() {
helpText="Введите community в формате AS:VALUE"
/>
-
-
-
- {allGateways.length === 0 && (
-
-
- Нет доступных gateway. Добавьте шлюзы в разделе Серверы.
-
- )}
- {newFilter.gateway && (
-
- {(() => {
- const selected = allGateways.find(g => g.name === newFilter.gateway);
- if (!selected) return null;
- return (
-
-
- {selected.country ? countryToFlag(selected.country) : }
-
-
-
{selected.name}
-
- {[selected.ip, selected.serverDns, selected.comment].filter(Boolean).join(' · ')}
-
-
- {selected.primary &&
Основной}
-
- );
- })()}
-
- )}
-
+ setNewFilter({ ...newFilter, gateway: val })}
+ gateways={selectedServerGateways}
+ required
+ />
-
-
-
- {allGateways.length === 0 && (
-
-
- Нет доступных gateway. Добавьте шлюзы в разделе Серверы.
-
- )}
- {editingFilter.gateway && (
-
- {(() => {
- const selected = allGateways.find(g => g.name === editingFilter.gateway);
- if (!selected) {
- return (
-
-
-
Gateway "{editingFilter.gateway}" не найден в списке серверов
-
- );
- }
- return (
-
-
- {selected.country ? countryToFlag(selected.country) : }
-
-
-
{selected.name}
-
- {[selected.ip, selected.serverDns, selected.comment].filter(Boolean).join(' · ')}
-
-
- {selected.primary &&
Основной}
-
- );
- })()}
-
- )}
-
+ setEditingFilter({ ...editingFilter, gateway: val })}
+ gateways={selectedServerGateways}
+ required
+ />