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;