mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35: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
+72
-446
@@ -1,24 +1,16 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import {
|
||||
Box,
|
||||
Boxes,
|
||||
Braces,
|
||||
FolderSearch,
|
||||
HardDrive,
|
||||
LayoutDashboard,
|
||||
LayoutGrid,
|
||||
LayoutPanelLeft,
|
||||
MessagesSquare,
|
||||
Monitor,
|
||||
MousePointerClick,
|
||||
Network,
|
||||
ArrowLeftRight,
|
||||
Server,
|
||||
Settings,
|
||||
Terminal,
|
||||
Usb,
|
||||
User,
|
||||
Activity,
|
||||
TerminalSquare,
|
||||
Layers, // --- tmux-monitor ---
|
||||
Clock,
|
||||
@@ -26,43 +18,25 @@ import {
|
||||
Hammer,
|
||||
Play,
|
||||
ScrollText,
|
||||
Sparkles,
|
||||
Presentation,
|
||||
Workflow,
|
||||
Globe,
|
||||
} from "lucide-react";
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SerialHandle } from "@/features/serial/serial-types";
|
||||
import type {
|
||||
TerminalHandle,
|
||||
TerminalHostConfig,
|
||||
} from "@/features/terminal/Terminal";
|
||||
import type { GuacamoleAppHandle } from "../../../plugins/remote-desktop/src/frontend/GuacamoleApp";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import type { Tab, TabType, Host } from "@/types/ui-types";
|
||||
import type { Tab, TabType } from "@/types/ui-types";
|
||||
import { hostToSSHHost } from "@/lib/host-to-ssh-host";
|
||||
import { PluginViewPlaceholder } from "@/plugin-host/PluginViewPlaceholder";
|
||||
import {
|
||||
isQuickConnectHost,
|
||||
quickConnectGuacHost,
|
||||
} from "@/sidebar/quick-connect-host";
|
||||
import type { SSHHost } from "@/types";
|
||||
import { useTabsSafe } from "@/shell/TabContext";
|
||||
getTabType,
|
||||
type TabShellCallbacks,
|
||||
type TabTypeDef,
|
||||
} from "./tab-registry";
|
||||
import {
|
||||
markAdaptiveResourceUsed,
|
||||
runAdaptiveBackgroundTask,
|
||||
} from "@/lib/adaptive-resource-budget";
|
||||
|
||||
// Heavy tab surfaces — keep out of the AppShell critical path.
|
||||
const CommandHistoryProvider = lazy(() =>
|
||||
import("@/features/terminal/command-history/CommandHistoryContext").then(
|
||||
(m) => ({ default: m.CommandHistoryProvider }),
|
||||
),
|
||||
);
|
||||
const loadTerminalFeature = () =>
|
||||
import("@/features/terminal/Terminal").then((m) => ({
|
||||
default: m.Terminal,
|
||||
}));
|
||||
const TerminalFeature = lazy(loadTerminalFeature);
|
||||
const CollabRoomTab = lazy(() =>
|
||||
import("@/features/collab/CollabRoomTab").then((m) => ({
|
||||
default: m.CollabRoomTab,
|
||||
@@ -73,52 +47,16 @@ const LocalTerminal = lazy(() =>
|
||||
default: m.LocalTerminal,
|
||||
})),
|
||||
);
|
||||
const MobileTerminalKeyboard = lazy(() =>
|
||||
import("@/features/terminal/MobileTerminalKeyboard").then((m) => ({
|
||||
default: m.MobileTerminalKeyboard,
|
||||
})),
|
||||
);
|
||||
const loadFileManager = () =>
|
||||
import("@/features/file-manager/FileManager").then((m) => ({
|
||||
default: m.FileManager,
|
||||
}));
|
||||
const FileManager = lazy(loadFileManager);
|
||||
const loadDockerManager = () =>
|
||||
import("../../../plugins/docker/src/frontend/DockerManager").then((m) => ({
|
||||
default: m.DockerManager,
|
||||
}));
|
||||
const DockerManager = lazy(loadDockerManager);
|
||||
const loadWebEndpointTab = () =>
|
||||
import("../../../plugins/web-endpoint/src/frontend/WebEndpointTab").then(
|
||||
(m) => ({
|
||||
default: m.WebEndpointTab,
|
||||
}),
|
||||
);
|
||||
const WebEndpointTab = lazy(loadWebEndpointTab);
|
||||
const loadHostMetricsTab = () =>
|
||||
import("../../../plugins/host-metrics/src/frontend/HostMetricsTab").then(
|
||||
(m) => ({
|
||||
default: m.HostMetricsTab,
|
||||
}),
|
||||
);
|
||||
const HostMetricsTab = lazy(loadHostMetricsTab);
|
||||
const loadProxmoxStatsTab = () =>
|
||||
import("@/features/proxmox-stats/ProxmoxStatsTab").then((m) => ({
|
||||
default: m.ProxmoxStatsTab,
|
||||
}));
|
||||
const ProxmoxStatsTab = lazy(loadProxmoxStatsTab);
|
||||
const loadTmuxMonitor = () =>
|
||||
import("@/features/tmux-monitor/TmuxMonitor").then((m) => ({
|
||||
default: m.TmuxMonitor,
|
||||
}));
|
||||
const TmuxMonitor = lazy(loadTmuxMonitor);
|
||||
const loadGuacamoleApp = () =>
|
||||
import("../../../plugins/remote-desktop/src/frontend/GuacamoleApp").then(
|
||||
(m) => ({
|
||||
default: m.default,
|
||||
}),
|
||||
);
|
||||
const GuacamoleApp = lazy(loadGuacamoleApp);
|
||||
const DashboardTab = lazy(() =>
|
||||
import("@/dashboard/DashboardTab").then((m) => ({
|
||||
default: m.DashboardTab,
|
||||
@@ -139,26 +77,11 @@ const SftpTransferTab = lazy(() =>
|
||||
default: m.SftpTransferTab,
|
||||
})),
|
||||
);
|
||||
const NetworkGraphCard = lazy(() =>
|
||||
import("../../../plugins/network-topology/src/frontend/NetworkGraphCard").then(
|
||||
(m) => ({
|
||||
default: m.NetworkGraphCard,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const Serial = lazy(() =>
|
||||
import("@/features/serial/Serial").then((m) => ({
|
||||
default: m.Serial,
|
||||
})),
|
||||
);
|
||||
const FleetInventoryTab = lazy(() =>
|
||||
import("../../../plugins/fleets/src/frontend/FleetInventoryTab").then(
|
||||
(m) => ({
|
||||
default: m.FleetInventoryTab,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Rail panels promoted to full tabs.
|
||||
const TermixIdPanel = lazy(() =>
|
||||
import("@/sidebar/TermixIdPanel").then((m) => ({ default: m.TermixIdPanel })),
|
||||
@@ -180,72 +103,15 @@ const HistoryPanel = lazy(() =>
|
||||
const SshToolsPanel = lazy(() =>
|
||||
import("@/sidebar/SshToolsPanel").then((m) => ({ default: m.SshToolsPanel })),
|
||||
);
|
||||
const AutomationsPanel = lazy(() =>
|
||||
import("../../../plugins/automations/src/frontend/AutomationsPanel").then(
|
||||
(m) => ({
|
||||
default: m.AutomationsPanel,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const AiPanel = lazy(() =>
|
||||
import("@/features/ai/AiPanel").then((m) => ({
|
||||
default: m.AiPanel,
|
||||
})),
|
||||
);
|
||||
|
||||
const tabSurfaceLoaders: Partial<Record<TabType, () => Promise<unknown>>> = {
|
||||
terminal: loadTerminalFeature,
|
||||
files: loadFileManager,
|
||||
docker: loadDockerManager,
|
||||
"host-metrics": loadHostMetricsTab,
|
||||
"proxmox-stats": loadProxmoxStatsTab,
|
||||
tmux_monitor: loadTmuxMonitor,
|
||||
tunnel: loadTunnelTab,
|
||||
rdp: loadGuacamoleApp,
|
||||
vnc: loadGuacamoleApp,
|
||||
telnet: loadGuacamoleApp,
|
||||
};
|
||||
|
||||
/**
|
||||
* A plugin-contributed tab surface. `component` is the loader's resolved
|
||||
* default export, rendered with the same Tab it would have received from the
|
||||
* switch in renderTabContent -- plugins get the raw tab record and decide
|
||||
* for themselves what they need out of it.
|
||||
*/
|
||||
export type PluginTabComponent = React.ComponentType<{ tab: Tab }>;
|
||||
export type PluginTabLoader = () => Promise<{ default: PluginTabComponent }>;
|
||||
|
||||
interface PluginTabEntry {
|
||||
loader: PluginTabLoader;
|
||||
icon?: React.ReactNode;
|
||||
// Built lazily on first render and cached here, so every render of the same
|
||||
// plugin tab reuses one lazy component instead of remounting it each time.
|
||||
LazyComponent?: PluginTabComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime registry for plugin tab ids that don't exist in the built-in
|
||||
* TabType union. Registered once a plugin loads, looked up by tabIcon and
|
||||
* renderTabContent instead of a switch case.
|
||||
*/
|
||||
const registeredTabComponents = new Map<string, PluginTabEntry>();
|
||||
|
||||
export function registerTabComponent(
|
||||
id: string,
|
||||
loader: PluginTabLoader,
|
||||
icon?: React.ReactNode,
|
||||
): void {
|
||||
registeredTabComponents.set(id, { loader, icon });
|
||||
}
|
||||
|
||||
export function unregisterTabComponent(id: string): void {
|
||||
registeredTabComponents.delete(id);
|
||||
}
|
||||
|
||||
/** Download a likely next tab without starting a connection or mounting UI. */
|
||||
export function preloadTabSurface(type: TabType): void {
|
||||
const loader = tabSurfaceLoaders[type];
|
||||
const loader = tabSurfaceLoaders[type] ?? getTabType(type)?.preload;
|
||||
if (loader) runAdaptiveBackgroundTask("module", `tab:${type}`, loader);
|
||||
}
|
||||
|
||||
@@ -253,54 +119,6 @@ export function markTabSurfaceUsed(type: TabType): void {
|
||||
markAdaptiveResourceUsed("module", `tab:${type}`);
|
||||
}
|
||||
|
||||
function hostToSSHHost(h: Host): SSHHost {
|
||||
return {
|
||||
id: parseInt(h.id, 10),
|
||||
name: h.name,
|
||||
ip: h.ip,
|
||||
port: h.port,
|
||||
username: h.username,
|
||||
folder: h.folder ?? "",
|
||||
tags: h.tags ?? [],
|
||||
pin: h.pin ?? false,
|
||||
authType: h.authType,
|
||||
password: h.password,
|
||||
hasPassword: h.hasPassword,
|
||||
key: h.key,
|
||||
keyPassword: h.keyPassword,
|
||||
hasKey: h.hasKey,
|
||||
hasKeyPassword: h.hasKeyPassword,
|
||||
keyType: h.keyType,
|
||||
credentialId: h.credentialId ? parseInt(h.credentialId, 10) : undefined,
|
||||
terminalConfig: h.terminalConfig,
|
||||
hasSudoPassword: h.hasSudoPassword,
|
||||
enableTerminal: h.enableTerminal ?? false,
|
||||
enableTunnel: h.enableTunnel ?? false,
|
||||
enableFileManager: h.enableFileManager ?? false,
|
||||
enableDocker: h.enableDocker ?? false,
|
||||
enableTerminalToolbar: h.enableTerminalToolbar ?? true,
|
||||
enableAiAssistant: h.enableAiAssistant ?? false,
|
||||
dockerConfig: h.dockerConfig ?? null,
|
||||
enableWebUi: h.enableWebUi ?? false,
|
||||
webUiConfig: h.webUiConfig ?? { endpoints: [] },
|
||||
showTerminalInSidebar: true,
|
||||
showFileManagerInSidebar: true,
|
||||
showTunnelInSidebar: true,
|
||||
showDockerInSidebar: true,
|
||||
showServerStatsInSidebar: true,
|
||||
defaultPath: h.defaultPath ?? "",
|
||||
tunnelConnections: [],
|
||||
connectionType: "ssh",
|
||||
connectionOrigin: h.connectionOrigin ?? null,
|
||||
isShared: h.isShared ?? false,
|
||||
// Carries the host's identity to a delegated backend. Without it the
|
||||
// remote side resolves our local row id against its own table.
|
||||
syncId: h.syncId ?? null,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
} as unknown as SSHHost;
|
||||
}
|
||||
|
||||
function EmptyState({
|
||||
icon: Icon,
|
||||
messageKey,
|
||||
@@ -352,20 +170,8 @@ export function tabIcon(type: TabType) {
|
||||
switch (type) {
|
||||
case "dashboard":
|
||||
return <LayoutDashboard className="size-3.5" />;
|
||||
case "terminal":
|
||||
return <Terminal className="size-3.5" />;
|
||||
case "local-terminal":
|
||||
return <TerminalSquare className="size-3.5" />;
|
||||
case "rdp":
|
||||
return <Monitor className="size-3.5" />;
|
||||
case "vnc":
|
||||
return <MousePointerClick className="size-3.5" />;
|
||||
case "telnet":
|
||||
return <MessagesSquare className="size-3.5" />;
|
||||
case "host-metrics":
|
||||
return <Server className="size-3.5" />;
|
||||
case "proxmox-stats":
|
||||
return <HardDrive className="size-3.5" />;
|
||||
case "files":
|
||||
return <FolderSearch className="size-3.5" />;
|
||||
case "host-manager":
|
||||
@@ -374,16 +180,10 @@ export function tabIcon(type: TabType) {
|
||||
return <User className="size-3.5" />;
|
||||
case "admin-settings":
|
||||
return <Settings className="size-3.5" />;
|
||||
case "docker":
|
||||
return <Box className="size-3.5" />;
|
||||
case "web-endpoint":
|
||||
return <Globe className="size-3.5" />;
|
||||
case "tunnel":
|
||||
return <Network className="size-3.5" />;
|
||||
case "sftp":
|
||||
return <ArrowLeftRight className="size-3.5" />;
|
||||
case "network_graph":
|
||||
return <Network className="size-3.5" />;
|
||||
// --- tmux-monitor ---
|
||||
case "tmux_monitor":
|
||||
return <Layers className="size-3.5" />;
|
||||
@@ -391,8 +191,6 @@ export function tabIcon(type: TabType) {
|
||||
return <Usb className="size-3.5" />;
|
||||
case "homepage":
|
||||
return <LayoutGrid className="size-3.5" />;
|
||||
case "fleet-inventory":
|
||||
return <Boxes className="size-3.5" />;
|
||||
case "collab":
|
||||
return <Presentation className="size-3.5" />;
|
||||
case "termix-id":
|
||||
@@ -407,90 +205,60 @@ export function tabIcon(type: TabType) {
|
||||
return <Clock className="size-3.5" />;
|
||||
case "ssh-tools":
|
||||
return <Hammer className="size-3.5" />;
|
||||
case "automations":
|
||||
return <Workflow className="size-3.5" />;
|
||||
case "ai":
|
||||
return <Sparkles className="size-3.5" />;
|
||||
case "split-screen":
|
||||
return <LayoutPanelLeft className="size-3.5" />;
|
||||
default:
|
||||
return registeredTabComponents.get(type)?.icon;
|
||||
default: {
|
||||
const Icon = getTabType(type)?.icon;
|
||||
return Icon ? <Icon className="size-3.5" /> : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function TerminalTabContent({
|
||||
/**
|
||||
* A plugin's tab: its registered component with the generic render props.
|
||||
* Unregistered types get the ownership placeholder in renderTabContent.
|
||||
*/
|
||||
function RegisteredTab({
|
||||
def,
|
||||
tab,
|
||||
host,
|
||||
label,
|
||||
isVisible,
|
||||
isFocusedPane,
|
||||
onCloseTab,
|
||||
onRenameTab,
|
||||
onOpenFileInEditor,
|
||||
onOpenFileManager,
|
||||
onOpenTab,
|
||||
onSaveQuickConnect,
|
||||
shell,
|
||||
}: {
|
||||
def: TabTypeDef;
|
||||
tab: Tab;
|
||||
host: Host;
|
||||
label: string;
|
||||
isVisible: boolean;
|
||||
isFocusedPane: boolean;
|
||||
onCloseTab?: (id: string) => void;
|
||||
onRenameTab?: (tabId: string, newLabel: string) => void;
|
||||
onOpenFileInEditor?: (filePath: string) => void;
|
||||
onOpenFileManager?: (path?: string) => void;
|
||||
onOpenTab?: (type: TabType) => void;
|
||||
onSaveQuickConnect?: () => Promise<void>;
|
||||
shell: TabShellCallbacks;
|
||||
}) {
|
||||
const { previewTerminalTheme } = useTabsSafe();
|
||||
const isMobile = useIsMobile();
|
||||
const { host, label } = tab;
|
||||
if (def.requiresHost && !host) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Server}
|
||||
messageKey={def.noHostMessageKey ?? "hosts.noHostSelected"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Component = def.component;
|
||||
const content = (
|
||||
<Component
|
||||
tab={tab}
|
||||
host={host}
|
||||
sshHost={
|
||||
host
|
||||
? (hostToSSHHost(host) as unknown as Record<string, unknown>)
|
||||
: undefined
|
||||
}
|
||||
label={label}
|
||||
isVisible={isVisible}
|
||||
isFocusedPane={isFocusedPane}
|
||||
handleRef={tab.terminalRef as React.Ref<unknown>}
|
||||
shell={shell}
|
||||
/>
|
||||
);
|
||||
return withTabSuspense(
|
||||
<CommandHistoryProvider>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<div className="flex-1 min-h-0">
|
||||
<TerminalFeature
|
||||
ref={tab.terminalRef as React.Ref<TerminalHandle>}
|
||||
hostConfig={
|
||||
{
|
||||
...hostToSSHHost(host),
|
||||
sshPort: host.sshPort ?? host.port,
|
||||
instanceId: tab.instanceId ?? tab.id,
|
||||
restoredSessionId: tab.restoredSessionId ?? null,
|
||||
joinSharedSessionId: tab.joinSharedSessionId ?? null,
|
||||
joinShareId: tab.joinShareId ?? null,
|
||||
} as TerminalHostConfig
|
||||
}
|
||||
isVisible={isVisible}
|
||||
initialPath={tab.initialFilePath}
|
||||
title={label}
|
||||
showTitle={false}
|
||||
splitScreen={false}
|
||||
onClose={() => onCloseTab?.(tab.id)}
|
||||
onTitleChange={
|
||||
onRenameTab && host.terminalConfig?.useSSHTitle
|
||||
? (title) => onRenameTab(tab.id, title)
|
||||
: undefined
|
||||
}
|
||||
previewTheme={previewTerminalTheme}
|
||||
onOpenFileInEditor={onOpenFileInEditor}
|
||||
onOpenFileManager={onOpenFileManager}
|
||||
isQuickConnect={host.id.startsWith("quick-connect-")}
|
||||
onSaveQuickConnect={onSaveQuickConnect}
|
||||
host={host}
|
||||
onOpenTab={onOpenTab}
|
||||
isFocusedPane={isFocusedPane}
|
||||
/>
|
||||
</div>
|
||||
{isMobile && (
|
||||
<MobileTerminalKeyboard
|
||||
terminalRef={
|
||||
tab.terminalRef as React.RefObject<TerminalHandle | null>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CommandHistoryProvider>,
|
||||
def.panelFrame ? <PanelTabFrame>{content}</PanelTabFrame> : content,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -505,64 +273,27 @@ export type PromotedPanelProps = {
|
||||
storageMode?: "local" | "cloud";
|
||||
};
|
||||
|
||||
export function renderTabContent(
|
||||
tab: Tab,
|
||||
onOpenSingletonTab?: (type: TabType) => void,
|
||||
onOpenTab?: (host: Host, type: TabType) => void,
|
||||
onCloseTab?: (id: string) => void,
|
||||
isVisible = true,
|
||||
onOpenFileInEditor?: (host: Host, filePath: string) => void,
|
||||
onOpenFileManager?: (host: Host, path?: string) => void,
|
||||
onOpenTerminalTab?: (host: Host, path?: string) => void,
|
||||
onRenameTab?: (tabId: string, newLabel: string) => void,
|
||||
onSaveQuickConnect?: (tab: Tab, host: Host) => Promise<void>,
|
||||
isFocusedPane = true,
|
||||
panelProps?: PromotedPanelProps,
|
||||
) {
|
||||
export interface TabRenderContext {
|
||||
shell: TabShellCallbacks;
|
||||
isVisible?: boolean;
|
||||
isFocusedPane?: boolean;
|
||||
panelProps?: PromotedPanelProps;
|
||||
}
|
||||
|
||||
export function renderTabContent(tab: Tab, context: TabRenderContext) {
|
||||
const { shell, isVisible = true, isFocusedPane = true, panelProps } = context;
|
||||
const { host, label } = tab;
|
||||
|
||||
switch (tab.type) {
|
||||
case "dashboard":
|
||||
return withTabSuspense(
|
||||
<DashboardTab
|
||||
onOpenSingletonTab={onOpenSingletonTab!}
|
||||
onOpenTab={onOpenTab!}
|
||||
onOpenSingletonTab={(type) => shell.openSingletonTab(type)}
|
||||
onOpenTab={(host, type) => shell.openTab(host, type)}
|
||||
isVisible={isVisible}
|
||||
/>,
|
||||
);
|
||||
|
||||
case "terminal":
|
||||
if (!host)
|
||||
return (
|
||||
<EmptyState
|
||||
icon={TerminalSquare}
|
||||
messageKey="terminal.noHostSelected"
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<TerminalTabContent
|
||||
tab={tab}
|
||||
host={host}
|
||||
label={label}
|
||||
isVisible={isVisible}
|
||||
isFocusedPane={isFocusedPane}
|
||||
onCloseTab={onCloseTab}
|
||||
onRenameTab={onRenameTab}
|
||||
onOpenFileInEditor={
|
||||
onOpenFileInEditor
|
||||
? (fp) => onOpenFileInEditor(host, fp)
|
||||
: undefined
|
||||
}
|
||||
onOpenFileManager={
|
||||
onOpenFileManager ? (p) => onOpenFileManager(host, p) : undefined
|
||||
}
|
||||
onOpenTab={onOpenTab ? (type) => onOpenTab(host, type) : undefined}
|
||||
onSaveQuickConnect={
|
||||
onSaveQuickConnect ? () => onSaveQuickConnect(tab, host) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
case "local-terminal":
|
||||
return withTabSuspense(
|
||||
<LocalTerminal instanceId={tab.instanceId} isVisible={isVisible} />,
|
||||
@@ -582,69 +313,7 @@ export function renderTabContent(
|
||||
initialFilePath={tab.initialFilePath}
|
||||
initialPath={tab.initialPath}
|
||||
isVisible={isVisible}
|
||||
onOpenTerminalTab={
|
||||
onOpenTerminalTab
|
||||
? (path) => onOpenTerminalTab(host, path)
|
||||
: undefined
|
||||
}
|
||||
/>,
|
||||
);
|
||||
|
||||
case "web-endpoint":
|
||||
if (!host)
|
||||
return (
|
||||
<EmptyState icon={Globe} messageKey="webEndpoint.noHostSelected" />
|
||||
);
|
||||
// Passed the endpoint ID, not the endpoint object: the tab resolves it
|
||||
// against the host on every render, so an endpoint deleted while its tab
|
||||
// is open shows a plain message instead of throwing.
|
||||
return withTabSuspense(
|
||||
<WebEndpointTab host={host} endpointId={tab.endpointId} />,
|
||||
);
|
||||
|
||||
case "docker":
|
||||
if (!host)
|
||||
return <EmptyState icon={Box} messageKey="docker.noHostSelected" />;
|
||||
return withTabSuspense(
|
||||
<DockerManager
|
||||
hostConfig={hostToSSHHost(host)}
|
||||
title={label}
|
||||
isVisible={isVisible}
|
||||
isTopbarOpen={false}
|
||||
embedded={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
case "host-metrics":
|
||||
if (!host)
|
||||
return (
|
||||
<EmptyState icon={Activity} messageKey="hostMetrics.noHostSelected" />
|
||||
);
|
||||
return withTabSuspense(
|
||||
<HostMetricsTab
|
||||
hostConfig={hostToSSHHost(host)}
|
||||
title={label}
|
||||
isVisible={isVisible}
|
||||
isTopbarOpen={false}
|
||||
embedded={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
case "proxmox-stats":
|
||||
if (!host)
|
||||
return (
|
||||
<EmptyState
|
||||
icon={HardDrive}
|
||||
messageKey="proxmoxStats.noHostSelected"
|
||||
/>
|
||||
);
|
||||
return withTabSuspense(
|
||||
<ProxmoxStatsTab
|
||||
hostConfig={hostToSSHHost(host)}
|
||||
title={label}
|
||||
isVisible={isVisible}
|
||||
isTopbarOpen={false}
|
||||
embedded={true}
|
||||
onOpenTerminalTab={(path) => shell.openTerminalTab(host, path)}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -656,34 +325,6 @@ export function renderTabContent(
|
||||
case "sftp":
|
||||
return withTabSuspense(<SftpTransferTab />);
|
||||
|
||||
case "rdp":
|
||||
case "vnc":
|
||||
case "telnet":
|
||||
if (!host)
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Monitor}
|
||||
messageKey="remoteDesktop.noHostSelected"
|
||||
/>
|
||||
);
|
||||
return withTabSuspense(
|
||||
<GuacamoleApp
|
||||
ref={tab.terminalRef as React.Ref<GuacamoleAppHandle>}
|
||||
hostId={host.id}
|
||||
tabId={tab.id}
|
||||
protocol={tab.type as "rdp" | "vnc" | "telnet"}
|
||||
isVisible={isVisible}
|
||||
quickConnectHost={
|
||||
isQuickConnectHost(host) ? quickConnectGuacHost(host) : undefined
|
||||
}
|
||||
/>,
|
||||
);
|
||||
|
||||
case "network_graph":
|
||||
return withTabSuspense(
|
||||
<NetworkGraphCard embedded={false} isVisible={isVisible} />,
|
||||
);
|
||||
|
||||
// --- tmux-monitor ---
|
||||
case "tmux_monitor":
|
||||
return withTabSuspense(
|
||||
@@ -708,11 +349,6 @@ export function renderTabContent(
|
||||
case "homepage":
|
||||
return withTabSuspense(<HomepageCanvas />);
|
||||
|
||||
case "fleet-inventory":
|
||||
return withTabSuspense(
|
||||
<FleetInventoryTab fleetId={tab.fleetId} isVisible={isVisible} />,
|
||||
);
|
||||
|
||||
case "collab":
|
||||
return withTabSuspense(
|
||||
<CollabRoomTab roomId={tab.collabRoomId} isVisible={isVisible} />,
|
||||
@@ -732,20 +368,6 @@ export function renderTabContent(
|
||||
</PanelTabFrame>,
|
||||
);
|
||||
|
||||
case "automations":
|
||||
return withTabSuspense(
|
||||
<PanelTabFrame>
|
||||
<AutomationsPanel active={isVisible} />
|
||||
</PanelTabFrame>,
|
||||
);
|
||||
|
||||
case "ai":
|
||||
return withTabSuspense(
|
||||
<PanelTabFrame>
|
||||
<AiPanel />
|
||||
</PanelTabFrame>,
|
||||
);
|
||||
|
||||
case "split-screen":
|
||||
return null;
|
||||
|
||||
@@ -797,15 +419,19 @@ export function renderTabContent(
|
||||
return null;
|
||||
|
||||
default: {
|
||||
const entry = registeredTabComponents.get(tab.type);
|
||||
if (!entry) return null;
|
||||
if (!entry.LazyComponent) {
|
||||
entry.LazyComponent = lazy(() =>
|
||||
entry.loader().then((m) => ({ default: m.default })),
|
||||
) as unknown as PluginTabComponent;
|
||||
const def = getTabType(tab.type);
|
||||
if (!def) {
|
||||
return <PluginViewPlaceholder kind="tab" viewId={tab.type} />;
|
||||
}
|
||||
const PluginTab = entry.LazyComponent;
|
||||
return withTabSuspense(<PluginTab tab={tab} />);
|
||||
return (
|
||||
<RegisteredTab
|
||||
def={def}
|
||||
tab={tab}
|
||||
isVisible={isVisible}
|
||||
isFocusedPane={isFocusedPane}
|
||||
shell={shell}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user