v1.6.0 #221

Merged
LukeGus merged 74 commits from dev-1.6.0 into main 2025-09-12 19:42:00 +00:00
8 changed files with 293 additions and 32 deletions
Showing only changes of commit 51cced8f83 - Show all commits

21
package-lock.json generated
View File

@@ -7,7 +7,6 @@
"": { "": {
"name": "termix", "name": "termix",
"version": "1.6.0", "version": "1.6.0",
"hasInstallScript": true,
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.1.1", "@hookform/resolvers": "^5.1.1",
"@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-accordion": "^1.2.11",
@@ -5152,26 +5151,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/compare-version": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
"integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",

View File

@@ -388,6 +388,7 @@ export function LeftSidebar({
<DropdownMenuItem <DropdownMenuItem
className="rounded px-2 py-1.5 hover:bg-white/15 hover:text-accent-foreground focus:bg-white/20 focus:text-accent-foreground cursor-pointer focus:outline-none" className="rounded px-2 py-1.5 hover:bg-white/15 hover:text-accent-foreground focus:bg-white/20 focus:text-accent-foreground cursor-pointer focus:outline-none"
onClick={handleLogout}> onClick={handleLogout}>
<span>{t('common.logout')}</span> <span>{t('common.logout')}</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem

View File

@@ -59,13 +59,9 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
setSelectedTabIds([]); setSelectedTabIds([]);
}; };
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (selectedTabIds.length === 0) return; if (selectedTabIds.length === 0) return;
const value = e.currentTarget.value;
let commandToSend = ''; let commandToSend = '';
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {

View File

@@ -0,0 +1,16 @@
import { Button } from "@/components/ui/button";
import {Menu} from "lucide-react";
interface MenuProps {
onSidebarOpenClick?: () => void;
}
export function BottomNavbar({onSidebarOpenClick}: MenuProps) {
return (
<div className="w-full h-[80px] bg-[#18181BFF] flex flex-col justify-center">
<Button className="w-[40px] h-[40px] ml-2" variant="outline" onClick={onSidebarOpenClick}>
<Menu />
</Button>
</div>
)
}

View File

@@ -0,0 +1,80 @@
import React, {useState} from "react";
import {CardTitle} from "@/components/ui/card.tsx";
import {ChevronDown, Folder} from "lucide-react";
import {Button} from "@/components/ui/button.tsx";
import {Separator} from "@/components/ui/separator.tsx";
import {Host} from "@/ui/Mobile/Apps/Navigation/Hosts/Host.tsx";
interface SSHHost {
id: number;
name: string;
ip: string;
port: number;
username: string;
folder: string;
tags: string[];
pin: boolean;
authType: string;
password?: string;
key?: string;
keyPassword?: string;
keyType?: string;
enableTerminal: boolean;
enableTunnel: boolean;
enableFileManager: boolean;
defaultPath: string;
tunnelConnections: any[];
createdAt: string;
updatedAt: string;
}
interface FolderCardProps {
folderName: string,
hosts: SSHHost[],
}
export function FolderCard({folderName, hosts}: FolderCardProps): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(true);
const toggleExpanded = () => {
setIsExpanded(!isExpanded);
};
return (
<div className="bg-[#0e0e10] border-2 border-[#303032] rounded-lg overflow-hidden"
style={{padding: '0', margin: '0'}}>
<div className={`px-4 py-3 relative ${isExpanded ? 'border-b-2' : ''} bg-[#131316]`}>
<div className="flex gap-2 pr-10">
<div className="flex-shrink-0 flex items-center">
<Folder size={16} strokeWidth={3}/>
</div>
<div className="flex-1 min-w-0">
<CardTitle className="mb-0 leading-tight break-words text-md">{folderName}</CardTitle>
</div>
</div>
<Button
variant="outline"
className="w-[28px] h-[28px] absolute right-4 top-1/2 -translate-y-1/2 flex-shrink-0"
onClick={toggleExpanded}
>
<ChevronDown className={`h-4 w-4 transition-transform ${isExpanded ? '' : 'rotate-180'}`}/>
</Button>
</div>
{isExpanded && (
<div className="flex flex-col p-2 gap-y-3">
{hosts.map((host, index) => (
<React.Fragment key={`${folderName}-host-${host.id}-${host.name || host.ip}`}>
<Host host={host}/>
{index < hosts.length - 1 && (
<div className="relative -mx-2">
<Separator className="p-0.25 absolute inset-x-0"/>
</div>
)}
</React.Fragment>
))}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,107 @@
import React, {useEffect, useState} from "react";
import {Status, StatusIndicator} from "@/components/ui/shadcn-io/status";
import {Button} from "@/components/ui/button.tsx";
import {ButtonGroup} from "@/components/ui/button-group.tsx";
import {Server, Terminal} from "lucide-react";
import {getServerStatusById} from "@/ui/main-axios.ts";
interface SSHHost {
id: number;
name: string;
ip: string;
port: number;
username: string;
folder: string;
tags: string[];
pin: boolean;
authType: string;
password?: string;
key?: string;
keyPassword?: string;
keyType?: string;
enableTerminal: boolean;
enableTunnel: boolean;
enableFileManager: boolean;
defaultPath: string;
tunnelConnections: any[];
createdAt: string;
updatedAt: string;
}
interface HostProps {
host: SSHHost;
}
export function Host({host}: HostProps): React.ReactElement {
const [serverStatus, setServerStatus] = useState<'online' | 'offline' | 'degraded'>('degraded');
const tags = Array.isArray(host.tags) ? host.tags : [];
const hasTags = tags.length > 0;
const title = host.name?.trim() ? host.name : `${host.username}@${host.ip}:${host.port}`;
useEffect(() => {
let intervalId: number | undefined;
let cancelled = false;
const fetchStatus = async () => {
try {
const res = await getServerStatusById(host.id);
if (!cancelled) {
setServerStatus(res?.status === 'online' ? 'online' : 'offline');
}
} catch {
if (!cancelled) setServerStatus('offline');
}
};
fetchStatus();
intervalId = window.setInterval(fetchStatus, 10000);
return () => {
cancelled = true;
if (intervalId) window.clearInterval(intervalId);
};
}, [host.id]);
const handleTerminalClick = () => {
};
const handleServerClick = () => {
};
return (
<div>
<div className="flex items-center gap-2">
<Status status={serverStatus} className="!bg-transparent !p-0.75 flex-shrink-0">
<StatusIndicator/>
</Status>
<p className="font-semibold flex-1 min-w-0 break-words text-sm">
{host.name || host.ip}
</p>
<ButtonGroup className="flex-shrink-0">
{host.enableTerminal && (
<Button
variant="outline"
className="!px-2 border-1 w-[60px] border-[#303032]"
onClick={handleTerminalClick}
>
<Terminal/>
</Button>
)}
</ButtonGroup>
</div>
{hasTags && (
<div className="flex flex-wrap items-center gap-2 mt-1">
{tags.map((tag: string) => (
<div key={tag} className="bg-[#18181b] border-1 border-[#303032] pl-2 pr-2 rounded-[10px]">
<p className="text-sm">{tag}</p>
</div>
))}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,63 @@
import {Sidebar, SidebarContent, SidebarGroupLabel, SidebarHeader, SidebarProvider} from "@/components/ui/sidebar.tsx";
import {Button} from "@/components/ui/button.tsx";
import {Menu} from "lucide-react";
import React from "react";
import {Separator} from "@/components/ui/separator.tsx";
import {FolderCard} from "@/ui/Mobile/Apps/Navigation/Hosts/FolderCard.tsx";
import {Host} from "@/ui/Mobile/Apps/Navigation/Hosts/Host.tsx";
interface LeftSidebarProps {
isSidebarOpen: boolean;
setIsSidebarOpen: (type: boolean) => void;
}
export function LeftSidebar({ isSidebarOpen, setIsSidebarOpen }: LeftSidebarProps) {
return (
<div className="">
<SidebarProvider open={isSidebarOpen}>
<Sidebar>
<SidebarHeader>
<SidebarGroupLabel className="text-lg font-bold text-white">
Termix
<Button
variant="outline"
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className="w-[28px] h-[28px] absolute right-5"
>
<Menu className="h-4 w-4"/>
</Button>
</SidebarGroupLabel>
</SidebarHeader>
<Separator/>
<SidebarContent className="px-2 py-2">
<FolderCard
folderName="Folder"
hosts={[{
id: 1,
name: "My Server",
ip: "192.168.1.100",
port: 22,
username: "admin",
folder: "/home/admin",
tags: ["production", "backend"],
pin: true,
authType: "password",
password: "securePassword123",
key: undefined,
keyPassword: undefined,
keyType: undefined,
enableTerminal: true,
enableTunnel: false,
enableFileManager: true,
defaultPath: "/home/admin/projects",
tunnelConnections: [],
createdAt: "2025-09-05T12:00:00Z",
updatedAt: "2025-09-05T12:00:00Z"
}]}
/>
</SidebarContent>
</Sidebar>
</SidebarProvider>
</div>
)
}

View File

@@ -1,9 +1,12 @@
import {useRef, FC} from "react"; import React, {useRef, FC} from "react";
import {Terminal} from "@/ui/Mobile/Apps/Terminal/Terminal.tsx"; import {Terminal} from "@/ui/Mobile/Apps/Terminal/Terminal.tsx";
import {TerminalKeyboard} from "@/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx"; import {TerminalKeyboard} from "@/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx";
import {BottomNavbar} from "@/ui/Mobile/Apps/Navigation/BottomNavbar.tsx";
import {LeftSidebar} from "@/ui/Mobile/Apps/Navigation/LeftSidebar.tsx";
export const MobileApp: FC = () => { export const MobileApp: FC = () => {
const terminalRef = useRef<any>(null); const terminalRef = useRef<any>(null);
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);
function handleKeyboardInput(input: string) { function handleKeyboardInput(input: string) {
if (!terminalRef.current?.sendInput) return; if (!terminalRef.current?.sendInput) return;
@@ -12,7 +15,7 @@ export const MobileApp: FC = () => {
"{backspace}": "\x7f", "{backspace}": "\x7f",
"{space}": " ", "{space}": " ",
"{tab}": "\t", "{tab}": "\t",
"{enter}": "\r", "{enter}": "",
"{escape}": "\x1b", "{escape}": "\x1b",
"{arrowUp}": "\x1b[A", "{arrowUp}": "\x1b[A",
"{arrowDown}": "\x1b[B", "{arrowDown}": "\x1b[B",
@@ -22,7 +25,7 @@ export const MobileApp: FC = () => {
"{home}": "\x1b[H", "{home}": "\x1b[H",
"{end}": "\x1b[F", "{end}": "\x1b[F",
"{pageUp}": "\x1b[5~", "{pageUp}": "\x1b[5~",
"{pageDown}": "\x1b[6~", "{pageDown}": "\x1b[6~"
}; };
if (input in keyMap) { if (input in keyMap) {
@@ -33,7 +36,7 @@ export const MobileApp: FC = () => {
} }
return ( return (
<div className="h-screen w-screen flex flex-col bg-[#09090b] overflow-y-hidden overflow-x-hidden"> <div className="h-screen w-screen flex flex-col bg-[#09090b] overflow-y-hidden overflow-x-hidden relative">
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
<Terminal <Terminal
ref={terminalRef} ref={terminalRef}
@@ -49,9 +52,25 @@ export const MobileApp: FC = () => {
<TerminalKeyboard <TerminalKeyboard
onSendInput={handleKeyboardInput} onSendInput={handleKeyboardInput}
/> />
<div className="w-full h-[80px] bg-[#18181BFF]"> <BottomNavbar
onSidebarOpenClick={() => setIsSidebarOpen(true)}
/>
{isSidebarOpen && (
<div
className="absolute inset-0 bg-black/30 backdrop-blur-sm z-10"
onClick={() => setIsSidebarOpen(false)}
/>
)}
<div className="absolute top-0 left-0 h-full z-20 pointer-events-none">
<div onClick={(e) => { e.stopPropagation(); }} className="pointer-events-auto">
<LeftSidebar
isSidebarOpen={isSidebarOpen}
setIsSidebarOpen={setIsSidebarOpen}
/>
</div>
</div> </div>
</div> </div>
) );
} }