import { Copy, FileSymlink, Files, Folder, FolderOpen, Monitor, Pencil, Plus, Server, Settings2 } from 'lucide-react'; import React from 'react'; import { useI18n } from '../../application/i18n/I18nProvider'; import { requestOpenDualPaneSftp } from '../../application/state/sftp/sftpDualPaneOpenStore'; import { useSettingsChromeStore } from '../../application/state/settingsChromeStore'; import { sanitizeHost } from '../../domain/host'; import { isPluginHostProtocol } from '../../domain/pluginConnection'; import { canOpenDualPaneSftp } from '../../domain/sftpDualPaneOpen'; import type { Host } from '../../types'; import { ContextMenuContent, ContextMenuItem, ContextMenuShortcut } from '../ui/context-menu'; import { collectOwnedPluginMenus, comparePluginMenus, usePluginContributions } from '../../application/state/usePluginContributions'; import { PluginContributionIcon } from '../plugins/PluginContributionIcon'; export interface HostTreeHostContextMenuHandlers { onConnect: (host: Host) => void; onEditHost?: (host: Host) => void; onRenameHost?: (host: Host) => void; onDuplicateHost: (host: Host) => void; onCopyHostname?: (host: Host) => void; onCopyCredentials: (host: Host) => void; onDeleteHost: (host: Host) => void; } export const OpenDualPaneSftpMenuItem: React.FC<{ host: Host }> = ({ host }) => { const { t } = useI18n(); const { showSftpTab } = useSettingsChromeStore(); if (!showSftpTab || !canOpenDualPaneSftp(host)) return null; return ( requestOpenDualPaneSftp(host.id)}> {t('vault.hosts.openSftp')} ); }; export const HostTreeHostContextMenuContent: React.FC< HostTreeHostContextMenuHandlers & { host: Host } > = ({ host, onConnect, onEditHost, onRenameHost, onDuplicateHost, onCopyHostname, onCopyCredentials, onDeleteHost, }) => { const { t } = useI18n(); const safeHost = sanitizeHost(host); const pluginContributions = usePluginContributions({ context: { 'netcatty.surface': 'host/context', 'host.id': safeHost.id, 'host.protocol': safeHost.protocol ?? 'ssh', }, }); const pluginMenus = collectOwnedPluginMenus(pluginContributions.snapshot.plugins) .filter((menu) => menu.location === 'host/context' && menu.visible) .sort(comparePluginMenus); const canCopyHostname = Boolean(onCopyHostname) && !isPluginHostProtocol(safeHost.protocol); return ( onConnect(safeHost)}> {t('vault.hosts.connect')} {onEditHost && ( onEditHost(host)}> {t('terminal.layer.hostTree.editHost')} )} {onRenameHost && ( onRenameHost(host)}> {t('common.rename')} )} onDuplicateHost(host)}> {t('action.duplicate')} {canCopyHostname ? ( onCopyHostname?.(host)}> {t('terminal.statusbar.copyHostname.label')} ) : null} onCopyCredentials(host)}> {t('vault.hosts.copyCredentials')} onDeleteHost(host)} className="text-destructive focus:text-destructive" > {t('action.delete')} {pluginMenus.map((menu) => ( void pluginContributions.executeCommand(event.altKey && menu.alt ? menu.alt : menu.command, { hostId: safeHost.id }, { 'netcatty.surface': 'host/context', 'host.id': safeHost.id, 'host.protocol': safeHost.protocol ?? 'ssh', }).catch(() => {})} > {menu.title} {menu.checked && } {menu.shortcut && {menu.shortcut}} ))} ); }; export interface HostTreeGroupContextMenuHandlers { onNewHost?: (groupPath: string) => void; onNewGroup: (parentPath?: string) => void; onRenameGroup: (groupPath: string) => void; onDeleteGroup: (groupPath: string) => void; onUnmanageGroup?: (groupPath: string) => void; } export const HostTreeGroupContextMenuContent: React.FC< HostTreeGroupContextMenuHandlers & { groupPath: string; isManaged: boolean } > = ({ groupPath, isManaged, onNewHost, onNewGroup, onRenameGroup, onDeleteGroup, onUnmanageGroup, }) => { const { t } = useI18n(); return ( {onNewHost && ( onNewHost(groupPath)}> {t('terminal.layer.hostTree.newHostInGroup')} )} onNewGroup(groupPath)}> {t('vault.hosts.newGroup')} onRenameGroup(groupPath)}> {t('vault.groups.rename')} onDeleteGroup(groupPath)} className="text-destructive focus:text-destructive" > {t('vault.groups.delete')} {isManaged && onUnmanageGroup && ( onUnmanageGroup(groupPath)}> {t('vault.managedSource.unmanage')} )} ); };