Added config editor to tab system, (W.I.P)
This commit is contained in:
@@ -88,7 +88,7 @@ function AppContent() {
|
|||||||
|
|
||||||
// Determine what to show based on current tab
|
// Determine what to show based on current tab
|
||||||
const currentTabData = tabs.find(tab => tab.id === currentTab);
|
const currentTabData = tabs.find(tab => tab.id === currentTab);
|
||||||
const showTerminalView = currentTabData?.type === 'terminal' || currentTabData?.type === 'server';
|
const showTerminalView = currentTabData?.type === 'terminal' || currentTabData?.type === 'server' || currentTabData?.type === 'config';
|
||||||
const showHome = currentTabData?.type === 'home';
|
const showHome = currentTabData?.type === 'home';
|
||||||
const showSshManager = currentTabData?.type === 'ssh_manager';
|
const showSshManager = currentTabData?.type === 'ssh_manager';
|
||||||
const showAdmin = currentTabData?.type === 'admin';
|
const showAdmin = currentTabData?.type === 'admin';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { createContext, useContext, useState, useRef, type ReactNode } fr
|
|||||||
|
|
||||||
export interface Tab {
|
export interface Tab {
|
||||||
id: number;
|
id: number;
|
||||||
type: 'home' | 'terminal' | 'ssh_manager' | 'server' | 'admin';
|
type: 'home' | 'terminal' | 'ssh_manager' | 'server' | 'admin' | 'config';
|
||||||
title: string;
|
title: string;
|
||||||
hostConfig?: any;
|
hostConfig?: any;
|
||||||
terminalRef?: React.RefObject<any>;
|
terminalRef?: React.RefObject<any>;
|
||||||
@@ -42,7 +42,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
const nextTabId = useRef(2);
|
const nextTabId = useRef(2);
|
||||||
|
|
||||||
function computeUniqueTitle(tabType: Tab['type'], desiredTitle: string | undefined): string {
|
function computeUniqueTitle(tabType: Tab['type'], desiredTitle: string | undefined): string {
|
||||||
const defaultTitle = tabType === 'server' ? 'Server' : 'Terminal';
|
const defaultTitle = tabType === 'server' ? 'Server' : (tabType === 'config' ? 'Config' : 'Terminal');
|
||||||
const baseTitle = (desiredTitle || defaultTitle).trim();
|
const baseTitle = (desiredTitle || defaultTitle).trim();
|
||||||
// Extract base name without trailing " (n)"
|
// Extract base name without trailing " (n)"
|
||||||
const match = baseTitle.match(/^(.*) \((\d+)\)$/);
|
const match = baseTitle.match(/^(.*) \((\d+)\)$/);
|
||||||
@@ -72,7 +72,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
|
|
||||||
const addTab = (tabData: Omit<Tab, 'id'>): number => {
|
const addTab = (tabData: Omit<Tab, 'id'>): number => {
|
||||||
const id = nextTabId.current++;
|
const id = nextTabId.current++;
|
||||||
const needsUniqueTitle = tabData.type === 'terminal' || tabData.type === 'server';
|
const needsUniqueTitle = tabData.type === 'terminal' || tabData.type === 'server' || tabData.type === 'config';
|
||||||
const effectiveTitle = needsUniqueTitle ? computeUniqueTitle(tabData.type, tabData.title) : (tabData.title || '');
|
const effectiveTitle = needsUniqueTitle ? computeUniqueTitle(tabData.type, tabData.title) : (tabData.title || '');
|
||||||
const newTab: Tab = {
|
const newTab: Tab = {
|
||||||
...tabData,
|
...tabData,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import {TerminalComponent} from "../SSH/Terminal/TerminalComponent.tsx";
|
import {TerminalComponent} from "../SSH/Terminal/TerminalComponent.tsx";
|
||||||
import {Server as ServerView} from "@/ui/SSH/Server/Server.tsx";
|
import {Server as ServerView} from "@/ui/SSH/Server/Server.tsx";
|
||||||
|
import {ConfigEditor} from "@/ui/SSH/Config Editor/ConfigEditor.tsx";
|
||||||
import {useTabs} from "@/contexts/TabContext.tsx";
|
import {useTabs} from "@/contexts/TabContext.tsx";
|
||||||
import {ResizablePanelGroup, ResizablePanel, ResizableHandle} from '@/components/ui/resizable.tsx';
|
import {ResizablePanelGroup, ResizablePanel, ResizableHandle} from '@/components/ui/resizable.tsx';
|
||||||
import * as ResizablePrimitive from "react-resizable-panels";
|
import * as ResizablePrimitive from "react-resizable-panels";
|
||||||
@@ -16,7 +17,7 @@ export function AppView({ isTopbarOpen = true }: TerminalViewProps): React.React
|
|||||||
const {tabs, currentTab, allSplitScreenTab} = useTabs() as any;
|
const {tabs, currentTab, allSplitScreenTab} = useTabs() as any;
|
||||||
const { state: sidebarState } = useSidebar();
|
const { state: sidebarState } = useSidebar();
|
||||||
|
|
||||||
const terminalTabs = tabs.filter((tab: any) => tab.type === 'terminal' || tab.type === 'server');
|
const terminalTabs = tabs.filter((tab: any) => tab.type === 'terminal' || tab.type === 'server' || tab.type === 'config');
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const panelRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
const panelRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||||
@@ -158,7 +159,7 @@ export function AppView({ isTopbarOpen = true }: TerminalViewProps): React.React
|
|||||||
showTitle={false}
|
showTitle={false}
|
||||||
splitScreen={allSplitScreenTab.length>0}
|
splitScreen={allSplitScreenTab.length>0}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : t.type === 'server' ? (
|
||||||
<ServerView
|
<ServerView
|
||||||
hostConfig={t.hostConfig}
|
hostConfig={t.hostConfig}
|
||||||
title={t.title}
|
title={t.title}
|
||||||
@@ -166,6 +167,11 @@ export function AppView({ isTopbarOpen = true }: TerminalViewProps): React.React
|
|||||||
isTopbarOpen={isTopbarOpen}
|
isTopbarOpen={isTopbarOpen}
|
||||||
embedded
|
embedded
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigEditor
|
||||||
|
embedded
|
||||||
|
initialHost={t.hostConfig}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {ButtonGroup} from "@/components/ui/button-group.tsx";
|
import {ButtonGroup} from "@/components/ui/button-group.tsx";
|
||||||
import {Button} from "@/components/ui/button.tsx";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import {Home, SeparatorVertical, X, Terminal as TerminalIcon, Server as ServerIcon} from "lucide-react";
|
import {Home, SeparatorVertical, X, Terminal as TerminalIcon, Server as ServerIcon, Folder as FolderIcon} from "lucide-react";
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
tabType: string;
|
tabType: string;
|
||||||
@@ -31,8 +31,9 @@ export function Tab({tabType, title, isActive, onActivate, onClose, onSplit, can
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tabType === "terminal" || tabType === "server") {
|
if (tabType === "terminal" || tabType === "server" || tabType === "config") {
|
||||||
const isServer = tabType === 'server';
|
const isServer = tabType === 'server';
|
||||||
|
const isConfig = tabType === 'config';
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button
|
<Button
|
||||||
@@ -41,8 +42,8 @@ export function Tab({tabType, title, isActive, onActivate, onClose, onSplit, can
|
|||||||
onClick={onActivate}
|
onClick={onActivate}
|
||||||
disabled={disableActivate}
|
disabled={disableActivate}
|
||||||
>
|
>
|
||||||
{isServer ? <ServerIcon className="mr-1 h-4 w-4"/> : <TerminalIcon className="mr-1 h-4 w-4"/>}
|
{isServer ? <ServerIcon className="mr-1 h-4 w-4"/> : isConfig ? <FolderIcon className="mr-1 h-4 w-4"/> : <TerminalIcon className="mr-1 h-4 w-4"/>}
|
||||||
{title || (isServer ? 'Server' : 'Terminal')}
|
{title || (isServer ? 'Server' : isConfig ? 'Config' : 'Terminal')}
|
||||||
</Button>
|
</Button>
|
||||||
{canSplit && (
|
{canSplit && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -53,10 +53,11 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
|||||||
const isSplit = Array.isArray(allSplitScreenTab) && allSplitScreenTab.includes(tab.id);
|
const isSplit = Array.isArray(allSplitScreenTab) && allSplitScreenTab.includes(tab.id);
|
||||||
const isTerminal = tab.type === 'terminal';
|
const isTerminal = tab.type === 'terminal';
|
||||||
const isServer = tab.type === 'server';
|
const isServer = tab.type === 'server';
|
||||||
|
const isConfig = tab.type === 'config';
|
||||||
const isSshManager = tab.type === 'ssh_manager';
|
const isSshManager = tab.type === 'ssh_manager';
|
||||||
const isAdmin = tab.type === 'admin';
|
const isAdmin = tab.type === 'admin';
|
||||||
// Split availability
|
// Split availability
|
||||||
const isSplittable = isTerminal || isServer;
|
const isSplittable = isTerminal || isServer || isConfig;
|
||||||
// Disable split entirely when on Home or SSH Manager
|
// Disable split entirely when on Home or SSH Manager
|
||||||
const isSplitButtonDisabled = (isActive && !isSplitScreenActive) || ((allSplitScreenTab?.length || 0) >= 3 && !isSplit);
|
const isSplitButtonDisabled = (isActive && !isSplitScreenActive) || ((allSplitScreenTab?.length || 0) >= 3 && !isSplit);
|
||||||
const disableSplit = !isSplittable || isSplitButtonDisabled || isActive || currentTabIsHome || currentTabIsSshManager || currentTabIsAdmin;
|
const disableSplit = !isSplittable || isSplitButtonDisabled || isActive || currentTabIsHome || currentTabIsSshManager || currentTabIsAdmin;
|
||||||
@@ -69,10 +70,10 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
|||||||
title={tab.title}
|
title={tab.title}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
onActivate={() => handleTabActivate(tab.id)}
|
onActivate={() => handleTabActivate(tab.id)}
|
||||||
onClose={isTerminal || isServer || isSshManager || isAdmin ? () => handleTabClose(tab.id) : undefined}
|
onClose={isTerminal || isServer || isConfig || isSshManager || isAdmin ? () => handleTabClose(tab.id) : undefined}
|
||||||
onSplit={isSplittable ? () => handleTabSplit(tab.id) : undefined}
|
onSplit={isSplittable ? () => handleTabSplit(tab.id) : undefined}
|
||||||
canSplit={isSplittable}
|
canSplit={isSplittable}
|
||||||
canClose={isTerminal || isServer || isSshManager || isAdmin}
|
canClose={isTerminal || isServer || isConfig || isSshManager || isAdmin}
|
||||||
disableActivate={disableActivate}
|
disableActivate={disableActivate}
|
||||||
disableSplit={disableSplit}
|
disableSplit={disableSplit}
|
||||||
disableClose={disableClose}
|
disableClose={disableClose}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ interface SSHHost {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfigEditor({onSelectView}: { onSelectView: (view: string) => void }): React.ReactElement {
|
export function ConfigEditor({onSelectView, embedded = false, initialHost = null}: { onSelectView?: (view: string) => void, embedded?: boolean, initialHost?: SSHHost | null }): React.ReactElement {
|
||||||
const [tabs, setTabs] = useState<Tab[]>([]);
|
const [tabs, setTabs] = useState<Tab[]>([]);
|
||||||
const [activeTab, setActiveTab] = useState<string | number>('home');
|
const [activeTab, setActiveTab] = useState<string | number>('home');
|
||||||
const [recent, setRecent] = useState<any[]>([]);
|
const [recent, setRecent] = useState<any[]>([]);
|
||||||
@@ -71,6 +71,22 @@ export function ConfigEditor({onSelectView}: { onSelectView: (view: string) => v
|
|||||||
|
|
||||||
const sidebarRef = useRef<any>(null);
|
const sidebarRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialHost && (!currentHost || currentHost.id !== initialHost.id)) {
|
||||||
|
setCurrentHost(initialHost);
|
||||||
|
// Defer to ensure sidebar is mounted
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const path = initialHost.defaultPath || '/';
|
||||||
|
if (sidebarRef.current && sidebarRef.current.openFolder) {
|
||||||
|
sidebarRef.current.openFolder(initialHost, path);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, [initialHost]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentHost) {
|
if (currentHost) {
|
||||||
fetchHomeData();
|
fetchHomeData();
|
||||||
@@ -428,22 +444,19 @@ export function ConfigEditor({onSelectView}: { onSelectView: (view: string) => v
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHostChange = (host: SSHHost | null) => {
|
// Host is locked; no external host change from UI
|
||||||
setCurrentHost(host);
|
const handleHostChange = (_host: SSHHost | null) => {};
|
||||||
setTabs([]);
|
|
||||||
setActiveTab('home');
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!currentHost) {
|
if (!currentHost) {
|
||||||
return (
|
return (
|
||||||
<div style={{position: 'relative', width: '100vw', height: '100vh', overflow: 'hidden'}}>
|
<div style={{position: 'relative', width: '100%', height: '100%', overflow: 'hidden'}}>
|
||||||
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100vh', zIndex: 20}}>
|
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100%', zIndex: 20}}>
|
||||||
<ConfigEditorSidebar
|
<ConfigEditorSidebar
|
||||||
onSelectView={onSelectView}
|
onSelectView={onSelectView || (() => {})}
|
||||||
onOpenFile={handleOpenFile}
|
onOpenFile={handleOpenFile}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
onHostChange={handleHostChange}
|
host={initialHost as SSHHost}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -467,23 +480,23 @@ export function ConfigEditor({onSelectView}: { onSelectView: (view: string) => v
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{position: 'relative', width: '100vw', height: '100vh', overflow: 'hidden'}}>
|
<div style={{position: 'relative', width: '100%', height: '100%', overflow: 'hidden'}}>
|
||||||
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100vh', zIndex: 20}}>
|
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100%', zIndex: 20}}>
|
||||||
<ConfigEditorSidebar
|
<ConfigEditorSidebar
|
||||||
onSelectView={onSelectView}
|
onSelectView={onSelectView || (() => {})}
|
||||||
onOpenFile={handleOpenFile}
|
onOpenFile={handleOpenFile}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
onHostChange={handleHostChange}
|
host={currentHost as SSHHost}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{position: 'absolute', top: 0, left: 256, right: 0, height: 44, zIndex: 30}}>
|
<div style={{position: 'absolute', top: 0, left: 256, right: 0, height: 44, zIndex: 30}}>
|
||||||
<div className="flex items-center w-full bg-[#18181b] border-b border-[#222224] h-11 relative px-4"
|
<div className="flex items-center w-full bg-[#18181b] border-b-2 border-[#303032] h-11 relative px-4"
|
||||||
style={{height: 44}}>
|
style={{height: 44}}>
|
||||||
{/* Tab list scrollable area */}
|
{/* Tab list scrollable area */}
|
||||||
<div className="flex-1 min-w-0 h-full flex items-center">
|
<div className="flex-1 min-w-0 h-full flex items-center">
|
||||||
<div
|
<div
|
||||||
className="h-9 w-full bg-[#09090b] border border-[#23232a] rounded-md flex items-center overflow-x-auto scrollbar-thin scrollbar-thumb-muted-foreground/30 scrollbar-track-transparent"
|
className="h-9 w-full bg-[#09090b] border-2 border-[#303032] rounded-md flex items-center overflow-x-auto scrollbar-thin scrollbar-thumb-muted-foreground/30 scrollbar-track-transparent"
|
||||||
style={{minWidth: 0}}>
|
style={{minWidth: 0}}>
|
||||||
<ConfigTopbar
|
<ConfigTopbar
|
||||||
tabs={tabs.map(t => ({id: t.id, title: t.title}))}
|
tabs={tabs.map(t => ({id: t.id, title: t.title}))}
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
import React, {useEffect, useState, useRef, forwardRef, useImperativeHandle} from 'react';
|
import React, {useEffect, useState, useRef, forwardRef, useImperativeHandle} from 'react';
|
||||||
import {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel, SidebarMenu, SidebarMenuItem,
|
|
||||||
SidebarProvider
|
|
||||||
} from '@/components/ui/sidebar.tsx';
|
|
||||||
import {Separator} from '@/components/ui/separator.tsx';
|
import {Separator} from '@/components/ui/separator.tsx';
|
||||||
import {CornerDownLeft, Folder, File, Server, ArrowUp, Pin} from 'lucide-react';
|
import {CornerDownLeft, Folder, File, Server, ArrowUp, Pin} from 'lucide-react';
|
||||||
import {ScrollArea} from '@/components/ui/scroll-area.tsx';
|
import {ScrollArea} from '@/components/ui/scroll-area.tsx';
|
||||||
import {cn} from '@/lib/utils.ts';
|
import {cn} from '@/lib/utils.ts';
|
||||||
import {Input} from '@/components/ui/input.tsx';
|
import {Input} from '@/components/ui/input.tsx';
|
||||||
import {Button} from '@/components/ui/button.tsx';
|
import {Button} from '@/components/ui/button.tsx';
|
||||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from '@/components/ui/accordion.tsx';
|
|
||||||
import {
|
import {
|
||||||
getSSHHosts,
|
|
||||||
listSSHFiles,
|
listSSHFiles,
|
||||||
connectSSH,
|
connectSSH,
|
||||||
getSSHStatus,
|
getSSHStatus,
|
||||||
@@ -48,19 +38,14 @@ interface SSHHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
||||||
{onSelectView, onOpenFile, tabs, onHostChange}: {
|
{onSelectView, onOpenFile, tabs, host}: {
|
||||||
onSelectView: (view: string) => void;
|
onSelectView?: (view: string) => void;
|
||||||
onOpenFile: (file: any) => void;
|
onOpenFile: (file: any) => void;
|
||||||
tabs: any[];
|
tabs: any[];
|
||||||
onHostChange?: (host: SSHHost | null) => void;
|
host: SSHHost;
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const [sshConnections, setSSHConnections] = useState<SSHHost[]>([]);
|
|
||||||
const [loadingSSH, setLoadingSSH] = useState(false);
|
|
||||||
const [errorSSH, setErrorSSH] = useState<string | undefined>(undefined);
|
|
||||||
const [view, setView] = useState<'servers' | 'files'>('servers');
|
|
||||||
const [activeServer, setActiveServer] = useState<SSHHost | null>(null);
|
|
||||||
const [currentPath, setCurrentPath] = useState('/');
|
const [currentPath, setCurrentPath] = useState('/');
|
||||||
const [files, setFiles] = useState<any[]>([]);
|
const [files, setFiles] = useState<any[]>([]);
|
||||||
const pathInputRef = useRef<HTMLInputElement>(null);
|
const pathInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -89,27 +74,14 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
const [fetchingFiles, setFetchingFiles] = useState(false);
|
const [fetchingFiles, setFetchingFiles] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSSH();
|
// when host changes, set path and connect
|
||||||
}, []);
|
const nextPath = host?.defaultPath || '/';
|
||||||
|
setCurrentPath(nextPath);
|
||||||
async function fetchSSH() {
|
(async () => {
|
||||||
setLoadingSSH(true);
|
await connectToSSH(host);
|
||||||
setErrorSSH(undefined);
|
})();
|
||||||
try {
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const hosts = await getSSHHosts();
|
}, [host?.id]);
|
||||||
const configEditorHosts = hosts.filter(host => host.enableConfigEditor);
|
|
||||||
|
|
||||||
if (configEditorHosts.length > 0) {
|
|
||||||
const firstHost = configEditorHosts[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
setSSHConnections(configEditorHosts);
|
|
||||||
} catch (err: any) {
|
|
||||||
setErrorSSH('Failed to load SSH connections');
|
|
||||||
} finally {
|
|
||||||
setLoadingSSH(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectToSSH(server: SSHHost): Promise<string | null> {
|
async function connectToSSH(server: SSHHost): Promise<string | null> {
|
||||||
const sessionId = server.id.toString();
|
const sessionId = server.id.toString();
|
||||||
@@ -173,19 +145,19 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
try {
|
try {
|
||||||
let pinnedFiles: any[] = [];
|
let pinnedFiles: any[] = [];
|
||||||
try {
|
try {
|
||||||
if (activeServer) {
|
if (host) {
|
||||||
pinnedFiles = await getConfigEditorPinned(activeServer.id);
|
pinnedFiles = await getConfigEditorPinned(host.id);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeServer && sshSessionId) {
|
if (host && sshSessionId) {
|
||||||
let res: any[] = [];
|
let res: any[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const status = await getSSHStatus(sshSessionId);
|
const status = await getSSHStatus(sshSessionId);
|
||||||
if (!status.connected) {
|
if (!status.connected) {
|
||||||
const newSessionId = await connectToSSH(activeServer);
|
const newSessionId = await connectToSSH(host);
|
||||||
if (newSessionId) {
|
if (newSessionId) {
|
||||||
setSshSessionId(newSessionId);
|
setSshSessionId(newSessionId);
|
||||||
res = await listSSHFiles(newSessionId, currentPath);
|
res = await listSSHFiles(newSessionId, currentPath);
|
||||||
@@ -196,7 +168,7 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
res = await listSSHFiles(sshSessionId, currentPath);
|
res = await listSSHFiles(sshSessionId, currentPath);
|
||||||
}
|
}
|
||||||
} catch (sessionErr) {
|
} catch (sessionErr) {
|
||||||
const newSessionId = await connectToSSH(activeServer);
|
const newSessionId = await connectToSSH(host);
|
||||||
if (newSessionId) {
|
if (newSessionId) {
|
||||||
setSshSessionId(newSessionId);
|
setSshSessionId(newSessionId);
|
||||||
res = await listSSHFiles(newSessionId, currentPath);
|
res = await listSSHFiles(newSessionId, currentPath);
|
||||||
@@ -229,48 +201,21 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view === 'files' && activeServer && sshSessionId && !connectingSSH && !fetchingFiles) {
|
if (host && sshSessionId && !connectingSSH && !fetchingFiles) {
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
fetchFiles();
|
fetchFiles();
|
||||||
}, 100);
|
}, 100);
|
||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
}, [currentPath, view, activeServer, sshSessionId]);
|
}, [currentPath, host, sshSessionId]);
|
||||||
|
|
||||||
async function handleSelectServer(server: SSHHost) {
|
|
||||||
if (connectingSSH) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFetchingFiles(false);
|
|
||||||
setFilesLoading(false);
|
|
||||||
setFilesError(null);
|
|
||||||
setFiles([]);
|
|
||||||
|
|
||||||
setActiveServer(server);
|
|
||||||
setCurrentPath(server.defaultPath || '/');
|
|
||||||
setView('files');
|
|
||||||
|
|
||||||
const sessionId = await connectToSSH(server);
|
|
||||||
if (sessionId) {
|
|
||||||
setSshSessionId(sessionId);
|
|
||||||
if (onHostChange) {
|
|
||||||
onHostChange(server);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
w
|
|
||||||
setView('servers');
|
|
||||||
setActiveServer(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
openFolder: async (server: SSHHost, path: string) => {
|
openFolder: async (_server: SSHHost, path: string) => {
|
||||||
if (connectingSSH || fetchingFiles) {
|
if (connectingSSH || fetchingFiles) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeServer?.id === server.id && currentPath === path) {
|
if (currentPath === path) {
|
||||||
setTimeout(() => fetchFiles(), 100);
|
setTimeout(() => fetchFiles(), 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -280,29 +225,14 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
setFilesError(null);
|
setFilesError(null);
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
|
|
||||||
setActiveServer(server);
|
|
||||||
setCurrentPath(path);
|
setCurrentPath(path);
|
||||||
setView('files');
|
if (!sshSessionId) {
|
||||||
|
const sessionId = await connectToSSH(host);
|
||||||
if (!sshSessionId || activeServer?.id !== server.id) {
|
if (sessionId) setSshSessionId(sessionId);
|
||||||
const sessionId = await connectToSSH(server);
|
|
||||||
if (sessionId) {
|
|
||||||
setSshSessionId(sessionId);
|
|
||||||
if (onHostChange && activeServer?.id !== server.id) {
|
|
||||||
onHostChange(server);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setView('servers');
|
|
||||||
setActiveServer(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (onHostChange && activeServer?.id !== server.id) {
|
|
||||||
onHostChange(server);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchFiles: () => {
|
fetchFiles: () => {
|
||||||
if (activeServer && sshSessionId) {
|
if (host && sshSessionId) {
|
||||||
fetchFiles();
|
fetchFiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,30 +244,6 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
}
|
}
|
||||||
}, [currentPath]);
|
}, [currentPath]);
|
||||||
|
|
||||||
const sshByFolder: Record<string, SSHHost[]> = {};
|
|
||||||
sshConnections.forEach(conn => {
|
|
||||||
const folder = conn.folder && conn.folder.trim() ? conn.folder : 'No Folder';
|
|
||||||
if (!sshByFolder[folder]) sshByFolder[folder] = [];
|
|
||||||
sshByFolder[folder].push(conn);
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortedFolders = Object.keys(sshByFolder);
|
|
||||||
if (sortedFolders.includes('No Folder')) {
|
|
||||||
sortedFolders.splice(sortedFolders.indexOf('No Folder'), 1);
|
|
||||||
sortedFolders.unshift('No Folder');
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredSshByFolder: Record<string, SSHHost[]> = {};
|
|
||||||
Object.entries(sshByFolder).forEach(([folder, hosts]) => {
|
|
||||||
filteredSshByFolder[folder] = hosts.filter(conn => {
|
|
||||||
const q = debouncedSearch.trim().toLowerCase();
|
|
||||||
if (!q) return true;
|
|
||||||
return (conn.name || '').toLowerCase().includes(q) || (conn.ip || '').toLowerCase().includes(q) ||
|
|
||||||
(conn.username || '').toLowerCase().includes(q) || (conn.folder || '').toLowerCase().includes(q) ||
|
|
||||||
(conn.tags || []).join(' ').toLowerCase().includes(q);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredFiles = files.filter(file => {
|
const filteredFiles = files.filter(file => {
|
||||||
const q = debouncedFileSearch.trim().toLowerCase();
|
const q = debouncedFileSearch.trim().toLowerCase();
|
||||||
if (!q) return true;
|
if (!q) return true;
|
||||||
@@ -345,107 +251,16 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<div className="flex flex-col h-full w-[256px]" style={{maxWidth: 256}}>
|
||||||
<Sidebar style={{height: '100vh', maxHeight: '100vh', overflow: 'hidden'}}>
|
<div className="flex flex-col flex-grow min-h-0">
|
||||||
<SidebarContent style={{height: '100vh', maxHeight: '100vh', overflow: 'hidden'}}>
|
<div className="flex-1 w-full h-full flex flex-col bg-[#09090b] border-r-2 border-[#303032] overflow-hidden p-0 relative min-h-0">
|
||||||
<SidebarGroup className="flex flex-col flex-grow h-full overflow-hidden">
|
{host && (
|
||||||
<SidebarGroupLabel className="text-lg font-bold text-white flex items-center gap-2">
|
|
||||||
Termix / Config
|
|
||||||
</SidebarGroupLabel>
|
|
||||||
<Separator className="p-0.25 mt-1 mb-1"/>
|
|
||||||
<SidebarGroupContent className="flex flex-col flex-grow min-h-0">
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem key={"Homepage"}>
|
|
||||||
<Button className="w-full mt-2 mb-2 h-8" onClick={() => onSelectView("homepage")}
|
|
||||||
variant="outline">
|
|
||||||
<CornerDownLeft/>
|
|
||||||
Return
|
|
||||||
</Button>
|
|
||||||
<Separator className="p-0.25 mt-1 mb-1"/>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
<div
|
|
||||||
className="flex-1 w-full flex flex-col rounded-md bg-[#09090b] border border-[#434345] overflow-hidden p-0 relative min-h-0 mt-1">
|
|
||||||
{view === 'servers' && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="w-full px-2 pt-2 pb-2 bg-[#09090b] z-10 border-b border-[#23232a]">
|
|
||||||
<Input
|
|
||||||
value={search}
|
|
||||||
onChange={e => setSearch(e.target.value)}
|
|
||||||
placeholder="Search hosts by name, username, IP, folder, tags..."
|
|
||||||
className="w-full h-8 text-sm bg-[#18181b] border border-[#23232a] text-white placeholder:text-muted-foreground rounded"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ScrollArea className="flex-1 w-full h-full"
|
|
||||||
style={{height: '100%', maxHeight: '100%'}}>
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<div
|
|
||||||
className="w-full flex-grow overflow-hidden p-0 m-0 relative flex flex-col min-h-0">
|
|
||||||
<div style={{display: 'flex', justifyContent: 'center'}}>
|
|
||||||
<Separator className="w-full h-px bg-[#434345] my-2"
|
|
||||||
style={{maxWidth: 213, margin: '0 auto'}}/>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto" style={{maxWidth: '213px', width: '100%'}}>
|
|
||||||
<div className="flex-1 min-h-0">
|
|
||||||
<Accordion type="multiple" className="w-full"
|
|
||||||
value={sortedFolders}>
|
|
||||||
{sortedFolders.map((folder, idx) => (
|
|
||||||
<React.Fragment key={folder}>
|
|
||||||
<AccordionItem value={folder}
|
|
||||||
className="mt-0 w-full !border-b-transparent">
|
|
||||||
<AccordionTrigger
|
|
||||||
className="text-base font-semibold rounded-t-none py-2 w-full">{folder}</AccordionTrigger>
|
|
||||||
<AccordionContent
|
|
||||||
className="flex flex-col gap-1 pb-2 pt-1 w-full">
|
|
||||||
{filteredSshByFolder[folder].map(conn => (
|
|
||||||
<Button
|
|
||||||
key={conn.id}
|
|
||||||
variant="outline"
|
|
||||||
className="w-full h-10 px-2 bg-[#18181b] border border-[#434345] hover:bg-[#2d2d30] transition-colors text-left justify-start"
|
|
||||||
onClick={() => handleSelectServer(conn)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="flex items-center w-full">
|
|
||||||
{conn.pin && <Pin
|
|
||||||
className="w-0.5 h-0.5 text-yellow-400 mr-1 flex-shrink-0"/>}
|
|
||||||
<span
|
|
||||||
className="font-medium truncate">{conn.name || conn.ip}</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
{idx < sortedFolders.length - 1 && (
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center'
|
|
||||||
}}>
|
|
||||||
<Separator
|
|
||||||
className="h-px bg-[#434345] my-1"
|
|
||||||
style={{width: 213}}/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{view === 'files' && activeServer && (
|
|
||||||
<div className="flex flex-col h-full w-full" style={{maxWidth: 260}}>
|
<div className="flex flex-col h-full w-full" style={{maxWidth: 260}}>
|
||||||
<div
|
<div className="flex items-center gap-2 px-2 py-2 border-b-2 border-[#303032] bg-[#18181b] z-20" style={{maxWidth: 260}}>
|
||||||
className="flex items-center gap-2 px-2 py-2 border-b border-[#23232a] bg-[#18181b] z-20"
|
|
||||||
style={{maxWidth: 260}}>
|
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-8 w-8 bg-[#18181b] border border-[#23232a] rounded-md hover:bg-[#2d2d30] focus:outline-none focus:ring-2 focus:ring-ring"
|
className="h-8 w-8 bg-[#18181b] border-2 border-[#303032] rounded-md hover:bg-[#2d2d30] focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let path = currentPath;
|
let path = currentPath;
|
||||||
if (path && path !== '/' && path !== '') {
|
if (path && path !== '/' && path !== '') {
|
||||||
@@ -457,10 +272,7 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
setCurrentPath('/');
|
setCurrentPath('/');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setView('servers');
|
setCurrentPath('/');
|
||||||
if (onHostChange) {
|
|
||||||
onHostChange(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -471,7 +283,7 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
className="flex-1 bg-[#18181b] border border-[#434345] text-white truncate rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring hover:border-[#5a5a5d]"
|
className="flex-1 bg-[#18181b] border border-[#434345] text-white truncate rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring hover:border-[#5a5a5d]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-2 border-b border-[#23232a] bg-[#18181b]">
|
<div className="px-2 py-2 border-b-1 border-[#303032] bg-[#18181b]">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search files and folders..."
|
placeholder="Search files and folders..."
|
||||||
className="w-full h-7 text-sm bg-[#23232a] border border-[#434345] text-white placeholder:text-muted-foreground rounded"
|
className="w-full h-7 text-sm bg-[#23232a] border border-[#434345] text-white placeholder:text-muted-foreground rounded"
|
||||||
@@ -480,7 +292,7 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
onChange={e => setFileSearch(e.target.value)}
|
onChange={e => setFileSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 w-full h-full bg-[#09090b] border-t border-[#23232a]">
|
<div className="flex-1 w-full h-full bg-[#09090b] border-t-1 border-[#303032]">
|
||||||
<ScrollArea className="w-full h-full bg-[#09090b]" style={{
|
<ScrollArea className="w-full h-full bg-[#09090b]" style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
@@ -494,8 +306,7 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
) : filesError ? (
|
) : filesError ? (
|
||||||
<div className="text-xs text-red-500">{filesError}</div>
|
<div className="text-xs text-red-500">{filesError}</div>
|
||||||
) : filteredFiles.length === 0 ? (
|
) : filteredFiles.length === 0 ? (
|
||||||
<div className="text-xs text-muted-foreground">No files or
|
<div className="text-xs text-muted-foreground">No files or folders found.</div>
|
||||||
folders found.</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{filteredFiles.map((item: any) => {
|
{filteredFiles.map((item: any) => {
|
||||||
@@ -504,7 +315,7 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
<div
|
<div
|
||||||
key={item.path}
|
key={item.path}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 px-3 py-2 bg-[#18181b] border border-[#23232a] rounded group max-w-full",
|
"flex items-center gap-2 px-3 py-2 bg-[#18181b] border-2 border-[#303032] rounded group max-w-full",
|
||||||
isOpen && "opacity-60 cursor-not-allowed pointer-events-none"
|
isOpen && "opacity-60 cursor-not-allowed pointer-events-none"
|
||||||
)}
|
)}
|
||||||
style={{maxWidth: 220, marginBottom: 8}}
|
style={{maxWidth: 220, marginBottom: 8}}
|
||||||
@@ -519,17 +330,13 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
}))}
|
}))}
|
||||||
>
|
>
|
||||||
{item.type === 'directory' ?
|
{item.type === 'directory' ?
|
||||||
<Folder
|
<Folder className="w-4 h-4 text-blue-400"/> :
|
||||||
className="w-4 h-4 text-blue-400"/> :
|
<File className="w-4 h-4 text-muted-foreground"/>}
|
||||||
<File
|
<span className="text-sm text-white truncate max-w-[120px]">{item.name}</span>
|
||||||
className="w-4 h-4 text-muted-foreground"/>}
|
|
||||||
<span
|
|
||||||
className="text-sm text-white truncate max-w-[120px]">{item.name}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{item.type === 'file' && (
|
{item.type === 'file' && (
|
||||||
<Button size="icon" variant="ghost"
|
<Button size="icon" variant="ghost" className="h-7 w-7"
|
||||||
className="h-7 w-7"
|
|
||||||
disabled={isOpen}
|
disabled={isOpen}
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -538,29 +345,23 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
await removeConfigEditorPinned({
|
await removeConfigEditorPinned({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
path: item.path,
|
path: item.path,
|
||||||
hostId: activeServer?.id,
|
hostId: host?.id,
|
||||||
isSSH: true,
|
isSSH: true,
|
||||||
sshSessionId: activeServer?.id.toString()
|
sshSessionId: host?.id.toString()
|
||||||
});
|
});
|
||||||
setFiles(files.map(f =>
|
setFiles(files.map(f =>
|
||||||
f.path === item.path ? {
|
f.path === item.path ? { ...f, isPinned: false } : f
|
||||||
...f,
|
|
||||||
isPinned: false
|
|
||||||
} : f
|
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
await addConfigEditorPinned({
|
await addConfigEditorPinned({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
path: item.path,
|
path: item.path,
|
||||||
hostId: activeServer?.id,
|
hostId: host?.id,
|
||||||
isSSH: true,
|
isSSH: true,
|
||||||
sshSessionId: activeServer?.id.toString()
|
sshSessionId: host?.id.toString()
|
||||||
});
|
});
|
||||||
setFiles(files.map(f =>
|
setFiles(files.map(f =>
|
||||||
f.path === item.path ? {
|
f.path === item.path ? { ...f, isPinned: true } : f
|
||||||
...f,
|
|
||||||
isPinned: true
|
|
||||||
} : f
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -568,8 +369,7 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pin
|
<Pin className={`w-1 h-1 ${item.isPinned ? 'text-yellow-400 fill-current' : 'text-muted-foreground'}`}/>
|
||||||
className={`w-1 h-1 ${item.isPinned ? 'text-yellow-400 fill-current' : 'text-muted-foreground'}`}/>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -584,11 +384,8 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SidebarGroupContent>
|
</div>
|
||||||
</SidebarGroup>
|
</div>
|
||||||
</SidebarContent>
|
|
||||||
</Sidebar>
|
|
||||||
</SidebarProvider>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
export {ConfigEditorSidebar};
|
export {ConfigEditorSidebar};
|
||||||
@@ -64,7 +64,7 @@ export function ConfigFileSidebarViewer({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* SSH Connections */}
|
{/* SSH Connections */}
|
||||||
<div className="p-2 bg-[#18181b] border-b border-[#23232a]">
|
<div className="p-2 bg-[#18181b] border-b-2 border-[#303032]">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-xs text-muted-foreground font-semibold">SSH Connections</span>
|
<span className="text-xs text-muted-foreground font-semibold">SSH Connections</span>
|
||||||
<Button size="icon" variant="outline" onClick={onAddSSH} className="ml-2 h-7 w-7">
|
<Button size="icon" variant="outline" onClick={onAddSSH} className="ml-2 h-7 w-7">
|
||||||
@@ -119,7 +119,7 @@ export function ConfigFileSidebarViewer({
|
|||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{files.map((item) => (
|
{files.map((item) => (
|
||||||
<Card key={item.path}
|
<Card key={item.path}
|
||||||
className="flex items-center gap-2 px-2 py-1 bg-[#18181b] border border-[#23232a] rounded">
|
className="flex items-center gap-2 px-2 py-1 bg-[#18181b] border-2 border-[#303032] rounded">
|
||||||
<div className="flex items-center gap-2 flex-1 cursor-pointer"
|
<div className="flex items-center gap-2 flex-1 cursor-pointer"
|
||||||
onClick={() => item.type === 'directory' ? onOpenFolder(item) : onOpenFile(item)}>
|
onClick={() => item.type === 'directory' ? onOpenFolder(item) : onOpenFile(item)}>
|
||||||
{item.type === 'directory' ? <Folder className="w-4 h-4 text-blue-400"/> :
|
{item.type === 'directory' ? <Folder className="w-4 h-4 text-blue-400"/> :
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function ConfigHomeView({
|
|||||||
|
|
||||||
const renderFileCard = (file: FileItem, onRemove: () => void, onPin?: () => void, isPinned = false) => (
|
const renderFileCard = (file: FileItem, onRemove: () => void, onPin?: () => void, isPinned = false) => (
|
||||||
<div key={file.path}
|
<div key={file.path}
|
||||||
className="flex items-center gap-2 px-3 py-2 bg-[#18181b] border border-[#23232a] rounded hover:border-[#434345] transition-colors">
|
className="flex items-center gap-2 px-3 py-2 bg-[#18181b] border-2 border-[#303032] rounded hover:border-[#434345] transition-colors">
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 flex-1 cursor-pointer min-w-0"
|
className="flex items-center gap-2 flex-1 cursor-pointer min-w-0"
|
||||||
onClick={() => onOpenFile(file)}
|
onClick={() => onOpenFile(file)}
|
||||||
@@ -92,7 +92,7 @@ export function ConfigHomeView({
|
|||||||
|
|
||||||
const renderShortcutCard = (shortcut: ShortcutItem) => (
|
const renderShortcutCard = (shortcut: ShortcutItem) => (
|
||||||
<div key={shortcut.path}
|
<div key={shortcut.path}
|
||||||
className="flex items-center gap-2 px-3 py-2 bg-[#18181b] border border-[#23232a] rounded hover:border-[#434345] transition-colors">
|
className="flex items-center gap-2 px-3 py-2 bg-[#18181b] border-2 border-[#303032] rounded hover:border-[#434345] transition-colors">
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 flex-1 cursor-pointer min-w-0"
|
className="flex items-center gap-2 flex-1 cursor-pointer min-w-0"
|
||||||
onClick={() => onOpenShortcut(shortcut)}
|
onClick={() => onOpenShortcut(shortcut)}
|
||||||
@@ -120,7 +120,7 @@ export function ConfigHomeView({
|
|||||||
return (
|
return (
|
||||||
<div className="p-4 flex flex-col gap-4 h-full bg-[#09090b]">
|
<div className="p-4 flex flex-col gap-4 h-full bg-[#09090b]">
|
||||||
<Tabs value={tab} onValueChange={v => setTab(v as 'recent' | 'pinned' | 'shortcuts')} className="w-full">
|
<Tabs value={tab} onValueChange={v => setTab(v as 'recent' | 'pinned' | 'shortcuts')} className="w-full">
|
||||||
<TabsList className="mb-4 bg-[#18181b] border border-[#23232a]">
|
<TabsList className="mb-4 bg-[#18181b] border-2 border-[#303032]">
|
||||||
<TabsTrigger value="recent" className="data-[state=active]:bg-[#23232a]">Recent</TabsTrigger>
|
<TabsTrigger value="recent" className="data-[state=active]:bg-[#23232a]">Recent</TabsTrigger>
|
||||||
<TabsTrigger value="pinned" className="data-[state=active]:bg-[#23232a]">Pinned</TabsTrigger>
|
<TabsTrigger value="pinned" className="data-[state=active]:bg-[#23232a]">Pinned</TabsTrigger>
|
||||||
<TabsTrigger value="shortcuts" className="data-[state=active]:bg-[#23232a]">Folder
|
<TabsTrigger value="shortcuts" className="data-[state=active]:bg-[#23232a]">Folder
|
||||||
@@ -162,12 +162,12 @@ export function ConfigHomeView({
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="shortcuts" className="mt-0">
|
<TabsContent value="shortcuts" className="mt-0">
|
||||||
<div className="flex items-center gap-3 mb-4 p-3 bg-[#18181b] border border-[#23232a] rounded-lg">
|
<div className="flex items-center gap-3 mb-4 p-3 bg-[#18181b] border-2 border-[#303032] rounded-lg">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter folder path"
|
placeholder="Enter folder path"
|
||||||
value={newShortcut}
|
value={newShortcut}
|
||||||
onChange={e => setNewShortcut(e.target.value)}
|
onChange={e => setNewShortcut(e.target.value)}
|
||||||
className="flex-1 bg-[#23232a] border-[#434345] text-white placeholder:text-muted-foreground"
|
className="flex-1 bg-[#23232a] border-2 border-[#303032] text-white placeholder:text-muted-foreground"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && newShortcut.trim()) {
|
if (e.key === 'Enter' && newShortcut.trim()) {
|
||||||
onAddShortcut(newShortcut.trim());
|
onAddShortcut(newShortcut.trim());
|
||||||
@@ -178,7 +178,7 @@ export function ConfigHomeView({
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-8 px-2 bg-[#23232a] border-[#434345] hover:bg-[#2d2d30] rounded-md"
|
className="h-8 px-2 bg-[#23232a] border-2 border-[#303032] hover:bg-[#2d2d30] rounded-md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (newShortcut.trim()) {
|
if (newShortcut.trim()) {
|
||||||
onAddShortcut(newShortcut.trim());
|
onAddShortcut(newShortcut.trim());
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function ConfigTabList({tabs, activeTab, setActiveTab, closeTab, onHomeCl
|
|||||||
<Button
|
<Button
|
||||||
onClick={onHomeClick}
|
onClick={onHomeClick}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`h-7 mr-[0.5rem] rounded-md flex items-center ${activeTab === 'home' ? '!bg-[#1d1d1f] !text-white !border-[#2d2d30] !hover:bg-[#1d1d1f] !active:bg-[#1d1d1f] !focus:bg-[#1d1d1f] !hover:text-white !active:text-white !focus:text-white' : ''}`}
|
className={`h-7 mr-[0.5rem] rounded-md flex items-center ${activeTab === 'home' ? '!bg-[#1d1d1f] !text-white !border-2 !border-[#303032] !hover:bg-[#1d1d1f] !active:bg-[#1d1d1f] !focus:bg-[#1d1d1f] !hover:text-white !active:text-white !focus:text-white' : ''}`}
|
||||||
>
|
>
|
||||||
<Home className="w-4 h-4"/>
|
<Home className="w-4 h-4"/>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -36,7 +36,7 @@ export function ConfigTabList({tabs, activeTab, setActiveTab, closeTab, onHomeCl
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`h-7 rounded-r-none ${isActive ? '!bg-[#1d1d1f] !text-white !border-[#2d2d30] !hover:bg-[#1d1d1f] !active:bg-[#1d1d1f] !focus:bg-[#1d1d1f] !hover:text-white !active:text-white !focus:text-white' : ''}`}
|
className={`h-7 rounded-r-none ${isActive ? '!bg-[#1d1d1f] !text-white !border-2 !border-[#303032] !hover:bg-[#1d1d1f] !active:bg-[#1d1d1f] !focus:bg-[#1d1d1f] !hover:text-white !active:text-white !focus:text-white' : ''}`}
|
||||||
>
|
>
|
||||||
{tab.title}
|
{tab.title}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Progress } from "@/components/ui/progress"
|
|||||||
import {Cpu, HardDrive, MemoryStick} from "lucide-react";
|
import {Cpu, HardDrive, MemoryStick} from "lucide-react";
|
||||||
import {SSHTunnel} from "@/ui/SSH/Tunnel/SSHTunnel.tsx";
|
import {SSHTunnel} from "@/ui/SSH/Tunnel/SSHTunnel.tsx";
|
||||||
import { getServerStatusById, getServerMetricsById, ServerMetrics } from "@/ui/SSH/ssh-axios";
|
import { getServerStatusById, getServerMetricsById, ServerMetrics } from "@/ui/SSH/ssh-axios";
|
||||||
|
import { useTabs } from "@/contexts/TabContext";
|
||||||
|
|
||||||
interface ServerProps {
|
interface ServerProps {
|
||||||
hostConfig?: any;
|
hostConfig?: any;
|
||||||
@@ -18,6 +19,7 @@ interface ServerProps {
|
|||||||
|
|
||||||
export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = true, embedded = false }: ServerProps): React.ReactElement {
|
export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = true, embedded = false }: ServerProps): React.ReactElement {
|
||||||
const { state: sidebarState } = useSidebar();
|
const { state: sidebarState } = useSidebar();
|
||||||
|
const { addTab } = useTabs() as any;
|
||||||
const [serverStatus, setServerStatus] = React.useState<'online' | 'offline'>('offline');
|
const [serverStatus, setServerStatus] = React.useState<'online' | 'offline'>('offline');
|
||||||
const [metrics, setMetrics] = React.useState<ServerMetrics | null>(null);
|
const [metrics, setMetrics] = React.useState<ServerMetrics | null>(null);
|
||||||
|
|
||||||
@@ -95,7 +97,22 @@ export function Server({ hostConfig, title, isVisible = true, isTopbarOpen = tru
|
|||||||
</Status>
|
</Status>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button variant="outline">File Manager</Button>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (!hostConfig) return;
|
||||||
|
const titleBase = hostConfig?.name && hostConfig.name.trim() !== ''
|
||||||
|
? hostConfig.name.trim()
|
||||||
|
: `${hostConfig.username}@${hostConfig.ip}`;
|
||||||
|
addTab({
|
||||||
|
type: 'config',
|
||||||
|
title: titleBase,
|
||||||
|
hostConfig: hostConfig,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
File Manager
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="p-0.25 w-full"/>
|
<Separator className="p-0.25 w-full"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user