import { Check, ChevronRight, LayoutGrid, Plus, Search, } from 'lucide-react'; import React, { useEffect, useMemo, useState } from 'react'; import { cn } from '../lib/utils'; import { matchesHostSearchQuery, matchesSearchQuery } from '../lib/searchMatcher'; import { useI18n } from '../application/i18n/I18nProvider'; import { Host, ProxyProfile, SSHKey } from '../types'; import { ManagedSource } from '../domain/models'; import { DistroAvatar } from './DistroAvatar'; import HostDetailsPanel from './HostDetailsPanel'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { ScrollArea } from './ui/scroll-area'; import { SortDropdown, SortMode } from './ui/sort-dropdown'; import { TagFilterDropdown } from './ui/tag-filter-dropdown'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from './ui/tooltip'; export interface SelectHostPanelContentProps { hosts: Host[]; customGroups?: string[]; selectedHostIds: string[]; multiSelect?: boolean; onSelect: (host: Host) => void; onConfirm: () => void; onNewHost?: () => void; availableKeys?: SSHKey[]; identities?: import('../domain/models').Identity[]; proxyProfiles?: ProxyProfile[]; managedSources?: ManagedSource[]; onSaveHost?: (host: Host) => void; onCreateGroup?: (groupPath: string) => void; onNewHostPanelOpenChange?: (open: boolean) => void; className?: string; } /** Shared host-picker body used by aside panel and dialog variants. */ export const SelectHostPanelContent: React.FC = ({ hosts, customGroups = [], selectedHostIds, multiSelect = false, onSelect, onConfirm, onNewHost, availableKeys = [], identities = [], proxyProfiles = [], managedSources = [], onSaveHost, onCreateGroup, onNewHostPanelOpenChange, className, }) => { const { t } = useI18n(); const [searchQuery, setSearchQuery] = useState(''); const [currentPath, setCurrentPath] = useState(null); const [sortMode, setSortMode] = useState('az'); const [selectedTags, setSelectedTags] = useState([]); const [showNewHostPanel, setShowNewHostPanel] = useState(false); useEffect(() => { onNewHostPanelOpenChange?.(showNewHostPanel); }, [onNewHostPanelOpenChange, showNewHostPanel]); const selectableHosts = useMemo( () => hosts.filter((host) => host.protocol !== 'serial'), [hosts], ); const selectedHostIdSet = useMemo( () => new Set(selectedHostIds), [selectedHostIds], ); const allTags = useMemo(() => { const tagSet = new Set(); selectableHosts.forEach((host) => { host.tags?.forEach((tag) => tagSet.add(tag)); }); return Array.from(tagSet).sort(); }, [selectableHosts]); const allGroupPaths = useMemo(() => { const pathSet = new Set(); selectableHosts.forEach((host) => { if (host.group) { const parts = host.group.split('/'); for (let i = 1; i <= parts.length; i += 1) { pathSet.add(parts.slice(0, i).join('/')); } } }); customGroups.forEach((group) => pathSet.add(group)); return Array.from(pathSet).sort(); }, [selectableHosts, customGroups]); const groupHostCounts = useMemo(() => { const counts = new Map(); selectableHosts.forEach((host) => { if (!host.group) return; const parts = host.group.split('/'); for (let i = 1; i <= parts.length; i += 1) { const path = parts.slice(0, i).join('/'); counts.set(path, (counts.get(path) ?? 0) + 1); } }); return counts; }, [selectableHosts]); const groupsWithCounts = useMemo(() => { const prefix = currentPath ? `${currentPath}/` : ''; const groups: { path: string; name: string; count: number }[] = []; const seen = new Set(); allGroupPaths.forEach((path) => { if (currentPath === null) { const topLevel = path.split('/')[0]; if (!seen.has(topLevel)) { seen.add(topLevel); groups.push({ path: topLevel, name: topLevel, count: groupHostCounts.get(topLevel) ?? 0 }); } } else if (path.startsWith(prefix) && path !== currentPath) { const rest = path.slice(prefix.length); const nextLevel = rest.split('/')[0]; const fullPath = `${prefix}${nextLevel}`; if (!seen.has(fullPath)) { seen.add(fullPath); groups.push({ path: fullPath, name: nextLevel, count: groupHostCounts.get(fullPath) ?? 0 }); } } }); return groups; }, [allGroupPaths, currentPath, groupHostCounts]); const filteredHosts = useMemo(() => { let result = selectableHosts; if (currentPath) { result = result.filter( (host) => host.group === currentPath || host.group?.startsWith(`${currentPath}/`), ); } if (searchQuery) { result = result.filter( (host) => matchesHostSearchQuery(searchQuery, host) || matchesSearchQuery(searchQuery, host.username, host.notes), ); } if (selectedTags.length > 0) { result = result.filter( (host) => host.tags && selectedTags.some((tag) => host.tags.includes(tag)), ); } result = [...result].sort((a, b) => { switch (sortMode) { case 'az': return a.label.localeCompare(b.label); case 'za': return b.label.localeCompare(a.label); case 'newest': return b.id.localeCompare(a.id); case 'oldest': return a.id.localeCompare(b.id); default: return 0; } }); return result; }, [selectableHosts, currentPath, searchQuery, selectedTags, sortMode]); const breadcrumbs = useMemo(() => { if (!currentPath) return []; const parts = currentPath.split('/'); return parts.map((part, index) => ({ name: part, path: parts.slice(0, index + 1).join('/'), })); }, [currentPath]); return (
{(onNewHost || onSaveHost) ? ( ) : null}
setSearchQuery(event.target.value)} />
{currentPath ? (
{breadcrumbs.map((crumb, index) => ( ))}
) : null} {groupsWithCounts.length > 0 ? (

{t('vault.groups.title')}

{groupsWithCounts.map((group) => (
setCurrentPath(group.path)} >
{group.name}
{t('vault.groups.hostsCount', { count: group.count })}
))}
) : null} {filteredHosts.length > 0 ? (

{t('vault.nav.hosts')}

{filteredHosts.map((host) => { const isSelected = selectedHostIdSet.has(host.id); const connectionStr = `${host.username}@${host.hostname}:${host.port || 22}`; return (
onSelect(host)} >
{host.label}

{host.label}

{connectionStr}

{connectionStr}

{isSelected ? : null}
); })}
) : null} {groupsWithCounts.length === 0 && filteredHosts.length === 0 ? (

{t('selectHost.noHostsFound')}

) : null}
{showNewHostPanel && onSaveHost ? ( { onSaveHost(host); setShowNewHostPanel(false); }} onCancel={() => setShowNewHostPanel(false)} onCreateGroup={onCreateGroup} /> ) : null}
); };