Config editor rename to file manager + fixed up file manager UI

This commit is contained in:
LukeGus
2025-08-17 15:57:46 -05:00
parent 22162e5b9b
commit 880907cc93
33 changed files with 223 additions and 175 deletions

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useRef, useState } from "react";
import {TerminalComponent} from "../SSH/Terminal/TerminalComponent.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 {TerminalComponent} from "@/ui/apps/Terminal/TerminalComponent.tsx";
import {Server as ServerView} from "@/ui/apps/Server/Server.tsx";
import {FileManager} from "@/ui/apps/File Manager/FileManager.tsx";
import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
import {ResizablePanelGroup, ResizablePanel, ResizableHandle} from '@/components/ui/resizable.tsx';
import * as ResizablePrimitive from "react-resizable-panels";
import { useSidebar } from "@/components/ui/sidebar.tsx";
@@ -168,7 +168,7 @@ export function AppView({ isTopbarOpen = true }: TerminalViewProps): React.React
embedded
/>
) : (
<ConfigEditor
<FileManager
embedded
initialHost={t.hostConfig}
/>

View File

@@ -3,8 +3,8 @@ 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 {useTabs} from "@/contexts/TabContext";
import { getServerStatusById } from "@/ui/SSH/ssh-axios";
import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
import { getServerStatusById } from "@/ui/main-axios.ts";
interface SSHHost {
id: number;
@@ -86,13 +86,15 @@ export function Host({ host }: HostProps): React.ReactElement {
<Button variant="outline" className="!px-2 border-1 border-[#303032]" onClick={handleServerClick}>
<Server/>
</Button>
<Button
variant="outline"
className="!px-2 border-1 border-[#303032]"
onClick={handleTerminalClick}
>
<Terminal/>
</Button>
{host.enableTerminal && (
<Button
variant="outline"
className="!px-2 border-1 border-[#303032]"
onClick={handleTerminalClick}
>
<Terminal/>
</Button>
)}
</ButtonGroup>
</div>
{hasTags && (

View File

@@ -48,8 +48,8 @@ import {
import axios from "axios";
import {Card} from "@/components/ui/card.tsx";
import {FolderCard} from "@/ui/Navigation/Hosts/FolderCard.tsx";
import {getSSHHosts} from "@/ui/SSH/ssh-axios";
import { useTabs } from "@/contexts/TabContext";
import {getSSHHosts} from "@/ui/main-axios.ts";
import { useTabs } from "@/ui/Navigation/Tabs/TabContext.tsx";
interface SSHHost {
id: number;
@@ -206,19 +206,20 @@ export function LeftSidebar({
const fetchHosts = React.useCallback(async () => {
try {
const newHosts = await getSSHHosts();
const terminalHosts = newHosts.filter(host => host.enableTerminal);
// Show all hosts in sidebar, regardless of terminal setting
// Terminal visibility is handled in the UI components
const prevHosts = prevHostsRef.current;
// Create a stable map of existing hosts by ID for comparison
const existingHostsMap = new Map(prevHosts.map(h => [h.id, h]));
const newHostsMap = new Map(terminalHosts.map(h => [h.id, h]));
const newHostsMap = new Map(newHosts.map(h => [h.id, h]));
// Check if there are any meaningful changes
let hasChanges = false;
// Check for new hosts, removed hosts, or changed hosts
if (terminalHosts.length !== prevHosts.length) {
if (newHosts.length !== prevHosts.length) {
hasChanges = true;
} else {
for (const [id, newHost] of newHostsMap) {
@@ -248,8 +249,8 @@ export function LeftSidebar({
if (hasChanges) {
// Use a small delay to batch updates and reduce jittering
setTimeout(() => {
setHosts(terminalHosts);
prevHostsRef.current = terminalHosts;
setHosts(newHosts);
prevHostsRef.current = newHosts;
}, 50);
}
} catch (err: any) {

View File

@@ -0,0 +1,136 @@
import React, { createContext, useContext, useState, useRef, type ReactNode } from 'react';
export interface Tab {
id: number;
type: 'home' | 'terminal' | 'ssh_manager' | 'server' | 'admin' | 'config';
title: string;
hostConfig?: any;
terminalRef?: React.RefObject<any>;
}
interface TabContextType {
tabs: Tab[];
currentTab: number | null;
allSplitScreenTab: number[];
addTab: (tab: Omit<Tab, 'id'>) => number;
removeTab: (tabId: number) => void;
setCurrentTab: (tabId: number) => void;
setSplitScreenTab: (tabId: number) => void;
getTab: (tabId: number) => Tab | undefined;
}
const TabContext = createContext<TabContextType | undefined>(undefined);
export function useTabs() {
const context = useContext(TabContext);
if (context === undefined) {
throw new Error('useTabs must be used within a TabProvider');
}
return context;
}
interface TabProviderProps {
children: ReactNode;
}
export function TabProvider({ children }: TabProviderProps) {
const [tabs, setTabs] = useState<Tab[]>([
{ id: 1, type: 'home', title: 'Home' }
]);
const [currentTab, setCurrentTab] = useState<number>(1);
const [allSplitScreenTab, setAllSplitScreenTab] = useState<number[]>([]);
const nextTabId = useRef(2);
function computeUniqueTitle(tabType: Tab['type'], desiredTitle: string | undefined): string {
const defaultTitle = tabType === 'server' ? 'Server' : (tabType === 'config' ? 'Config' : 'Terminal');
const baseTitle = (desiredTitle || defaultTitle).trim();
// Extract base name without trailing " (n)"
const match = baseTitle.match(/^(.*) \((\d+)\)$/);
const root = match ? match[1] : baseTitle;
const usedNumbers = new Set<number>();
let rootUsed = false;
tabs.forEach(t => {
if (t.type !== tabType || !t.title) return;
if (t.title === root) {
rootUsed = true;
return;
}
const m = t.title.match(new RegExp(`^${root.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")} \\((\\d+)\\)$`));
if (m) {
const n = parseInt(m[1], 10);
if (!isNaN(n)) usedNumbers.add(n);
}
});
if (!rootUsed) return root;
// Start at (2) for the second instance
let n = 2;
while (usedNumbers.has(n)) n += 1;
return `${root} (${n})`;
}
const addTab = (tabData: Omit<Tab, 'id'>): number => {
const id = nextTabId.current++;
const needsUniqueTitle = tabData.type === 'terminal' || tabData.type === 'server' || tabData.type === 'config';
const effectiveTitle = needsUniqueTitle ? computeUniqueTitle(tabData.type, tabData.title) : (tabData.title || '');
const newTab: Tab = {
...tabData,
id,
title: effectiveTitle,
terminalRef: tabData.type === 'terminal' ? React.createRef<any>() : undefined
};
console.log('Adding new tab:', newTab);
setTabs(prev => [...prev, newTab]);
setCurrentTab(id);
setAllSplitScreenTab(prev => prev.filter(tid => tid !== id));
return id;
};
const removeTab = (tabId: number) => {
const tab = tabs.find(t => t.id === tabId);
if (tab && tab.terminalRef?.current && typeof tab.terminalRef.current.disconnect === "function") {
tab.terminalRef.current.disconnect();
}
setTabs(prev => prev.filter(tab => tab.id !== tabId));
setAllSplitScreenTab(prev => prev.filter(id => id !== tabId));
if (currentTab === tabId) {
const remainingTabs = tabs.filter(tab => tab.id !== tabId);
setCurrentTab(remainingTabs.length > 0 ? remainingTabs[0].id : 1);
}
};
const setSplitScreenTab = (tabId: number) => {
setAllSplitScreenTab(prev => {
if (prev.includes(tabId)) {
return prev.filter(id => id !== tabId);
} else if (prev.length < 3) {
return [...prev, tabId];
}
return prev;
});
};
const getTab = (tabId: number) => {
return tabs.find(tab => tab.id === tabId);
};
const value: TabContextType = {
tabs,
currentTab,
allSplitScreenTab,
addTab,
removeTab,
setCurrentTab,
setSplitScreenTab,
getTab,
};
return (
<TabContext.Provider value={value}>
{children}
</TabContext.Provider>
);
}

View File

@@ -3,7 +3,7 @@ import {useSidebar} from "@/components/ui/sidebar";
import {Button} from "@/components/ui/button.tsx";
import {ChevronDown, ChevronUpIcon} from "lucide-react";
import {Tab} from "@/ui/Navigation/Tabs/Tab.tsx";
import {useTabs} from "@/contexts/TabContext";
import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
interface TopNavbarProps {
isTopbarOpen: boolean;