diff --git a/src/backend/utils/system-crypto.ts b/src/backend/utils/system-crypto.ts index 41aa2ff1..d00330a7 100644 --- a/src/backend/utils/system-crypto.ts +++ b/src/backend/utils/system-crypto.ts @@ -111,7 +111,6 @@ class SystemCrypto { } else { } } catch (fileError) { - // OK: .env file not found or unreadable, will generate new database key databaseLogger.debug( ".env file not accessible, will generate new database key", { diff --git a/src/types/index.ts b/src/types/index.ts index c19b57e8..cf94b2f4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -371,6 +371,8 @@ export interface HostManagerProps { isTopbarOpen?: boolean; initialTab?: string; hostConfig?: SSHHost; + rightSidebarOpen?: boolean; + rightSidebarWidth?: number; } export interface SSHManagerHostEditorProps { diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index bd652536..1aba3b30 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -24,9 +24,13 @@ function AppContent() { return saved !== null ? JSON.parse(saved) : true; }); const [isTransitioning, setIsTransitioning] = useState(false); - const [transitionPhase, setTransitionPhase] = useState<'idle' | 'fadeOut' | 'fadeIn'>('idle'); + const [transitionPhase, setTransitionPhase] = useState< + "idle" | "fadeOut" | "fadeIn" + >("idle"); const { currentTab, tabs } = useTabs(); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); + const [rightSidebarOpen, setRightSidebarOpen] = useState(false); + const [rightSidebarWidth, setRightSidebarWidth] = useState(400); const lastShiftPressTime = useRef(0); @@ -101,17 +105,17 @@ function AppContent() { userId: string | null; }) => { setIsTransitioning(true); - setTransitionPhase('fadeOut'); + setTransitionPhase("fadeOut"); setTimeout(() => { setIsAuthenticated(true); setIsAdmin(authData.isAdmin); setUsername(authData.username); - setTransitionPhase('fadeIn'); + setTransitionPhase("fadeIn"); setTimeout(() => { setIsTransitioning(false); - setTransitionPhase('idle'); + setTransitionPhase("idle"); }, 800); }, 1200); }, @@ -120,7 +124,7 @@ function AppContent() { const handleLogout = useCallback(async () => { setIsTransitioning(true); - setTransitionPhase('fadeOut'); + setTransitionPhase("fadeOut"); setTimeout(async () => { try { @@ -168,17 +172,21 @@ function AppContent() { {isAuthenticated && ( + onSelectView={handleSelectView} + disabled={!isAuthenticated || authLoading} + isAdmin={isAdmin} + username={username} + onLogout={handleLogout} + >
- +
{showHome && ( @@ -189,6 +197,8 @@ function AppContent() { authLoading={authLoading} onAuthSuccess={handleAuthSuccess} isTopbarOpen={isTopbarOpen} + rightSidebarOpen={rightSidebarOpen} + rightSidebarWidth={rightSidebarWidth} /> )} @@ -200,19 +210,29 @@ function AppContent() { isTopbarOpen={isTopbarOpen} initialTab={currentTabData?.initialTab} hostConfig={currentTabData?.hostConfig} + rightSidebarOpen={rightSidebarOpen} + rightSidebarWidth={rightSidebarWidth} /> )} {showAdmin && (
- +
)} {showProfile && (
- +
)} @@ -220,6 +240,10 @@ function AppContent() { isTopbarOpen={isTopbarOpen} setIsTopbarOpen={setIsTopbarOpen} onOpenCommandPalette={() => setIsCommandPaletteOpen(true)} + onRightSidebarStateChange={(isOpen, width) => { + setRightSidebarOpen(isOpen); + setRightSidebarWidth(width); + }} />
)} @@ -227,51 +251,69 @@ function AppContent() { {isTransitioning && (
- {transitionPhase === 'fadeOut' && ( + {transitionPhase === "fadeOut" && ( <>
-
-
-
-
-
-
+
+
TERMIX
-
+
SSH TERMINAL MANAGER
diff --git a/src/ui/desktop/admin/AdminSettings.tsx b/src/ui/desktop/admin/AdminSettings.tsx index 02c45dfd..a25cabc5 100644 --- a/src/ui/desktop/admin/AdminSettings.tsx +++ b/src/ui/desktop/admin/AdminSettings.tsx @@ -59,10 +59,14 @@ import { interface AdminSettingsProps { isTopbarOpen?: boolean; + rightSidebarOpen?: boolean; + rightSidebarWidth?: number; } export function AdminSettings({ isTopbarOpen = true, + rightSidebarOpen = false, + rightSidebarWidth = 400, }: AdminSettingsProps): React.ReactElement { const { t } = useTranslation(); const { confirmWithToast } = useConfirmation(); @@ -637,7 +641,7 @@ export function AdminSettings({ const bottomMarginPx = 8; const wrapperStyle: React.CSSProperties = { marginLeft: leftMarginPx, - marginRight: 17, + marginRight: rightSidebarOpen ? rightSidebarWidth + 17 : 17, marginTop: topMarginPx, marginBottom: bottomMarginPx, height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`, diff --git a/src/ui/desktop/apps/dashboard/Dashboard.tsx b/src/ui/desktop/apps/dashboard/Dashboard.tsx index 69e2962e..249dd98c 100644 --- a/src/ui/desktop/apps/dashboard/Dashboard.tsx +++ b/src/ui/desktop/apps/dashboard/Dashboard.tsx @@ -50,6 +50,8 @@ interface DashboardProps { userId: string | null; }) => void; isTopbarOpen: boolean; + rightSidebarOpen?: boolean; + rightSidebarWidth?: number; } export function Dashboard({ @@ -58,6 +60,8 @@ export function Dashboard({ onAuthSuccess, isTopbarOpen, onSelectView, + rightSidebarOpen = false, + rightSidebarWidth = 400, }: DashboardProps): React.ReactElement { const { t } = useTranslation(); const [loggedIn, setLoggedIn] = useState(isAuthenticated); @@ -97,6 +101,7 @@ export function Dashboard({ const topMarginPx = isTopbarOpen ? 74 : 26; const leftMarginPx = sidebarState === "collapsed" ? 26 : 8; + const rightMarginPx = rightSidebarOpen ? rightSidebarWidth + 17 : 17; const bottomMarginPx = 8; useEffect(() => { @@ -336,7 +341,7 @@ export function Dashboard({ className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden flex" style={{ marginLeft: leftMarginPx, - marginRight: 17, + marginRight: rightMarginPx, marginTop: topMarginPx, marginBottom: bottomMarginPx, height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`, diff --git a/src/ui/desktop/apps/host-manager/HostManager.tsx b/src/ui/desktop/apps/host-manager/HostManager.tsx index dff7b923..31b4af93 100644 --- a/src/ui/desktop/apps/host-manager/HostManager.tsx +++ b/src/ui/desktop/apps/host-manager/HostManager.tsx @@ -18,6 +18,8 @@ export function HostManager({ isTopbarOpen, initialTab = "host_viewer", hostConfig, + rightSidebarOpen = false, + rightSidebarWidth = 400, }: HostManagerProps): React.ReactElement { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState(initialTab); @@ -90,7 +92,7 @@ export function HostManager({ className="bg-dark-bg text-white p-4 pt-0 rounded-lg border-2 border-dark-border flex flex-col min-h-0 overflow-hidden" style={{ marginLeft: leftMarginPx, - marginRight: 17, + marginRight: rightSidebarOpen ? rightSidebarWidth + 17 : 17, marginTop: topMarginPx, marginBottom: bottomMarginPx, height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`, diff --git a/src/ui/desktop/apps/terminal/SnippetsSidebar.tsx b/src/ui/desktop/apps/terminal/SnippetsSidebar.tsx deleted file mode 100644 index 424ec142..00000000 --- a/src/ui/desktop/apps/terminal/SnippetsSidebar.tsx +++ /dev/null @@ -1,480 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Separator } from "@/components/ui/separator"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { Plus, Play, Edit, Trash2, Copy, X } from "lucide-react"; -import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; -import { useConfirmation } from "@/hooks/use-confirmation.ts"; -import { - getSnippets, - createSnippet, - updateSnippet, - deleteSnippet, -} from "@/ui/main-axios"; -import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; -import type { Snippet, SnippetData } from "../../../../types/index.js"; - -interface TabData { - id: number; - type: string; - title: string; - terminalRef?: { - current?: { - sendInput?: (data: string) => void; - }; - }; - [key: string]: unknown; -} - -interface SnippetsSidebarProps { - isOpen: boolean; - onClose: () => void; - onExecute: (content: string) => void; -} - -export function SnippetsSidebar({ - isOpen, - onClose, - onExecute, -}: SnippetsSidebarProps) { - const { t } = useTranslation(); - const { confirmWithToast } = useConfirmation(); - const { tabs } = useTabs() as { tabs: TabData[] }; - const [snippets, setSnippets] = useState([]); - const [loading, setLoading] = useState(true); - const [showDialog, setShowDialog] = useState(false); - const [editingSnippet, setEditingSnippet] = useState(null); - const [formData, setFormData] = useState({ - name: "", - content: "", - description: "", - }); - const [formErrors, setFormErrors] = useState({ - name: false, - content: false, - }); - const [selectedTabIds, setSelectedTabIds] = useState([]); - - useEffect(() => { - if (isOpen) { - fetchSnippets(); - } - }, [isOpen]); - - const fetchSnippets = async () => { - try { - setLoading(true); - const data = await getSnippets(); - setSnippets(Array.isArray(data) ? data : []); - } catch { - toast.error(t("snippets.failedToFetch")); - setSnippets([]); - } finally { - setLoading(false); - } - }; - - const handleCreate = () => { - setEditingSnippet(null); - setFormData({ name: "", content: "", description: "" }); - setFormErrors({ name: false, content: false }); - setShowDialog(true); - }; - - const handleEdit = (snippet: Snippet) => { - setEditingSnippet(snippet); - setFormData({ - name: snippet.name, - content: snippet.content, - description: snippet.description || "", - }); - setFormErrors({ name: false, content: false }); - setShowDialog(true); - }; - - const handleDelete = (snippet: Snippet) => { - confirmWithToast( - t("snippets.deleteConfirmDescription", { name: snippet.name }), - async () => { - try { - await deleteSnippet(snippet.id); - toast.success(t("snippets.deleteSuccess")); - fetchSnippets(); - } catch { - toast.error(t("snippets.deleteFailed")); - } - }, - "destructive", - ); - }; - - const handleSubmit = async () => { - const errors = { - name: !formData.name.trim(), - content: !formData.content.trim(), - }; - - setFormErrors(errors); - - if (errors.name || errors.content) { - return; - } - - try { - if (editingSnippet) { - await updateSnippet(editingSnippet.id, formData); - toast.success(t("snippets.updateSuccess")); - } else { - await createSnippet(formData); - toast.success(t("snippets.createSuccess")); - } - setShowDialog(false); - fetchSnippets(); - } catch { - toast.error( - editingSnippet - ? t("snippets.updateFailed") - : t("snippets.createFailed"), - ); - } - }; - - const handleTabToggle = (tabId: number) => { - setSelectedTabIds((prev) => - prev.includes(tabId) - ? prev.filter((id) => id !== tabId) - : [...prev, tabId], - ); - }; - - const handleExecute = (snippet: Snippet) => { - if (selectedTabIds.length > 0) { - selectedTabIds.forEach((tabId) => { - const tab = tabs.find((t: TabData) => t.id === tabId); - if (tab?.terminalRef?.current?.sendInput) { - tab.terminalRef.current.sendInput(snippet.content + "\n"); - } - }); - toast.success( - t("snippets.executeSuccess", { - name: snippet.name, - count: selectedTabIds.length, - }), - ); - } else { - onExecute(snippet.content); - toast.success(t("snippets.executeSuccess", { name: snippet.name })); - } - }; - - const handleCopy = (snippet: Snippet) => { - navigator.clipboard.writeText(snippet.content); - toast.success(t("snippets.copySuccess", { name: snippet.name })); - }; - - const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal"); - - if (!isOpen) return null; - - return ( - <> -
-
- -
e.stopPropagation()} - > -
-

- {t("snippets.title")} -

- -
- -
-
- {terminalTabs.length > 0 && ( - <> -
- -

- {selectedTabIds.length > 0 - ? t("snippets.executeOnSelected", { - defaultValue: `Execute on ${selectedTabIds.length} selected terminal(s)`, - count: selectedTabIds.length, - }) - : t("snippets.executeOnCurrent", { - defaultValue: - "Execute on current terminal (click to select multiple)", - })} -

-
- {terminalTabs.map((tab) => ( - - ))} -
-
- - - )} - - - - {loading ? ( -
-

{t("common.loading")}

-
- ) : snippets.length === 0 ? ( -
-

{t("snippets.empty")}

-

{t("snippets.emptyHint")}

-
- ) : ( - -
- {snippets.map((snippet) => ( -
-
-

- {snippet.name} -

- {snippet.description && ( -

- {snippet.description} -

- )} -
- -
- - {snippet.content} - -
- -
- - - - - -

{t("snippets.runTooltip")}

-
-
- - - - - - -

{t("snippets.copyTooltip")}

-
-
- - - - - - -

{t("snippets.editTooltip")}

-
-
- - - - - - -

{t("snippets.deleteTooltip")}

-
-
-
-
- ))} -
-
- )} -
-
-
-
- - {showDialog && ( -
setShowDialog(false)} - > -
e.stopPropagation()} - > -
-

- {editingSnippet ? t("snippets.edit") : t("snippets.create")} -

-

- {editingSnippet - ? t("snippets.editDescription") - : t("snippets.createDescription")} -

-
- -
-
- - - setFormData({ ...formData, name: e.target.value }) - } - placeholder={t("snippets.namePlaceholder")} - className={`${formErrors.name ? "border-destructive focus-visible:ring-destructive" : ""}`} - autoFocus - /> - {formErrors.name && ( -

- {t("snippets.nameRequired")} -

- )} -
- -
- - - setFormData({ ...formData, description: e.target.value }) - } - placeholder={t("snippets.descriptionPlaceholder")} - /> -
- -
- -