Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m41s
223 lines
8.1 KiB
React
223 lines
8.1 KiB
React
import { useState, useEffect, useRef } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import {
|
|
IconSearch,
|
|
IconWorld,
|
|
IconNetwork,
|
|
IconServer,
|
|
IconFilter,
|
|
IconHome,
|
|
IconDatabase,
|
|
IconCreditCard,
|
|
IconDownload,
|
|
IconKeyboard
|
|
} from '@tabler/icons-react'
|
|
|
|
/**
|
|
* Command Palette - глобальный поиск по командам (Ctrl+K)
|
|
* Простой подход со встроенным backdrop (как в HistoryModal)
|
|
*/
|
|
// Создаем контекст для управления открытием/закрытием Command Palette
|
|
const CommandPaletteContext = { open: null };
|
|
|
|
function CommandPalette() {
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
const [searchTerm, setSearchTerm] = useState('')
|
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
const navigate = useNavigate()
|
|
const inputRef = useRef(null)
|
|
|
|
// Сохраняем функцию открытия в контекст
|
|
useEffect(() => {
|
|
CommandPaletteContext.open = () => setIsOpen(true);
|
|
return () => {
|
|
CommandPaletteContext.open = null;
|
|
};
|
|
}, [])
|
|
|
|
// Список команд
|
|
const commands = [
|
|
{ icon: IconHome, label: 'Главная', description: 'Панель управления', action: () => navigate('/dashboard'), keywords: ['главная', 'панель', 'dashboard'] },
|
|
{ icon: IconWorld, label: 'Домены', description: 'Управление доменами', action: () => navigate('/domains'), keywords: ['домены', 'domains'] },
|
|
{ icon: IconNetwork, label: 'IP-диапазоны', description: 'Управление IP диапазонами', action: () => navigate('/ip-ranges'), keywords: ['ip', 'диапазоны', 'ranges'] },
|
|
{ icon: IconNetwork, label: 'ASN', description: 'Управление Autonomous Systems', action: () => navigate('/asns'), keywords: ['asn', 'as', 'autonomous'] },
|
|
{ icon: IconFilter, label: 'Community', description: 'Справочник BGP Community', action: () => navigate('/communities'), keywords: ['community', 'справочник'] },
|
|
{ icon: IconServer, label: 'Серверы', description: 'Управление серверами', action: () => navigate('/servers'), keywords: ['серверы', 'servers'] },
|
|
{ icon: IconFilter, label: 'Фильтры', description: 'Filter Manager', action: () => navigate('/filters'), keywords: ['фильтры', 'filters', 'mikrotik'] },
|
|
{ icon: IconCreditCard, label: 'Биллинг', description: 'Управление биллингом', action: () => navigate('/billing'), keywords: ['биллинг', 'billing', 'оплата'] },
|
|
{ icon: IconDownload, label: 'Авто-URL', description: 'Генератор ссылок', action: () => navigate('/auto-urls'), keywords: ['url', 'ссылки', 'генератор'] },
|
|
]
|
|
|
|
// Фильтрация команд по поисковому запросу
|
|
const filteredCommands = searchTerm.trim() === ''
|
|
? commands
|
|
: commands.filter(cmd =>
|
|
cmd.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
cmd.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
cmd.keywords.some(kw => kw.includes(searchTerm.toLowerCase()))
|
|
)
|
|
|
|
// Открытие/закрытие по Ctrl+K или Cmd+K
|
|
useEffect(() => {
|
|
const handleKeyDown = (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
e.preventDefault()
|
|
setIsOpen(prev => !prev)
|
|
}
|
|
|
|
if (e.key === 'Escape') {
|
|
setIsOpen(false)
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
}, [])
|
|
|
|
// Навигация по списку стрелками
|
|
useEffect(() => {
|
|
if (!isOpen) return
|
|
|
|
const handleKeyDown = (e) => {
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault()
|
|
setSelectedIndex(prev => Math.min(prev + 1, filteredCommands.length - 1))
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault()
|
|
setSelectedIndex(prev => Math.max(prev - 1, 0))
|
|
} else if (e.key === 'Enter') {
|
|
e.preventDefault()
|
|
if (filteredCommands[selectedIndex]) {
|
|
executeCommand(filteredCommands[selectedIndex])
|
|
}
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
}, [isOpen, selectedIndex, filteredCommands])
|
|
|
|
// Фокус на input при открытии
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setSearchTerm('')
|
|
setSelectedIndex(0)
|
|
setTimeout(() => inputRef.current?.focus(), 50)
|
|
}
|
|
}, [isOpen])
|
|
|
|
const executeCommand = (command) => {
|
|
command.action()
|
|
setIsOpen(false)
|
|
}
|
|
|
|
if (!isOpen) return null
|
|
|
|
return (
|
|
<div
|
|
className="modal show d-block"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
|
|
onClick={() => setIsOpen(false)}
|
|
>
|
|
<div
|
|
className="modal-dialog modal-dialog-centered"
|
|
style={{ maxWidth: '600px' }}
|
|
onClick={(e) => e.stopPropagation()}
|
|
role="document"
|
|
>
|
|
<div className="modal-content" tabIndex={-1}>
|
|
{/* Search input */}
|
|
<div className="modal-header border-0 pb-0">
|
|
<div className="input-icon w-100">
|
|
<span className="input-icon-addon">
|
|
<IconSearch size={18} />
|
|
</span>
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
className="form-control form-control-lg border-0"
|
|
placeholder="Поиск команд..."
|
|
value={searchTerm}
|
|
onChange={(e) => {
|
|
setSearchTerm(e.target.value)
|
|
setSelectedIndex(0)
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Commands list */}
|
|
<div className="modal-body pt-2">
|
|
{filteredCommands.length === 0 ? (
|
|
<div className="text-center text-muted py-4">
|
|
<IconSearch size={48} className="mb-2 opacity-50" />
|
|
<div>Команды не найдены</div>
|
|
</div>
|
|
) : (
|
|
<div className="list-group list-group-flush">
|
|
{filteredCommands.map((cmd, index) => {
|
|
const Icon = cmd.icon
|
|
return (
|
|
<button
|
|
key={index}
|
|
type="button"
|
|
className={`list-group-item list-group-item-action d-flex align-items-center${
|
|
index === selectedIndex ? ' active' : ''
|
|
}`}
|
|
onClick={() => executeCommand(cmd)}
|
|
onMouseEnter={() => setSelectedIndex(index)}
|
|
>
|
|
<Icon size={20} className="me-3" />
|
|
<div className="flex-grow-1 text-start">
|
|
<div className="fw-bold">{cmd.label}</div>
|
|
<small className="text-muted">{cmd.description}</small>
|
|
</div>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer with hint */}
|
|
<div className="modal-footer border-0 pt-0">
|
|
<div className="text-muted small d-flex align-items-center gap-2">
|
|
<IconKeyboard size={16} />
|
|
<span>
|
|
<kbd>↑</kbd> <kbd>↓</kbd> навигация • <kbd>Enter</kbd> выбрать • <kbd>ESC</kbd> закрыть
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Кнопка для открытия Command Palette через клик
|
|
*/
|
|
export function KeyboardShortcutsButton({ className = '' }) {
|
|
const handleClick = (e) => {
|
|
e.preventDefault();
|
|
if (CommandPaletteContext.open) {
|
|
CommandPaletteContext.open();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<a
|
|
href="#"
|
|
className={className}
|
|
onClick={handleClick}
|
|
title="Командная палитра (Ctrl+K)"
|
|
>
|
|
<IconKeyboard size={20} />
|
|
</a>
|
|
);
|
|
}
|
|
|
|
export default CommandPalette
|