mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 10:21:34 +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
241 lines
6.0 KiB
TypeScript
241 lines
6.0 KiB
TypeScript
import {
|
|
Boxes,
|
|
Braces,
|
|
Clock,
|
|
Fingerprint,
|
|
Hammer,
|
|
KeyRound,
|
|
LayoutPanelLeft,
|
|
LayoutTemplate,
|
|
Network,
|
|
Play,
|
|
Plug,
|
|
ScrollText,
|
|
Server,
|
|
Settings,
|
|
Sparkles,
|
|
TerminalSquare,
|
|
Usb,
|
|
User,
|
|
Workflow,
|
|
Zap,
|
|
Presentation,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
import { isElectron } from "@/lib/electron";
|
|
|
|
/**
|
|
* The one list of navigation destinations.
|
|
*
|
|
* This used to be duplicated in four places -- AppRail's button array, the
|
|
* visibility toggles in UserProfilePanel, AppShell's sidebar title map, and
|
|
* MobileBottomBar's own primary/more lists -- which drifted: half the sidebar
|
|
* titles were hardcoded English, the alerts entry had no visibility toggle,
|
|
* and the mobile bar ignored hidden tabs entirely. Everything now derives from
|
|
* here, so adding a destination is a single edit.
|
|
*/
|
|
export interface RailItemDef {
|
|
/** Matches RailView, or a TabType for entries that open a tab instead. */
|
|
id: string;
|
|
icon: LucideIcon;
|
|
/** i18n key; every label goes through t() so nothing is hardcoded English. */
|
|
labelKey: string;
|
|
/** Tab-opening entries (network_graph) rather than sidebar panels. */
|
|
kind?: "tab";
|
|
/** Always-available destinations that users cannot hide. */
|
|
alwaysVisible?: boolean;
|
|
/** Renders a separator after this item in the rail. */
|
|
separatorAfter?: boolean;
|
|
/** Shown on the mobile bottom bar's primary row rather than its More menu. */
|
|
mobilePrimary?: boolean;
|
|
/**
|
|
* Can also open as a full-width tab in the main area, via ctrl/middle-click
|
|
* or the rail context menu. The id doubles as the TabType.
|
|
*/
|
|
promotable?: boolean;
|
|
/**
|
|
* Can be opened in the right dock. Reference panels only -- editors stay in
|
|
* the left sidebar, which is the only dock that widens for them.
|
|
*/
|
|
rightDockable?: boolean;
|
|
/** Desktop app only. Hidden in the browser build, including its toggle. */
|
|
electronOnly?: boolean;
|
|
}
|
|
|
|
export const RAIL_ITEMS: RailItemDef[] = [
|
|
{ id: "hosts", icon: Server, labelKey: "nav.hosts", mobilePrimary: true },
|
|
{
|
|
id: "credentials",
|
|
icon: KeyRound,
|
|
labelKey: "nav.credentials",
|
|
separatorAfter: true,
|
|
},
|
|
{
|
|
id: "termix-id",
|
|
icon: Fingerprint,
|
|
labelKey: "nav.termixId",
|
|
separatorAfter: true,
|
|
promotable: true,
|
|
},
|
|
{
|
|
id: "connections",
|
|
icon: Plug,
|
|
labelKey: "nav.connections",
|
|
separatorAfter: true,
|
|
rightDockable: true,
|
|
},
|
|
{
|
|
id: "collab",
|
|
icon: Presentation,
|
|
labelKey: "nav.collab",
|
|
separatorAfter: true,
|
|
},
|
|
{
|
|
id: "quick-connect",
|
|
icon: Zap,
|
|
labelKey: "nav.quickConnect",
|
|
separatorAfter: true,
|
|
mobilePrimary: true,
|
|
},
|
|
{ id: "serial", icon: Usb, labelKey: "nav.serial", separatorAfter: true },
|
|
{
|
|
id: "ssh-tools",
|
|
icon: Hammer,
|
|
labelKey: "nav.sshTools",
|
|
separatorAfter: true,
|
|
mobilePrimary: true,
|
|
promotable: true,
|
|
rightDockable: true,
|
|
},
|
|
{
|
|
id: "snippets",
|
|
icon: Play,
|
|
labelKey: "nav.snippets",
|
|
separatorAfter: true,
|
|
mobilePrimary: true,
|
|
promotable: true,
|
|
rightDockable: true,
|
|
},
|
|
{
|
|
id: "macros",
|
|
icon: Braces,
|
|
labelKey: "nav.macros",
|
|
separatorAfter: true,
|
|
promotable: true,
|
|
rightDockable: true,
|
|
},
|
|
{ id: "fleets", icon: Boxes, labelKey: "nav.fleets", separatorAfter: true },
|
|
{
|
|
id: "automations",
|
|
icon: Workflow,
|
|
labelKey: "nav.automations",
|
|
separatorAfter: true,
|
|
promotable: true,
|
|
},
|
|
{
|
|
id: "ai",
|
|
icon: Sparkles,
|
|
labelKey: "nav.ai",
|
|
separatorAfter: true,
|
|
promotable: true,
|
|
rightDockable: true,
|
|
},
|
|
{
|
|
id: "history",
|
|
icon: Clock,
|
|
labelKey: "nav.history",
|
|
separatorAfter: true,
|
|
promotable: true,
|
|
rightDockable: true,
|
|
},
|
|
{
|
|
id: "session-logs",
|
|
icon: ScrollText,
|
|
labelKey: "nav.sessionLogs",
|
|
separatorAfter: true,
|
|
promotable: true,
|
|
rightDockable: true,
|
|
},
|
|
{
|
|
id: "split-screen",
|
|
icon: LayoutPanelLeft,
|
|
labelKey: "nav.splitScreen",
|
|
separatorAfter: true,
|
|
},
|
|
{
|
|
id: "workspaces",
|
|
icon: LayoutTemplate,
|
|
labelKey: "nav.workspaces",
|
|
separatorAfter: true,
|
|
},
|
|
{
|
|
id: "local-terminal",
|
|
icon: TerminalSquare,
|
|
labelKey: "nav.localTerminal",
|
|
kind: "tab",
|
|
separatorAfter: true,
|
|
electronOnly: true,
|
|
},
|
|
{
|
|
id: "network_graph",
|
|
icon: Network,
|
|
labelKey: "nav.networkGraph",
|
|
kind: "tab",
|
|
separatorAfter: true,
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Rail items available in the current build. Electron-only destinations are
|
|
* dropped in the browser build so they never reach the rail, the mobile bar,
|
|
* or the visibility toggles.
|
|
*/
|
|
export function visibleRailItems(): RailItemDef[] {
|
|
const electron = isElectron();
|
|
return RAIL_ITEMS.filter((item) => !item.electronOnly || electron);
|
|
}
|
|
|
|
/**
|
|
* Destinations that live outside the rail's hideable list but still need a
|
|
* title and a mobile entry.
|
|
*/
|
|
export const RAIL_UTILITY_ITEMS: RailItemDef[] = [
|
|
{
|
|
id: "alerts",
|
|
icon: Plug,
|
|
labelKey: "nav.alerts",
|
|
promotable: true,
|
|
rightDockable: true,
|
|
},
|
|
{ id: "user-profile", icon: User, labelKey: "nav.userProfile" },
|
|
{ id: "admin-settings", icon: Settings, labelKey: "nav.admin" },
|
|
];
|
|
|
|
/** Ids that may be opened in the right dock. */
|
|
export const RIGHT_DOCKABLE_IDS = [...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]
|
|
.filter((item) => item.rightDockable)
|
|
.map((item) => item.id);
|
|
|
|
/** Ids that may be opened as a full-width tab. */
|
|
export const PROMOTABLE_IDS = [...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]
|
|
.filter((item) => item.promotable)
|
|
.map((item) => item.id);
|
|
|
|
/** Ids a user is allowed to hide, mirroring HideableRailView. */
|
|
export const HIDEABLE_RAIL_IDS = RAIL_ITEMS.filter(
|
|
(item) => !item.alwaysVisible,
|
|
).map((item) => item.id);
|
|
|
|
const LABEL_KEYS: Record<string, string> = Object.fromEntries(
|
|
[...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS].map((item) => [
|
|
item.id,
|
|
item.labelKey,
|
|
]),
|
|
);
|
|
|
|
/** Translated label for any rail destination. */
|
|
export function railItemLabel(id: string, t: (key: string) => string): string {
|
|
const key = LABEL_KEYS[id];
|
|
return key ? t(key) : id;
|
|
}
|