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 + />