diff --git a/src/locales/en.json b/src/locales/en.json index 4e7d6466..d3d522c2 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2224,9 +2224,8 @@ "cannotShareWithSelf": "Cannot share host with yourself", "noCustomRolesToAssign": "No custom roles available. System roles are auto-assigned.", "credentialSharingWarning": "Credential Authentication Not Supported for Sharing", - "credentialSharingWarningDescription": "This host uses credential-based authentication. Shared users will not be able to connect because credentials are encrypted per-user and cannot be shared. Please use password or key-based authentication for hosts you intend to share.", - "credentialRequired": "Credential is required when using credential authentication", - "credentialRequiredDescription": "This host uses credential-based authentication. Shared users will not be able to connect because credentials are encrypted per-user and cannot be shared. Please use password or key-based authentication for hosts you intend to share.", + "credentialRequired": "Credential is required when sharing a host", + "credentialRequiredDescription": "This host does not use credential-based authentication. In order to share hosts, due to per-user-encryption, the host must use credential based authentication.", "auditLogs": "Audit Logs", "viewAuditLogs": "View Audit Logs", "action": "Action", diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index 394a6915..dfb9a599 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -2,13 +2,13 @@ import React, { useState, useEffect, useCallback, useRef } from "react"; import { LeftSidebar } from "@/ui/desktop/navigation/LeftSidebar.tsx"; import { Dashboard } from "@/ui/desktop/apps/dashboard/Dashboard.tsx"; import { AppView } from "@/ui/desktop/navigation/AppView.tsx"; -import { HostManager } from "@/ui/desktop/apps/host-manager/HostManager.tsx"; +import { HostManager } from "@/ui/desktop/apps/host-manager/hosts/HostManager.tsx"; import { TabProvider, useTabs, } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { TopNavbar } from "@/ui/desktop/navigation/TopNavbar.tsx"; -import { CommandHistoryProvider } from "@/ui/desktop/apps/terminal/command-history/CommandHistoryContext.tsx"; +import { CommandHistoryProvider } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.tsx"; import { AdminSettings } from "@/ui/desktop/admin/AdminSettings.tsx"; import { UserProfile } from "@/ui/desktop/user/UserProfile.tsx"; import { Toaster } from "@/components/ui/sonner.tsx"; @@ -63,8 +63,10 @@ function AppContent() { const now = Date.now(); if (now - lastAltPressTime.current < 300) { // Use setTheme to properly update React state (not just DOM class) - const currentIsDark = theme === "dark" || - (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); + const currentIsDark = + theme === "dark" || + (theme === "system" && + window.matchMedia("(prefers-color-scheme: dark)").matches); const newTheme = currentIsDark ? "light" : "dark"; setTheme(newTheme); console.log("[DEBUG] Theme toggled:", newTheme); diff --git a/src/ui/desktop/apps/docker/DockerManager.tsx b/src/ui/desktop/apps/features/docker/DockerManager.tsx similarity index 99% rename from src/ui/desktop/apps/docker/DockerManager.tsx rename to src/ui/desktop/apps/features/docker/DockerManager.tsx index 4e8f255d..b236ac91 100644 --- a/src/ui/desktop/apps/docker/DockerManager.tsx +++ b/src/ui/desktop/apps/features/docker/DockerManager.tsx @@ -9,11 +9,7 @@ import { } from "@/components/ui/tabs.tsx"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; -import type { - SSHHost, - DockerContainer, - DockerValidation, -} from "@/types/index.js"; +import type { SSHHost, DockerContainer, DockerValidation } from "@/types"; import { connectDockerSession, disconnectDockerSession, diff --git a/src/ui/desktop/apps/docker/components/ConsoleTerminal.tsx b/src/ui/desktop/apps/features/docker/components/ConsoleTerminal.tsx similarity index 99% rename from src/ui/desktop/apps/docker/components/ConsoleTerminal.tsx rename to src/ui/desktop/apps/features/docker/components/ConsoleTerminal.tsx index 7fa4ce83..6b953860 100644 --- a/src/ui/desktop/apps/docker/components/ConsoleTerminal.tsx +++ b/src/ui/desktop/apps/features/docker/components/ConsoleTerminal.tsx @@ -14,7 +14,7 @@ import { import { Card, CardContent } from "@/components/ui/card.tsx"; import { Terminal as TerminalIcon, Power, PowerOff } from "lucide-react"; import { toast } from "sonner"; -import type { SSHHost } from "@/types/index.js"; +import type { SSHHost } from "@/types"; import { getCookie, isElectron } from "@/ui/main-axios.ts"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; import { useTranslation } from "react-i18next"; diff --git a/src/ui/desktop/apps/docker/components/ContainerCard.tsx b/src/ui/desktop/apps/features/docker/components/ContainerCard.tsx similarity index 99% rename from src/ui/desktop/apps/docker/components/ContainerCard.tsx rename to src/ui/desktop/apps/features/docker/components/ContainerCard.tsx index bdb920ea..c45ab169 100644 --- a/src/ui/desktop/apps/docker/components/ContainerCard.tsx +++ b/src/ui/desktop/apps/features/docker/components/ContainerCard.tsx @@ -17,7 +17,7 @@ import { } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; -import type { DockerContainer } from "@/types/index.js"; +import type { DockerContainer } from "@/types"; import { startDockerContainer, stopDockerContainer, diff --git a/src/ui/desktop/apps/docker/components/ContainerDetail.tsx b/src/ui/desktop/apps/features/docker/components/ContainerDetail.tsx similarity index 98% rename from src/ui/desktop/apps/docker/components/ContainerDetail.tsx rename to src/ui/desktop/apps/features/docker/components/ContainerDetail.tsx index ffeac92c..45061e37 100644 --- a/src/ui/desktop/apps/docker/components/ContainerDetail.tsx +++ b/src/ui/desktop/apps/features/docker/components/ContainerDetail.tsx @@ -9,7 +9,7 @@ import { } from "@/components/ui/tabs.tsx"; import { ArrowLeft } from "lucide-react"; import { useTranslation } from "react-i18next"; -import type { DockerContainer, SSHHost } from "@/types/index.js"; +import type { DockerContainer, SSHHost } from "@/types"; import { LogViewer } from "./LogViewer.tsx"; import { ContainerStats } from "./ContainerStats.tsx"; import { ConsoleTerminal } from "./ConsoleTerminal.tsx"; diff --git a/src/ui/desktop/apps/docker/components/ContainerList.tsx b/src/ui/desktop/apps/features/docker/components/ContainerList.tsx similarity index 98% rename from src/ui/desktop/apps/docker/components/ContainerList.tsx rename to src/ui/desktop/apps/features/docker/components/ContainerList.tsx index f956673f..de718815 100644 --- a/src/ui/desktop/apps/docker/components/ContainerList.tsx +++ b/src/ui/desktop/apps/features/docker/components/ContainerList.tsx @@ -9,7 +9,7 @@ import { } from "@/components/ui/select.tsx"; import { Search, Filter } from "lucide-react"; import { useTranslation } from "react-i18next"; -import type { DockerContainer } from "@/types/index.js"; +import type { DockerContainer } from "@/types"; import { ContainerCard } from "./ContainerCard.tsx"; interface ContainerListProps { diff --git a/src/ui/desktop/apps/docker/components/ContainerStats.tsx b/src/ui/desktop/apps/features/docker/components/ContainerStats.tsx similarity index 99% rename from src/ui/desktop/apps/docker/components/ContainerStats.tsx rename to src/ui/desktop/apps/features/docker/components/ContainerStats.tsx index f7fff216..8ab671b7 100644 --- a/src/ui/desktop/apps/docker/components/ContainerStats.tsx +++ b/src/ui/desktop/apps/features/docker/components/ContainerStats.tsx @@ -7,7 +7,7 @@ import { } from "@/components/ui/card.tsx"; import { Progress } from "@/components/ui/progress.tsx"; import { Cpu, MemoryStick, Network, HardDrive, Activity } from "lucide-react"; -import type { DockerStats } from "@/types/index.js"; +import type { DockerStats } from "@/types"; import { getContainerStats } from "@/ui/main-axios.ts"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; import { useTranslation } from "react-i18next"; diff --git a/src/ui/desktop/apps/docker/components/LogViewer.tsx b/src/ui/desktop/apps/features/docker/components/LogViewer.tsx similarity index 99% rename from src/ui/desktop/apps/docker/components/LogViewer.tsx rename to src/ui/desktop/apps/features/docker/components/LogViewer.tsx index 0e6b87f4..e00d2387 100644 --- a/src/ui/desktop/apps/docker/components/LogViewer.tsx +++ b/src/ui/desktop/apps/features/docker/components/LogViewer.tsx @@ -12,7 +12,7 @@ import { Switch } from "@/components/ui/switch.tsx"; import { Label } from "@/components/ui/label.tsx"; import { Download, RefreshCw, Filter } from "lucide-react"; import { toast } from "sonner"; -import type { DockerLogOptions } from "@/types/index.js"; +import type { DockerLogOptions } from "@/types"; import { getContainerLogs, downloadContainerLogs } from "@/ui/main-axios.ts"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; diff --git a/src/ui/desktop/apps/file-manager/DragIndicator.tsx b/src/ui/desktop/apps/features/file-manager/DragIndicator.tsx similarity index 100% rename from src/ui/desktop/apps/file-manager/DragIndicator.tsx rename to src/ui/desktop/apps/features/file-manager/DragIndicator.tsx diff --git a/src/ui/desktop/apps/file-manager/FileManager.tsx b/src/ui/desktop/apps/features/file-manager/FileManager.tsx similarity index 97% rename from src/ui/desktop/apps/file-manager/FileManager.tsx rename to src/ui/desktop/apps/features/file-manager/FileManager.tsx index 44d7be18..1a25c73e 100644 --- a/src/ui/desktop/apps/file-manager/FileManager.tsx +++ b/src/ui/desktop/apps/features/file-manager/FileManager.tsx @@ -1,23 +1,26 @@ import React, { useState, useEffect, useRef, useCallback } from "react"; -import { FileManagerGrid } from "./FileManagerGrid"; -import { FileManagerSidebar } from "./FileManagerSidebar"; -import { FileManagerContextMenu } from "./FileManagerContextMenu"; -import { useFileSelection } from "./hooks/useFileSelection"; -import { useDragAndDrop } from "./hooks/useDragAndDrop"; -import { WindowManager, useWindowManager } from "./components/WindowManager"; -import { FileWindow } from "./components/FileWindow"; -import { DiffWindow } from "./components/DiffWindow"; -import { useDragToDesktop } from "../../../hooks/useDragToDesktop"; -import { useDragToSystemDesktop } from "../../../hooks/useDragToSystemDesktop"; +import { FileManagerGrid } from "./FileManagerGrid.tsx"; +import { FileManagerSidebar } from "./FileManagerSidebar.tsx"; +import { FileManagerContextMenu } from "./FileManagerContextMenu.tsx"; +import { useFileSelection } from "./hooks/useFileSelection.ts"; +import { useDragAndDrop } from "./hooks/useDragAndDrop.ts"; +import { + WindowManager, + useWindowManager, +} from "./components/WindowManager.tsx"; +import { FileWindow } from "./components/FileWindow.tsx"; +import { DiffWindow } from "./components/DiffWindow.tsx"; +import { useDragToDesktop } from "../../../../hooks/useDragToDesktop.ts"; +import { useDragToSystemDesktop } from "../../../../hooks/useDragToSystemDesktop.ts"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button.tsx"; +import { Input } from "@/components/ui/input.tsx"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { TOTPDialog } from "@/ui/desktop/navigation/TOTPDialog.tsx"; import { SSHAuthDialog } from "@/ui/desktop/navigation/SSHAuthDialog.tsx"; -import { PermissionsDialog } from "./components/PermissionsDialog"; -import { CompressDialog } from "./components/CompressDialog"; +import { PermissionsDialog } from "./components/PermissionsDialog.tsx"; +import { CompressDialog } from "./components/CompressDialog.tsx"; import { Upload, FolderPlus, @@ -27,7 +30,7 @@ import { Grid3X3, List, } from "lucide-react"; -import { TerminalWindow } from "./components/TerminalWindow"; +import { TerminalWindow } from "./components/TerminalWindow.tsx"; import type { SSHHost, FileItem } from "../../../types/index.js"; import { listSSHFiles, @@ -55,7 +58,7 @@ import { extractSSHArchive, compressSSHFiles, } from "@/ui/main-axios.ts"; -import type { SidebarItem } from "./FileManagerSidebar"; +import type { SidebarItem } from "./FileManagerSidebar.tsx"; interface FileManagerProps { initialHost?: SSHHost | null; @@ -586,7 +589,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { error.message?.includes("established") ) { toast.error( - t("fileManager.sshConnectionFailed", { name: currentHost?.name, ip: currentHost?.ip, port: currentHost?.port }), + t("fileManager.sshConnectionFailed", { + name: currentHost?.name, + ip: currentHost?.ip, + port: currentHost?.port, + }), ); } else { toast.error(t("fileManager.failedToUploadFile")); @@ -633,7 +640,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { error.message?.includes("established") ) { toast.error( - t("fileManager.sshConnectionFailed", { name: currentHost?.name, ip: currentHost?.ip, port: currentHost?.port }), + t("fileManager.sshConnectionFailed", { + name: currentHost?.name, + ip: currentHost?.ip, + port: currentHost?.port, + }), ); } else { toast.error(t("fileManager.failedToDownloadFile")); @@ -1943,7 +1954,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
-

{currentHost.name}

+

+ {currentHost.name} +

{currentHost.ip}:{currentHost.port} diff --git a/src/ui/desktop/apps/file-manager/FileManagerContextMenu.tsx b/src/ui/desktop/apps/features/file-manager/FileManagerContextMenu.tsx similarity index 99% rename from src/ui/desktop/apps/file-manager/FileManagerContextMenu.tsx rename to src/ui/desktop/apps/features/file-manager/FileManagerContextMenu.tsx index baeeb34f..f1ea609c 100644 --- a/src/ui/desktop/apps/file-manager/FileManagerContextMenu.tsx +++ b/src/ui/desktop/apps/features/file-manager/FileManagerContextMenu.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils.ts"; import { Download, Edit3, @@ -20,7 +20,7 @@ import { FileArchive, } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { Kbd, KbdGroup } from "@/components/ui/kbd"; +import { Kbd, KbdGroup } from "@/components/ui/kbd.tsx"; interface FileItem { name: string; diff --git a/src/ui/desktop/apps/file-manager/FileManagerGrid.tsx b/src/ui/desktop/apps/features/file-manager/FileManagerGrid.tsx similarity index 99% rename from src/ui/desktop/apps/file-manager/FileManagerGrid.tsx rename to src/ui/desktop/apps/features/file-manager/FileManagerGrid.tsx index 3f3c7e16..11768f29 100644 --- a/src/ui/desktop/apps/file-manager/FileManagerGrid.tsx +++ b/src/ui/desktop/apps/features/file-manager/FileManagerGrid.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useCallback, useEffect } from "react"; import { createPortal } from "react-dom"; -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils.ts"; import { Folder, File, diff --git a/src/ui/desktop/apps/file-manager/FileManagerSidebar.tsx b/src/ui/desktop/apps/features/file-manager/FileManagerSidebar.tsx similarity index 98% rename from src/ui/desktop/apps/file-manager/FileManagerSidebar.tsx rename to src/ui/desktop/apps/features/file-manager/FileManagerSidebar.tsx index 77a75e37..9f3f0bca 100644 --- a/src/ui/desktop/apps/file-manager/FileManagerSidebar.tsx +++ b/src/ui/desktop/apps/features/file-manager/FileManagerSidebar.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils.ts"; import { ChevronRight, ChevronDown, @@ -11,7 +11,7 @@ import { FolderOpen, } from "lucide-react"; import { useTranslation } from "react-i18next"; -import type { SSHHost } from "@/types/index"; +import type { SSHHost } from "@/types"; import { getRecentFiles, getPinnedFiles, @@ -474,9 +474,7 @@ export function FileManagerSidebar({ )}
diff --git a/src/ui/desktop/apps/file-manager/components/CompressDialog.tsx b/src/ui/desktop/apps/features/file-manager/components/CompressDialog.tsx similarity index 95% rename from src/ui/desktop/apps/file-manager/components/CompressDialog.tsx rename to src/ui/desktop/apps/features/file-manager/components/CompressDialog.tsx index ce38ca32..8f3bd88c 100644 --- a/src/ui/desktop/apps/file-manager/components/CompressDialog.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/CompressDialog.tsx @@ -6,17 +6,17 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +} from "@/components/ui/dialog.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { Label } from "@/components/ui/label.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; +} from "@/components/ui/select.tsx"; import { useTranslation } from "react-i18next"; interface CompressDialogProps { diff --git a/src/ui/desktop/apps/file-manager/components/DiffViewer.tsx b/src/ui/desktop/apps/features/file-manager/components/DiffViewer.tsx similarity index 98% rename from src/ui/desktop/apps/file-manager/components/DiffViewer.tsx rename to src/ui/desktop/apps/features/file-manager/components/DiffViewer.tsx index ca74c3f5..8d7126c9 100644 --- a/src/ui/desktop/apps/file-manager/components/DiffViewer.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/DiffViewer.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import { DiffEditor } from "@monaco-editor/react"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/components/ui/button.tsx"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { @@ -16,8 +16,8 @@ import { downloadSSHFile, getSSHStatus, connectSSH, -} from "@/ui/main-axios"; -import type { FileItem, SSHHost } from "@/types/index"; +} from "@/ui/main-axios.ts"; +import type { FileItem, SSHHost } from "@/types"; interface DiffViewerProps { file1: FileItem; diff --git a/src/ui/desktop/apps/file-manager/components/DiffWindow.tsx b/src/ui/desktop/apps/features/file-manager/components/DiffWindow.tsx similarity index 90% rename from src/ui/desktop/apps/file-manager/components/DiffWindow.tsx rename to src/ui/desktop/apps/features/file-manager/components/DiffWindow.tsx index 78bd5ac7..f1931e7a 100644 --- a/src/ui/desktop/apps/file-manager/components/DiffWindow.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/DiffWindow.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { DraggableWindow } from "./DraggableWindow"; -import { DiffViewer } from "./DiffViewer"; -import { useWindowManager } from "./WindowManager"; +import { DraggableWindow } from "./DraggableWindow.tsx"; +import { DiffViewer } from "./DiffViewer.tsx"; +import { useWindowManager } from "./WindowManager.tsx"; import { useTranslation } from "react-i18next"; import type { FileItem, SSHHost } from "../../../../types/index.js"; diff --git a/src/ui/desktop/apps/file-manager/components/DraggableWindow.tsx b/src/ui/desktop/apps/features/file-manager/components/DraggableWindow.tsx similarity index 99% rename from src/ui/desktop/apps/file-manager/components/DraggableWindow.tsx rename to src/ui/desktop/apps/features/file-manager/components/DraggableWindow.tsx index b24be004..f3b5e054 100644 --- a/src/ui/desktop/apps/file-manager/components/DraggableWindow.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/DraggableWindow.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useCallback, useEffect } from "react"; -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils.ts"; import { Minus, X, Maximize2, Minimize2 } from "lucide-react"; import { useTranslation } from "react-i18next"; diff --git a/src/ui/desktop/apps/file-manager/components/FileViewer.tsx b/src/ui/desktop/apps/features/file-manager/components/FileViewer.tsx similarity index 99% rename from src/ui/desktop/apps/file-manager/components/FileViewer.tsx rename to src/ui/desktop/apps/features/file-manager/components/FileViewer.tsx index 00186a04..8de9c877 100644 --- a/src/ui/desktop/apps/file-manager/components/FileViewer.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/FileViewer.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils.ts"; import { useTranslation } from "react-i18next"; import { FileText, @@ -45,7 +45,7 @@ import { SiMysql, SiDocker, } from "react-icons/si"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/components/ui/button.tsx"; import CodeMirror from "@uiw/react-codemirror"; import { oneDark } from "@codemirror/theme-one-dark"; import { loadLanguage } from "@uiw/codemirror-extensions-langs"; @@ -813,7 +813,8 @@ export function FileViewer({ ".cm-scroller": { overflow: "auto", scrollbarWidth: "thin", - scrollbarColor: "var(--scrollbar-thumb) var(--scrollbar-track)", + scrollbarColor: + "var(--scrollbar-thumb) var(--scrollbar-track)", }, ".cm-editor": { height: "100%", diff --git a/src/ui/desktop/apps/file-manager/components/FileWindow.tsx b/src/ui/desktop/apps/features/file-manager/components/FileWindow.tsx similarity index 98% rename from src/ui/desktop/apps/file-manager/components/FileWindow.tsx rename to src/ui/desktop/apps/features/file-manager/components/FileWindow.tsx index 1075775d..5768a949 100644 --- a/src/ui/desktop/apps/file-manager/components/FileWindow.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/FileWindow.tsx @@ -1,14 +1,14 @@ import React, { useState, useEffect, useRef } from "react"; -import { DraggableWindow } from "./DraggableWindow"; -import { FileViewer } from "./FileViewer"; -import { useWindowManager } from "./WindowManager"; +import { DraggableWindow } from "./DraggableWindow.tsx"; +import { FileViewer } from "./FileViewer.tsx"; +import { useWindowManager } from "./WindowManager.tsx"; import { downloadSSHFile, readSSHFile, writeSSHFile, getSSHStatus, connectSSH, -} from "@/ui/main-axios"; +} from "@/ui/main-axios.ts"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; diff --git a/src/ui/desktop/apps/file-manager/components/PermissionsDialog.tsx b/src/ui/desktop/apps/features/file-manager/components/PermissionsDialog.tsx similarity index 97% rename from src/ui/desktop/apps/file-manager/components/PermissionsDialog.tsx rename to src/ui/desktop/apps/features/file-manager/components/PermissionsDialog.tsx index 40364cef..eab5965a 100644 --- a/src/ui/desktop/apps/file-manager/components/PermissionsDialog.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/PermissionsDialog.tsx @@ -6,11 +6,11 @@ import { DialogHeader, DialogTitle, DialogFooter, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Input } from "@/components/ui/input"; +} from "@/components/ui/dialog.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Label } from "@/components/ui/label.tsx"; +import { Checkbox } from "@/components/ui/checkbox.tsx"; +import { Input } from "@/components/ui/input.tsx"; import { useTranslation } from "react-i18next"; import { Shield } from "lucide-react"; diff --git a/src/ui/desktop/apps/file-manager/components/TerminalWindow.tsx b/src/ui/desktop/apps/features/file-manager/components/TerminalWindow.tsx similarity index 93% rename from src/ui/desktop/apps/file-manager/components/TerminalWindow.tsx rename to src/ui/desktop/apps/features/file-manager/components/TerminalWindow.tsx index 5dc47dfa..d3e9b409 100644 --- a/src/ui/desktop/apps/file-manager/components/TerminalWindow.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/TerminalWindow.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { DraggableWindow } from "./DraggableWindow"; -import { Terminal } from "@/ui/desktop/apps/terminal/Terminal"; -import { useWindowManager } from "./WindowManager"; +import { DraggableWindow } from "./DraggableWindow.tsx"; +import { Terminal } from "@/ui/desktop/apps/features/terminal/Terminal.tsx"; +import { useWindowManager } from "./WindowManager.tsx"; import { useTranslation } from "react-i18next"; interface SSHHost { diff --git a/src/ui/desktop/apps/file-manager/components/WindowManager.tsx b/src/ui/desktop/apps/features/file-manager/components/WindowManager.tsx similarity index 100% rename from src/ui/desktop/apps/file-manager/components/WindowManager.tsx rename to src/ui/desktop/apps/features/file-manager/components/WindowManager.tsx diff --git a/src/ui/desktop/apps/file-manager/hooks/useDragAndDrop.ts b/src/ui/desktop/apps/features/file-manager/hooks/useDragAndDrop.ts similarity index 100% rename from src/ui/desktop/apps/file-manager/hooks/useDragAndDrop.ts rename to src/ui/desktop/apps/features/file-manager/hooks/useDragAndDrop.ts diff --git a/src/ui/desktop/apps/file-manager/hooks/useFileSelection.ts b/src/ui/desktop/apps/features/file-manager/hooks/useFileSelection.ts similarity index 100% rename from src/ui/desktop/apps/file-manager/hooks/useFileSelection.ts rename to src/ui/desktop/apps/features/file-manager/hooks/useFileSelection.ts diff --git a/src/ui/desktop/apps/server-stats/ServerStats.tsx b/src/ui/desktop/apps/features/server-stats/ServerStats.tsx similarity index 99% rename from src/ui/desktop/apps/server-stats/ServerStats.tsx rename to src/ui/desktop/apps/features/server-stats/ServerStats.tsx index ab0bc813..5fdc286b 100644 --- a/src/ui/desktop/apps/server-stats/ServerStats.tsx +++ b/src/ui/desktop/apps/features/server-stats/ServerStats.tsx @@ -16,7 +16,7 @@ import { type WidgetType, type StatsConfig, DEFAULT_STATS_CONFIG, -} from "@/types/stats-widgets"; +} from "@/types/stats-widgets.ts"; import { CpuWidget, MemoryWidget, diff --git a/src/ui/desktop/apps/server-stats/widgets/CpuWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/CpuWidget.tsx similarity index 100% rename from src/ui/desktop/apps/server-stats/widgets/CpuWidget.tsx rename to src/ui/desktop/apps/features/server-stats/widgets/CpuWidget.tsx diff --git a/src/ui/desktop/apps/server-stats/widgets/DiskWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/DiskWidget.tsx similarity index 100% rename from src/ui/desktop/apps/server-stats/widgets/DiskWidget.tsx rename to src/ui/desktop/apps/features/server-stats/widgets/DiskWidget.tsx diff --git a/src/ui/desktop/apps/server-stats/widgets/LoginStatsWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/LoginStatsWidget.tsx similarity index 100% rename from src/ui/desktop/apps/server-stats/widgets/LoginStatsWidget.tsx rename to src/ui/desktop/apps/features/server-stats/widgets/LoginStatsWidget.tsx diff --git a/src/ui/desktop/apps/server-stats/widgets/MemoryWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/MemoryWidget.tsx similarity index 100% rename from src/ui/desktop/apps/server-stats/widgets/MemoryWidget.tsx rename to src/ui/desktop/apps/features/server-stats/widgets/MemoryWidget.tsx diff --git a/src/ui/desktop/apps/server-stats/widgets/NetworkWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/NetworkWidget.tsx similarity index 100% rename from src/ui/desktop/apps/server-stats/widgets/NetworkWidget.tsx rename to src/ui/desktop/apps/features/server-stats/widgets/NetworkWidget.tsx diff --git a/src/ui/desktop/apps/server-stats/widgets/ProcessesWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/ProcessesWidget.tsx similarity index 100% rename from src/ui/desktop/apps/server-stats/widgets/ProcessesWidget.tsx rename to src/ui/desktop/apps/features/server-stats/widgets/ProcessesWidget.tsx diff --git a/src/ui/desktop/apps/server-stats/widgets/SystemWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/SystemWidget.tsx similarity index 100% rename from src/ui/desktop/apps/server-stats/widgets/SystemWidget.tsx rename to src/ui/desktop/apps/features/server-stats/widgets/SystemWidget.tsx diff --git a/src/ui/desktop/apps/server-stats/widgets/UptimeWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/UptimeWidget.tsx similarity index 100% rename from src/ui/desktop/apps/server-stats/widgets/UptimeWidget.tsx rename to src/ui/desktop/apps/features/server-stats/widgets/UptimeWidget.tsx diff --git a/src/ui/desktop/apps/features/server-stats/widgets/index.ts b/src/ui/desktop/apps/features/server-stats/widgets/index.ts new file mode 100644 index 00000000..5f47b6dc --- /dev/null +++ b/src/ui/desktop/apps/features/server-stats/widgets/index.ts @@ -0,0 +1,8 @@ +export { CpuWidget } from "./CpuWidget.tsx"; +export { MemoryWidget } from "./MemoryWidget.tsx"; +export { DiskWidget } from "./DiskWidget.tsx"; +export { NetworkWidget } from "./NetworkWidget.tsx"; +export { UptimeWidget } from "./UptimeWidget.tsx"; +export { ProcessesWidget } from "./ProcessesWidget.tsx"; +export { SystemWidget } from "./SystemWidget.tsx"; +export { LoginStatsWidget } from "./LoginStatsWidget.tsx"; diff --git a/src/ui/desktop/apps/terminal/Terminal.tsx b/src/ui/desktop/apps/features/terminal/Terminal.tsx similarity index 98% rename from src/ui/desktop/apps/terminal/Terminal.tsx rename to src/ui/desktop/apps/features/terminal/Terminal.tsx index 72531729..c553e935 100644 --- a/src/ui/desktop/apps/terminal/Terminal.tsx +++ b/src/ui/desktop/apps/features/terminal/Terminal.tsx @@ -25,13 +25,13 @@ import { TERMINAL_THEMES, DEFAULT_TERMINAL_CONFIG, TERMINAL_FONTS, -} from "@/constants/terminal-themes"; +} from "@/constants/terminal-themes.ts"; import type { TerminalConfig } from "@/types"; -import { useTheme } from "@/components/theme-provider"; -import { useCommandTracker } from "@/ui/hooks/useCommandTracker"; +import { useTheme } from "@/components/theme-provider.tsx"; +import { useCommandTracker } from "@/ui/hooks/useCommandTracker.ts"; import { highlightTerminalOutput } from "@/lib/terminal-syntax-highlighter.ts"; -import { useCommandHistory as useCommandHistoryHook } from "@/ui/hooks/useCommandHistory"; -import { useCommandHistory } from "@/ui/desktop/apps/terminal/command-history/CommandHistoryContext.tsx"; +import { useCommandHistory as useCommandHistoryHook } from "@/ui/hooks/useCommandHistory.ts"; +import { useCommandHistory } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.tsx"; import { CommandAutocomplete } from "./command-history/CommandAutocomplete.tsx"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; @@ -101,8 +101,10 @@ export const Terminal = forwardRef( const config = { ...DEFAULT_TERMINAL_CONFIG, ...hostConfig.terminalConfig }; // Auto-switch terminal theme based on app theme when using "termix" (default) - const isDarkMode = appTheme === "dark" || - (appTheme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); + const isDarkMode = + appTheme === "dark" || + (appTheme === "system" && + window.matchMedia("(prefers-color-scheme: dark)").matches); let themeColors; if (config.theme === "termix") { @@ -111,7 +113,9 @@ export const Terminal = forwardRef( ? TERMINAL_THEMES.termixDark.colors : TERMINAL_THEMES.termixLight.colors; } else { - themeColors = TERMINAL_THEMES[config.theme]?.colors || TERMINAL_THEMES.termixDark.colors; + themeColors = + TERMINAL_THEMES[config.theme]?.colors || + TERMINAL_THEMES.termixDark.colors; } const backgroundColor = themeColors.background; const fitAddonRef = useRef(null); @@ -698,7 +702,9 @@ export const Terminal = forwardRef( !sudoPromptShownRef.current ) { sudoPromptShownRef.current = true; - confirmWithToast(t("terminal.sudoPasswordPopupTitle"), async () => { + confirmWithToast( + t("terminal.sudoPasswordPopupTitle"), + async () => { if ( webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN @@ -1067,7 +1073,9 @@ export const Terminal = forwardRef( ? TERMINAL_THEMES.termixDark.colors : TERMINAL_THEMES.termixLight.colors; } else { - themeColors = TERMINAL_THEMES[config.theme]?.colors || TERMINAL_THEMES.termixDark.colors; + themeColors = + TERMINAL_THEMES[config.theme]?.colors || + TERMINAL_THEMES.termixDark.colors; } const fontConfig = TERMINAL_FONTS.find( diff --git a/src/ui/desktop/apps/terminal/TerminalPreview.tsx b/src/ui/desktop/apps/features/terminal/TerminalPreview.tsx similarity index 77% rename from src/ui/desktop/apps/terminal/TerminalPreview.tsx rename to src/ui/desktop/apps/features/terminal/TerminalPreview.tsx index bdf2d300..3978af94 100644 --- a/src/ui/desktop/apps/terminal/TerminalPreview.tsx +++ b/src/ui/desktop/apps/features/terminal/TerminalPreview.tsx @@ -1,6 +1,9 @@ -import type { TerminalTheme } from "@/constants/terminal-themes"; -import { TERMINAL_THEMES, TERMINAL_FONTS } from "@/constants/terminal-themes"; -import { useTheme } from "@/components/theme-provider"; +import type { TerminalTheme } from "@/constants/terminal-themes.ts"; +import { + TERMINAL_THEMES, + TERMINAL_FONTS, +} from "@/constants/terminal-themes.ts"; +import { useTheme } from "@/components/theme-provider.tsx"; interface TerminalPreviewProps { theme: string; @@ -24,11 +27,14 @@ export function TerminalPreview({ const { theme: appTheme } = useTheme(); // Resolve "termix" to termixDark or termixLight based on app theme - const resolvedTheme = theme === "termix" - ? (appTheme === "dark" || (appTheme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches) - ? "termixDark" - : "termixLight") - : theme; + const resolvedTheme = + theme === "termix" + ? appTheme === "dark" || + (appTheme === "system" && + window.matchMedia("(prefers-color-scheme: dark)").matches) + ? "termixDark" + : "termixLight" + : theme; return (
@@ -41,8 +47,12 @@ export function TerminalPreview({ TERMINAL_FONTS[0].fallback, letterSpacing: `${letterSpacing}px`, lineHeight, - background: TERMINAL_THEMES[resolvedTheme]?.colors.background || "var(--bg-base)", - color: TERMINAL_THEMES[resolvedTheme]?.colors.foreground || "var(--foreground)", + background: + TERMINAL_THEMES[resolvedTheme]?.colors.background || + "var(--bg-base)", + color: + TERMINAL_THEMES[resolvedTheme]?.colors.foreground || + "var(--foreground)", }} >
@@ -50,7 +60,9 @@ export function TerminalPreview({ user@termix : - ~ + + ~ + $ ls -la
@@ -81,7 +93,9 @@ export function TerminalPreview({ user@termix : - ~ + + ~ + $ ("user"); - const [selectedUserId, setSelectedUserId] = React.useState(""); - const [selectedRoleId, setSelectedRoleId] = React.useState( - null, - ); - const [permissionLevel, setPermissionLevel] = React.useState("view"); - const [expiresInHours, setExpiresInHours] = React.useState(""); - - const [roles, setRoles] = React.useState([]); - const [users, setUsers] = React.useState([]); - const [accessList, setAccessList] = React.useState([]); - const [loading, setLoading] = React.useState(false); - const [currentUserId, setCurrentUserId] = React.useState(""); - const [hostData, setHostData] = React.useState(null); - - const [userComboOpen, setUserComboOpen] = React.useState(false); - const [roleComboOpen, setRoleComboOpen] = React.useState(false); - - // Load roles - const loadRoles = React.useCallback(async () => { - try { - const response = await getRoles(); - setRoles(response.roles || []); - } catch (error) { - console.error("Failed to load roles:", error); - setRoles([]); - } - }, []); - - // Load users - const loadUsers = React.useCallback(async () => { - try { - const response = await getUserList(); - // Map UserInfo to User format - const mappedUsers = (response.users || []).map((user) => ({ - id: user.id, - username: user.username, - is_admin: user.is_admin, - })); - setUsers(mappedUsers); - } catch (error) { - console.error("Failed to load users:", error); - setUsers([]); - } - }, []); - - // Load access list - const loadAccessList = React.useCallback(async () => { - if (!hostId) return; - - setLoading(true); - try { - const response = await getHostAccess(hostId); - setAccessList(response.accessList || []); - } catch (error) { - console.error("Failed to load access list:", error); - setAccessList([]); - } finally { - setLoading(false); - } - }, [hostId]); - - // Load host data - const loadHostData = React.useCallback(async () => { - if (!hostId) return; - - try { - const host = await getSSHHostById(hostId); - setHostData(host); - } catch (error) { - console.error("Failed to load host data:", error); - setHostData(null); - } - }, [hostId]); - - React.useEffect(() => { - loadRoles(); - loadUsers(); - if (!isNewHost) { - loadAccessList(); - loadHostData(); - } - }, [loadRoles, loadUsers, loadAccessList, loadHostData, isNewHost]); - - // Load current user ID - React.useEffect(() => { - const fetchCurrentUser = async () => { - try { - const userInfo = await getUserInfo(); - setCurrentUserId(userInfo.userId); - } catch (error) { - console.error("Failed to load current user:", error); - } - }; - fetchCurrentUser(); - }, []); - - // Share host - const handleShare = async () => { - if (!hostId) { - toast.error(t("rbac.saveHostFirst")); - return; - } - - if (shareType === "user" && !selectedUserId) { - toast.error(t("rbac.selectUser")); - return; - } - - if (shareType === "role" && !selectedRoleId) { - toast.error(t("rbac.selectRole")); - return; - } - - // Prevent sharing with self - if (shareType === "user" && selectedUserId === currentUserId) { - toast.error(t("rbac.cannotShareWithSelf")); - return; - } - - try { - await shareHost(hostId, { - targetType: shareType, - targetUserId: shareType === "user" ? selectedUserId : undefined, - targetRoleId: shareType === "role" ? selectedRoleId : undefined, - permissionLevel, - durationHours: expiresInHours - ? parseInt(expiresInHours, 10) - : undefined, - }); - - toast.success(t("rbac.sharedSuccessfully")); - setSelectedUserId(""); - setSelectedRoleId(null); - setExpiresInHours(""); - loadAccessList(); - } catch (error) { - toast.error(t("rbac.failedToShare")); - } - }; - - // Revoke access - const handleRevoke = async (accessId: number) => { - if (!hostId) return; - - const confirmed = await confirmWithToast({ - title: t("rbac.confirmRevokeAccess"), - description: t("rbac.confirmRevokeAccessDescription"), - confirmText: t("common.revoke"), - cancelText: t("common.cancel"), - }); - - if (!confirmed) return; - - try { - await revokeHostAccess(hostId, accessId); - toast.success(t("rbac.accessRevokedSuccessfully")); - loadAccessList(); - } catch (error) { - toast.error(t("rbac.failedToRevokeAccess")); - } - }; - - // Format date - const formatDate = (dateString: string | null) => { - if (!dateString) return "-"; - return new Date(dateString).toLocaleString(); - }; - - // Check if expired - const isExpired = (expiresAt: string | null) => { - if (!expiresAt) return false; - return new Date(expiresAt) < new Date(); - }; - - // Filter out current user from the users list - const availableUsers = React.useMemo(() => { - return users.filter((user) => user.id !== currentUserId); - }, [users, currentUserId]); - - const selectedUser = availableUsers.find((u) => u.id === selectedUserId); - const selectedRole = roles.find((r) => r.id === selectedRoleId); - - if (isNewHost) { - return ( - - - {t("rbac.saveHostFirst")} - - {t("rbac.saveHostFirstDescription")} - - - ); - } - - return ( -
- {/* Credential Requirement Warning */} - {!hostData?.credentialId && ( - - - {t("rbac.credentialRequired")} - - {t("rbac.credentialRequiredDescription")} - - - )} - - {/* Share Form */} -
-

- - {t("rbac.shareHost")} -

- - {/* Share Type Selection */} - setShareType(v as "user" | "role")} - > - - - - {t("rbac.shareWithUser")} - - - - {t("rbac.shareWithRole")} - - - - -
- - - - - - - - - {t("rbac.noUserFound")} - - {availableUsers.map((user) => ( - { - setSelectedUserId(user.id); - setUserComboOpen(false); - }} - > - - {user.username} - {user.is_admin ? " (Admin)" : ""} - - ))} - - - - -
-
- - -
- - - - - - - - - {t("rbac.noRoleFound")} - - {roles.map((role) => ( - { - setSelectedRoleId(role.id); - setRoleComboOpen(false); - }} - > - - {t(role.displayName)} - {role.isSystem ? ` (${t("rbac.systemRole")})` : ""} - - ))} - - - - -
-
-
- - {/* Permission Level */} -
- - -
- - {/* Expiration */} -
- - { - const value = e.target.value; - if (value === "" || /^\d+$/.test(value)) { - setExpiresInHours(value); - } - }} - placeholder={t("rbac.neverExpires")} - min="1" - /> -
- - -
- - {/* Access List */} -
-

- - {t("rbac.accessList")} -

- - - - - {t("rbac.type")} - {t("rbac.target")} - {t("rbac.permissionLevel")} - {t("rbac.grantedBy")} - {t("rbac.expires")} - {t("rbac.accessCount")} - - {t("common.actions")} - - - - - {loading ? ( - - - {t("common.loading")} - - - ) : accessList.length === 0 ? ( - - - {t("rbac.noAccessRecords")} - - - ) : ( - accessList.map((access) => ( - - - {access.targetType === "user" ? ( - - - {t("rbac.user")} - - ) : ( - - - {t("rbac.role")} - - )} - - - {access.targetType === "user" - ? access.username - : t(access.roleDisplayName || access.roleName || "")} - - - {access.permissionLevel} - - {access.grantedByUsername} - - {access.expiresAt ? ( -
- - - {formatDate(access.expiresAt)} - {isExpired(access.expiresAt) && ( - ({t("rbac.expired")}) - )} - -
- ) : ( - t("rbac.never") - )} -
- {access.accessCount} - - - -
- )) - )} -
-
-
-
- ); -} diff --git a/src/ui/desktop/apps/host-manager/components/FolderEditDialog.tsx b/src/ui/desktop/apps/host-manager/components/FolderEditDialog.tsx index 5f7e535d..6c559c73 100644 --- a/src/ui/desktop/apps/host-manager/components/FolderEditDialog.tsx +++ b/src/ui/desktop/apps/host-manager/components/FolderEditDialog.tsx @@ -6,9 +6,9 @@ import { DialogHeader, DialogTitle, DialogFooter, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; +} from "@/components/ui/dialog.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Label } from "@/components/ui/label.tsx"; import { useTranslation } from "react-i18next"; import { Folder, diff --git a/src/ui/desktop/apps/credentials/CredentialEditor.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx similarity index 98% rename from src/ui/desktop/apps/credentials/CredentialEditor.tsx rename to src/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx index bb1b093c..bc9ff2fe 100644 --- a/src/ui/desktop/apps/credentials/CredentialEditor.tsx +++ b/src/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/components/ui/button.tsx"; import { Form, FormControl, @@ -10,13 +10,18 @@ import { FormField, FormItem, FormLabel, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { PasswordInput } from "@/components/ui/password-input"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Alert, AlertDescription } from "@/components/ui/alert"; +} from "@/components/ui/form.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { PasswordInput } from "@/components/ui/password-input.tsx"; +import { ScrollArea } from "@/components/ui/scroll-area.tsx"; +import { Separator } from "@/components/ui/separator.tsx"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs.tsx"; +import { Alert, AlertDescription } from "@/components/ui/alert.tsx"; import React, { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { @@ -28,18 +33,18 @@ import { detectPublicKeyType, generatePublicKeyFromPrivate, generateKeyPair, -} from "@/ui/main-axios"; +} from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; import CodeMirror from "@uiw/react-codemirror"; import { oneDark } from "@codemirror/theme-one-dark"; import { githubLight } from "@uiw/codemirror-theme-github"; import { EditorView } from "@codemirror/view"; -import { useTheme } from "@/components/theme-provider"; +import { useTheme } from "@/components/theme-provider.tsx"; import type { Credential, CredentialEditorProps, CredentialData, -} from "../../../../types/index.js"; +} from "../../../../../types"; export function CredentialEditor({ editingCredential, @@ -635,7 +640,7 @@ export function CredentialEditor({ )} setTagInput(e.target.value)} onKeyDown={(e) => { @@ -950,7 +955,7 @@ export function CredentialEditor({ "placeholders.pastePrivateKey", )} theme={editorTheme} - className="border border-input rounded-md" + className="border border-input rounded-md overflow-hidden" minHeight="120px" basicSetup={{ lineNumbers: true, @@ -1113,7 +1118,7 @@ export function CredentialEditor({ }} placeholder={t("placeholders.pastePublicKey")} theme={editorTheme} - className="border border-input rounded-md" + className="border border-input rounded-md overflow-hidden" minHeight="120px" basicSetup={{ lineNumbers: true, diff --git a/src/ui/desktop/apps/credentials/CredentialSelector.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx similarity index 99% rename from src/ui/desktop/apps/credentials/CredentialSelector.tsx rename to src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx index fbd2e724..8be74967 100644 --- a/src/ui/desktop/apps/credentials/CredentialSelector.tsx +++ b/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx @@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input.tsx"; import { FormControl, FormItem, FormLabel } from "@/components/ui/form.tsx"; import { getCredentials } from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; -import type { Credential } from "../../../../types"; +import type { Credential } from "../../../../../types"; interface CredentialSelectorProps { value?: number | null; diff --git a/src/ui/desktop/apps/credentials/CredentialViewer.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialViewer.tsx similarity index 98% rename from src/ui/desktop/apps/credentials/CredentialViewer.tsx rename to src/ui/desktop/apps/host-manager/credentials/CredentialViewer.tsx index 9feebb88..c60b8813 100644 --- a/src/ui/desktop/apps/credentials/CredentialViewer.tsx +++ b/src/ui/desktop/apps/host-manager/credentials/CredentialViewer.tsx @@ -1,22 +1,22 @@ import React, { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/components/ui/button.tsx"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Separator } from "@/components/ui/separator"; -import { ScrollArea } from "@/components/ui/scroll-area"; +} from "@/components/ui/card.tsx"; +import { Badge } from "@/components/ui/badge.tsx"; +import { Separator } from "@/components/ui/separator.tsx"; +import { ScrollArea } from "@/components/ui/scroll-area.tsx"; import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, -} from "@/components/ui/sheet"; +} from "@/components/ui/sheet.tsx"; import { Key, User, @@ -34,7 +34,7 @@ import { CheckCircle, FileText, } from "lucide-react"; -import { getCredentialDetails, getCredentialHosts } from "@/ui/main-axios"; +import { getCredentialDetails, getCredentialHosts } from "@/ui/main-axios.ts"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import type { diff --git a/src/ui/desktop/apps/credentials/CredentialsManager.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx similarity index 98% rename from src/ui/desktop/apps/credentials/CredentialsManager.tsx rename to src/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx index 7e9ad320..9b4457fc 100644 --- a/src/ui/desktop/apps/credentials/CredentialsManager.tsx +++ b/src/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx @@ -1,33 +1,33 @@ import React, { useState, useEffect, useMemo, useRef } from "react"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Input } from "@/components/ui/input"; -import { ScrollArea } from "@/components/ui/scroll-area"; +import { Button } from "@/components/ui/button.tsx"; +import { Badge } from "@/components/ui/badge.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { ScrollArea } from "@/components/ui/scroll-area.tsx"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, -} from "@/components/ui/accordion"; -import { Sheet, SheetContent } from "@/components/ui/sheet"; +} from "@/components/ui/accordion.tsx"; +import { Sheet, SheetContent } from "@/components/ui/sheet.tsx"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, -} from "@/components/ui/tooltip"; +} from "@/components/ui/tooltip.tsx"; import { Popover, PopoverContent, PopoverTrigger, -} from "@/components/ui/popover"; +} from "@/components/ui/popover.tsx"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, -} from "@/components/ui/command"; +} from "@/components/ui/command.tsx"; import { Search, Key, @@ -46,7 +46,7 @@ import { User, ChevronsUpDown, } from "lucide-react"; -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils.ts"; import { getCredentials, deleteCredential, @@ -54,15 +54,12 @@ import { renameCredentialFolder, deployCredentialToHost, getSSHHosts, -} from "@/ui/main-axios"; +} from "@/ui/main-axios.ts"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; -import CredentialViewer from "./CredentialViewer"; -import type { - Credential, - CredentialsManagerProps, -} from "../../../../types/index.js"; +import CredentialViewer from "./CredentialViewer.tsx"; +import type { Credential, CredentialsManagerProps } from "../../../../../types"; export function CredentialsManager({ onEditCredential, @@ -622,7 +619,8 @@ export function CredentialsManager({ {credential.username}

- {t("credentials.idLabel")} {credential.id} + {t("credentials.idLabel")}{" "} + {credential.id}

{credential.authType === "password" @@ -867,7 +865,8 @@ export function CredentialsManager({ {t("credentials.keyType")}

- {deployingCredential.keyType || t("credentials.sshKey")} + {deployingCredential.keyType || + t("credentials.sshKey")}
diff --git a/src/ui/desktop/apps/host-manager/HostManager.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx similarity index 84% rename from src/ui/desktop/apps/host-manager/HostManager.tsx rename to src/ui/desktop/apps/host-manager/hosts/HostManager.tsx index ef0e0467..ea40cd29 100644 --- a/src/ui/desktop/apps/host-manager/HostManager.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; -import { HostManagerViewer } from "@/ui/desktop/apps/host-manager/HostManagerViewer.tsx"; +import { HostManagerViewer } from "@/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx"; import { Tabs, TabsContent, @@ -7,9 +7,9 @@ import { TabsTrigger, } from "@/components/ui/tabs.tsx"; import { Separator } from "@/components/ui/separator.tsx"; -import { HostManagerEditor } from "@/ui/desktop/apps/host-manager/HostManagerEditor.tsx"; -import { CredentialsManager } from "@/ui/desktop/apps/credentials/CredentialsManager.tsx"; -import { CredentialEditor } from "@/ui/desktop/apps/credentials/CredentialEditor.tsx"; +import { HostManagerEditor } from "@/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx"; +import { CredentialsManager } from "@/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx"; +import { CredentialEditor } from "@/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx"; import { useSidebar } from "@/components/ui/sidebar.tsx"; import { useTranslation } from "react-i18next"; import type { SSHHost, HostManagerProps } from "../../../types/index"; @@ -117,10 +117,16 @@ export function HostManager({ className="flex-1 flex flex-col h-full min-h-0" > - + {t("hosts.hostViewer")} - + {editingHost ? editingHost.id ? t("hosts.editHost") @@ -128,10 +134,16 @@ export function HostManager({ : t("hosts.addHost")}
- + {t("credentials.credentialsViewer")} - + {editingCredential ? t("credentials.editCredential") : t("credentials.addCredential")} diff --git a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx similarity index 96% rename from src/ui/desktop/apps/host-manager/HostManagerEditor.tsx rename to src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx index 53b357c4..aa8bbc90 100644 --- a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx @@ -1,7 +1,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils.ts"; import { Button } from "@/components/ui/button.tsx"; import { @@ -37,13 +37,15 @@ import { getSnippets, } from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; -import { CredentialSelector } from "@/ui/desktop/apps/credentials/CredentialSelector.tsx"; +import { CredentialSelector } from "@/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx"; import { HostSharingTab } from "./HostSharingTab.tsx"; import CodeMirror from "@uiw/react-codemirror"; import { oneDark } from "@codemirror/theme-one-dark"; +import { githubLight } from "@uiw/codemirror-theme-github"; import { EditorView } from "@codemirror/view"; -import type { StatsConfig } from "@/types/stats-widgets"; -import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets"; +import { useTheme } from "@/components/theme-provider.tsx"; +import type { StatsConfig } from "@/types/stats-widgets.ts"; +import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets.ts"; import { Checkbox } from "@/components/ui/checkbox.tsx"; import { Select, @@ -86,8 +88,8 @@ import { BELL_STYLES, FAST_SCROLL_MODIFIERS, DEFAULT_TERMINAL_CONFIG, -} from "@/constants/terminal-themes"; -import { TerminalPreview } from "@/ui/desktop/apps/terminal/TerminalPreview.tsx"; +} from "@/constants/terminal-themes.ts"; +import { TerminalPreview } from "@/ui/desktop/apps/features/terminal/TerminalPreview.tsx"; import type { TerminalConfig, SSHHost, Credential } from "@/types"; import { Plus, X, Check, ChevronsUpDown, Save } from "lucide-react"; @@ -300,6 +302,14 @@ export function HostManagerEditor({ onFormSubmit, }: SSHManagerHostEditorProps) { const { t } = useTranslation(); + const { theme: appTheme } = useTheme(); + + // Determine CodeMirror theme based on app theme + const isDarkMode = + appTheme === "dark" || + (appTheme === "system" && + window.matchMedia("(prefers-color-scheme: dark)").matches); + const editorTheme = isDarkMode ? oneDark : githubLight; const [folders, setFolders] = useState([]); const [sshConfigurations, setSshConfigurations] = useState([]); const [hosts, setHosts] = useState([]); @@ -1160,18 +1170,40 @@ export function HostManagerEditor({ className="w-full" > - + {t("hosts.general")} - + {t("hosts.terminal")} - Docker - {t("hosts.tunnel")} - + + Docker + + + {t("hosts.tunnel")} + + {t("hosts.fileManager")} - + {t("hosts.statistics")} {!editingHost?.isShared && ( @@ -1359,7 +1391,7 @@ export function HostManagerEditor({ ))} setTagInput(e.target.value)} onKeyDown={(e) => { @@ -1444,14 +1476,30 @@ export function HostManagerEditor({ className="flex-1 flex flex-col h-full min-h-0" > - + {t("hosts.password")} - {t("hosts.key")} - + + {t("hosts.key")} + + {t("hosts.credential")} - {t("hosts.none")} + + {t("hosts.none")} + { field.onChange( @@ -1925,7 +1976,9 @@ export function HostManagerEditor({ field.onChange( @@ -1947,7 +2000,8 @@ export function HostManagerEditor({ render={({ field }) => ( - {t("hosts.socks5Username")} {t("hosts.optional")} + {t("hosts.socks5Username")}{" "} + {t("hosts.optional")} ( - {t("hosts.socks5Password")} {t("hosts.optional")} + {t("hosts.socks5Password")}{" "} + {t("hosts.optional")} { const currentChain = @@ -2104,7 +2161,9 @@ export function HostManagerEditor({ { const currentChain = @@ -2167,7 +2226,8 @@ export function HostManagerEditor({
- {t("hosts.socks5Username")} {t("hosts.optional")} + {t("hosts.socks5Username")}{" "} + {t("hosts.optional")} - {t("hosts.socks5Password")} {t("hosts.optional")} + {t("hosts.socks5Password")}{" "} + {t("hosts.optional")} {t("hosts.moshCommand")} { field.onChange(e.target.value.trim()); field.onBlur(); @@ -3071,7 +3133,9 @@ export function HostManagerEditor({ @@ -3090,7 +3154,9 @@ export function HostManagerEditor({ diff --git a/src/ui/desktop/apps/host-manager/HostManagerViewer.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx similarity index 99% rename from src/ui/desktop/apps/host-manager/HostManagerViewer.tsx rename to src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx index 19b30e14..79f996c4 100644 --- a/src/ui/desktop/apps/host-manager/HostManagerViewer.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx @@ -68,10 +68,10 @@ import type { SSHHost, SSHFolder, SSHManagerHostViewerProps, -} from "../../../../types/index.js"; -import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets"; -import { FolderEditDialog } from "./components/FolderEditDialog"; -import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext"; +} from "../../../../../types"; +import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets.ts"; +import { FolderEditDialog } from "../components/FolderEditDialog.tsx"; +import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) { const { t } = useTranslation(); diff --git a/src/ui/desktop/apps/host-manager/hosts/HostSharingTab.tsx b/src/ui/desktop/apps/host-manager/hosts/HostSharingTab.tsx new file mode 100644 index 00000000..6ce4df4b --- /dev/null +++ b/src/ui/desktop/apps/host-manager/hosts/HostSharingTab.tsx @@ -0,0 +1,606 @@ +import React from "react"; +import { Button } from "@/components/ui/button.tsx"; +import { Label } from "@/components/ui/label.tsx"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select.tsx"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { Badge } from "@/components/ui/badge.tsx"; +import { + AlertCircle, + Plus, + Trash2, + Users, + Shield, + Clock, + UserCircle, + Check, + ChevronsUpDown, +} from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs.tsx"; +import { toast } from "sonner"; +import { useTranslation } from "react-i18next"; +import { useConfirmation } from "@/hooks/use-confirmation.ts"; +import { + getRoles, + getUserList, + getUserInfo, + shareHost, + getHostAccess, + revokeHostAccess, + getSSHHostById, + type Role, + type AccessRecord, + type SSHHost, +} from "@/ui/main-axios.ts"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command.tsx"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover.tsx"; +import { cn } from "@/lib/utils.ts"; + +interface HostSharingTabProps { + hostId: number | undefined; + isNewHost: boolean; +} + +interface User { + id: string; + username: string; + is_admin: boolean; +} + +const PERMISSION_LEVELS = [ + { value: "view", labelKey: "rbac.view" }, + { value: "manage", labelKey: "rbac.manage" }, +]; + +export function HostSharingTab({ + hostId, + isNewHost, +}: HostSharingTabProps): React.ReactElement { + const { t } = useTranslation(); + const { confirmWithToast } = useConfirmation(); + + const [shareType, setShareType] = React.useState<"user" | "role">("user"); + const [selectedUserId, setSelectedUserId] = React.useState(""); + const [selectedRoleId, setSelectedRoleId] = React.useState( + null, + ); + const [permissionLevel, setPermissionLevel] = React.useState("view"); + const [expiresInHours, setExpiresInHours] = React.useState(""); + + const [roles, setRoles] = React.useState([]); + const [users, setUsers] = React.useState([]); + const [accessList, setAccessList] = React.useState([]); + const [loading, setLoading] = React.useState(false); + const [currentUserId, setCurrentUserId] = React.useState(""); + const [hostData, setHostData] = React.useState(null); + + const [userComboOpen, setUserComboOpen] = React.useState(false); + const [roleComboOpen, setRoleComboOpen] = React.useState(false); + + // Load roles + const loadRoles = React.useCallback(async () => { + try { + const response = await getRoles(); + setRoles(response.roles || []); + } catch (error) { + console.error("Failed to load roles:", error); + setRoles([]); + } + }, []); + + // Load users + const loadUsers = React.useCallback(async () => { + try { + const response = await getUserList(); + // Map UserInfo to User format + const mappedUsers = (response.users || []).map((user) => ({ + id: user.id, + username: user.username, + is_admin: user.is_admin, + })); + setUsers(mappedUsers); + } catch (error) { + console.error("Failed to load users:", error); + setUsers([]); + } + }, []); + + // Load access list + const loadAccessList = React.useCallback(async () => { + if (!hostId) return; + + setLoading(true); + try { + const response = await getHostAccess(hostId); + setAccessList(response.accessList || []); + } catch (error) { + console.error("Failed to load access list:", error); + setAccessList([]); + } finally { + setLoading(false); + } + }, [hostId]); + + // Load host data + const loadHostData = React.useCallback(async () => { + if (!hostId) return; + + try { + const host = await getSSHHostById(hostId); + setHostData(host); + } catch (error) { + console.error("Failed to load host data:", error); + setHostData(null); + } + }, [hostId]); + + React.useEffect(() => { + loadRoles(); + loadUsers(); + if (!isNewHost) { + loadAccessList(); + loadHostData(); + } + }, [loadRoles, loadUsers, loadAccessList, loadHostData, isNewHost]); + + // Load current user ID + React.useEffect(() => { + const fetchCurrentUser = async () => { + try { + const userInfo = await getUserInfo(); + setCurrentUserId(userInfo.userId); + } catch (error) { + console.error("Failed to load current user:", error); + } + }; + fetchCurrentUser(); + }, []); + + // Share host + const handleShare = async () => { + if (!hostId) { + toast.error(t("rbac.saveHostFirst")); + return; + } + + if (shareType === "user" && !selectedUserId) { + toast.error(t("rbac.selectUser")); + return; + } + + if (shareType === "role" && !selectedRoleId) { + toast.error(t("rbac.selectRole")); + return; + } + + // Prevent sharing with self + if (shareType === "user" && selectedUserId === currentUserId) { + toast.error(t("rbac.cannotShareWithSelf")); + return; + } + + try { + await shareHost(hostId, { + targetType: shareType, + targetUserId: shareType === "user" ? selectedUserId : undefined, + targetRoleId: shareType === "role" ? selectedRoleId : undefined, + permissionLevel, + durationHours: expiresInHours + ? parseInt(expiresInHours, 10) + : undefined, + }); + + toast.success(t("rbac.sharedSuccessfully")); + setSelectedUserId(""); + setSelectedRoleId(null); + setExpiresInHours(""); + loadAccessList(); + } catch (error) { + toast.error(t("rbac.failedToShare")); + } + }; + + // Revoke access + const handleRevoke = async (accessId: number) => { + if (!hostId) return; + + const confirmed = await confirmWithToast({ + title: t("rbac.confirmRevokeAccess"), + description: t("rbac.confirmRevokeAccessDescription"), + confirmText: t("common.revoke"), + cancelText: t("common.cancel"), + }); + + if (!confirmed) return; + + try { + await revokeHostAccess(hostId, accessId); + toast.success(t("rbac.accessRevokedSuccessfully")); + loadAccessList(); + } catch (error) { + toast.error(t("rbac.failedToRevokeAccess")); + } + }; + + // Format date + const formatDate = (dateString: string | null) => { + if (!dateString) return "-"; + return new Date(dateString).toLocaleString(); + }; + + // Check if expired + const isExpired = (expiresAt: string | null) => { + if (!expiresAt) return false; + return new Date(expiresAt) < new Date(); + }; + + // Filter out current user from the users list + const availableUsers = React.useMemo(() => { + return users.filter((user) => user.id !== currentUserId); + }, [users, currentUserId]); + + const selectedUser = availableUsers.find((u) => u.id === selectedUserId); + const selectedRole = roles.find((r) => r.id === selectedRoleId); + + if (isNewHost) { + return ( + + + {t("rbac.saveHostFirst")} + + {t("rbac.saveHostFirstDescription")} + + + ); + } + + return ( +
+ {/* Credential Requirement Warning */} + {!hostData?.credentialId && ( + + + {t("rbac.credentialRequired")} + + {t("rbac.credentialRequiredDescription")} + + + )} + + {/* Share Form */} + {hostData?.credentialId && ( + <> +
+

+ + {t("rbac.shareHost")} +

+ + {/* Share Type Selection */} + setShareType(v as "user" | "role")} + > + + + + {t("rbac.shareWithUser")} + + + + {t("rbac.shareWithRole")} + + + + +
+ + + + + + + + + {t("rbac.noUserFound")} + + {availableUsers.map((user) => ( + { + setSelectedUserId(user.id); + setUserComboOpen(false); + }} + > + + {user.username} + {user.is_admin ? " (Admin)" : ""} + + ))} + + + + +
+
+ + +
+ + + + + + + + + {t("rbac.noRoleFound")} + + {roles.map((role) => ( + { + setSelectedRoleId(role.id); + setRoleComboOpen(false); + }} + > + + {t(role.displayName)} + {role.isSystem + ? ` (${t("rbac.systemRole")})` + : ""} + + ))} + + + + +
+
+
+ + {/* Permission Level */} +
+ + +
+ + {/* Expiration */} +
+ + { + const value = e.target.value; + if (value === "" || /^\d+$/.test(value)) { + setExpiresInHours(value); + } + }} + placeholder={t("rbac.neverExpires")} + min="1" + /> +
+ + +
+ + {/* Access List */} +
+

+ + {t("rbac.accessList")} +

+ + + + + {t("rbac.type")} + {t("rbac.target")} + {t("rbac.permissionLevel")} + {t("rbac.grantedBy")} + {t("rbac.expires")} + {t("rbac.accessCount")} + + {t("common.actions")} + + + + + {loading ? ( + + + {t("common.loading")} + + + ) : accessList.length === 0 ? ( + + + {t("rbac.noAccessRecords")} + + + ) : ( + accessList.map((access) => ( + + + {access.targetType === "user" ? ( + + + {t("rbac.user")} + + ) : ( + + + {t("rbac.role")} + + )} + + + {access.targetType === "user" + ? access.username + : t(access.roleDisplayName || access.roleName || "")} + + + + {access.permissionLevel} + + + {access.grantedByUsername} + + {access.expiresAt ? ( +
+ + + {formatDate(access.expiresAt)} + {isExpired(access.expiresAt) && ( + + ({t("rbac.expired")}) + + )} + +
+ ) : ( + t("rbac.never") + )} +
+ {access.accessCount} + + + +
+ )) + )} +
+
+
+ + )} +
+ ); +} diff --git a/src/ui/desktop/apps/server-stats/widgets/index.ts b/src/ui/desktop/apps/server-stats/widgets/index.ts deleted file mode 100644 index b72f8a11..00000000 --- a/src/ui/desktop/apps/server-stats/widgets/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { CpuWidget } from "./CpuWidget"; -export { MemoryWidget } from "./MemoryWidget"; -export { DiskWidget } from "./DiskWidget"; -export { NetworkWidget } from "./NetworkWidget"; -export { UptimeWidget } from "./UptimeWidget"; -export { ProcessesWidget } from "./ProcessesWidget"; -export { SystemWidget } from "./SystemWidget"; -export { LoginStatsWidget } from "./LoginStatsWidget"; diff --git a/src/ui/desktop/navigation/AppView.tsx b/src/ui/desktop/navigation/AppView.tsx index 24530b37..cf7c7720 100644 --- a/src/ui/desktop/navigation/AppView.tsx +++ b/src/ui/desktop/navigation/AppView.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useRef, useState, useMemo } from "react"; -import { Terminal } from "@/ui/desktop/apps/terminal/Terminal.tsx"; -import { ServerStats as ServerView } from "@/ui/desktop/apps/server-stats/ServerStats.tsx"; -import { FileManager } from "@/ui/desktop/apps/file-manager/FileManager.tsx"; -import { TunnelManager } from "@/ui/desktop/apps/tunnel/TunnelManager.tsx"; -import { DockerManager } from "@/ui/desktop/apps/docker/DockerManager.tsx"; +import { Terminal } from "@/ui/desktop/apps/features/terminal/Terminal.tsx"; +import { ServerStats as ServerView } from "@/ui/desktop/apps/features/server-stats/ServerStats.tsx"; +import { FileManager } from "@/ui/desktop/apps/features/file-manager/FileManager.tsx"; +import { TunnelManager } from "@/ui/desktop/apps/features/tunnel/TunnelManager.tsx"; +import { DockerManager } from "@/ui/desktop/apps/features/docker/DockerManager.tsx"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { ResizablePanelGroup, diff --git a/src/ui/desktop/navigation/TopNavbar.tsx b/src/ui/desktop/navigation/TopNavbar.tsx index b6ae4a23..d4d808ee 100644 --- a/src/ui/desktop/navigation/TopNavbar.tsx +++ b/src/ui/desktop/navigation/TopNavbar.tsx @@ -8,7 +8,7 @@ import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { useTranslation } from "react-i18next"; import { TabDropdown } from "@/ui/desktop/navigation/tabs/TabDropdown.tsx"; import { SSHToolsSidebar } from "@/ui/desktop/apps/tools/SSHToolsSidebar.tsx"; -import { useCommandHistory } from "@/ui/desktop/apps/terminal/command-history/CommandHistoryContext.tsx"; +import { useCommandHistory } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.tsx"; interface TabData { id: number;