mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
* feat: add collaboration rooms with switchable presenter Rooms are a group of members watching one stage - the live SSH/RDP/VNC session the current presenter shares. Any member can take over the stage; the host can invite, force-stop and end the meeting. Stages reuse session_shares (new room share type), so gating, recording, expiry and the global sharing toggle all apply unchanged. * feat: add stage control handoff to collaboration rooms The presenter or host can grant any member write access to the live stage and take it back; members can raise a hand to ask. SSH flips the participant's permission on the live gate; RDP/VNC re-mint the viewer's join token. Control clears on every stage switch. * feat: guest links, role invites and invite awareness for collab rooms - Anonymous guest link per room (host toggles/rotates), followed by polling the public resolve endpoint; SSH guests join over the terminal WS with roomGuestToken, guac guests get read-only join tokens - Invite by role (expands to current members, snapshot semantics) - Toast when a room you were invited to appears - Stale stages are cleared lazily when the presenter is gone - Telnet presenting, expired-tab fallback, documented single-instance and guac-kick limits - Tests for the collab routes, room hub, share access and control flip * fix: keep remote desktop collaboration read-only
717 lines
20 KiB
TypeScript
717 lines
20 KiB
TypeScript
/* eslint-disable react-refresh/only-export-components */
|
|
import {
|
|
Box,
|
|
Boxes,
|
|
Braces,
|
|
FolderSearch,
|
|
HardDrive,
|
|
LayoutDashboard,
|
|
LayoutGrid,
|
|
LayoutPanelLeft,
|
|
MessagesSquare,
|
|
Monitor,
|
|
MousePointerClick,
|
|
Network,
|
|
Server,
|
|
Settings,
|
|
Terminal,
|
|
Usb,
|
|
User,
|
|
Activity,
|
|
TerminalSquare,
|
|
Layers, // --- tmux-monitor ---
|
|
Clock,
|
|
Fingerprint,
|
|
Hammer,
|
|
Play,
|
|
Plug,
|
|
ScrollText,
|
|
Sparkles,
|
|
Presentation,
|
|
Workflow,
|
|
} 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 "@/features/guacamole/GuacamoleApp";
|
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
import type { Tab, TabType, Host } from "@/types/ui-types";
|
|
import type { SSHHost } from "@/types";
|
|
import { useTabsSafe } from "@/shell/TabContext";
|
|
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,
|
|
})),
|
|
);
|
|
const LocalTerminal = lazy(() =>
|
|
import("@/features/local-terminal/LocalTerminal").then((m) => ({
|
|
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("@/features/docker/DockerManager").then((m) => ({
|
|
default: m.DockerManager,
|
|
}));
|
|
const DockerManager = lazy(loadDockerManager);
|
|
const loadHostMetricsTab = () =>
|
|
import("@/features/host-metrics/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("@/features/guacamole/GuacamoleApp").then((m) => ({
|
|
default: m.default,
|
|
}));
|
|
const GuacamoleApp = lazy(loadGuacamoleApp);
|
|
const DashboardTab = lazy(() =>
|
|
import("@/dashboard/DashboardTab").then((m) => ({
|
|
default: m.DashboardTab,
|
|
})),
|
|
);
|
|
const HomepageCanvas = lazy(() =>
|
|
import("@/features/homepage/HomepageCanvas").then((m) => ({
|
|
default: m.HomepageCanvas,
|
|
})),
|
|
);
|
|
const loadTunnelTab = () =>
|
|
import("@/features/tunnel/TunnelTab").then((m) => ({
|
|
default: m.TunnelTab,
|
|
}));
|
|
const TunnelTab = lazy(loadTunnelTab);
|
|
const NetworkGraphCard = lazy(() =>
|
|
import("@/dashboard/cards/NetworkGraphCard").then((m) => ({
|
|
default: m.NetworkGraphCard,
|
|
})),
|
|
);
|
|
const Serial = lazy(() =>
|
|
import("@/features/serial/Serial").then((m) => ({
|
|
default: m.Serial,
|
|
})),
|
|
);
|
|
const FleetInventoryTab = lazy(() =>
|
|
import("@/sidebar/FleetInventoryTab").then((m) => ({
|
|
default: m.FleetInventoryTab,
|
|
})),
|
|
);
|
|
|
|
// Rail panels promoted to full tabs.
|
|
const TermixIdPanel = lazy(() =>
|
|
import("@/sidebar/TermixIdPanel").then((m) => ({ default: m.TermixIdPanel })),
|
|
);
|
|
const AlertsPanel = lazy(() =>
|
|
import("@/sidebar/AlertsPanel").then((m) => ({ default: m.AlertsPanel })),
|
|
);
|
|
const SessionLogsPanel = lazy(() =>
|
|
import("@/sidebar/SessionLogsPanel").then((m) => ({
|
|
default: m.SessionLogsPanel,
|
|
})),
|
|
);
|
|
const SnippetsPanel = lazy(() =>
|
|
import("@/sidebar/SnippetsPanel").then((m) => ({ default: m.SnippetsPanel })),
|
|
);
|
|
const MacrosPanel = lazy(() =>
|
|
import("@/sidebar/MacrosPanel").then((m) => ({ default: m.MacrosPanel })),
|
|
);
|
|
const HistoryPanel = lazy(() =>
|
|
import("@/sidebar/HistoryPanel").then((m) => ({ default: m.HistoryPanel })),
|
|
);
|
|
const SshToolsPanel = lazy(() =>
|
|
import("@/sidebar/SshToolsPanel").then((m) => ({ default: m.SshToolsPanel })),
|
|
);
|
|
const AutomationsPanel = lazy(() =>
|
|
import("@/sidebar/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,
|
|
};
|
|
|
|
/** Download a likely next tab without starting a connection or mounting UI. */
|
|
export function preloadTabSurface(type: TabType): void {
|
|
const loader = tabSurfaceLoaders[type];
|
|
if (loader) runAdaptiveBackgroundTask("module", `tab:${type}`, loader);
|
|
}
|
|
|
|
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,
|
|
dockerConfig: h.dockerConfig ?? null,
|
|
showTerminalInSidebar: true,
|
|
showFileManagerInSidebar: true,
|
|
showTunnelInSidebar: true,
|
|
showDockerInSidebar: true,
|
|
showServerStatsInSidebar: true,
|
|
defaultPath: h.defaultPath ?? "",
|
|
tunnelConnections: [],
|
|
connectionType: "ssh",
|
|
connectionOrigin: h.connectionOrigin ?? null,
|
|
// 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,
|
|
}: {
|
|
icon: React.ElementType;
|
|
messageKey: string;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<div className="flex flex-col items-center justify-center flex-1 gap-3 p-6 text-center">
|
|
<div className="size-10 rounded-full bg-muted/40 flex items-center justify-center">
|
|
<Icon className="size-5 text-muted-foreground/30" />
|
|
</div>
|
|
<span className="text-sm font-semibold text-muted-foreground/60">
|
|
{t(messageKey)}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TabChunkFallback() {
|
|
return (
|
|
<div className="flex h-full w-full items-center justify-center bg-background">
|
|
<div className="size-5 rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground/70 animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function withTabSuspense(node: React.ReactNode) {
|
|
return <Suspense fallback={<TabChunkFallback />}>{node}</Suspense>;
|
|
}
|
|
|
|
/**
|
|
* Host frame for rail panels opened as tabs. Panels expect a full-height flex
|
|
* column like the sidebar gives them. The max width keeps forms readable on a
|
|
* wide monitor instead of stretching them edge to edge.
|
|
*/
|
|
function PanelTabFrame({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<div className="flex h-full w-full justify-center overflow-y-auto bg-background">
|
|
<div className="flex flex-col flex-1 min-h-0 w-full max-w-5xl">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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":
|
|
return <Server className="size-3.5" />;
|
|
case "user-profile":
|
|
return <User className="size-3.5" />;
|
|
case "admin-settings":
|
|
return <Settings className="size-3.5" />;
|
|
case "docker":
|
|
return <Box className="size-3.5" />;
|
|
case "tunnel":
|
|
return <Network className="size-3.5" />;
|
|
case "network_graph":
|
|
return <Network className="size-3.5" />;
|
|
// --- tmux-monitor ---
|
|
case "tmux_monitor":
|
|
return <Layers className="size-3.5" />;
|
|
case "serial":
|
|
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":
|
|
return <Fingerprint className="size-3.5" />;
|
|
case "alerts":
|
|
return <Plug className="size-3.5" />;
|
|
case "session-logs":
|
|
return <ScrollText className="size-3.5" />;
|
|
case "snippets":
|
|
return <Play className="size-3.5" />;
|
|
case "macros":
|
|
return <Braces className="size-3.5" />;
|
|
case "history":
|
|
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" />;
|
|
}
|
|
}
|
|
|
|
function TerminalTabContent({
|
|
tab,
|
|
host,
|
|
label,
|
|
isVisible,
|
|
isFocusedPane,
|
|
onCloseTab,
|
|
onRenameTab,
|
|
onOpenFileInEditor,
|
|
onOpenFileManager,
|
|
onOpenTab,
|
|
onSaveQuickConnect,
|
|
}: {
|
|
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>;
|
|
}) {
|
|
const { previewTerminalTheme } = useTabsSafe();
|
|
const isMobile = useIsMobile();
|
|
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>,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Everything the promoted rail panels need from AppShell. Passed as one bag
|
|
* rather than more positional params, which renderTabContent already has too
|
|
* many of.
|
|
*/
|
|
export type PromotedPanelProps = {
|
|
terminalTabs?: Tab[];
|
|
targetTerminalTabId?: string;
|
|
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,
|
|
) {
|
|
const { host, label } = tab;
|
|
|
|
switch (tab.type) {
|
|
case "dashboard":
|
|
return withTabSuspense(
|
|
<DashboardTab
|
|
onOpenSingletonTab={onOpenSingletonTab!}
|
|
onOpenTab={onOpenTab!}
|
|
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} />,
|
|
);
|
|
|
|
case "files":
|
|
if (!host)
|
|
return (
|
|
<EmptyState
|
|
icon={FolderSearch}
|
|
messageKey="fileManager.noHostSelected"
|
|
/>
|
|
);
|
|
return withTabSuspense(
|
|
<FileManager
|
|
initialHost={hostToSSHHost(host)}
|
|
initialFilePath={tab.initialFilePath}
|
|
initialPath={tab.initialPath}
|
|
isVisible={isVisible}
|
|
onOpenTerminalTab={
|
|
onOpenTerminalTab
|
|
? (path) => onOpenTerminalTab(host, path)
|
|
: undefined
|
|
}
|
|
/>,
|
|
);
|
|
|
|
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}
|
|
/>,
|
|
);
|
|
|
|
case "tunnel":
|
|
return withTabSuspense(
|
|
<TunnelTab label={label} host={host} isVisible={isVisible} />,
|
|
);
|
|
|
|
case "rdp":
|
|
case "vnc":
|
|
case "telnet":
|
|
if (!host)
|
|
return (
|
|
<EmptyState icon={Monitor} messageKey="guacamole.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}
|
|
/>,
|
|
);
|
|
|
|
case "network_graph":
|
|
return withTabSuspense(
|
|
<NetworkGraphCard embedded={false} isVisible={isVisible} />,
|
|
);
|
|
|
|
// --- tmux-monitor ---
|
|
case "tmux_monitor":
|
|
return withTabSuspense(
|
|
<TmuxMonitor
|
|
initialHostId={host ? parseInt(host.id, 10) : undefined}
|
|
isVisible={isVisible}
|
|
/>,
|
|
);
|
|
|
|
case "serial":
|
|
if (!tab.serialConfig)
|
|
return <EmptyState icon={Usb} messageKey="serial.notSupportedTitle" />;
|
|
return withTabSuspense(
|
|
<Serial
|
|
ref={tab.terminalRef as React.Ref<SerialHandle>}
|
|
config={tab.serialConfig}
|
|
isVisible={isVisible}
|
|
instanceId={tab.instanceId}
|
|
/>,
|
|
);
|
|
|
|
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} />,
|
|
);
|
|
|
|
case "termix-id":
|
|
return withTabSuspense(
|
|
<PanelTabFrame>
|
|
<TermixIdPanel />
|
|
</PanelTabFrame>,
|
|
);
|
|
|
|
case "alerts":
|
|
return withTabSuspense(
|
|
<PanelTabFrame>
|
|
<AlertsPanel />
|
|
</PanelTabFrame>,
|
|
);
|
|
|
|
case "session-logs":
|
|
return withTabSuspense(
|
|
<PanelTabFrame>
|
|
<SessionLogsPanel />
|
|
</PanelTabFrame>,
|
|
);
|
|
|
|
case "automations":
|
|
return withTabSuspense(
|
|
<PanelTabFrame>
|
|
<AutomationsPanel active={isVisible} />
|
|
</PanelTabFrame>,
|
|
);
|
|
|
|
case "ai":
|
|
return withTabSuspense(
|
|
<PanelTabFrame>
|
|
<AiPanel />
|
|
</PanelTabFrame>,
|
|
);
|
|
|
|
case "split-screen":
|
|
return null;
|
|
|
|
case "snippets":
|
|
return withTabSuspense(
|
|
<PanelTabFrame>
|
|
<SnippetsPanel
|
|
terminalTabs={panelProps?.terminalTabs ?? []}
|
|
activeTabId={panelProps?.targetTerminalTabId ?? ""}
|
|
storageMode={panelProps?.storageMode ?? "local"}
|
|
/>
|
|
</PanelTabFrame>,
|
|
);
|
|
|
|
case "macros":
|
|
return withTabSuspense(
|
|
<PanelTabFrame>
|
|
<MacrosPanel
|
|
terminalTabs={panelProps?.terminalTabs ?? []}
|
|
activeTabId={panelProps?.targetTerminalTabId ?? ""}
|
|
storageMode={panelProps?.storageMode ?? "local"}
|
|
/>
|
|
</PanelTabFrame>,
|
|
);
|
|
|
|
case "history":
|
|
return withTabSuspense(
|
|
<PanelTabFrame>
|
|
<HistoryPanel
|
|
terminalTabs={panelProps?.terminalTabs ?? []}
|
|
activeTabId={panelProps?.targetTerminalTabId ?? ""}
|
|
/>
|
|
</PanelTabFrame>,
|
|
);
|
|
|
|
case "ssh-tools":
|
|
return withTabSuspense(
|
|
<PanelTabFrame>
|
|
<SshToolsPanel
|
|
terminalTabs={panelProps?.terminalTabs ?? []}
|
|
activeTabId={panelProps?.targetTerminalTabId ?? ""}
|
|
/>
|
|
</PanelTabFrame>,
|
|
);
|
|
|
|
case "host-manager":
|
|
case "user-profile":
|
|
case "admin-settings":
|
|
return null;
|
|
}
|
|
}
|