mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat: load plugin frontends at runtime and remove plugin ids from the shell
This commit is contained in:
1 parent
49f0286a14
commit
e88d5a52e3
721 files changed
+54265
-44416
No files matched your search
+91
-115
@@ -14,22 +14,16 @@ import {
|
||||
Server,
|
||||
Settings,
|
||||
Terminal,
|
||||
FolderOpen,
|
||||
FolderSearch,
|
||||
Box,
|
||||
Globe,
|
||||
HardDrive,
|
||||
Plus,
|
||||
MessagesSquare,
|
||||
LifeBuoy,
|
||||
Search,
|
||||
Activity,
|
||||
Network,
|
||||
User,
|
||||
KeyRound,
|
||||
Layers, // --- tmux-monitor ---
|
||||
Monitor,
|
||||
MousePointerClick,
|
||||
Clock,
|
||||
Folder,
|
||||
Pencil,
|
||||
@@ -43,10 +37,17 @@ import {
|
||||
} from "@/main-axios";
|
||||
import type { Host, TabType, Tab, Snippet } from "@/types/ui-types";
|
||||
import { canEditHost } from "@/sidebar/host-permissions";
|
||||
import { RAIL_ITEMS, RAIL_UTILITY_ITEMS } from "@/sidebar/rail-items";
|
||||
import { useAiAvailability } from "@/hooks/use-ai-availability";
|
||||
import { RAIL_UTILITY_ITEMS, useRailItems } from "@/sidebar/rail-items";
|
||||
import { useSnippetRunner } from "@/hooks/use-snippet-runner.tsx";
|
||||
import { isTabTypeAvailable } from "@/shell/pluginLoader";
|
||||
import {
|
||||
defaultConnectAction,
|
||||
hostActionsFor,
|
||||
runHostAction,
|
||||
useHostActions,
|
||||
} from "@/sidebar/host-contributions";
|
||||
import { paletteEntriesFor, usePaletteEntries } from "./palette-registry";
|
||||
import { activityTarget } from "@/lib/activity-types";
|
||||
import { shell } from "@/plugin-host/shell-bridge";
|
||||
|
||||
interface CommandPaletteProps {
|
||||
isOpen: boolean;
|
||||
@@ -59,47 +60,18 @@ interface CommandPaletteProps {
|
||||
onOpenPanel?: (view: string) => void;
|
||||
}
|
||||
|
||||
const ACTIVITY_ICONS: Record<string, React.ReactNode> = {
|
||||
terminal: <Terminal className="size-3.5" />,
|
||||
file_manager: <FolderOpen className="size-3.5" />,
|
||||
server_stats: <Activity className="size-3.5" />,
|
||||
tunnel: <Network className="size-3.5" />,
|
||||
docker: <Box className="size-3.5" />,
|
||||
telnet: <MessagesSquare className="size-3.5" />,
|
||||
vnc: <MousePointerClick className="size-3.5" />,
|
||||
rdp: <Monitor className="size-3.5" />,
|
||||
};
|
||||
|
||||
const ACTIVITY_TAB_TYPE: Record<string, TabType> = {
|
||||
terminal: "terminal",
|
||||
file_manager: "files",
|
||||
server_stats: "host-metrics",
|
||||
tunnel: "tunnel",
|
||||
docker: "docker",
|
||||
telnet: "telnet",
|
||||
vnc: "vnc",
|
||||
rdp: "rdp",
|
||||
};
|
||||
|
||||
/** Core per-host tools. Plugins add theirs as host actions. */
|
||||
function getSshActions(host: Host): {
|
||||
type: TabType;
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
}[] {
|
||||
const metricsEnabled = host.statsConfig?.metricsEnabled !== false;
|
||||
return [
|
||||
host.enableTerminal !== false &&
|
||||
isTabTypeAvailable("terminal") && {
|
||||
type: "terminal",
|
||||
icon: Terminal,
|
||||
label: "Terminal",
|
||||
},
|
||||
host.enableFileManager && {
|
||||
type: "files",
|
||||
icon: FolderSearch,
|
||||
label: "Files",
|
||||
},
|
||||
host.enableDocker && { type: "docker", icon: Box, label: "Docker" },
|
||||
// --- tmux-monitor --- opt-in per host, off by default
|
||||
host.enableTerminal !== false &&
|
||||
host.enableTmuxMonitor && {
|
||||
@@ -108,16 +80,6 @@ function getSshActions(host: Host): {
|
||||
label: "Tmux Monitor",
|
||||
},
|
||||
host.enableTunnel && { type: "tunnel", icon: Network, label: "Tunnels" },
|
||||
metricsEnabled && {
|
||||
type: "host-metrics",
|
||||
icon: Activity,
|
||||
label: "Host Metrics",
|
||||
},
|
||||
host.enableProxmoxStats === true && {
|
||||
type: "proxmox-stats",
|
||||
icon: HardDrive,
|
||||
label: "Proxmox Stats",
|
||||
},
|
||||
].filter(Boolean) as {
|
||||
type: TabType;
|
||||
icon: React.ElementType;
|
||||
@@ -135,7 +97,9 @@ export function CommandPalette({
|
||||
onOpenPanel,
|
||||
}: CommandPaletteProps) {
|
||||
const { t } = useTranslation();
|
||||
const { globallyEnabled: aiGloballyEnabled } = useAiAvailability();
|
||||
const railItems = useRailItems();
|
||||
const hostActions = useHostActions();
|
||||
const paletteEntries = usePaletteEntries();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [recentActivity, setRecentActivity] = useState<RecentActivityItem[]>(
|
||||
@@ -396,9 +360,8 @@ export function CommandPalette({
|
||||
heading={t("commandPalette.navigation")}
|
||||
className="px-2"
|
||||
>
|
||||
{[...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]
|
||||
{[...railItems, ...RAIL_UTILITY_ITEMS]
|
||||
.filter((item) => item.kind !== "tab")
|
||||
.filter((item) => item.id !== "ai" || aiGloballyEnabled)
|
||||
.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
@@ -471,6 +434,37 @@ export function CommandPalette({
|
||||
</>
|
||||
)}
|
||||
|
||||
{paletteEntriesFor(paletteEntries, "global").length > 0 && (
|
||||
<>
|
||||
<CommandSeparator className="my-2" />
|
||||
<CommandGroup
|
||||
heading={t("commandPalette.pluginActions")}
|
||||
className="px-2"
|
||||
>
|
||||
{paletteEntriesFor(paletteEntries, "global").map((entry) => {
|
||||
const Icon = entry.icon;
|
||||
return (
|
||||
<CommandItem
|
||||
key={`plugin-${entry.id}`}
|
||||
value={`plugin-${entry.id} ${t(entry.titleKey)} ${(entry.keywords ?? []).join(" ")}`}
|
||||
onSelect={() => handleAction(() => entry.run(shell))}
|
||||
className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer"
|
||||
>
|
||||
<div className="size-8 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors">
|
||||
{Icon && (
|
||||
<Icon className="size-4 text-accent-brand" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm font-semibold flex-1">
|
||||
{t(entry.titleKey)}
|
||||
</span>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
{recentActivity.length > 0 && (
|
||||
<>
|
||||
<CommandSeparator className="my-2" />
|
||||
@@ -483,17 +477,18 @@ export function CommandPalette({
|
||||
key={item.id}
|
||||
value={`recent-activity-${item.id}`}
|
||||
onSelect={() =>
|
||||
handleAction(() =>
|
||||
onOpenTab(
|
||||
ACTIVITY_TAB_TYPE[item.type],
|
||||
item.hostName,
|
||||
),
|
||||
)
|
||||
handleAction(() => {
|
||||
const target = activityTarget(item.type);
|
||||
if (target) onOpenTab(target.tab, item.hostName);
|
||||
})
|
||||
}
|
||||
className="group flex items-center gap-3 px-3 py-2 rounded-none hover:bg-accent-brand/10 cursor-pointer"
|
||||
>
|
||||
<div className="size-7 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors text-muted-foreground group-hover:text-accent-brand">
|
||||
{ACTIVITY_ICONS[item.type]}
|
||||
{(() => {
|
||||
const Icon = activityTarget(item.type)?.icon;
|
||||
return Icon ? <Icon className="size-3.5" /> : null;
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<span className="text-sm font-semibold truncate">
|
||||
@@ -537,17 +532,11 @@ export function CommandPalette({
|
||||
value={`host-${host.id}`}
|
||||
onSelect={() =>
|
||||
handleAction(() => {
|
||||
const type = host.enableSsh
|
||||
? "terminal"
|
||||
: host.enableRdp && isTabTypeAvailable("rdp")
|
||||
? "rdp"
|
||||
: host.enableVnc && isTabTypeAvailable("vnc")
|
||||
? "vnc"
|
||||
: host.enableTelnet &&
|
||||
isTabTypeAvailable("telnet")
|
||||
? "telnet"
|
||||
: "terminal";
|
||||
onOpenTab(type, host.name);
|
||||
const action = defaultConnectAction(
|
||||
hostActions,
|
||||
host,
|
||||
);
|
||||
if (action) runHostAction(action, host, shell);
|
||||
})
|
||||
}
|
||||
className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer"
|
||||
@@ -599,59 +588,46 @@ export function CommandPalette({
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
{host.enableSsh &&
|
||||
((host.enableRdp && isTabTypeAvailable("rdp")) ||
|
||||
(host.enableVnc && isTabTypeAvailable("vnc")) ||
|
||||
(host.enableTelnet &&
|
||||
isTabTypeAvailable("telnet"))) && (
|
||||
<div className="w-px h-3.5 bg-border/60 mx-0.5 shrink-0" />
|
||||
)}
|
||||
{host.enableRdp && isTabTypeAvailable("rdp") && (
|
||||
<button
|
||||
title="RDP"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAction(() =>
|
||||
onOpenTab("rdp", host.name),
|
||||
);
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40"
|
||||
>
|
||||
<Monitor className="size-3" />
|
||||
RDP
|
||||
</button>
|
||||
)}
|
||||
{host.enableVnc && isTabTypeAvailable("vnc") && (
|
||||
<button
|
||||
title="VNC"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAction(() =>
|
||||
onOpenTab("vnc", host.name),
|
||||
);
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40"
|
||||
>
|
||||
<MousePointerClick className="size-3" />
|
||||
VNC
|
||||
</button>
|
||||
)}
|
||||
{host.enableTelnet &&
|
||||
isTabTypeAvailable("telnet") && (
|
||||
{hostActionsFor(hostActions, host).map((action) => {
|
||||
const Icon = action.icon;
|
||||
const label =
|
||||
action.label?.(host) ?? t(action.titleKey);
|
||||
return (
|
||||
<button
|
||||
title="Telnet"
|
||||
key={action.id}
|
||||
title={label}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAction(() =>
|
||||
onOpenTab("telnet", host.name),
|
||||
runHostAction(action, host, shell),
|
||||
);
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40"
|
||||
className="flex items-center justify-center size-7 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted-foreground/10 transition-colors"
|
||||
>
|
||||
<Terminal className="size-3" />
|
||||
Telnet
|
||||
<Icon className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
{paletteEntriesFor(
|
||||
paletteEntries,
|
||||
"host",
|
||||
host,
|
||||
).map((entry) => {
|
||||
const Icon = entry.icon;
|
||||
return (
|
||||
<button
|
||||
key={entry.id}
|
||||
title={t(entry.titleKey)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAction(() => entry.run(shell, host));
|
||||
}}
|
||||
className="flex items-center justify-center size-7 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted-foreground/10 transition-colors"
|
||||
>
|
||||
{Icon && <Icon className="size-3.5" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{canEditHost(host) && (
|
||||
<>
|
||||
<div className="w-px h-3.5 bg-border/60 mx-0.5 shrink-0" />
|
||||
|
||||
Reference in new issue
Block a user