Prep for release, added ssh sidebar/topbar shrinking.
This commit is contained in:
@@ -31,12 +31,12 @@ Termix is an open-source, forever-free, self-hosted all-in-one server management
|
|||||||
- **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring
|
- **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring
|
||||||
- **Remote Config Editor** - Edit files directly on remote servers with syntax highlighting and file management
|
- **Remote Config Editor** - Edit files directly on remote servers with syntax highlighting and file management
|
||||||
- **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders
|
- **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders
|
||||||
- **User Authentication** - Secure user management with admin controls
|
- **User Authentication** - Secure user management with admin controls and OIDC support with more auth types planned
|
||||||
- **Modern UI** - Clean interface built with React, Tailwind CSS, and the amazing Shadcn
|
- **Modern UI** - Clean interface built with React, Tailwind CSS, and the amazing Shadcn
|
||||||
|
|
||||||
# Planned Features
|
# Planned Features
|
||||||
- **Improved Admin Control** - Ability to manage admins, and give more fine-grained control over their permissions, share hosts, reset passwords, delete accounts, etc
|
- **Improved Admin Control** - Ability to manage admins, and give more fine-grained control over their permissions, share hosts, reset passwords, delete accounts, etc
|
||||||
- **More auth types** - Add 2FA, OCID support, etc
|
- **More auth types** - Add 2FA, TOTP, etc
|
||||||
- **Theming** - Modify themeing for all tools
|
- **Theming** - Modify themeing for all tools
|
||||||
- **Improved SFTP Support** - Ability to manage files easier with the config editor by uploading, creating, and removing files
|
- **Improved SFTP Support** - Ability to manage files easier with the config editor by uploading, creating, and removing files
|
||||||
- **Improved Terminal Support** - Add more terminal protocols such as VNC and RDP (anyone who has experience in integrating RDP into a web-application similar to Apache Guacamole, please contact me by creating an issue)
|
- **Improved Terminal Support** - Add more terminal protocols such as VNC and RDP (anyone who has experience in integrating RDP into a web-application similar to Apache Guacamole, please contact me by creating an issue)
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
const [allSplitScreenTab, setAllSplitScreenTab] = useState<number[]>([]);
|
const [allSplitScreenTab, setAllSplitScreenTab] = useState<number[]>([]);
|
||||||
const nextTabId = useRef(1);
|
const nextTabId = useRef(1);
|
||||||
|
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
|
||||||
|
const [isTopbarOpen, setIsTopbarOpen] = useState<boolean>(true);
|
||||||
|
const SIDEBAR_WIDTH = 256;
|
||||||
|
const HANDLE_THICKNESS = 6;
|
||||||
|
|
||||||
const [panelRects, setPanelRects] = useState<Record<string, DOMRect | null>>({});
|
const [panelRects, setPanelRects] = useState<Record<string, DOMRect | null>>({});
|
||||||
const panelRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
const panelRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||||
const panelGroupRefs = useRef<{ [key: string]: any }>({});
|
const panelGroupRefs = useRef<{ [key: string]: any }>({});
|
||||||
@@ -587,20 +592,25 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', width: '100vw', height: '100vh', overflow: 'hidden'}}>
|
<div style={{display: 'flex', width: '100vw', height: '100vh', overflow: 'hidden', position: 'relative'}}>
|
||||||
<div style={{
|
{/* Sidebar (collapsible) */}
|
||||||
width: 256,
|
<div
|
||||||
flexShrink: 0,
|
style={{
|
||||||
height: '100vh',
|
width: isSidebarOpen ? SIDEBAR_WIDTH : 0,
|
||||||
position: 'relative',
|
flexShrink: 0,
|
||||||
zIndex: 2,
|
height: '100vh',
|
||||||
margin: 0,
|
position: 'relative',
|
||||||
padding: 0,
|
zIndex: 2,
|
||||||
border: 'none'
|
margin: 0,
|
||||||
}}>
|
padding: 0,
|
||||||
|
border: 'none',
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: 'width 240ms ease-in-out',
|
||||||
|
willChange: 'width',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SSHSidebar
|
<SSHSidebar
|
||||||
onSelectView={onSelectView}
|
onSelectView={onSelectView}
|
||||||
onAddHostSubmit={onAddHostSubmit}
|
|
||||||
onHostConnect={onHostConnect}
|
onHostConnect={onHostConnect}
|
||||||
allTabs={allTabs}
|
allTabs={allTabs}
|
||||||
runCommandOnTabs={(tabIds: number[], command: string) => {
|
runCommandOnTabs={(tabIds: number[], command: string) => {
|
||||||
@@ -610,8 +620,12 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onCloseSidebar={() => setIsSidebarOpen(false)}
|
||||||
|
open={isSidebarOpen}
|
||||||
|
onOpenChange={setIsSidebarOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="terminal-container"
|
className="terminal-container"
|
||||||
style={{
|
style={{
|
||||||
@@ -621,10 +635,26 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
paddingLeft: isSidebarOpen ? 0 : HANDLE_THICKNESS,
|
||||||
|
paddingTop: isTopbarOpen ? 0 : HANDLE_THICKNESS,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
transition: 'padding-left 240ms ease-in-out, padding-top 240ms ease-in-out',
|
||||||
|
willChange: 'padding',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{position: 'absolute', top: 0, left: 0, width: '100%', zIndex: 10}}>
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: isTopbarOpen ? 46 : 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
zIndex: 10,
|
||||||
|
transition: 'height 240ms ease-in-out',
|
||||||
|
willChange: 'height',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SSHTopbar
|
<SSHTopbar
|
||||||
allTabs={allTabs}
|
allTabs={allTabs}
|
||||||
currentTab={currentTab ?? -1}
|
currentTab={currentTab ?? -1}
|
||||||
@@ -632,9 +662,35 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
allSplitScreenTab={allSplitScreenTab}
|
allSplitScreenTab={allSplitScreenTab}
|
||||||
setSplitScreenTab={setSplitScreenTab}
|
setSplitScreenTab={setSplitScreenTab}
|
||||||
setCloseTab={setCloseTab}
|
setCloseTab={setCloseTab}
|
||||||
|
onHideTopbar={() => setIsTopbarOpen(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{height: 'calc(100% - 46px)', marginTop: 46, position: 'relative'}}>
|
{!isTopbarOpen && (
|
||||||
|
<div
|
||||||
|
onClick={() => setIsTopbarOpen(true)}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: HANDLE_THICKNESS,
|
||||||
|
background: '#222224',
|
||||||
|
cursor: 'pointer',
|
||||||
|
zIndex: 12,
|
||||||
|
}}
|
||||||
|
title="Show top bar"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main terminal area (height adapts to topbar) */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: isTopbarOpen ? 'calc(100% - 46px)' : '100%',
|
||||||
|
marginTop: isTopbarOpen ? 46 : 0,
|
||||||
|
position: 'relative',
|
||||||
|
transition: 'margin-top 240ms ease-in-out, height 240ms ease-in-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{allTabs.length === 0 && (
|
{allTabs.length === 0 && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -699,6 +755,24 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
{renderSplitOverlays()}
|
{renderSplitOverlays()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar reopen handle */}
|
||||||
|
{!isSidebarOpen && (
|
||||||
|
<div
|
||||||
|
onClick={() => setIsSidebarOpen(true)}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: HANDLE_THICKNESS,
|
||||||
|
height: '100%',
|
||||||
|
background: '#222224',
|
||||||
|
cursor: 'pointer',
|
||||||
|
zIndex: 20,
|
||||||
|
}}
|
||||||
|
title="Show sidebar"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ import React, {useState} from 'react';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CornerDownLeft,
|
CornerDownLeft,
|
||||||
Hammer, Pin
|
Hammer, Pin, Menu
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -63,14 +63,26 @@ interface SSHHost {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SidebarProps {
|
export interface SidebarProps {
|
||||||
onSelectView: (view: string) => void;
|
onSelectView: (view: string) => void;
|
||||||
onHostConnect: (hostConfig: any) => void;
|
onHostConnect: (hostConfig: any) => void;
|
||||||
allTabs: { id: number; title: string; terminalRef: React.RefObject<any> }[];
|
allTabs: { id: number; title: string; terminalRef: React.RefObject<any> }[];
|
||||||
runCommandOnTabs: (tabIds: number[], command: string) => void;
|
runCommandOnTabs: (tabIds: number[], command: string) => void;
|
||||||
|
onCloseSidebar?: () => void;
|
||||||
|
onAddHostSubmit?: (data: any) => void;
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SSHSidebar({onSelectView, onHostConnect, allTabs, runCommandOnTabs}: SidebarProps): React.ReactElement {
|
export function SSHSidebar({
|
||||||
|
onSelectView,
|
||||||
|
onHostConnect,
|
||||||
|
allTabs,
|
||||||
|
runCommandOnTabs,
|
||||||
|
onCloseSidebar,
|
||||||
|
open,
|
||||||
|
onOpenChange
|
||||||
|
}: SidebarProps): React.ReactElement {
|
||||||
const [hosts, setHosts] = useState<SSHHost[]>([]);
|
const [hosts, setHosts] = useState<SSHHost[]>([]);
|
||||||
const [hostsLoading, setHostsLoading] = useState(false);
|
const [hostsLoading, setHostsLoading] = useState(false);
|
||||||
const [hostsError, setHostsError] = useState<string | null>(null);
|
const [hostsError, setHostsError] = useState<string | null>(null);
|
||||||
@@ -199,12 +211,32 @@ export function SSHSidebar({onSelectView, onHostConnect, allTabs, runCommandOnTa
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider open={open} onOpenChange={onOpenChange}>
|
||||||
<Sidebar className="h-full flex flex-col overflow-hidden">
|
<Sidebar className="h-full flex flex-col overflow-hidden">
|
||||||
<SidebarContent className="flex flex-col flex-grow h-full overflow-hidden">
|
<SidebarContent className="flex flex-col flex-grow h-full overflow-hidden">
|
||||||
<SidebarGroup className="flex flex-col flex-grow h-full overflow-hidden">
|
<SidebarGroup className="flex flex-col flex-grow h-full overflow-hidden">
|
||||||
<SidebarGroupLabel className="text-lg font-bold text-white flex items-center gap-2">
|
<SidebarGroupLabel
|
||||||
Termix / Terminal
|
className="text-lg font-bold text-white flex items-center justify-between gap-2 w-full">
|
||||||
|
<span>Termix / Terminal</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onCloseSidebar?.()}
|
||||||
|
title="Hide sidebar"
|
||||||
|
style={{
|
||||||
|
height: 28,
|
||||||
|
width: 28,
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'hsl(240 5% 9%)',
|
||||||
|
color: 'hsl(240 5% 64.9%)',
|
||||||
|
border: '1px solid hsl(240 3.7% 15.9%)',
|
||||||
|
borderRadius: 6,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu className="h-4 w-4"/>
|
||||||
|
</button>
|
||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
<Separator className="p-0.25 mt-1 mb-1"/>
|
<Separator className="p-0.25 mt-1 mb-1"/>
|
||||||
<SidebarGroupContent className="flex flex-col flex-grow h-full overflow-hidden">
|
<SidebarGroupContent className="flex flex-col flex-grow h-full overflow-hidden">
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export const SSHTerminal = forwardRef<any, SSHTerminalProps>(function SSHTermina
|
|||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
|
|
||||||
const cols = terminal.cols + 1;
|
const cols = terminal.cols;
|
||||||
const rows = terminal.rows;
|
const rows = terminal.rows;
|
||||||
const wsUrl = window.location.hostname === 'localhost'
|
const wsUrl = window.location.hostname === 'localhost'
|
||||||
? 'ws://localhost:8082'
|
? 'ws://localhost:8082'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {SSHTabList} from "@/apps/SSH/Terminal/SSHTabList.tsx";
|
import {SSHTabList} from "@/apps/SSH/Terminal/SSHTabList.tsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {ChevronUp} from "lucide-react";
|
||||||
|
|
||||||
interface TerminalTab {
|
interface TerminalTab {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -13,6 +14,7 @@ interface SSHTopbarProps {
|
|||||||
allSplitScreenTab: number[];
|
allSplitScreenTab: number[];
|
||||||
setSplitScreenTab: (tab: number) => void;
|
setSplitScreenTab: (tab: number) => void;
|
||||||
setCloseTab: (tab: number) => void;
|
setCloseTab: (tab: number) => void;
|
||||||
|
onHideTopbar?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SSHTopbar({
|
export function SSHTopbar({
|
||||||
@@ -21,7 +23,8 @@ export function SSHTopbar({
|
|||||||
setActiveTab,
|
setActiveTab,
|
||||||
allSplitScreenTab,
|
allSplitScreenTab,
|
||||||
setSplitScreenTab,
|
setSplitScreenTab,
|
||||||
setCloseTab
|
setCloseTab,
|
||||||
|
onHideTopbar
|
||||||
}: SSHTopbarProps): React.ReactElement {
|
}: SSHTopbarProps): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-11.5 z-100" style={{
|
<div className="flex h-11.5 z-100" style={{
|
||||||
@@ -30,15 +33,41 @@ export function SSHTopbar({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
backgroundColor: '#18181b',
|
backgroundColor: '#18181b',
|
||||||
borderBottom: '1px solid #222224',
|
borderBottom: '1px solid #222224',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
}}>
|
}}>
|
||||||
<SSHTabList
|
<div style={{flex: 1, minWidth: 0, height: '100%', overflowX: 'auto'}}>
|
||||||
allTabs={allTabs}
|
<div style={{minWidth: 'max-content', height: '100%', paddingLeft: 8}}>
|
||||||
currentTab={currentTab}
|
<SSHTabList
|
||||||
setActiveTab={setActiveTab}
|
allTabs={allTabs}
|
||||||
allSplitScreenTab={allSplitScreenTab}
|
currentTab={currentTab}
|
||||||
setSplitScreenTab={setSplitScreenTab}
|
setActiveTab={setActiveTab}
|
||||||
setCloseTab={setCloseTab}
|
allSplitScreenTab={allSplitScreenTab}
|
||||||
/>
|
setSplitScreenTab={setSplitScreenTab}
|
||||||
|
setCloseTab={setCloseTab}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{flex: '0 0 auto', paddingRight: 8, paddingLeft: 16}}>
|
||||||
|
<button
|
||||||
|
onClick={() => onHideTopbar?.()}
|
||||||
|
style={{
|
||||||
|
height: 28,
|
||||||
|
width: 28,
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'hsl(240 5% 9%)',
|
||||||
|
color: 'hsl(240 5% 64.9%)',
|
||||||
|
border: '1px solid hsl(240 3.7% 15.9%)',
|
||||||
|
borderRadius: 6,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
title="Hide top bar"
|
||||||
|
>
|
||||||
|
<ChevronUp size={16} strokeWidth={2}/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user