diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 9a0cefda..e210dea1 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -119,7 +119,7 @@ reviews: - Identify and fix potential null/undefined access errors - Fix improper event handling and memory leaks - Resolve improper state management and data flow issues - + - path: "**/backend/**/*.{ts,js}" instructions: | Review backend code for Termix server management platform. Key considerations: @@ -167,7 +167,7 @@ reviews: - Implement proper health checks and status endpoints Highlight any security vulnerabilities, performance issues, or architectural deviations. - + - path: "**/components/**/*.{ts,tsx}" instructions: | Review UI components for Termix server management platform. Key considerations: @@ -207,7 +207,7 @@ reviews: - Use proper tunnel status and management UI Highlight any UI/UX issues, accessibility problems, or performance concerns. - + - path: "**/types/**/*.{ts,js}" instructions: | Review type definitions for Termix server management platform. Key considerations: @@ -237,7 +237,7 @@ reviews: - Use proper type assertions and casting Highlight any type safety issues, missing types, or type inconsistencies. - + - path: "**/hooks/**/*.{ts,tsx}" instructions: | Review custom hooks for Termix server management platform. Key considerations: @@ -285,7 +285,7 @@ reviews: - Fix improper error handling in custom hooks Highlight any hook design issues, performance problems, or reusability concerns. - + - path: "**/lib/**/*.{ts,js}" instructions: | Review utility libraries and helper functions for Termix server management platform. Key considerations: @@ -337,7 +337,7 @@ reviews: - Resolve improper configuration and environment variable handling Highlight any utility design issues, performance problems, or security concerns. - + - path: "**/main-axios.ts" instructions: | Review main-axios.ts API client configuration for Termix server management platform. Key considerations: @@ -405,7 +405,7 @@ reviews: - Identify and fix potential security vulnerabilities in API handling Highlight any API design issues, error handling problems, or security concerns. - + - path: "**/electron/**/*.{ts,js,cjs}" instructions: | Review Electron application code for Termix server management platform. Key considerations: @@ -443,7 +443,7 @@ reviews: - Identify and fix potential security vulnerabilities in Electron setup Highlight any Electron-specific issues, security vulnerabilities, or performance problems. - + - path: "**/docker/**/*" instructions: | Review Docker configuration files for Termix server management platform. Key considerations: @@ -505,7 +505,7 @@ reviews: - Use proper visual aids and diagrams where appropriate Highlight any documentation issues, inaccuracies, or missing information. - + - path: "**/index.css" instructions: | Review index.css styling configuration for Termix server management platform. Key considerations: diff --git a/.env b/.env index 3b7d55ee..6f985423 100644 --- a/.env +++ b/.env @@ -1,3 +1,2 @@ VERSION=1.6.0 -VITE_API_HOST=localhost -CREDENTIAL_ENCRYPTION_KEY=98fbfabe84b125db7cbbb5168eb584aaecc2f3779a2aaa955c57bdd305071a84 \ No newline at end of file +VITE_API_HOST=localhost \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ceda9f68..da7949ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing +_# Contributing ## Prerequisites @@ -26,6 +26,7 @@ npm run dev npm run dev:backend ``` +a This will start the backend and the frontend Vite server. You can access Termix by going to `http://localhost:5174/`. ## Contributing @@ -59,43 +60,48 @@ This will start the backend and the frontend Vite server. You can access Termix ## Color Scheme ### Background Colors -| CSS Variable | Color Value | Usage | Description | -|--------------|-------------|-------|-------------| -| `--color-dark-bg` | `#18181b` | Main dark background | Primary dark background color | -| `--color-dark-bg-darker` | `#0e0e10` | Darker backgrounds | Darker variant for panels and containers | -| `--color-dark-bg-darkest` | `#09090b` | Darkest backgrounds | Darkest background (terminal) | -| `--color-dark-bg-light` | `#141416` | Light dark backgrounds | Lighter variant of dark background | -| `--color-dark-bg-very-light` | `#101014` | Very light dark backgrounds | Very light variant of dark background | -| `--color-dark-bg-panel` | `#1b1b1e` | Panel backgrounds | Background for panels and cards | -| `--color-dark-bg-panel-hover` | `#232327` | Panel hover states | Background for panels on hover | + +| CSS Variable | Color Value | Usage | Description | +|-------------------------------|-------------|-----------------------------|------------------------------------------| +| `--color-dark-bg` | `#18181b` | Main dark background | Primary dark background color | +| `--color-dark-bg-darker` | `#0e0e10` | Darker backgrounds | Darker variant for panels and containers | +| `--color-dark-bg-darkest` | `#09090b` | Darkest backgrounds | Darkest background (terminal) | +| `--color-dark-bg-light` | `#141416` | Light dark backgrounds | Lighter variant of dark background | +| `--color-dark-bg-very-light` | `#101014` | Very light dark backgrounds | Very light variant of dark background | +| `--color-dark-bg-panel` | `#1b1b1e` | Panel backgrounds | Background for panels and cards | +| `--color-dark-bg-panel-hover` | `#232327` | Panel hover states | Background for panels on hover | ### Element-Specific Backgrounds -| CSS Variable | Color Value | Usage | Description | -|--------------|-------------|-------|-------------| -| `--color-dark-bg-input` | `#222225` | Input fields | Background for input fields and form elements | -| `--color-dark-bg-button` | `#23232a` | Button backgrounds | Background for buttons and clickable elements | -| `--color-dark-bg-active` | `#1d1d1f` | Active states | Background for active/selected elements | -| `--color-dark-bg-header` | `#131316` | Header backgrounds | Background for headers and navigation bars | + +| CSS Variable | Color Value | Usage | Description | +|--------------------------|-------------|--------------------|-----------------------------------------------| +| `--color-dark-bg-input` | `#222225` | Input fields | Background for input fields and form elements | +| `--color-dark-bg-button` | `#23232a` | Button backgrounds | Background for buttons and clickable elements | +| `--color-dark-bg-active` | `#1d1d1f` | Active states | Background for active/selected elements | +| `--color-dark-bg-header` | `#131316` | Header backgrounds | Background for headers and navigation bars | ### Border Colors -| CSS Variable | Color Value | Usage | Description | -|--------------|-------------|-------|-------------| -| `--color-dark-border` | `#303032` | Default borders | Standard border color | -| `--color-dark-border-active` | `#2d2d30` | Active borders | Border color for active elements | -| `--color-dark-border-hover` | `#434345` | Hover borders | Border color on hover states | -| `--color-dark-border-light` | `#5a5a5d` | Light borders | Lighter border color for subtle elements | -| `--color-dark-border-medium` | `#373739` | Medium borders | Medium weight border color | -| `--color-dark-border-panel` | `#222224` | Panel borders | Border color for panels and cards | + +| CSS Variable | Color Value | Usage | Description | +|------------------------------|-------------|-----------------|------------------------------------------| +| `--color-dark-border` | `#303032` | Default borders | Standard border color | +| `--color-dark-border-active` | `#2d2d30` | Active borders | Border color for active elements | +| `--color-dark-border-hover` | `#434345` | Hover borders | Border color on hover states | +| `--color-dark-border-light` | `#5a5a5d` | Light borders | Lighter border color for subtle elements | +| `--color-dark-border-medium` | `#373739` | Medium borders | Medium weight border color | +| `--color-dark-border-panel` | `#222224` | Panel borders | Border color for panels and cards | ### Interactive States -| CSS Variable | Color Value | Usage | Description | -|--------------|-------------|-------|-------------| -| `--color-dark-hover` | `#2d2d30` | Hover states | Background color for hover effects | -| `--color-dark-active` | `#2a2a2c` | Active states | Background color for active elements | -| `--color-dark-pressed` | `#1a1a1c` | Pressed states | Background color for pressed/clicked elements | -| `--color-dark-hover-alt` | `#2a2a2d` | Alternative hover | Alternative hover state color | +| CSS Variable | Color Value | Usage | Description | +|--------------------------|-------------|-------------------|-----------------------------------------------| +| `--color-dark-hover` | `#2d2d30` | Hover states | Background color for hover effects | +| `--color-dark-active` | `#2a2a2c` | Active states | Background color for active elements | +| `--color-dark-pressed` | `#1a1a1c` | Pressed states | Background color for pressed/clicked elements | +| `--color-dark-hover-alt` | `#2a2a2d` | Alternative hover | Alternative hover state color | ## Support -If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) repo. \ No newline at end of file +If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support +channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) +repo. \ No newline at end of file diff --git a/README-CN.md b/README-CN.md index 54611470..39400868 100644 --- a/README-CN.md +++ b/README-CN.md @@ -1,7 +1,7 @@ -# Repo Stats +# 仓库统计

- English English | + English 英文 | 中文 中文

@@ -9,7 +9,9 @@ ![GitHub forks](https://img.shields.io/github/forks/LukeGus/Termix?style=flat&label=Forks) ![GitHub Release](https://img.shields.io/github/v/release/LukeGus/Termix?style=flat&label=Release) Discord -#### Top Technologies + +#### 核心技术 + [![React Badge](https://img.shields.io/badge/-React-61DBFB?style=flat-square&labelColor=black&logo=react&logoColor=61DBFB)](#) [![TypeScript Badge](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&labelColor=black&logo=typescript&logoColor=3178C6)](#) [![Node.js Badge](https://img.shields.io/badge/-Node.js-3C873A?style=flat-square&labelColor=black&logo=node.js&logoColor=3C873A)](#) @@ -28,16 +30,18 @@ 如果你愿意,可以在这里支持这个项目!\ [![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) -# Overview +# 概览

Termix Banner

-Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix 提供 SSH 终端访问、SSH 隧道功能以及远程文件编辑,还会陆续添加更多工具。 +Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix +提供 SSH 终端访问、SSH 隧道功能以及远程文件编辑,还会陆续添加更多工具。 + +# 功能 -# Features - **SSH 终端访问** - 功能完整的终端,支持分屏(最多 4 个面板)和标签系统 - **SSH 隧道管理** - 创建和管理 SSH 隧道,支持自动重连和健康监控 - **远程文件编辑器** - 直接在远程服务器编辑文件,支持语法高亮和文件管理功能(上传、删除、重命名等) @@ -47,14 +51,17 @@ Termix 是一个开源、永久免费、自托管的一体化服务器管理平 - **现代化界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁界面 - **语言支持** - 内置中英文支持 -# Planned Features +# 计划功能 + - **增强管理员控制** - 提供更精细的用户和管理员权限控制、共享主机等功能 - **主题定制** - 修改所有工具的主题风格 - **增强终端支持** - 添加更多终端协议,如 VNC 和 RDP(有类似 Apache Guacamole 的 RDP 集成经验者请通过创建 issue 联系我) - **移动端支持** - 支持移动应用或 Termix 网站移动版,让你在手机上管理服务器 -# Installation +# 安装 + 访问 Termix [文档](https://docs.termix.site/install) 获取安装信息。或者可以参考以下示例 docker-compose 文件: + ```yaml services: termix: @@ -70,13 +77,15 @@ services: volumes: termix-data: - driver: local + driver: local ``` -# Support -如果你需要 Termix 的帮助,可以加入 [Discord](https://discord.gg/jVQGdvHDrf) 服务器并访问支持频道。你也可以在 [GitHub](https://github.com/LukeGus/Termix/issues) 仓库提交 issue 或 pull request。 +# 支持 -# Show-off +如果你需要 Termix 的帮助,可以加入 [Discord](https://discord.gg/jVQGdvHDrf) +服务器并访问支持频道。你也可以在 [GitHub](https://github.com/LukeGus/Termix/issues) 仓库提交 issue 或 pull request。 + +# 展示

Termix Demo 1 @@ -95,6 +104,6 @@ volumes:

-# License -根据 Apache 2.0 许可证发布。更多信息请参见 LICENSE。 +# 许可证 +根据 Apache 2.0 许可证发布。更多信息请参见 LICENSE。 \ No newline at end of file diff --git a/README.md b/README.md index d1c2d9a2..dcbd7a8c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Repo Stats +

English English | 中文 中文 @@ -9,7 +10,9 @@ ![GitHub forks](https://img.shields.io/github/forks/LukeGus/Termix?style=flat&label=Forks) ![GitHub Release](https://img.shields.io/github/v/release/LukeGus/Termix?style=flat&label=Release) Discord + #### Top Technologies + [![React Badge](https://img.shields.io/badge/-React-61DBFB?style=flat-square&labelColor=black&logo=react&logoColor=61DBFB)](#) [![TypeScript Badge](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&labelColor=black&logo=typescript&logoColor=3178C6)](#) [![Node.js Badge](https://img.shields.io/badge/-Node.js-3C873A?style=flat-square&labelColor=black&logo=node.js&logoColor=3C873A)](#) @@ -35,24 +38,34 @@ If you would like, you can support the project here!\ Termix Banner

-Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a web-based solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal access, SSH tunneling capabilities, and remote file editing, with many more tools to come. +Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a web-based +solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal +access, SSH tunneling capabilities, and remote file editing, with many more tools to come. # Features + - **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) and tab system - **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring -- **Remote File Editor** - Edit files directly on remote servers with syntax highlighting, file management features (uploading, removing, renaming, deleting files) +- **Remote File Editor** - Edit files directly on remote servers with syntax highlighting, file management features ( + uploading, removing, renaming, deleting files) - **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders - **Server Stats** - View CPU, memory, and HDD usage on any SSH server - **User Authentication** - Secure user management with admin controls and OIDC and 2FA (TOTP) support - **Modern UI** - Clean mobile friendly (in progress) interface built with React, Tailwind CSS, and Shadcn - **Languages** - Built-in support for English and Chinese -- **Improved Platform Support** - Now includes an installable Electron app (in progress) for desktop, with a dedicated mobile app also planned. +- **Improved Platform Support** - Now includes an installable Electron app (in progress) for desktop, with a dedicated + mobile app also planned. # Planned Features -See [Projects](https://github.com/users/LukeGus/projects/3). If you are looking to contribute, see [Contributing](https://github.com/LukeGus/Termix/blob/main/CONTRIBUTING.md), + +See [Projects](https://github.com/users/LukeGus/projects/3). If you are looking to contribute, +see [Contributing](https://github.com/LukeGus/Termix/blob/main/CONTRIBUTING.md), # Installation -Visit the Termix [Docs](https://docs.termix.site/install) for more information on how to install Termix. Otherwise, view a sample docker-compose file here: + +Visit the Termix [Docs](https://docs.termix.site/install) for more information on how to install Termix. Otherwise, view +a sample docker-compose file here: + ```yaml services: termix: @@ -70,10 +83,16 @@ volumes: termix-data: driver: local ``` -Pre-built binaries are now available for download, including a Windows installer/portable app and a Linux portable app (built with Electron). See [Docs](http://localhost:5174/install#pre-built-binaries) for details. A native iOS/Android app is planned. + +Pre-built binaries are now available for download, including a Windows installer/portable app and a Linux portable app ( +built with Electron). See [Docs](http://localhost:5174/install#pre-built-binaries) for details. A native iOS/Android app +is planned. # Support -If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) repo. + +If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support +channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) +repo. # Show-off @@ -95,4 +114,5 @@ If you need help with Termix, you can join the [Discord](https://discord.gg/jVQG

# License + Distributed under the Apache License Version 2.0. See LICENSE for more information. diff --git a/src/hooks/use-confirmation.ts b/src/hooks/use-confirmation.ts index cbbcf531..de250e0b 100644 --- a/src/hooks/use-confirmation.ts +++ b/src/hooks/use-confirmation.ts @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import { toast } from 'sonner'; +import {useState} from 'react'; +import {toast} from 'sonner'; interface ConfirmationOptions { title: string; @@ -35,11 +35,10 @@ export function useConfirmation() { setOnConfirm(null); }; - // For simple confirmations, we can use a toast with action const confirmWithToast = (message: string, callback: () => void, variant: 'default' | 'destructive' = 'default') => { const actionText = variant === 'destructive' ? 'Delete' : 'Confirm'; const cancelText = 'Cancel'; - + toast(message, { action: { label: actionText, @@ -47,9 +46,10 @@ export function useConfirmation() { }, cancel: { label: cancelText, - onClick: () => {} + onClick: () => { + } }, - duration: 10000, // Longer duration for confirmations + duration: 10000, className: variant === 'destructive' ? 'border-red-500' : '' }); }; diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts index 2b0fe1df..81e9106f 100644 --- a/src/hooks/use-mobile.ts +++ b/src/hooks/use-mobile.ts @@ -3,17 +3,17 @@ import * as React from "react" const MOBILE_BREAKPOINT = 768 export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined) + const [isMobile, setIsMobile] = React.useState(undefined) - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + } + mql.addEventListener("change", onChange) + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + return () => mql.removeEventListener("change", onChange) + }, []) - return !!isMobile + return !!isMobile } diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index 2a472cc2..5b0274b5 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -1,47 +1,42 @@ -// i18n configuration for multi-language support import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; +import {initReactI18next} from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; -// Import translation files directly import enTranslation from '../locales/en/translation.json'; import zhTranslation from '../locales/zh/translation.json'; -// Initialize i18n i18n - .use(LanguageDetector) // Detect user language - .use(initReactI18next) // Pass i18n instance to react-i18next - .init({ - supportedLngs: ['en', 'zh'], // Supported languages - fallbackLng: 'en', // Fallback language - debug: false, - - // Detection options - disabled to always use English by default - detection: { - order: ['localStorage', 'cookie'], // Only check user's saved preference - caches: ['localStorage', 'cookie'], - lookupLocalStorage: 'i18nextLng', - lookupCookie: 'i18nextLng', - checkWhitelist: true, - }, - - // Resources - load translations directly - resources: { - en: { - translation: enTranslation - }, - zh: { - translation: zhTranslation - } - }, - - interpolation: { - escapeValue: false, // React already escapes values - }, - - react: { - useSuspense: false, // Disable suspense for SSR compatibility - }, - }); + .use(LanguageDetector) + .use(initReactI18next) + .init({ + supportedLngs: ['en', 'zh'], + fallbackLng: 'en', + debug: false, + + detection: { + order: ['localStorage', 'cookie'], + caches: ['localStorage', 'cookie'], + lookupLocalStorage: 'i18nextLng', + lookupCookie: 'i18nextLng', + checkWhitelist: true, + }, + + resources: { + en: { + translation: enTranslation + }, + zh: { + translation: zhTranslation + } + }, + + interpolation: { + escapeValue: false, + }, + + react: { + useSuspense: false, + }, + }); export default i18n; \ No newline at end of file diff --git a/src/index.css b/src/index.css index 7ddaea28..73d25630 100644 --- a/src/index.css +++ b/src/index.css @@ -180,24 +180,24 @@ } .thin-scrollbar::-webkit-scrollbar { - width: 6px; - height: 6px; + width: 6px; + height: 6px; } .thin-scrollbar::-webkit-scrollbar-track { - background: #18181b; + background: #18181b; } .thin-scrollbar::-webkit-scrollbar-thumb { - background: #434345; - border-radius: 3px; + background: #434345; + border-radius: 3px; } .thin-scrollbar::-webkit-scrollbar-thumb:hover { - background: #5a5a5d; + background: #5a5a5d; } .thin-scrollbar { - scrollbar-width: thin; - scrollbar-color: #434345 #18181b; + scrollbar-width: thin; + scrollbar-color: #434345 #18181b; } \ No newline at end of file diff --git a/src/lib/frontend-logger.ts b/src/lib/frontend-logger.ts index a6193301..ea647378 100644 --- a/src/lib/frontend-logger.ts +++ b/src/lib/frontend-logger.ts @@ -1,8 +1,3 @@ -/** - * Frontend Logger - A comprehensive logging utility for the frontend - * Enhanced with better formatting, readability, and request/response grouping - */ - export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success'; export interface LogContext { @@ -21,6 +16,7 @@ export interface LogContext { retryCount?: number; errorCode?: string; errorMessage?: string; + [key: string]: any; } @@ -46,7 +42,7 @@ class FrontendLogger { const timestamp = this.getTimeStamp(); const levelTag = this.getLevelTag(level); const serviceTag = this.getServiceTag(); - + let contextStr = ''; if (context && this.isDevelopment) { const contextParts = []; @@ -58,7 +54,7 @@ class FrontendLogger { if (context.responseTime) contextParts.push(`${context.responseTime}ms`); if (context.status) contextParts.push(`status:${context.status}`); if (context.errorCode) contextParts.push(`code:${context.errorCode}`); - + if (contextParts.length > 0) { contextStr = ` (${contextParts.join(', ')})`; } @@ -91,9 +87,9 @@ class FrontendLogger { private log(level: LogLevel, message: string, context?: LogContext, error?: unknown): void { if (!this.shouldLog(level)) return; - + const formattedMessage = this.formatMessage(level, message, context); - + switch (level) { case 'debug': console.debug(formattedMessage); @@ -136,60 +132,58 @@ class FrontendLogger { this.log('success', message, context); } - // Convenience methods for common operations api(message: string, context?: LogContext): void { - this.info(`API: ${message}`, { ...context, operation: 'api' }); + this.info(`API: ${message}`, {...context, operation: 'api'}); } request(message: string, context?: LogContext): void { - this.info(`REQUEST: ${message}`, { ...context, operation: 'request' }); + this.info(`REQUEST: ${message}`, {...context, operation: 'request'}); } response(message: string, context?: LogContext): void { - this.info(`RESPONSE: ${message}`, { ...context, operation: 'response' }); + this.info(`RESPONSE: ${message}`, {...context, operation: 'response'}); } auth(message: string, context?: LogContext): void { - this.info(`AUTH: ${message}`, { ...context, operation: 'auth' }); + this.info(`AUTH: ${message}`, {...context, operation: 'auth'}); } ssh(message: string, context?: LogContext): void { - this.info(`SSH: ${message}`, { ...context, operation: 'ssh' }); + this.info(`SSH: ${message}`, {...context, operation: 'ssh'}); } tunnel(message: string, context?: LogContext): void { - this.info(`TUNNEL: ${message}`, { ...context, operation: 'tunnel' }); + this.info(`TUNNEL: ${message}`, {...context, operation: 'tunnel'}); } file(message: string, context?: LogContext): void { - this.info(`FILE: ${message}`, { ...context, operation: 'file' }); + this.info(`FILE: ${message}`, {...context, operation: 'file'}); } connection(message: string, context?: LogContext): void { - this.info(`CONNECTION: ${message}`, { ...context, operation: 'connection' }); + this.info(`CONNECTION: ${message}`, {...context, operation: 'connection'}); } disconnect(message: string, context?: LogContext): void { - this.info(`DISCONNECT: ${message}`, { ...context, operation: 'disconnect' }); + this.info(`DISCONNECT: ${message}`, {...context, operation: 'disconnect'}); } retry(message: string, context?: LogContext): void { - this.warn(`RETRY: ${message}`, { ...context, operation: 'retry' }); + this.warn(`RETRY: ${message}`, {...context, operation: 'retry'}); } performance(message: string, context?: LogContext): void { - this.info(`PERFORMANCE: ${message}`, { ...context, operation: 'performance' }); + this.info(`PERFORMANCE: ${message}`, {...context, operation: 'performance'}); } security(message: string, context?: LogContext): void { - this.warn(`SECURITY: ${message}`, { ...context, operation: 'security' }); + this.warn(`SECURITY: ${message}`, {...context, operation: 'security'}); } - // Enhanced API request/response logging methods requestStart(method: string, url: string, context?: LogContext): void { const cleanUrl = this.sanitizeUrl(url); const shortUrl = this.getShortUrl(cleanUrl); - + console.group(`🚀 ${method.toUpperCase()} ${shortUrl}`); this.request(`→ Starting request to ${cleanUrl}`, { ...context, @@ -203,7 +197,7 @@ class FrontendLogger { const shortUrl = this.getShortUrl(cleanUrl); const statusIcon = this.getStatusIcon(status); const performanceIcon = this.getPerformanceIcon(responseTime); - + this.response(`← ${statusIcon} ${status} ${performanceIcon} ${responseTime}ms`, { ...context, method: method.toUpperCase(), @@ -218,7 +212,7 @@ class FrontendLogger { const cleanUrl = this.sanitizeUrl(url); const shortUrl = this.getShortUrl(cleanUrl); const statusIcon = this.getStatusIcon(status); - + this.error(`← ${statusIcon} ${status} ${errorMessage}`, undefined, { ...context, method: method.toUpperCase(), @@ -233,7 +227,7 @@ class FrontendLogger { networkError(method: string, url: string, errorMessage: string, context?: LogContext): void { const cleanUrl = this.sanitizeUrl(url); const shortUrl = this.getShortUrl(cleanUrl); - + this.error(`🌐 Network Error: ${errorMessage}`, undefined, { ...context, method: method.toUpperCase(), @@ -247,7 +241,7 @@ class FrontendLogger { authError(method: string, url: string, context?: LogContext): void { const cleanUrl = this.sanitizeUrl(url); const shortUrl = this.getShortUrl(cleanUrl); - + this.security(`🔐 Authentication Required`, { ...context, method: method.toUpperCase(), @@ -260,7 +254,7 @@ class FrontendLogger { retryAttempt(method: string, url: string, attempt: number, maxAttempts: number, context?: LogContext): void { const cleanUrl = this.sanitizeUrl(url); const shortUrl = this.getShortUrl(cleanUrl); - + this.retry(`🔄 Retry ${attempt}/${maxAttempts}`, { ...context, method: method.toUpperCase(), @@ -269,25 +263,22 @@ class FrontendLogger { }); } - // Enhanced logging for API operations apiOperation(operation: string, details: string, context?: LogContext): void { - this.info(`🔧 ${operation}: ${details}`, { ...context, operation: 'api_operation' }); + this.info(`🔧 ${operation}: ${details}`, {...context, operation: 'api_operation'}); } - // Log request summary for better debugging requestSummary(method: string, url: string, status: number, responseTime: number, context?: LogContext): void { const cleanUrl = this.sanitizeUrl(url); const shortUrl = this.getShortUrl(cleanUrl); const statusIcon = this.getStatusIcon(status); const performanceIcon = this.getPerformanceIcon(responseTime); - - console.log(`%c📊 ${method} ${shortUrl} ${statusIcon} ${status} ${performanceIcon} ${responseTime}ms`, - 'color: #666; font-style: italic; font-size: 0.9em;', + + console.log(`%c📊 ${method} ${shortUrl} ${statusIcon} ${status} ${performanceIcon} ${responseTime}ms`, + 'color: #666; font-style: italic; font-size: 0.9em;', context ); } - // New helper methods for better formatting private getShortUrl(url: string): string { try { const urlObj = new URL(url); @@ -316,10 +307,8 @@ class FrontendLogger { } private sanitizeUrl(url: string): string { - // Remove sensitive information from URLs for logging try { const urlObj = new URL(url); - // Remove query parameters that might contain sensitive data if (urlObj.searchParams.has('password') || urlObj.searchParams.has('token')) { urlObj.search = ''; } @@ -330,7 +319,6 @@ class FrontendLogger { } } -// Service-specific loggers export const apiLogger = new FrontendLogger('API', '🌐', '#3b82f6'); export const authLogger = new FrontendLogger('AUTH', '🔐', '#dc2626'); export const sshLogger = new FrontendLogger('SSH', '🖥️', '#1e3a8a'); @@ -339,5 +327,4 @@ export const fileLogger = new FrontendLogger('FILE', '📁', '#1e3a8a'); export const statsLogger = new FrontendLogger('STATS', '📊', '#22c55e'); export const systemLogger = new FrontendLogger('SYSTEM', '🚀', '#1e3a8a'); -// Default logger for general use export const logger = systemLogger; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bd0c391d..d1a4eb4e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import {clsx, type ClassValue} from "clsx" +import {twMerge} from "tailwind-merge" export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)) } diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index cd8aceb7..86c59980 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -10,7 +10,7 @@ "deleteCredential": "Delete Credential", "updateCredential": "Update Credential", "credentialName": "Credential Name", - "credentialDescription": "Description", + "credentialDescription": "Description", "username": "Username", "searchCredentials": "Search credentials...", "selectFolder": "Select Folder", @@ -42,7 +42,7 @@ "credentialsCount": "{{count}} credentials", "refresh": "Refresh", "passwordRequired": "Password is required", - "sshKeyRequired": "SSH key is required", + "sshKeyRequired": "SSH key is required", "credentialAddedSuccessfully": "Credential \"{{name}}\" added successfully", "general": "General", "description": "Description", @@ -57,7 +57,7 @@ "keyPassword": "Key Password (optional)", "keyType": "Key Type", "keyTypeRSA": "RSA", - "keyTypeECDSA": "ECDSA", + "keyTypeECDSA": "ECDSA", "keyTypeEd25519": "Ed25519", "updateCredential": "Update Credential", "basicInfo": "Basic Info", @@ -224,7 +224,7 @@ "register": "Register", "username": "Username", "password": "Password", - "version" : "Version", + "version": "Version", "confirmPassword": "Confirm Password", "back": "Back", "email": "Email", diff --git a/src/main.tsx b/src/main.tsx index 13d7ff12..b8bdaf0f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,7 +2,7 @@ import {StrictMode, useEffect, useState, useRef} from 'react' import {createRoot} from 'react-dom/client' import './index.css' import DesktopApp from './ui/Desktop/DesktopApp.tsx' -import { MobileApp } from './ui/Mobile/MobileApp.tsx' +import {MobileApp} from './ui/Mobile/MobileApp.tsx' import {ThemeProvider} from "@/components/theme-provider" import './i18n/i18n' import {isElectron} from './ui/main-axios.ts' @@ -54,10 +54,10 @@ function RootApp() { const width = useWindowWidth(); const isMobile = width < 768; if (isElectron()) { - return ; + return ; } - return isMobile ? : ; + return isMobile ? : ; } createRoot(document.getElementById('root')!).render( diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts deleted file mode 100644 index f2176d2e..00000000 --- a/src/types/electron.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -interface ElectronAPI { - getAppVersion: () => Promise; - getPlatform: () => Promise; - getServerConfig: () => Promise<{ serverUrl: string; lastUpdated: string } | null>; - saveServerConfig: (config: { serverUrl: string; lastUpdated: string }) => Promise<{ success: boolean; error?: string }>; - testServerConnection: (serverUrl: string) => Promise<{ success: boolean; error?: string; status?: number }>; - showSaveDialog: (options: any) => Promise; - showOpenDialog: (options: any) => Promise; - onUpdateAvailable: (callback: () => void) => void; - onUpdateDownloaded: (callback: () => void) => void; - removeAllListeners: (channel: string) => void; - isElectron: boolean; - isDev: boolean; - invoke: (channel: string, ...args: any[]) => Promise; -} - -interface Window { - electronAPI?: ElectronAPI; - IS_ELECTRON?: boolean; -} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 302bed98..9232f3f5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,7 +4,7 @@ // This file contains all shared interfaces and types used across the application // to avoid duplication and ensure consistency. -import type { Client } from 'ssh2'; +import type {Client} from 'ssh2'; // ============================================================================ // SSH HOST TYPES diff --git a/src/ui/Desktop/Admin/AdminSettings.tsx b/src/ui/Desktop/Admin/AdminSettings.tsx index 73fdf0d8..e7a93ce3 100644 --- a/src/ui/Desktop/Admin/AdminSettings.tsx +++ b/src/ui/Desktop/Admin/AdminSettings.tsx @@ -74,24 +74,19 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. React.useEffect(() => { const jwt = getCookie("jwt"); if (!jwt) return; - - // Check if we're in Electron and have a server configured + if (isElectron()) { - // In Electron, check if we have a configured server const serverUrl = (window as any).configuredServerUrl; if (!serverUrl) { - console.log('No server configured in Electron, skipping API calls'); return; } } - + getOIDCConfig() .then(res => { if (res) setOidcConfig(res); }) .catch((err) => { - console.error('Failed to fetch OIDC config:', err); - // Only show error if it's not a "no server configured" error if (!err.message?.includes('No server configured')) { toast.error(t('admin.failedToFetchOidcConfig')); } @@ -100,15 +95,13 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. }, []); React.useEffect(() => { - // Check if we're in Electron and have a server configured if (isElectron()) { const serverUrl = (window as any).configuredServerUrl; if (!serverUrl) { - console.log('No server configured in Electron, skipping registration status check'); return; } } - + getRegistrationAllowed() .then(res => { if (typeof res?.allowed === 'boolean') { @@ -116,8 +109,6 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. } }) .catch((err) => { - console.error('Failed to fetch registration status:', err); - // Only show error if it's not a "no server configured" error if (!err.message?.includes('No server configured')) { toast.error(t('admin.failedToFetchRegistrationStatus')); } @@ -127,23 +118,19 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. const fetchUsers = async () => { const jwt = getCookie("jwt"); if (!jwt) return; - - // Check if we're in Electron and have a server configured + if (isElectron()) { const serverUrl = (window as any).configuredServerUrl; if (!serverUrl) { - console.log('No server configured in Electron, skipping user fetch'); return; } } - + setUsersLoading(true); try { const response = await getUserList(); setUsers(response.users); } catch (err) { - console.error('Failed to fetch users:', err); - // Only show error if it's not a "no server configured" error if (!err.message?.includes('No server configured')) { toast.error(t('admin.failedToFetchUsers')); } @@ -171,7 +158,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. const required = ['client_id', 'client_secret', 'issuer_url', 'authorization_url', 'token_url']; const missing = required.filter(f => !oidcConfig[f as keyof typeof oidcConfig]); if (missing.length > 0) { - setOidcError(t('admin.missingRequiredFields', { fields: missing.join(', ') })); + setOidcError(t('admin.missingRequiredFields', {fields: missing.join(', ')})); setOidcLoading(false); return; } @@ -199,7 +186,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. const jwt = getCookie("jwt"); try { await makeUserAdmin(newAdminUsername.trim()); - toast.success(t('admin.userIsNowAdmin', { username: newAdminUsername })); + toast.success(t('admin.userIsNowAdmin', {username: newAdminUsername})); setNewAdminUsername(""); fetchUsers(); } catch (err: any) { @@ -211,15 +198,14 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. const handleRemoveAdminStatus = async (username: string) => { confirmWithToast( - t('admin.removeAdminStatus', { username }), + t('admin.removeAdminStatus', {username}), async () => { const jwt = getCookie("jwt"); try { await removeAdminStatus(username); - toast.success(t('admin.adminStatusRemoved', { username })); + toast.success(t('admin.adminStatusRemoved', {username})); fetchUsers(); } catch (err: any) { - console.error('Failed to remove admin status:', err); toast.error(t('admin.failedToRemoveAdminStatus')); } } @@ -228,15 +214,14 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. const handleDeleteUser = async (username: string) => { confirmWithToast( - t('admin.deleteUser', { username }), + t('admin.deleteUser', {username}), async () => { const jwt = getCookie("jwt"); try { await deleteUser(username); - toast.success(t('admin.userDeletedSuccessfully', { username })); + toast.success(t('admin.userDeletedSuccessfully', {username})); fetchUsers(); } catch (err: any) { - console.error('Failed to delete user:', err); toast.error(t('admin.failedToDeleteUser')); } }, @@ -301,9 +286,9 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.

{t('admin.externalAuthentication')}

{t('admin.configureExternalProvider')}

-
{usersLoading ? ( -
{t('admin.loadingUsers')}
+
{t('admin.loadingUsers')}
) : (
diff --git a/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx index 62afa79d..d45cf67a 100644 --- a/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx +++ b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx @@ -1,31 +1,28 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { Controller, useForm } from "react-hook-form" -import { z } from "zod" +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" import { Form, FormControl, - FormDescription, FormField, FormItem, FormLabel, - FormMessage, } 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 React, { useEffect, useRef, useState } from "react" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { toast } from "sonner" -import { createCredential, updateCredential, getCredentials, getCredentialDetails } from '@/ui/main-axios' -import { useTranslation } from "react-i18next" -import type { Credential, CredentialEditorProps, CredentialData } from '../../../../types/index.js' +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 React, {useEffect, useRef, useState} from "react" +import {toast} from "sonner" +import {createCredential, updateCredential, getCredentials, getCredentialDetails} from '@/ui/main-axios' +import {useTranslation} from "react-i18next" +import type {Credential, CredentialEditorProps, CredentialData} from '../../../../types/index.js' -export function CredentialEditor({ editingCredential, onFormSubmit }: CredentialEditorProps) { - const { t } = useTranslation(); +export function CredentialEditor({editingCredential, onFormSubmit}: CredentialEditorProps) { + const {t} = useTranslation(); const [credentials, setCredentials] = useState([]); const [folders, setFolders] = useState([]); const [loading, setLoading] = useState(true); @@ -64,7 +61,6 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential const fullDetails = await getCredentialDetails(editingCredential.id); setFullCredentialDetails(fullDetails); } catch (error) { - console.error('Failed to fetch credential details:', error); toast.error(t('credentials.failedToFetchCredentialDetails')); } } else { @@ -139,7 +135,6 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential const defaultAuthType = fullCredentialDetails.authType; setAuthTab(defaultAuthType); - // Force form reset with a small delay to ensure proper rendering setTimeout(() => { const formData = { name: fullCredentialDetails.name || "", @@ -153,16 +148,15 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential keyPassword: "", keyType: "auto" as const, }; - - // Only set the relevant authentication fields based on authType + if (defaultAuthType === 'password') { formData.password = fullCredentialDetails.password || ""; } else if (defaultAuthType === 'key') { - formData.key = "existing_key"; // Placeholder to indicate existing key + formData.key = "existing_key"; formData.keyPassword = fullCredentialDetails.keyPassword || ""; formData.keyType = (fullCredentialDetails.keyType as any) || "auto" as const; } - + form.reset(formData); setTagInput(""); }, 100); @@ -222,10 +216,10 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential if (editingCredential) { await updateCredential(editingCredential.id, submitData); - toast.success(t('credentials.credentialUpdatedSuccessfully', { name: data.name })); + toast.success(t('credentials.credentialUpdatedSuccessfully', {name: data.name})); } else { await createCredential(submitData); - toast.success(t('credentials.credentialAddedSuccessfully', { name: data.name })); + toast.success(t('credentials.credentialAddedSuccessfully', {name: data.name})); } if (onFormSubmit) { @@ -233,8 +227,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential } window.dispatchEvent(new CustomEvent('credentials:changed')); - - // Reset form after successful submission + form.reset(); } catch (error) { toast.error(t('credentials.failedToSaveCredential')); @@ -282,15 +275,15 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential }, [folderDropdownOpen]); const keyTypeOptions = [ - { value: 'auto', label: t('hosts.autoDetect') }, - { value: 'ssh-rsa', label: t('hosts.rsa') }, - { value: 'ssh-ed25519', label: t('hosts.ed25519') }, - { value: 'ecdsa-sha2-nistp256', label: t('hosts.ecdsaNistP256') }, - { value: 'ecdsa-sha2-nistp384', label: t('hosts.ecdsaNistP384') }, - { value: 'ecdsa-sha2-nistp521', label: t('hosts.ecdsaNistP521') }, - { value: 'ssh-dss', label: t('hosts.dsa') }, - { value: 'ssh-rsa-sha2-256', label: t('hosts.rsaSha2256') }, - { value: 'ssh-rsa-sha2-512', label: t('hosts.rsaSha2512') }, + {value: 'auto', label: t('hosts.autoDetect')}, + {value: 'ssh-rsa', label: t('hosts.rsa')}, + {value: 'ssh-ed25519', label: t('hosts.ed25519')}, + {value: 'ecdsa-sha2-nistp256', label: t('hosts.ecdsaNistP256')}, + {value: 'ecdsa-sha2-nistp384', label: t('hosts.ecdsaNistP384')}, + {value: 'ecdsa-sha2-nistp521', label: t('hosts.ecdsaNistP521')}, + {value: 'ssh-dss', label: t('hosts.dsa')}, + {value: 'ssh-rsa-sha2-256', label: t('hosts.rsaSha2256')}, + {value: 'ssh-rsa-sha2-512', label: t('hosts.rsaSha2512')}, ]; const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false); @@ -330,7 +323,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential ( + render={({field}) => ( {t('credentials.credentialName')} @@ -343,7 +336,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential ( + render={({field}) => ( {t('credentials.username')} @@ -358,7 +351,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential ( + render={({field}) => ( {t('credentials.description')} @@ -371,7 +364,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential ( + render={({field}) => ( {t('credentials.folder')} @@ -416,7 +409,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential ( + render={({field}) => ( {t('credentials.tags')} @@ -482,18 +475,14 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential const newAuthType = value as 'password' | 'key'; setAuthTab(newAuthType); form.setValue('authType', newAuthType); - - // Clear ALL authentication fields first + form.setValue('password', ''); form.setValue('key', null); form.setValue('keyPassword', ''); form.setValue('keyType', 'auto'); - - // Then set only the relevant fields based on auth type + if (newAuthType === 'password') { - // Password fields will be filled by user } else if (newAuthType === 'key') { - // Key fields will be filled by user } }} className="flex-1 flex flex-col h-full min-h-0" @@ -506,11 +495,12 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential ( + render={({field}) => ( {t('credentials.password')} - + )} @@ -521,7 +511,6 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential value={keyInputMethod} onValueChange={(value) => { setKeyInputMethod(value as 'upload' | 'paste'); - // Clear the other field when switching if (value === 'upload') { form.setValue('key', null); } else { @@ -530,7 +519,8 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential }} className="w-full" > - + {t('hosts.uploadFile')} {t('hosts.pasteKey')} @@ -538,7 +528,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential ( + render={({field}) => ( {t('credentials.sshPrivateKey')} @@ -560,8 +550,8 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential > - {field.value === "existing_key" ? t('hosts.existingKey') : - field.value ? (editingCredential ? t('credentials.updateKey') : field.value.name) : t('credentials.upload')} + {field.value === "existing_key" ? t('hosts.existingKey') : + field.value ? (editingCredential ? t('credentials.updateKey') : field.value.name) : t('credentials.upload')} @@ -573,7 +563,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential ( + render={({field}) => ( {t('credentials.keyPassword')} @@ -588,7 +578,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential ( + render={({field}) => ( {t('credentials.keyType')} @@ -607,7 +597,8 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential ref={keyTypeDropdownRef} className="absolute bottom-full left-0 z-50 mb-1 w-full bg-dark-bg border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1" > -
+
{keyTypeOptions.map((opt) => ( diff --git a/src/ui/Desktop/Apps/Credentials/CredentialViewer.tsx b/src/ui/Desktop/Apps/Credentials/CredentialViewer.tsx index 87d11b7f..76d6f42c 100644 --- a/src/ui/Desktop/Apps/Credentials/CredentialViewer.tsx +++ b/src/ui/Desktop/Apps/Credentials/CredentialViewer.tsx @@ -1,36 +1,34 @@ -import React, { useState, useEffect } from 'react'; -import { Button } from "@/components/ui/button"; -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"; -import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet"; -import { - Key, - User, - Calendar, - Hash, +import React, {useState, useEffect} from 'react'; +import {Button} from "@/components/ui/button"; +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"; +import {Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle} from "@/components/ui/sheet"; +import { + Key, + User, + Calendar, + Hash, Folder, Edit3, Copy, - Settings, Shield, Clock, Server, Eye, EyeOff, - ExternalLink, AlertTriangle, CheckCircle, FileText } from 'lucide-react'; -import { getCredentialDetails, getCredentialHosts } from '@/ui/main-axios'; -import { toast } from 'sonner'; -import { useTranslation } from 'react-i18next'; -import type { Credential, HostInfo, CredentialViewerProps } from '../../../types/index.js'; +import {getCredentialDetails, getCredentialHosts} from '@/ui/main-axios'; +import {toast} from 'sonner'; +import {useTranslation} from 'react-i18next'; +import type {Credential, HostInfo, CredentialViewerProps} from '../../../types/index.js'; -const CredentialViewer: React.FC = ({ credential, onClose, onEdit }) => { - const { t } = useTranslation(); +const CredentialViewer: React.FC = ({credential, onClose, onEdit}) => { + const {t} = useTranslation(); const [credentialDetails, setCredentialDetails] = useState(null); const [hostsUsing, setHostsUsing] = useState([]); const [loading, setLoading] = useState(true); @@ -47,7 +45,6 @@ const CredentialViewer: React.FC = ({ credential, onClose const response = await getCredentialDetails(credential.id); setCredentialDetails(response); } catch (error) { - console.error('Failed to fetch credential details:', error); toast.error(t('credentials.failedToFetchCredentialDetails')); } }; @@ -57,7 +54,6 @@ const CredentialViewer: React.FC = ({ credential, onClose const response = await getCredentialHosts(credential.id); setHostsUsing(response); } catch (error) { - console.error('Failed to fetch hosts using credential:', error); toast.error(t('credentials.failedToFetchHostsUsing')); } finally { setLoading(false); @@ -74,7 +70,7 @@ const CredentialViewer: React.FC = ({ credential, onClose const copyToClipboard = async (text: string, fieldName: string) => { try { await navigator.clipboard.writeText(text); - toast.success(t('copiedToClipboard', { field: fieldName })); + toast.success(t('copiedToClipboard', {field: fieldName})); } catch (error) { toast.error(t('credentials.failedToCopy')); } @@ -86,9 +82,9 @@ const CredentialViewer: React.FC = ({ credential, onClose const getAuthIcon = (authType: string) => { return authType === 'password' ? ( - + ) : ( - + ); }; @@ -114,20 +110,21 @@ const CredentialViewer: React.FC = ({ credential, onClose size="sm" onClick={() => toggleSensitiveVisibility(fieldName)} > - {isVisible ? : } + {isVisible ? : }
{isVisible ? ( -
+                        
                             {value}
                         
) : ( @@ -167,11 +164,13 @@ const CredentialViewer: React.FC = ({ credential, onClose
- + {credentialDetails.authType} {credentialDetails.keyType && ( - + {credentialDetails.keyType} )} @@ -181,14 +180,15 @@ const CredentialViewer: React.FC = ({ credential, onClose
{/* Tab Navigation */} -
+
@@ -216,24 +216,28 @@ const CredentialViewer: React.FC = ({ credential, onClose
- {t('credentials.basicInformation')} + {t('credentials.basicInformation')}
- +
-
{t('common.username')}
-
{credentialDetails.username}
+
{t('common.username')}
+
{credentialDetails.username}
{credentialDetails.folder && (
- +
-
{t('common.folder')}
+
{t('common.folder')}
{credentialDetails.folder}
@@ -241,9 +245,10 @@ const CredentialViewer: React.FC = ({ credential, onClose {credentialDetails.tags.length > 0 && (
- +
-
{t('hosts.tags')}
+
{t('hosts.tags')}
{credentialDetails.tags.map((tag, index) => ( @@ -255,20 +260,22 @@ const CredentialViewer: React.FC = ({ credential, onClose
)} - +
- +
-
{t('credentials.created')}
+
{t('credentials.created')}
{formatDate(credentialDetails.createdAt)}
- +
-
{t('credentials.lastModified')}
+
{t('credentials.lastModified')}
{formatDate(credentialDetails.updatedAt)}
@@ -290,19 +297,24 @@ const CredentialViewer: React.FC = ({ credential, onClose
{credentialDetails.lastUsed && ( -
- +
+
-
{t('credentials.lastUsed')}
-
{formatDate(credentialDetails.lastUsed)}
+
{t('credentials.lastUsed')}
+
{formatDate(credentialDetails.lastUsed)}
)} -
- +
+
-
{t('credentials.connectedHosts')}
+
{t('credentials.connectedHosts')}
{hostsUsing.length}
@@ -315,7 +327,7 @@ const CredentialViewer: React.FC = ({ credential, onClose - + {t('credentials.securityDetails')} @@ -323,8 +335,9 @@ const CredentialViewer: React.FC = ({ credential, onClose -
- +
+
{t('credentials.credentialSecured')} @@ -345,10 +358,11 @@ const CredentialViewer: React.FC = ({ credential, onClose {credentialDetails.authType === 'key' && (

{t('credentials.keyAuthentication')}

- +
-
+
{t('credentials.keyType')}
@@ -358,17 +372,18 @@ const CredentialViewer: React.FC = ({ credential, onClose
{renderSensitiveField(credentialDetails.key, 'key', t('credentials.privateKey'), true)} - + {credentialDetails.keyPassword && renderSensitiveField( - credentialDetails.keyPassword, - 'keyPassword', + credentialDetails.keyPassword, + 'keyPassword', t('credentials.keyPassphrase') )}
)} -
- +
+
{t('credentials.securityReminder')} @@ -386,7 +401,7 @@ const CredentialViewer: React.FC = ({ credential, onClose - + {t('credentials.hostsUsingCredential')} {hostsUsing.length} @@ -394,20 +409,21 @@ const CredentialViewer: React.FC = ({ credential, onClose {hostsUsing.length === 0 ? (
- +

{t('credentials.noHostsUsingCredential')}

) : (
{hostsUsing.map((host) => ( -
- +
@@ -418,7 +434,8 @@ const CredentialViewer: React.FC = ({ credential, onClose
-
+
{formatDate(host.createdAt)}
@@ -436,7 +453,7 @@ const CredentialViewer: React.FC = ({ credential, onClose {t('common.close')} diff --git a/src/ui/Desktop/Apps/Credentials/CredentialsManager.tsx b/src/ui/Desktop/Apps/Credentials/CredentialsManager.tsx index 2f0cc64b..cf2bf35c 100644 --- a/src/ui/Desktop/Apps/Credentials/CredentialsManager.tsx +++ b/src/ui/Desktop/Apps/Credentials/CredentialsManager.tsx @@ -1,13 +1,13 @@ -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 { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { - Search, - Key, +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 {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion"; +import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"; +import { + Search, + Key, Folder, Edit, Trash2, @@ -20,16 +20,16 @@ import { X, Check } from 'lucide-react'; -import { getCredentials, deleteCredential, updateCredential, renameCredentialFolder } from '@/ui/main-axios'; -import { toast } from 'sonner'; -import { useTranslation } from 'react-i18next'; -import { useConfirmation } from '@/hooks/use-confirmation.ts'; +import {getCredentials, deleteCredential, updateCredential, renameCredentialFolder} from '@/ui/main-axios'; +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 type {Credential, CredentialsManagerProps} from '../../../../types/index.js'; -export function CredentialsManager({ onEditCredential }: CredentialsManagerProps) { - const { t } = useTranslation(); - const { confirmWithToast } = useConfirmation(); +export function CredentialsManager({onEditCredential}: CredentialsManagerProps) { + const {t} = useTranslation(); + const {confirmWithToast} = useConfirmation(); const [credentials, setCredentials] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -61,7 +61,6 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps }; - const handleEdit = (credential: Credential) => { if (onEditCredential) { onEditCredential(credential); @@ -71,11 +70,11 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps const handleDelete = async (credentialId: number, credentialName: string) => { confirmWithToast( - t('credentials.confirmDeleteCredential', { name: credentialName }), + t('credentials.confirmDeleteCredential', {name: credentialName}), async () => { try { await deleteCredential(credentialId); - toast.success(t('credentials.credentialDeletedSuccessfully', { name: credentialName })); + toast.success(t('credentials.credentialDeletedSuccessfully', {name: credentialName})); await fetchCredentials(); window.dispatchEvent(new CustomEvent('credentials:changed')); } catch (err: any) { @@ -93,13 +92,16 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps const handleRemoveFromFolder = async (credential: Credential) => { confirmWithToast( - t('credentials.confirmRemoveFromFolder', { name: credential.name || credential.username, folder: credential.folder }), + t('credentials.confirmRemoveFromFolder', { + name: credential.name || credential.username, + folder: credential.folder + }), async () => { try { setOperationLoading(true); - const updatedCredential = { ...credential, folder: '' }; + const updatedCredential = {...credential, folder: ''}; await updateCredential(credential.id, updatedCredential); - toast.success(t('credentials.removedFromFolder', { name: credential.name || credential.username })); + toast.success(t('credentials.removedFromFolder', {name: credential.name || credential.username})); await fetchCredentials(); window.dispatchEvent(new CustomEvent('credentials:changed')); } catch (err) { @@ -121,7 +123,7 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps try { setOperationLoading(true); await renameCredentialFolder(oldName, editingFolderName.trim()); - toast.success(t('credentials.folderRenamed', { oldName, newName: editingFolderName.trim() })); + toast.success(t('credentials.folderRenamed', {oldName, newName: editingFolderName.trim()})); await fetchCredentials(); window.dispatchEvent(new CustomEvent('credentials:changed')); setEditingFolder(null); @@ -143,11 +145,10 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps setEditingFolderName(''); }; - // Drag and drop handlers const handleDragStart = (e: React.DragEvent, credential: Credential) => { setDraggedCredential(credential); e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', ''); // Required for Firefox + e.dataTransfer.setData('text/plain', ''); }; const handleDragEnd = () => { @@ -182,7 +183,7 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps if (!draggedCredential) return; const newFolder = targetFolder === t('credentials.uncategorized') ? '' : targetFolder; - + if (draggedCredential.folder === newFolder) { setDraggedCredential(null); return; @@ -190,11 +191,11 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps try { setOperationLoading(true); - const updatedCredential = { ...draggedCredential, folder: newFolder }; + const updatedCredential = {...draggedCredential, folder: newFolder}; await updateCredential(draggedCredential.id, updatedCredential); - toast.success(t('credentials.movedToFolder', { + toast.success(t('credentials.movedToFolder', { name: draggedCredential.name || draggedCredential.username, - folder: targetFolder + folder: targetFolder })); await fetchCredentials(); window.dispatchEvent(new CustomEvent('credentials:changed')); @@ -287,7 +288,7 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps

{t('credentials.sshCredentials')}

- {t('credentials.credentialsCount', { count: 0 })} + {t('credentials.credentialsCount', {count: 0})}

@@ -316,7 +317,7 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps

{t('credentials.sshCredentials')}

- {t('credentials.credentialsCount', { count: filteredAndSortedCredentials.length })} + {t('credentials.credentialsCount', {count: filteredAndSortedCredentials.length})}

@@ -339,8 +340,8 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps
{Object.entries(credentialsByFolder).map(([folder, folderCredentials]) => ( -
{editingFolder === folder ? ( -
e.stopPropagation()}> +
e.stopPropagation()}> setEditingFolderName(e.target.value)} @@ -395,8 +397,8 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps
) : ( <> - { e.stopPropagation(); if (folder !== t('credentials.uncategorized')) { @@ -471,11 +473,13 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps className="h-5 w-5 p-0 text-orange-500 hover:text-orange-700 hover:bg-orange-500/10" disabled={operationLoading} > - + -

Remove from folder "{credential.folder}"

+

Remove from folder + "{credential.folder}"

)} @@ -538,7 +542,8 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps )}
- + {credential.authType === 'password' ? ( ) : ( @@ -547,7 +552,8 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps {credential.authType} {credential.authType === 'key' && credential.keyType && ( - + {credential.keyType} )} @@ -558,7 +564,8 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps

Click to edit credential

-

Drag to move between folders

+

Drag to + move between folders

diff --git a/src/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx b/src/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx index 84fb12c6..eea50322 100644 --- a/src/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx +++ b/src/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx @@ -1,8 +1,8 @@ import React from "react"; -import { FileManagerTabList } from "./FileManagerTabList.tsx"; +import {FileManagerTabList} from "./FileManagerTabList.tsx"; interface FileManagerTopNavbarProps { - tabs: {id: string | number, title: string}[]; + tabs: { id: string | number, title: string }[]; activeTab: string | number; setActiveTab: (tab: string | number) => void; closeTab: (tab: string | number) => void; @@ -10,8 +10,8 @@ interface FileManagerTopNavbarProps { } export function FIleManagerTopNavbar(props: FileManagerTopNavbarProps): React.ReactElement { - const { tabs, activeTab, setActiveTab, closeTab, onHomeClick } = props; - + const {tabs, activeTab, setActiveTab, closeTab, onHomeClick} = props; + return ( void, embedded?: boolean, initialHost?: SSHHost | null, @@ -122,10 +120,9 @@ export function FileManager({onSelectView, embedded = false, initialHost = null, type: 'directory' }))); } catch (err: any) { - console.error('Failed to fetch home data:', err); const {toast} = await import('sonner'); toast.error(t('fileManager.failedToFetchHomeData')); - // Close the file manager tab on connection failure + if (onClose) { onClose(); } @@ -371,7 +368,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null, loading: false } : t)); - // Handle toast notification from backend if (result?.toast) { toast[result.toast.type](result.toast.message); } else { @@ -389,7 +385,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null, hostId: currentHost.id }); } catch (recentErr) { - console.error('Failed to add recent file:', recentErr); } })(), ]).then(() => { @@ -443,14 +438,13 @@ export function FileManager({onSelectView, embedded = false, initialHost = null, try { const {deleteSSHItem} = await import('@/ui/main-axios.ts'); const response = await deleteSSHItem(currentHost.id.toString(), item.path, item.type === 'directory'); - - // Handle toast notification from backend + if (response?.toast) { toast[response.toast.type](response.toast.message); } else { toast.success(`${item.type === 'directory' ? t('fileManager.folder') : t('fileManager.file')} ${t('fileManager.deletedSuccessfully')}`); } - + setDeletingItem(null); handleOperationComplete(); } catch (error: any) { @@ -475,7 +469,8 @@ export function FileManager({onSelectView, embedded = false, initialHost = null, onPathChange={updateCurrentPath} />
-
+

{t('fileManager.connectToServer')}

{t('fileManager.selectServerToEdit')}

@@ -546,7 +541,8 @@ export function FileManager({onSelectView, embedded = false, initialHost = null,
-
+
{activeTab === 'home' ? ( @@ -605,7 +601,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null, {t('fileManager.confirmDelete')}

- {t('fileManager.confirmDeleteMessage', { name: deletingItem.name })} + {t('fileManager.confirmDeleteMessage', {name: deletingItem.name})} {deletingItem.type === 'directory' && ` ${t('fileManager.deleteDirectoryWarning')}`}

diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx index 57ec994f..c64c1559 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from "react"; +import React, {useEffect} from "react"; import CodeMirror from "@uiw/react-codemirror"; import {loadLanguage} from '@uiw/codemirror-extensions-langs'; import {hyperLink} from '@uiw/codemirror-extensions-hyper-link'; diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx index 10d6b6c6..4d6f4c05 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx @@ -5,7 +5,7 @@ import {Tabs, TabsList, TabsTrigger, TabsContent} from '@/components/ui/tabs.tsx import {Input} from '@/components/ui/input.tsx'; import {useState} from 'react'; import {useTranslation} from 'react-i18next'; -import type { FileItem, ShortcutItem } from '../../../types/index'; +import type {FileItem, ShortcutItem} from '../../../types/index'; interface FileManagerHomeViewProps { recent: FileItem[]; @@ -111,9 +111,12 @@ export function FileManagerHomeView({

setTab(v as 'recent' | 'pinned' | 'shortcuts')} className="w-full"> - {t('fileManager.recent')} - {t('fileManager.pinned')} - {t('fileManager.folderShortcuts')} + {t('fileManager.recent')} + {t('fileManager.pinned')} + {t('fileManager.folderShortcuts')} diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx index 0dee139e..cc4cf137 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx @@ -1,6 +1,5 @@ import React, {useEffect, useState, useRef, forwardRef, useImperativeHandle} from 'react'; -import {Separator} from '@/components/ui/separator.tsx'; -import {CornerDownLeft, Folder, File, Server, ArrowUp, Pin, MoreVertical, Trash2, Edit3} from 'lucide-react'; +import {Folder, File, ArrowUp, Pin, MoreVertical, Trash2, Edit3} from 'lucide-react'; import {ScrollArea} from '@/components/ui/scroll-area.tsx'; import {cn} from '@/lib/utils.ts'; import {Input} from '@/components/ui/input.tsx'; @@ -11,18 +10,16 @@ import { listSSHFiles, renameSSHItem, deleteSSHItem, - getFileManagerRecent, getFileManagerPinned, addFileManagerPinned, removeFileManagerPinned, - readSSHFile, getSSHStatus, connectSSH } from '@/ui/main-axios.ts'; -import type { SSHHost, FileManagerLeftSidebarProps } from '../../../types/index.js'; +import type {SSHHost} from '../../../types/index.js'; const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( - {onSelectView, onOpenFile, tabs, host, onOperationComplete, onError, onSuccess, onPathChange, onDeleteItem}: { + {onOpenFile, tabs, host, onOperationComplete, onPathChange, onDeleteItem}: { onSelectView?: (view: string) => void; onOpenFile: (file: any) => void; tabs: any[]; @@ -55,7 +52,6 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( const [sshSessionId, setSshSessionId] = useState(null); const [filesLoading, setFilesLoading] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); const [connectingSSH, setConnectingSSH] = useState(false); const [connectionCache, setConnectionCache] = useState { - setContextMenu({ visible: false, x: 0, y: 0, item: null }); + setContextMenu({visible: false, x: 0, y: 0, item: null}); }; const handleRename = async (item: any, newName: string) => { @@ -320,24 +316,8 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( } }; - const handleDelete = async (item: any) => { - if (!sshSessionId) return; - - try { - await deleteSSHItem(sshSessionId, item.path, item.type === 'directory'); - toast.success(`${item.type === 'directory' ? t('common.folder') : t('common.file')} ${t('common.deletedSuccessfully')}`); - if (onOperationComplete) { - onOperationComplete(); - } else { - fetchFiles(); - } - } catch (error: any) { - toast.error(error?.response?.data?.error || t('fileManager.failedToDeleteItem')); - } - }; - const startRename = (item: any) => { - setRenamingItem({ item, newName: item.name }); + setRenamingItem({item, newName: item.name}); closeContextMenu(); }; @@ -360,10 +340,12 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( return (
-
+
{host && (
-
+
) : filteredFiles.length === 0 ? ( -
{t('fileManager.noFilesOrFoldersFound')}
+
{t('fileManager.noFilesOrFoldersFound')}
) : (
{filteredFiles.map((item: any) => { const isOpen = (tabs || []).some((t: any) => t.id === item.path); const isRenaming = renamingItem?.item?.path === item.path; const isDeleting = false; - + return (
{item.type === 'directory' ? - : - } + : + } setRenamingItem(prev => prev ? {...prev, newName: e.target.value} : null)} + onChange={(e) => setRenamingItem(prev => prev ? { + ...prev, + newName: e.target.value + } : null)} className="flex-1 h-6 text-sm bg-dark-bg-button border border-dark-border-hover text-white" autoFocus onKeyDown={(e) => { if (e.key === 'Enter') { handleRename(item, renamingItem.newName); } else if (e.key === 'Escape') { - setRenamingItem(null); + setRenamingItem(null); } }} onBlur={() => handleRename(item, renamingItem.newName)} @@ -454,13 +442,17 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( }))} > {item.type === 'directory' ? - : - } - {item.name} + : + } + {item.name}
{item.type === 'file' && ( - )} {!isOpen && ( @@ -505,7 +504,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( handleContextMenu(e, item); }} > - + )}
@@ -536,14 +535,14 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( className="w-full px-3 py-2 text-left text-sm text-white hover:bg-dark-hover flex items-center gap-2" onClick={() => startRename(contextMenu.item)} > - + Rename
diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx index 5a86b5c0..f77fe27f 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx @@ -1,8 +1,7 @@ import React from 'react'; import {Button} from '@/components/ui/button.tsx'; import {Card} from '@/components/ui/card.tsx'; -import {Separator} from '@/components/ui/separator.tsx'; -import {Plus, Folder, File, Star, Trash2, Edit, Link2, Server, Pin} from 'lucide-react'; +import {Folder, File, Trash2, Pin} from 'lucide-react'; import {useTranslation} from 'react-i18next'; interface SSHConnection { @@ -43,12 +42,6 @@ interface FileManagerLeftSidebarVileViewerProps { } export function FileManagerLeftSidebarFileViewer({ - sshConnections, - onAddSSH, - onConnectSSH, - onEditSSH, - onDeleteSSH, - onPinSSH, currentPath, files, onOpenFile, @@ -58,12 +51,9 @@ export function FileManagerLeftSidebarFileViewer({ isLoading, error, isSSHMode, - onSwitchToLocal, - onSwitchToSSH, - currentSSH, }: FileManagerLeftSidebarVileViewerProps) { const {t} = useTranslation(); - + return (
diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx index d21a7199..258251f3 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx @@ -10,14 +10,13 @@ import { Trash2, Edit3, X, - Check, AlertCircle, FileText, Folder } from 'lucide-react'; import {cn} from '@/lib/utils.ts'; import {useTranslation} from 'react-i18next'; -import type { FileManagerOperationsProps } from '../../../types/index.js'; +import type {FileManagerOperationsProps} from '../../../types/index.js'; export function FileManagerOperations({ currentPath, @@ -56,7 +55,7 @@ export function FileManagerOperations({ }; checkContainerWidth(); - + const resizeObserver = new ResizeObserver(checkContainerWidth); if (containerRef.current) { resizeObserver.observe(containerRef.current); @@ -71,32 +70,28 @@ export function FileManagerOperations({ if (!uploadFile || !sshSessionId) return; setIsLoading(true); - - // Show loading toast + const {toast} = await import('sonner'); - const loadingToast = toast.loading(t('fileManager.uploadingFile', { name: uploadFile.name })); - + const loadingToast = toast.loading(t('fileManager.uploadingFile', {name: uploadFile.name})); + try { const content = await uploadFile.text(); const {uploadSSHFile} = await import('@/ui/main-axios.ts'); const response = await uploadSSHFile(sshSessionId, currentPath, uploadFile.name, content); - - // Dismiss loading toast and show success + toast.dismiss(loadingToast); - - // Handle toast notification from backend + if (response?.toast) { toast[response.toast.type](response.toast.message); } else { - onSuccess(t('fileManager.fileUploadedSuccessfully', { name: uploadFile.name })); + onSuccess(t('fileManager.fileUploadedSuccessfully', {name: uploadFile.name})); } - + setShowUpload(false); setUploadFile(null); onOperationComplete(); } catch (error: any) { - // Dismiss loading toast and show error toast.dismiss(loadingToast); onError(error?.response?.data?.error || t('fileManager.failedToUploadFile')); } finally { @@ -108,31 +103,27 @@ export function FileManagerOperations({ if (!newFileName.trim() || !sshSessionId) return; setIsLoading(true); - - // Show loading toast + const {toast} = await import('sonner'); - const loadingToast = toast.loading(t('fileManager.creatingFile', { name: newFileName.trim() })); - + const loadingToast = toast.loading(t('fileManager.creatingFile', {name: newFileName.trim()})); + try { const {createSSHFile} = await import('@/ui/main-axios.ts'); const response = await createSSHFile(sshSessionId, currentPath, newFileName.trim()); - - // Dismiss loading toast + toast.dismiss(loadingToast); - - // Handle toast notification from backend + if (response?.toast) { toast[response.toast.type](response.toast.message); } else { - onSuccess(t('fileManager.fileCreatedSuccessfully', { name: newFileName.trim() })); + onSuccess(t('fileManager.fileCreatedSuccessfully', {name: newFileName.trim()})); } - + setShowCreateFile(false); setNewFileName(''); onOperationComplete(); } catch (error: any) { - // Dismiss loading toast and show error toast.dismiss(loadingToast); onError(error?.response?.data?.error || t('fileManager.failedToCreateFile')); } finally { @@ -144,31 +135,27 @@ export function FileManagerOperations({ if (!newFolderName.trim() || !sshSessionId) return; setIsLoading(true); - - // Show loading toast + const {toast} = await import('sonner'); - const loadingToast = toast.loading(t('fileManager.creatingFolder', { name: newFolderName.trim() })); - + const loadingToast = toast.loading(t('fileManager.creatingFolder', {name: newFolderName.trim()})); + try { const {createSSHFolder} = await import('@/ui/main-axios.ts'); const response = await createSSHFolder(sshSessionId, currentPath, newFolderName.trim()); - - // Dismiss loading toast + toast.dismiss(loadingToast); - - // Handle toast notification from backend + if (response?.toast) { toast[response.toast.type](response.toast.message); } else { - onSuccess(t('fileManager.folderCreatedSuccessfully', { name: newFolderName.trim() })); + onSuccess(t('fileManager.folderCreatedSuccessfully', {name: newFolderName.trim()})); } - + setShowCreateFolder(false); setNewFolderName(''); onOperationComplete(); } catch (error: any) { - // Dismiss loading toast and show error toast.dismiss(loadingToast); onError(error?.response?.data?.error || t('fileManager.failedToCreateFolder')); } finally { @@ -180,35 +167,31 @@ export function FileManagerOperations({ if (!deletePath || !sshSessionId) return; setIsLoading(true); - - // Show loading toast + const {toast} = await import('sonner'); - const loadingToast = toast.loading(t('fileManager.deletingItem', { + const loadingToast = toast.loading(t('fileManager.deletingItem', { type: deleteIsDirectory ? t('fileManager.folder') : t('fileManager.file'), name: deletePath.split('/').pop() })); - + try { const {deleteSSHItem} = await import('@/ui/main-axios.ts'); const response = await deleteSSHItem(sshSessionId, deletePath, deleteIsDirectory); - - // Dismiss loading toast + toast.dismiss(loadingToast); - - // Handle toast notification from backend + if (response?.toast) { toast[response.toast.type](response.toast.message); } else { - onSuccess(t('fileManager.itemDeletedSuccessfully', { type: deleteIsDirectory ? t('fileManager.folder') : t('fileManager.file') })); + onSuccess(t('fileManager.itemDeletedSuccessfully', {type: deleteIsDirectory ? t('fileManager.folder') : t('fileManager.file')})); } - + setShowDelete(false); setDeletePath(''); setDeleteIsDirectory(false); onOperationComplete(); } catch (error: any) { - // Dismiss loading toast and show error toast.dismiss(loadingToast); onError(error?.response?.data?.error || t('fileManager.failedToDeleteItem')); } finally { @@ -220,37 +203,33 @@ export function FileManagerOperations({ if (!renamePath || !newName.trim() || !sshSessionId) return; setIsLoading(true); - - // Show loading toast + const {toast} = await import('sonner'); - const loadingToast = toast.loading(t('fileManager.renamingItem', { + const loadingToast = toast.loading(t('fileManager.renamingItem', { type: renameIsDirectory ? t('fileManager.folder') : t('fileManager.file'), oldName: renamePath.split('/').pop(), newName: newName.trim() })); - + try { const {renameSSHItem} = await import('@/ui/main-axios.ts'); const response = await renameSSHItem(sshSessionId, renamePath, newName.trim()); - - // Dismiss loading toast + toast.dismiss(loadingToast); - - // Handle toast notification from backend + if (response?.toast) { toast[response.toast.type](response.toast.message); } else { - onSuccess(t('fileManager.itemRenamedSuccessfully', { type: renameIsDirectory ? t('fileManager.folder') : t('fileManager.file') })); + onSuccess(t('fileManager.itemRenamedSuccessfully', {type: renameIsDirectory ? t('fileManager.folder') : t('fileManager.file')})); } - + setShowRename(false); setRenamePath(''); setRenameIsDirectory(false); setNewName(''); onOperationComplete(); } catch (error: any) { - // Dismiss loading toast and show error toast.dismiss(loadingToast); onError(error?.response?.data?.error || t('fileManager.failedToRenameItem')); } finally { @@ -577,7 +556,8 @@ export function FileManagerOperations({
- {t('fileManager.warningCannotUndo')} + {t('fileManager.warningCannotUndo')}
diff --git a/src/ui/Desktop/Apps/Host Manager/HostManager.tsx b/src/ui/Desktop/Apps/Host Manager/HostManager.tsx index 4eb426e4..fa8a8ea9 100644 --- a/src/ui/Desktop/Apps/Host Manager/HostManager.tsx +++ b/src/ui/Desktop/Apps/Host Manager/HostManager.tsx @@ -7,7 +7,7 @@ import {CredentialsManager} from "@/ui/Desktop/Apps/Credentials/CredentialsManag import {CredentialEditor} from "@/ui/Desktop/Apps/Credentials/CredentialEditor.tsx"; import {useSidebar} from "@/components/ui/sidebar.tsx"; import {useTranslation} from "react-i18next"; -import type { SSHHost, HostManagerProps } from '../../../types/index'; +import type {SSHHost, HostManagerProps} from '../../../types/index'; export function HostManager({onSelectView, isTopbarOpen}: HostManagerProps): React.ReactElement { const {t} = useTranslation(); @@ -40,7 +40,6 @@ export function HostManager({onSelectView, isTopbarOpen}: HostManagerProps): Rea const handleTabChange = (value: string) => { setActiveTab(value); - // Reset editing states when switching away from edit tabs if (value !== "add_host") { setEditingHost(null); } @@ -95,7 +94,7 @@ export function HostManager({onSelectView, isTopbarOpen}: HostManagerProps): Rea
- +
diff --git a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx index f4d3e35f..44889159 100644 --- a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx +++ b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx @@ -3,15 +3,7 @@ import {Controller, useForm} from "react-hook-form" import {z} from "zod" import {Button} from "@/components/ui/button.tsx" -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form.tsx"; +import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel,} 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" @@ -21,7 +13,7 @@ import React, {useEffect, useRef, useState} from "react"; import {Switch} from "@/components/ui/switch.tsx"; import {Alert, AlertDescription} from "@/components/ui/alert.tsx"; import {toast} from "sonner"; -import {createSSHHost, updateSSHHost, getSSHHosts, getCredentials} from '@/ui/main-axios.ts'; +import {createSSHHost, getCredentials, getSSHHosts, updateSSHHost} from '@/ui/main-axios.ts'; import {useTranslation} from "react-i18next"; import {CredentialSelector} from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx"; @@ -65,8 +57,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi const [authTab, setAuthTab] = useState<'password' | 'key' | 'credential'>('password'); const [keyInputMethod, setKeyInputMethod] = useState<'upload' | 'paste'>('upload'); const isSubmittingRef = useRef(false); - - // Ref for the IP address input to manage focus + const ipInputRef = useRef(null); useEffect(() => { @@ -103,7 +94,6 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi fetchData(); }, []); - // Listen for credential changes to refresh the credential list useEffect(() => { const handleCredentialChange = async () => { try { @@ -126,14 +116,13 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi setFolders(uniqueFolders); setSshConfigurations(uniqueConfigurations); } catch (error) { - // Handle error silently } finally { setLoading(false); } }; - + window.addEventListener('credentials:changed', handleCredentialChange); - + return () => { window.removeEventListener('credentials:changed', handleCredentialChange); }; @@ -247,7 +236,6 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi } }); - // Update username when switching to credential tab and a credential is selected useEffect(() => { if (authTab === 'credential') { const currentCredentialId = form.getValues('credentialId'); @@ -262,7 +250,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi useEffect(() => { if (editingHost) { - const cleanedHost = { ...editingHost }; + const cleanedHost = {...editingHost}; if (cleanedHost.credentialId && cleanedHost.key) { cleanedHost.key = undefined; cleanedHost.keyPassword = undefined; @@ -272,10 +260,10 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi } else if (cleanedHost.key && cleanedHost.password) { cleanedHost.password = undefined; } - + const defaultAuthType = cleanedHost.credentialId ? 'credential' : (cleanedHost.key ? 'key' : 'password'); setAuthTab(defaultAuthType); - + const formData = { name: cleanedHost.name || "", ip: cleanedHost.ip || "", @@ -296,12 +284,11 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi defaultPath: cleanedHost.defaultPath || "/", tunnelConnections: cleanedHost.tunnelConnections || [], }; - - // Only set the relevant authentication fields based on authType + if (defaultAuthType === 'password') { formData.password = cleanedHost.password || ""; } else if (defaultAuthType === 'key') { - formData.key = "existing_key"; // Placeholder to indicate existing key + formData.key = "existing_key"; formData.keyPassword = cleanedHost.keyPassword || ""; formData.keyType = (cleanedHost.keyType as any) || "auto"; } else if (defaultAuthType === 'credential') { @@ -349,7 +336,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi const onSubmit = async (data: FormData) => { try { isSubmittingRef.current = true; - + if (!data.name || data.name.trim() === '') { data.name = `${data.username}@${data.ip}`; } @@ -399,23 +386,22 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi if (editingHost) { const updatedHost = await updateSSHHost(editingHost.id, submitData); - toast.success(t('hosts.hostUpdatedSuccessfully', { name: data.name })); - + toast.success(t('hosts.hostUpdatedSuccessfully', {name: data.name})); + if (onFormSubmit) { onFormSubmit(updatedHost); } } else { const newHost = await createSSHHost(submitData); - toast.success(t('hosts.hostAddedSuccessfully', { name: data.name })); - + toast.success(t('hosts.hostAddedSuccessfully', {name: data.name})); + if (onFormSubmit) { onFormSubmit(newHost); } } window.dispatchEvent(new CustomEvent('ssh-hosts:changed')); - - // Reset form after successful submission + form.reset(); } catch (error) { toast.error(t('hosts.failedToSaveHost')); @@ -574,8 +560,8 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi {t('hosts.ipAddress')} - { field.ref(e); @@ -745,8 +731,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi const newAuthType = value as 'password' | 'key' | 'credential'; setAuthTab(newAuthType); form.setValue('authType', newAuthType); - - // Clear authentication fields based on what we're switching away from + if (newAuthType === 'password') { form.setValue('key', null); form.setValue('keyPassword', ''); @@ -773,11 +758,12 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi ( + render={({field}) => ( {t('hosts.password')} - + )} @@ -788,7 +774,6 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi value={keyInputMethod} onValueChange={(value) => { setKeyInputMethod(value as 'upload' | 'paste'); - // Clear the other field when switching if (value === 'upload') { form.setValue('key', null); } else { @@ -797,7 +782,8 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi }} className="w-full" > - + {t('hosts.uploadFile')} {t('hosts.pasteKey')} @@ -827,8 +813,8 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi > - {field.value === "existing_key" ? t('hosts.existingKey') : - field.value ? (editingHost ? t('hosts.updateKey') : field.value.name) : t('hosts.upload')} + {field.value === "existing_key" ? t('hosts.existingKey') : + field.value ? (editingHost ? t('hosts.updateKey') : field.value.name) : t('hosts.upload')}
@@ -856,7 +842,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi )} /> - +
( + render={({field}) => ( { if (credential) { - // Update username when credential is selected form.setValue('username', credential.username); } }} @@ -1002,7 +987,8 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi sshpass or sudo dnf install sshpass
-
• {t('hosts.macos')} brew +
• {t('hosts.macos')} brew install hudochenkov/sshpass/sshpass
• {t('hosts.windows')}
@@ -1026,9 +1012,9 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
-

- {t('hosts.tunnelForwardDescription', { + {t('hosts.tunnelForwardDescription', { sourcePort: form.watch(`tunnelConnections.${index}.sourcePort`) || '22', endpointPort: form.watch(`tunnelConnections.${index}.endpointPort`) || '224' })} diff --git a/src/ui/Desktop/Apps/Host Manager/HostManagerViewer.tsx b/src/ui/Desktop/Apps/Host Manager/HostManagerViewer.tsx index 0b480961..dfb0a73d 100644 --- a/src/ui/Desktop/Apps/Host Manager/HostManagerViewer.tsx +++ b/src/ui/Desktop/Apps/Host Manager/HostManagerViewer.tsx @@ -1,5 +1,4 @@ import React, {useState, useEffect, useMemo, useRef} from "react"; -import {Card, CardContent} from "@/components/ui/card.tsx"; import {Button} from "@/components/ui/button.tsx"; import {Badge} from "@/components/ui/badge.tsx"; import {ScrollArea} from "@/components/ui/scroll-area.tsx"; @@ -22,14 +21,12 @@ import { FileEdit, Search, Upload, - Info, X, Check, Pencil, FolderMinus } from "lucide-react"; -import {Separator} from "@/components/ui/separator.tsx"; -import type { SSHHost, SSHManagerHostViewerProps } from '../../../../types/index.js'; +import type {SSHHost, SSHManagerHostViewerProps} from '../../../../types/index.js'; export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { const {t} = useTranslation(); @@ -48,16 +45,15 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { useEffect(() => { fetchHosts(); - - // Listen for refresh events from other components + const handleHostsRefresh = () => { fetchHosts(); }; - + window.addEventListener('hosts:refresh', handleHostsRefresh); window.addEventListener('ssh-hosts:changed', handleHostsRefresh); window.addEventListener('folders:changed', handleHostsRefresh); - + return () => { window.removeEventListener('hosts:refresh', handleHostsRefresh); window.removeEventListener('ssh-hosts:changed', handleHostsRefresh); @@ -69,9 +65,9 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { try { setLoading(true); const data = await getSSHHosts(); - + const cleanedHosts = data.map(host => { - const cleanedHost = { ...host }; + const cleanedHost = {...host}; if (cleanedHost.credentialId && cleanedHost.key) { cleanedHost.key = undefined; cleanedHost.keyPassword = undefined; @@ -86,7 +82,7 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { } return cleanedHost; }); - + setHosts(cleanedHosts); setError(null); } catch (err) { @@ -98,11 +94,11 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { const handleDelete = async (hostId: number, hostName: string) => { confirmWithToast( - t('hosts.confirmDelete', { name: hostName }), + t('hosts.confirmDelete', {name: hostName}), async () => { try { await deleteSSHHost(hostId); - toast.success(t('hosts.hostDeletedSuccessfully', { name: hostName })); + toast.success(t('hosts.hostDeletedSuccessfully', {name: hostName})); await fetchHosts(); window.dispatchEvent(new CustomEvent('ssh-hosts:changed')); } catch (err) { @@ -115,41 +111,38 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { const handleExport = (host: SSHHost) => { const actualAuthType = host.credentialId ? 'credential' : (host.key ? 'key' : 'password'); - - // Check if host uses sensitive authentication data + if (actualAuthType === 'credential') { - const confirmMessage = t('hosts.exportCredentialWarning', { - name: host.name || `${host.username}@${host.ip}` + const confirmMessage = t('hosts.exportCredentialWarning', { + name: host.name || `${host.username}@${host.ip}` }); - + confirmWithToast(confirmMessage, () => { performExport(host, actualAuthType); }); return; } else if (actualAuthType === 'password' || actualAuthType === 'key') { - const confirmMessage = t('hosts.exportSensitiveDataWarning', { - name: host.name || `${host.username}@${host.ip}` + const confirmMessage = t('hosts.exportSensitiveDataWarning', { + name: host.name || `${host.username}@${host.ip}` }); - + confirmWithToast(confirmMessage, () => { performExport(host, actualAuthType); }); return; } - - // No sensitive data, proceed directly + performExport(host, actualAuthType); }; const performExport = (host: SSHHost, actualAuthType: string) => { - // Create export data with sensitive fields excluded const exportData: any = { name: host.name, ip: host.ip, port: host.port, username: host.username, - authType: actualAuthType, // Use the determined authType, not the stored one + authType: actualAuthType, folder: host.folder, tags: host.tags, pin: host.pin, @@ -160,18 +153,16 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { tunnelConnections: host.tunnelConnections, }; - // Only include credentialId if actualAuthType is credential, but set it to null for security if (actualAuthType === 'credential') { - exportData.credentialId = null; // Set to null instead of undefined so it's included but empty + exportData.credentialId = null; } - // Remove undefined values from export, but keep null values const cleanExportData = Object.fromEntries( Object.entries(exportData).filter(([_, value]) => value !== undefined) ); - const blob = new Blob([JSON.stringify(cleanExportData, null, 2)], { type: 'application/json' }); + const blob = new Blob([JSON.stringify(cleanExportData, null, 2)], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -193,13 +184,13 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { const handleRemoveFromFolder = async (host: SSHHost) => { confirmWithToast( - t('hosts.confirmRemoveFromFolder', { name: host.name || `${host.username}@${host.ip}`, folder: host.folder }), + t('hosts.confirmRemoveFromFolder', {name: host.name || `${host.username}@${host.ip}`, folder: host.folder}), async () => { try { setOperationLoading(true); - const updatedHost = { ...host, folder: '' }; + const updatedHost = {...host, folder: ''}; await updateSSHHost(host.id, updatedHost); - toast.success(t('hosts.removedFromFolder', { name: host.name || `${host.username}@${host.ip}` })); + toast.success(t('hosts.removedFromFolder', {name: host.name || `${host.username}@${host.ip}`})); await fetchHosts(); window.dispatchEvent(new CustomEvent('ssh-hosts:changed')); } catch (err) { @@ -221,7 +212,7 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { try { setOperationLoading(true); await renameFolder(oldName, editingFolderName.trim()); - toast.success(t('hosts.folderRenamed', { oldName, newName: editingFolderName.trim() })); + toast.success(t('hosts.folderRenamed', {oldName, newName: editingFolderName.trim()})); await fetchHosts(); window.dispatchEvent(new CustomEvent('ssh-hosts:changed')); setEditingFolder(null); @@ -243,11 +234,10 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { setEditingFolderName(''); }; - // Drag and drop handlers const handleDragStart = (e: React.DragEvent, host: SSHHost) => { setDraggedHost(host); e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', ''); // Required for Firefox + e.dataTransfer.setData('text/plain', ''); }; const handleDragEnd = () => { @@ -282,7 +272,7 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { if (!draggedHost) return; const newFolder = targetFolder === t('hosts.uncategorized') ? '' : targetFolder; - + if (draggedHost.folder === newFolder) { setDraggedHost(null); return; @@ -290,11 +280,11 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { try { setOperationLoading(true); - const updatedHost = { ...draggedHost, folder: newFolder }; + const updatedHost = {...draggedHost, folder: newFolder}; await updateSSHHost(draggedHost.id, updatedHost); - toast.success(t('hosts.movedToFolder', { + toast.success(t('hosts.movedToFolder', { name: draggedHost.name || `${draggedHost.username}@${draggedHost.ip}`, - folder: targetFolder + folder: targetFolder })); await fetchHosts(); window.dispatchEvent(new CustomEvent('ssh-hosts:changed')); @@ -332,7 +322,7 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { const result = await bulkImportSSHHosts(hostsArray); if (result.success > 0) { - toast.success(t('hosts.importCompleted', { success: result.success, failed: result.failed })); + toast.success(t('hosts.importCompleted', {success: result.success, failed: result.failed})); if (result.errors.length > 0) { toast.error(`Import errors: ${result.errors.join(', ')}`); } @@ -436,7 +426,7 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {

{t('hosts.sshHosts')}

- {t('hosts.hostsCount', { count: 0 })} + {t('hosts.hostsCount', {count: 0})}

@@ -469,66 +459,66 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { variant="outline" size="sm" onClick={() => { - const sampleData = { - hosts: [ - { - name: "Web Server - Production", - ip: "192.168.1.100", - port: 22, - username: "admin", - authType: "password", - password: "your_secure_password_here", - folder: "Production", - tags: ["web", "production", "nginx"], - pin: true, - enableTerminal: true, - enableTunnel: false, - enableFileManager: true, - defaultPath: "/var/www" - }, - { - name: "Database Server", - ip: "192.168.1.101", - port: 22, - username: "dbadmin", - authType: "key", - key: "-----BEGIN OPENSSH PRIVATE KEY-----\nYour SSH private key content here\n-----END OPENSSH PRIVATE KEY-----", - keyPassword: "optional_key_passphrase", - keyType: "ssh-ed25519", - folder: "Production", - tags: ["database", "production", "postgresql"], - pin: false, - enableTerminal: true, - enableTunnel: true, - enableFileManager: false, - tunnelConnections: [ + const sampleData = { + hosts: [ { - sourcePort: 5432, - endpointPort: 5432, - endpointHost: "Web Server - Production", - maxRetries: 3, - retryInterval: 10, - autoStart: true + name: "Web Server - Production", + ip: "192.168.1.100", + port: 22, + username: "admin", + authType: "password", + password: "your_secure_password_here", + folder: "Production", + tags: ["web", "production", "nginx"], + pin: true, + enableTerminal: true, + enableTunnel: false, + enableFileManager: true, + defaultPath: "/var/www" + }, + { + name: "Database Server", + ip: "192.168.1.101", + port: 22, + username: "dbadmin", + authType: "key", + key: "-----BEGIN OPENSSH PRIVATE KEY-----\nYour SSH private key content here\n-----END OPENSSH PRIVATE KEY-----", + keyPassword: "optional_key_passphrase", + keyType: "ssh-ed25519", + folder: "Production", + tags: ["database", "production", "postgresql"], + pin: false, + enableTerminal: true, + enableTunnel: true, + enableFileManager: false, + tunnelConnections: [ + { + sourcePort: 5432, + endpointPort: 5432, + endpointHost: "Web Server - Production", + maxRetries: 3, + retryInterval: 10, + autoStart: true + } + ] + }, + { + name: "Development Server", + ip: "192.168.1.102", + port: 2222, + username: "developer", + authType: "credential", + credentialId: 1, + folder: "Development", + tags: ["dev", "testing"], + pin: false, + enableTerminal: true, + enableTunnel: false, + enableFileManager: true, + defaultPath: "/home/developer" } ] - }, - { - name: "Development Server", - ip: "192.168.1.102", - port: 2222, - username: "developer", - authType: "credential", - credentialId: 1, - folder: "Development", - tags: ["dev", "testing"], - pin: false, - enableTerminal: true, - enableTunnel: false, - enableFileManager: true, - defaultPath: "/home/developer" - } - ] - }; + }; const blob = new Blob([JSON.stringify(sampleData, null, 2)], {type: 'application/json'}); const url = URL.createObjectURL(blob); @@ -590,7 +580,7 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {

{t('hosts.sshHosts')}

- {t('hosts.hostsCount', { count: filteredAndSortedHosts.length })} + {t('hosts.hostsCount', {count: filteredAndSortedHosts.length})}

@@ -738,8 +728,8 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
{Object.entries(hostsByFolder).map(([folder, folderHosts]) => ( -
{editingFolder === folder ? ( -
e.stopPropagation()}> +
e.stopPropagation()}> setEditingFolderName(e.target.value)} @@ -794,8 +785,8 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
) : ( <> - { e.stopPropagation(); if (folder !== t('hosts.uncategorized')) { @@ -851,143 +842,149 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) { {host.name || `${host.username}@${host.ip}`}
-

- {host.ip}:{host.port} -

-

- {host.username} -

-
-
- {host.folder && host.folder !== '' && ( - - - - - -

Remove from folder "{host.folder}"

-
-
- )} - - - - - -

Edit host

-
-
- - - - - -

Delete host

-
-
- - - - - -

Export host

-
-
+

+ {host.ip}:{host.port} +

+

+ {host.username} +

+
+
+ {host.folder && host.folder !== '' && ( + + + + + +

Remove from folder + "{host.folder}"

+
+
+ )} + + + + + +

Edit host

+
+
+ + + + + +

Delete host

+
+
+ + + + + +

Export host

+
+
-
-
+
+
-
- {host.tags && host.tags.length > 0 && ( -
- {host.tags.slice(0, 6).map((tag, index) => ( - - - {tag} - - ))} - {host.tags.length > 6 && ( - - +{host.tags.length - 6} - - )} -
- )} - -
- {host.enableTerminal && ( - - - {t('hosts.terminalBadge')} - - )} - {host.enableTunnel && ( - - - {t('hosts.tunnelBadge')} - {host.tunnelConnections && host.tunnelConnections.length > 0 && ( - ({host.tunnelConnections.length}) +
+ {host.tags && host.tags.length > 0 && ( +
+ {host.tags.slice(0, 6).map((tag, index) => ( + + + {tag} + + ))} + {host.tags.length > 6 && ( + + +{host.tags.length - 6} + + )} +
)} - - )} - {host.enableFileManager && ( - - - {t('hosts.fileManagerBadge')} - - )} -
-
-
- + +
+ {host.enableTerminal && ( + + + {t('hosts.terminalBadge')} + + )} + {host.enableTunnel && ( + + + {t('hosts.tunnelBadge')} + {host.tunnelConnections && host.tunnelConnections.length > 0 && ( + ({host.tunnelConnections.length}) + )} + + )} + {host.enableFileManager && ( + + + {t('hosts.fileManagerBadge')} + + )} +
+
+
+

Click to edit host

-

Drag to move between folders

+

Drag to + move between folders

diff --git a/src/ui/Desktop/Apps/Server/Server.tsx b/src/ui/Desktop/Apps/Server/Server.tsx index 84d43f58..36eb03e0 100644 --- a/src/ui/Desktop/Apps/Server/Server.tsx +++ b/src/ui/Desktop/Apps/Server/Server.tsx @@ -50,7 +50,6 @@ export function Server({ setCurrentHostConfig(updatedHost); } } catch (error) { - console.error('Failed to fetch latest host config:', error); toast.error(t('serverStats.failedToFetchHostConfig')); } } @@ -68,7 +67,6 @@ export function Server({ setCurrentHostConfig(updatedHost); } } catch (error) { - console.error('Failed to fetch updated host config:', error); toast.error(t('serverStats.failedToFetchHostConfig')); } } @@ -89,20 +87,14 @@ export function Server({ setServerStatus(res?.status === 'online' ? 'online' : 'offline'); } } catch (error: any) { - console.error('Failed to fetch server status:', error); if (!cancelled) { - // Handle different error types from the new backend if (error?.response?.status === 503) { - // Server is offline setServerStatus('offline'); } else if (error?.response?.status === 504) { - // Timeout - treat as degraded setServerStatus('offline'); } else if (error?.response?.status === 404) { - // Host not found setServerStatus('offline'); } else { - // Other errors - treat as offline setServerStatus('offline'); } toast.error(t('serverStats.failedToFetchStatus')); @@ -119,7 +111,6 @@ export function Server({ setMetrics(data); } } catch (error) { - console.error('Failed to fetch server metrics:', error); if (!cancelled) { setMetrics(null); toast.error(t('serverStats.failedToFetchMetrics')); @@ -154,8 +145,8 @@ export function Server({ const isFileManagerAlreadyOpen = React.useMemo(() => { if (!currentHostConfig) return false; - return tabs.some((tab: any) => - tab.type === 'file_manager' && + return tabs.some((tab: any) => + tab.type === 'file_manager' && tab.hostConfig?.id === currentHostConfig.id ); }, [tabs, currentHostConfig]); @@ -204,18 +195,13 @@ export function Server({ const data = await getServerMetricsById(currentHostConfig.id); setMetrics(data); } catch (error: any) { - // Handle different error types from the new backend if (error?.response?.status === 503) { - // Server is offline setServerStatus('offline'); } else if (error?.response?.status === 504) { - // Timeout - treat as offline setServerStatus('offline'); } else if (error?.response?.status === 404) { - // Host not found setServerStatus('offline'); } else { - // Other errors - treat as offline setServerStatus('offline'); } setMetrics(null); @@ -228,7 +214,8 @@ export function Server({ > {isRefreshing ? (
-
+
{t('serverStats.refreshing')}
) : ( @@ -265,14 +252,16 @@ export function Server({ {isLoadingMetrics && !metrics ? (
-
+
{t('serverStats.loadingMetrics')}
) : !metrics && serverStatus === 'offline' ? (
-
+

{t('serverStats.serverOffline')}

@@ -281,15 +270,16 @@ export function Server({
) : (
- {/* CPU Stats */} -
-
- -

{t('serverStats.cpuUsage')}

-
- -
-
+ {/* CPU Stats */} +
+
+ +

{t('serverStats.cpuUsage')}

+
+ +
+
{(() => { const pct = metrics?.cpu?.percent; @@ -299,33 +289,34 @@ export function Server({ return `${pctText} ${t('serverStats.of')} ${coresText}`; })()} -
- -
- -
- -
- {metrics?.cpu?.load ? - `Load: ${metrics.cpu.load[0].toFixed(2)}, ${metrics.cpu.load[1].toFixed(2)}, ${metrics.cpu.load[2].toFixed(2)}` : - 'Load: N/A' - } -
-
-
+
- {/* Memory Stats */} -
-
- -

{t('serverStats.memoryUsage')}

+
+ +
+ +
+ {metrics?.cpu?.load ? + `Load: ${metrics.cpu.load[0].toFixed(2)}, ${metrics.cpu.load[1].toFixed(2)}, ${metrics.cpu.load[2].toFixed(2)}` : + 'Load: N/A' + } +
+
- -
-
+ + {/* Memory Stats */} +
+
+ +

{t('serverStats.memoryUsage')}

+
+ +
+
{(() => { const pct = metrics?.memory?.percent; @@ -337,35 +328,36 @@ export function Server({ return `${pctText} (${usedText} ${t('serverStats.of')} ${totalText})`; })()} -
- -
- -
- -
- {(() => { - const used = metrics?.memory?.usedGiB; - const total = metrics?.memory?.totalGiB; - const free = (typeof used === 'number' && typeof total === 'number') ? (total - used).toFixed(1) : 'N/A'; - return `Free: ${free} GiB`; - })()} -
-
-
+
- {/* Disk Stats */} -
-
- -

{t('serverStats.rootStorageSpace')}

+
+ +
+ +
+ {(() => { + const used = metrics?.memory?.usedGiB; + const total = metrics?.memory?.totalGiB; + const free = (typeof used === 'number' && typeof total === 'number') ? (total - used).toFixed(1) : 'N/A'; + return `Free: ${free} GiB`; + })()} +
+
- -
-
+ + {/* Disk Stats */} +
+
+ +

{t('serverStats.rootStorageSpace')}

+
+ +
+
{(() => { const pct = metrics?.disk?.percent; @@ -377,25 +369,25 @@ export function Server({ return `${pctText} (${usedText} ${t('serverStats.of')} ${totalText})`; })()} -
- -
- -
- -
- {(() => { - const used = metrics?.disk?.usedHuman; - const total = metrics?.disk?.totalHuman; - return used && total ? `Available: ${total}` : 'Available: N/A'; - })()} +
+ +
+ +
+ +
+ {(() => { + const used = metrics?.disk?.usedHuman; + const total = metrics?.disk?.totalHuman; + return used && total ? `Available: ${total}` : 'Available: N/A'; + })()} +
-
)}
diff --git a/src/ui/Desktop/Apps/Terminal/Terminal.tsx b/src/ui/Desktop/Apps/Terminal/Terminal.tsx index f48c3f36..9e376b60 100644 --- a/src/ui/Desktop/Apps/Terminal/Terminal.tsx +++ b/src/ui/Desktop/Apps/Terminal/Terminal.tsx @@ -94,7 +94,7 @@ export const Terminal = forwardRef(function SSHTerminal( } webSocketRef.current?.close(); setIsConnected(false); - setIsConnecting(false); // Clear connecting state + setIsConnecting(false); }, fit: () => { fitAddonRef.current?.fit(); @@ -138,59 +138,48 @@ export const Terminal = forwardRef(function SSHTerminal( } function attemptReconnection() { - // Don't attempt reconnection if component is unmounting, shouldn't reconnect, or already reconnecting if (isUnmountingRef.current || shouldNotReconnectRef.current || isReconnectingRef.current) { return; } - // Check if we've already reached max attempts if (reconnectAttempts.current >= maxReconnectAttempts) { toast.error(t('terminal.maxReconnectAttemptsReached')); - // Close the terminal tab when max attempts reached if (onClose) { onClose(); } return; } - // Set reconnecting flag to prevent multiple simultaneous attempts isReconnectingRef.current = true; - - // Clear terminal immediately to prevent showing last line + if (terminal) { terminal.clear(); } - - // Increment attempt counter + reconnectAttempts.current++; - - // Show toast with current attempt number - toast.info(t('terminal.reconnecting', { attempt: reconnectAttempts.current, max: maxReconnectAttempts })); - + + toast.info(t('terminal.reconnecting', {attempt: reconnectAttempts.current, max: maxReconnectAttempts})); + reconnectTimeoutRef.current = setTimeout(() => { - // Check again if component is still mounted and should reconnect if (isUnmountingRef.current || shouldNotReconnectRef.current) { isReconnectingRef.current = false; return; } - - // Check if we haven't exceeded max attempts during the timeout + if (reconnectAttempts.current > maxReconnectAttempts) { isReconnectingRef.current = false; return; } - + if (terminal && hostConfig) { - // Ensure terminal is clear before reconnecting terminal.clear(); const cols = terminal.cols; const rows = terminal.rows; connectToHost(cols, rows); } - - // Reset reconnecting flag after attempting connection + isReconnectingRef.current = false; - }, 2000 * reconnectAttempts.current); // Exponential backoff + }, 2000 * reconnectAttempts.current); } function connectToHost(cols: number, rows: number) { @@ -200,39 +189,30 @@ export const Terminal = forwardRef(function SSHTerminal( const wsUrl = isDev ? 'ws://localhost:8082' : isElectron - ? (() => { - // Get configured server URL from window object (set by main-axios) - const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081'; - // Convert HTTP/HTTPS to WS/WSS and use nginx reverse proxy path - const wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://'; - const wsHost = baseUrl.replace(/^https?:\/\//, ''); // Keep the port - return `${wsProtocol}${wsHost}/ssh/websocket/`; - })() - : `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`; + ? (() => { + const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081'; + const wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://'; + const wsHost = baseUrl.replace(/^https?:\/\//, ''); + return `${wsProtocol}${wsHost}/ssh/websocket/`; + })() + : `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`; const ws = new WebSocket(wsUrl); webSocketRef.current = ws; wasDisconnectedBySSH.current = false; setConnectionError(null); - shouldNotReconnectRef.current = false; // Reset reconnection flag - isReconnectingRef.current = false; // Reset reconnecting flag - setIsConnecting(true); // Set connecting state + shouldNotReconnectRef.current = false; + isReconnectingRef.current = false; + setIsConnecting(true); setupWebSocketListeners(ws, cols, rows); } - function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) { ws.addEventListener('open', () => { - // Don't set isConnected to true here - wait for actual SSH connection - // Don't show reconnected toast here - wait for actual connection confirmation - - // Set a timeout for SSH connection establishment connectionTimeoutRef.current = setTimeout(() => { if (!isConnected) { - // SSH connection didn't establish within timeout - // Clear terminal immediately when connection times out if (terminal) { terminal.clear(); } @@ -240,18 +220,17 @@ export const Terminal = forwardRef(function SSHTerminal( if (webSocketRef.current) { webSocketRef.current.close(); } - // Attempt reconnection if this was a reconnection attempt if (reconnectAttempts.current > 0) { attemptReconnection(); } } - }, 10000); // 10 second timeout for SSH connection - + }, 10000); + ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}})); terminal.onData((data) => { ws.send(JSON.stringify({type: 'input', data})); }); - + pingIntervalRef.current = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({type: 'ping'})); @@ -265,73 +244,59 @@ export const Terminal = forwardRef(function SSHTerminal( if (msg.type === 'data') { terminal.write(msg.data); } else if (msg.type === 'error') { - // Handle different types of errors const errorMessage = msg.message || t('terminal.unknownError'); - - // Check if it's an authentication error - if (errorMessage.toLowerCase().includes('auth') || + + if (errorMessage.toLowerCase().includes('auth') || errorMessage.toLowerCase().includes('password') || errorMessage.toLowerCase().includes('permission') || errorMessage.toLowerCase().includes('denied') || errorMessage.toLowerCase().includes('invalid') || errorMessage.toLowerCase().includes('failed') || errorMessage.toLowerCase().includes('incorrect')) { - toast.error(t('terminal.authError', { message: errorMessage })); - shouldNotReconnectRef.current = true; // Don't reconnect on auth errors - // Close terminal on auth errors + toast.error(t('terminal.authError', {message: errorMessage})); + shouldNotReconnectRef.current = true; if (webSocketRef.current) { webSocketRef.current.close(); } - // Close the terminal tab immediately if (onClose) { onClose(); } return; } - - // Check if it's a connection error that should trigger reconnection + if (errorMessage.toLowerCase().includes('connection') || errorMessage.toLowerCase().includes('timeout') || errorMessage.toLowerCase().includes('network')) { - toast.error(t('terminal.connectionError', { message: errorMessage })); + toast.error(t('terminal.connectionError', {message: errorMessage})); setIsConnected(false); - // Clear terminal immediately when connection error occurs if (terminal) { terminal.clear(); } - // Set connecting state immediately for reconnection setIsConnecting(true); attemptReconnection(); return; } - - // For other errors, show toast but don't close terminal - toast.error(t('terminal.error', { message: errorMessage })); + + toast.error(t('terminal.error', {message: errorMessage})); } else if (msg.type === 'connected') { setIsConnected(true); - setIsConnecting(false); // Clear connecting state - // Clear connection timeout since SSH connection is established + setIsConnecting(false); if (connectionTimeoutRef.current) { clearTimeout(connectionTimeoutRef.current); connectionTimeoutRef.current = null; } - // Show reconnected toast if this was a reconnection attempt if (reconnectAttempts.current > 0) { toast.success(t('terminal.reconnected')); } - // Reset reconnection counter and flags on successful connection reconnectAttempts.current = 0; isReconnectingRef.current = false; } else if (msg.type === 'disconnected') { wasDisconnectedBySSH.current = true; setIsConnected(false); - // Clear terminal immediately when disconnected if (terminal) { terminal.clear(); } - // Set connecting state immediately for reconnection setIsConnecting(true); - // Attempt reconnection for disconnections if (!isUnmountingRef.current && !shouldNotReconnectRef.current) { attemptReconnection(); } @@ -343,28 +308,22 @@ export const Terminal = forwardRef(function SSHTerminal( ws.addEventListener('close', (event) => { setIsConnected(false); - // Clear terminal immediately when connection closes if (terminal) { terminal.clear(); } - // Set connecting state immediately for reconnection setIsConnecting(true); if (!wasDisconnectedBySSH.current && !isUnmountingRef.current && !shouldNotReconnectRef.current) { - // Attempt reconnection for unexpected disconnections attemptReconnection(); } }); - + ws.addEventListener('error', (event) => { setIsConnected(false); setConnectionError(t('terminal.websocketError')); - // Clear terminal immediately when WebSocket error occurs if (terminal) { terminal.clear(); } - // Set connecting state immediately for reconnection setIsConnecting(true); - // Attempt reconnection for WebSocket errors if (!isUnmountingRef.current && !shouldNotReconnectRef.current) { attemptReconnection(); } @@ -486,23 +445,6 @@ export const Terminal = forwardRef(function SSHTerminal( const cols = terminal.cols; const rows = terminal.rows; - const isDev = process.env.NODE_ENV === 'development' && - (window.location.port === '3000' || window.location.port === '5173' || window.location.port === ''); - - - const wsUrl = isDev - ? 'ws://localhost:8082' - : isElectron - ? (() => { - // Get configured server URL from window object (set by main-axios) - const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081'; - // Convert HTTP/HTTPS to WS/WSS and use nginx reverse proxy path - const wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://'; - const wsHost = baseUrl.replace(/^https?:\/\//, '').replace(/:\d+$/, ''); // Remove port if present - return `${wsProtocol}${wsHost}/ssh/websocket/`; - })() - : `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`; - connectToHost(cols, rows); }, 300); }); @@ -511,7 +453,7 @@ export const Terminal = forwardRef(function SSHTerminal( isUnmountingRef.current = true; shouldNotReconnectRef.current = true; isReconnectingRef.current = false; - setIsConnecting(false); // Clear connecting state + setIsConnecting(false); resizeObserver.disconnect(); element?.removeEventListener('contextmenu', handleContextMenu); if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); @@ -536,7 +478,7 @@ export const Terminal = forwardRef(function SSHTerminal( terminal.focus(); } }, 0); - + if (terminal && !splitScreen) { setTimeout(() => { terminal.focus(); @@ -560,8 +502,8 @@ export const Terminal = forwardRef(function SSHTerminal( return (
{/* Terminal */} -
{ if (terminal && !splitScreen) { @@ -569,12 +511,13 @@ export const Terminal = forwardRef(function SSHTerminal( } }} /> - + {/* Connecting State */} {isConnecting && (
-
+
{t('terminal.connecting')}
diff --git a/src/ui/Desktop/Apps/Tunnel/Tunnel.tsx b/src/ui/Desktop/Apps/Tunnel/Tunnel.tsx index f5edee6d..bafd6573 100644 --- a/src/ui/Desktop/Apps/Tunnel/Tunnel.tsx +++ b/src/ui/Desktop/Apps/Tunnel/Tunnel.tsx @@ -1,7 +1,7 @@ import React, {useState, useEffect, useCallback} from "react"; import {TunnelViewer} from "@/ui/Desktop/Apps/Tunnel/TunnelViewer.tsx"; import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/ui/main-axios.ts"; -import type { SSHHost, TunnelConnection, TunnelStatus, SSHTunnelProps } from '../../../types/index.js'; +import type {SSHHost, TunnelConnection, TunnelStatus, SSHTunnelProps} from '../../../types/index.js'; export function Tunnel({filterHostKey}: SSHTunnelProps): React.ReactElement { const [allHosts, setAllHosts] = useState([]); diff --git a/src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx b/src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx index b4782c34..c980ca5d 100644 --- a/src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx +++ b/src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx @@ -1,14 +1,12 @@ import React from "react"; import {Button} from "@/components/ui/button.tsx"; -import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card.tsx"; +import {Card} from "@/components/ui/card.tsx"; import {Separator} from "@/components/ui/separator.tsx"; import {useTranslation} from 'react-i18next'; import { Loader2, Pin, - Terminal, Network, - FileEdit, Tag, Play, Square, @@ -16,11 +14,10 @@ import { Clock, Wifi, WifiOff, - Zap, X } from "lucide-react"; import {Badge} from "@/components/ui/badge.tsx"; -import type { SSHHost, TunnelConnection, TunnelStatus, CONNECTION_STATES, SSHTunnelObjectProps } from '../../../types/index.js'; +import type {TunnelStatus, SSHTunnelObjectProps} from '../../../types/index.js'; export function TunnelObject({ host, @@ -227,9 +224,12 @@ export function TunnelObject({ {statusValue === 'WAITING' ? t('tunnels.waitingForRetry') : t('tunnels.retryingConnection')}
- {t('tunnels.attempt', { current: status.retryCount, max: status.maxRetries })} + {t('tunnels.attempt', { + current: status.retryCount, + max: status.maxRetries + })} {status.nextRetryIn && ( - • {t('tunnels.nextRetryIn', { seconds: status.nextRetryIn })} + • {t('tunnels.nextRetryIn', {seconds: status.nextRetryIn})} )}
@@ -408,9 +408,12 @@ export function TunnelObject({ {statusValue === 'WAITING' ? t('tunnels.waitingForRetry') : t('tunnels.retryingConnection')}
- {t('tunnels.attempt', { current: status.retryCount, max: status.maxRetries })} + {t('tunnels.attempt', { + current: status.retryCount, + max: status.maxRetries + })} {status.nextRetryIn && ( - • {t('tunnels.nextRetryIn', { seconds: status.nextRetryIn })} + • {t('tunnels.nextRetryIn', {seconds: status.nextRetryIn})} )}
diff --git a/src/ui/Desktop/Apps/Tunnel/TunnelViewer.tsx b/src/ui/Desktop/Apps/Tunnel/TunnelViewer.tsx index d4c059ce..8f4698f8 100644 --- a/src/ui/Desktop/Apps/Tunnel/TunnelViewer.tsx +++ b/src/ui/Desktop/Apps/Tunnel/TunnelViewer.tsx @@ -1,7 +1,7 @@ import React from "react"; import {TunnelObject} from "./TunnelObject.tsx"; import {useTranslation} from 'react-i18next'; -import type { SSHHost, TunnelConnection, TunnelStatus } from '../../../types/index.js'; +import type {SSHHost, TunnelConnection, TunnelStatus} from '../../../types/index.js'; interface SSHTunnelViewerProps { hosts: SSHHost[]; diff --git a/src/ui/Desktop/DesktopApp.tsx b/src/ui/Desktop/DesktopApp.tsx index 60eb5481..0a5eb9ef 100644 --- a/src/ui/Desktop/DesktopApp.tsx +++ b/src/ui/Desktop/DesktopApp.tsx @@ -5,10 +5,10 @@ import {AppView} from "@/ui/Desktop/Navigation/AppView.tsx" import {HostManager} from "@/ui/Desktop/Apps/Host Manager/HostManager.tsx" import {TabProvider, useTabs} from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx" import {TopNavbar} from "@/ui/Desktop/Navigation/TopNavbar.tsx"; -import { AdminSettings } from "@/ui/Desktop/Admin/AdminSettings.tsx"; -import { UserProfile } from "@/ui/Desktop/User/UserProfile.tsx"; -import { Toaster } from "@/components/ui/sonner.tsx"; -import { getUserInfo, getCookie, setCookie } from "@/ui/main-axios.ts"; +import {AdminSettings} from "@/ui/Desktop/Admin/AdminSettings.tsx"; +import {UserProfile} from "@/ui/Desktop/User/UserProfile.tsx"; +import {Toaster} from "@/components/ui/sonner.tsx"; +import {getUserInfo, getCookie} from "@/ui/main-axios.ts"; function AppContent() { const [view, setView] = useState("homepage") @@ -92,13 +92,13 @@ function AppContent() { transparent 100% )`, backgroundSize: '80px 80px' - }} /> + }}/>
)} {!isAuthenticated && !authLoading && (
- {showTerminalView && (
- +
)} {showHome && (
- - +
)} {showAdmin && (
- +
)} {showProfile && (
- +
)} )} - - + ); } diff --git a/src/ui/Desktop/Electron Only/ServerConfig.tsx b/src/ui/Desktop/Electron Only/ServerConfig.tsx new file mode 100644 index 00000000..f9d25b14 --- /dev/null +++ b/src/ui/Desktop/Electron Only/ServerConfig.tsx @@ -0,0 +1,216 @@ +import React, {useState, useEffect} from 'react'; +import {Button} from '@/components/ui/button.tsx'; +import {Input} from '@/components/ui/input.tsx'; +import {Label} from '@/components/ui/label.tsx'; +import {Alert, AlertTitle, AlertDescription} from '@/components/ui/alert.tsx'; +import {useTranslation} from 'react-i18next'; +import {getServerConfig, saveServerConfig, testServerConnection, type ServerConfig} from '@/ui/main-axios.ts'; +import {CheckCircle, XCircle, Server, Wifi} from 'lucide-react'; + +interface ServerConfigProps { + onServerConfigured: (serverUrl: string) => void; + onCancel?: () => void; + isFirstTime?: boolean; +} + +export function ServerConfig({onServerConfigured, onCancel, isFirstTime = false}: ServerConfigProps) { + const {t} = useTranslation(); + const [serverUrl, setServerUrl] = useState(''); + const [loading, setLoading] = useState(false); + const [testing, setTesting] = useState(false); + const [error, setError] = useState(null); + const [connectionStatus, setConnectionStatus] = useState<'unknown' | 'success' | 'error'>('unknown'); + + useEffect(() => { + loadServerConfig(); + }, []); + + const loadServerConfig = async () => { + try { + const config = await getServerConfig(); + if (config?.serverUrl) { + setServerUrl(config.serverUrl); + setConnectionStatus('success'); + } + } catch (error) { + } + }; + + const handleTestConnection = async () => { + if (!serverUrl.trim()) { + setError(t('serverConfig.enterServerUrl')); + return; + } + + setTesting(true); + setError(null); + + try { + let normalizedUrl = serverUrl.trim(); + if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) { + normalizedUrl = `http://${normalizedUrl}`; + } + + const result = await testServerConnection(normalizedUrl); + + if (result.success) { + setConnectionStatus('success'); + } else { + setConnectionStatus('error'); + setError(result.error || t('serverConfig.connectionFailed')); + } + } catch (error) { + setConnectionStatus('error'); + setError(t('serverConfig.connectionError')); + } finally { + setTesting(false); + } + }; + + const handleSaveConfig = async () => { + if (!serverUrl.trim()) { + setError(t('serverConfig.enterServerUrl')); + return; + } + + if (connectionStatus !== 'success') { + setError(t('serverConfig.testConnectionFirst')); + return; + } + + setLoading(true); + setError(null); + + try { + let normalizedUrl = serverUrl.trim(); + if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) { + normalizedUrl = `http://${normalizedUrl}`; + } + + const config: ServerConfig = { + serverUrl: normalizedUrl, + lastUpdated: new Date().toISOString() + }; + + const success = await saveServerConfig(config); + + if (success) { + onServerConfigured(normalizedUrl); + } else { + setError(t('serverConfig.saveFailed')); + } + } catch (error) { + setError(t('serverConfig.saveError')); + } finally { + setLoading(false); + } + }; + + const handleUrlChange = (value: string) => { + setServerUrl(value); + setConnectionStatus('unknown'); + setError(null); + }; + + return ( +
+
+
+ +
+

{t('serverConfig.title')}

+

+ {t('serverConfig.description')} +

+
+
+
+ +
+ handleUrlChange(e.target.value)} + className="flex-1 h-10" + disabled={loading} + /> + +
+
+ + {connectionStatus !== 'unknown' && ( +
+ {connectionStatus === 'success' ? ( + <> + + {t('serverConfig.connected')} + + ) : ( + <> + + {t('serverConfig.disconnected')} + + )} +
+ )} + + {error && ( + + {t('common.error')} + {error} + + )} + + +
+ {onCancel && !isFirstTime && ( + + )} + +
+ +
+ {t('serverConfig.helpText')} +
+
+
+ ); +} diff --git a/src/ui/Desktop/ElectronOnly/ServerConfig.tsx b/src/ui/Desktop/ElectronOnly/ServerConfig.tsx deleted file mode 100644 index 90fffbce..00000000 --- a/src/ui/Desktop/ElectronOnly/ServerConfig.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Button } from '@/components/ui/button.tsx'; -import { Input } from '@/components/ui/input.tsx'; -import { Label } from '@/components/ui/label.tsx'; -import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert.tsx'; -import { useTranslation } from 'react-i18next'; -import { getServerConfig, saveServerConfig, testServerConnection, type ServerConfig } from '@/ui/main-axios.ts'; -import { CheckCircle, XCircle, Server, Wifi } from 'lucide-react'; - -interface ServerConfigProps { - onServerConfigured: (serverUrl: string) => void; - onCancel?: () => void; - isFirstTime?: boolean; -} - -export function ServerConfig({ onServerConfigured, onCancel, isFirstTime = false }: ServerConfigProps) { - const { t } = useTranslation(); - const [serverUrl, setServerUrl] = useState(''); - const [loading, setLoading] = useState(false); - const [testing, setTesting] = useState(false); - const [error, setError] = useState(null); - const [connectionStatus, setConnectionStatus] = useState<'unknown' | 'success' | 'error'>('unknown'); - - useEffect(() => { - loadServerConfig(); - }, []); - - const loadServerConfig = async () => { - try { - const config = await getServerConfig(); - if (config?.serverUrl) { - setServerUrl(config.serverUrl); - setConnectionStatus('success'); - } - } catch (error) { - console.error('Failed to load server config:', error); - } - }; - - const handleTestConnection = async () => { - if (!serverUrl.trim()) { - setError(t('serverConfig.enterServerUrl')); - return; - } - - setTesting(true); - setError(null); - - try { - // Normalize URL - let normalizedUrl = serverUrl.trim(); - if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) { - normalizedUrl = `http://${normalizedUrl}`; - } - - const result = await testServerConnection(normalizedUrl); - - if (result.success) { - setConnectionStatus('success'); - } else { - setConnectionStatus('error'); - setError(result.error || t('serverConfig.connectionFailed')); - } - } catch (error) { - setConnectionStatus('error'); - setError(t('serverConfig.connectionError')); - } finally { - setTesting(false); - } - }; - - const handleSaveConfig = async () => { - if (!serverUrl.trim()) { - setError(t('serverConfig.enterServerUrl')); - return; - } - - if (connectionStatus !== 'success') { - setError(t('serverConfig.testConnectionFirst')); - return; - } - - setLoading(true); - setError(null); - - try { - // Normalize URL - let normalizedUrl = serverUrl.trim(); - if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) { - normalizedUrl = `http://${normalizedUrl}`; - } - - const config: ServerConfig = { - serverUrl: normalizedUrl, - lastUpdated: new Date().toISOString() - }; - - const success = await saveServerConfig(config); - - if (success) { - onServerConfigured(normalizedUrl); - } else { - setError(t('serverConfig.saveFailed')); - } - } catch (error) { - setError(t('serverConfig.saveError')); - } finally { - setLoading(false); - } - }; - - const handleUrlChange = (value: string) => { - setServerUrl(value); - setConnectionStatus('unknown'); - setError(null); - }; - - return ( -
-
-
- -
-

{t('serverConfig.title')}

-

- {t('serverConfig.description')} -

-
-
-
- -
- handleUrlChange(e.target.value)} - className="flex-1 h-10" - disabled={loading} - /> - -
-
- - {connectionStatus !== 'unknown' && ( -
- {connectionStatus === 'success' ? ( - <> - - {t('serverConfig.connected')} - - ) : ( - <> - - {t('serverConfig.disconnected')} - - )} -
- )} - - {error && ( - - {t('common.error')} - {error} - - )} - - -
- {onCancel && !isFirstTime && ( - - )} - -
- -
- {t('serverConfig.helpText')} -
-
-
- ); -} diff --git a/src/ui/Desktop/Homepage/Homepage.tsx b/src/ui/Desktop/Homepage/Homepage.tsx index c49bf446..66a1cf3c 100644 --- a/src/ui/Desktop/Homepage/Homepage.tsx +++ b/src/ui/Desktop/Homepage/Homepage.tsx @@ -1,9 +1,8 @@ import React, {useEffect, useState} from "react"; import {HomepageAuth} from "@/ui/Desktop/Homepage/HomepageAuth.tsx"; import {HomepageUpdateLog} from "@/ui/Desktop/Homepage/HompageUpdateLog.tsx"; -import {HomepageAlertManager} from "@/ui/Desktop/Homepage/HomepageAlertManager.tsx"; import {Button} from "@/components/ui/button.tsx"; -import { getUserInfo, getDatabaseHealth, setCookie, getCookie } from "@/ui/main-axios.ts"; +import {getUserInfo, getDatabaseHealth, getCookie} from "@/ui/main-axios.ts"; import {useTranslation} from "react-i18next"; interface HomepageProps { @@ -15,22 +14,19 @@ interface HomepageProps { } export function Homepage({ - onSelectView, isAuthenticated, authLoading, onAuthSuccess, isTopbarOpen }: HomepageProps): React.ReactElement { - const {t} = useTranslation(); const [loggedIn, setLoggedIn] = useState(isAuthenticated); const [isAdmin, setIsAdmin] = useState(false); const [username, setUsername] = useState(null); const [userId, setUserId] = useState(null); const [dbError, setDbError] = useState(null); - // Calculate margins based on topbar state (same logic as AppView.tsx) const topMarginPx = isTopbarOpen ? 74 : 26; - const leftMarginPx = 26; // Assuming sidebar is collapsed for homepage + const leftMarginPx = 26; const bottomMarginPx = 8; useEffect(() => { @@ -83,7 +79,7 @@ export function Homepage({ />
) : ( -
{ export function HomepageAlertCard({alert, onDismiss, onClose}: AlertCardProps): React.ReactElement { const {t} = useTranslation(); - + if (!alert) { return null; } diff --git a/src/ui/Desktop/Homepage/HomepageAlertManager.tsx b/src/ui/Desktop/Homepage/HomepageAlertManager.tsx index 704469b5..028b9c65 100644 --- a/src/ui/Desktop/Homepage/HomepageAlertManager.tsx +++ b/src/ui/Desktop/Homepage/HomepageAlertManager.tsx @@ -1,9 +1,9 @@ import React, {useEffect, useState} from "react"; import {HomepageAlertCard} from "./HomepageAlertCard.tsx"; import {Button} from "@/components/ui/button.tsx"; -import { getUserAlerts, dismissAlert } from "@/ui/main-axios.ts"; +import {getUserAlerts, dismissAlert} from "@/ui/main-axios.ts"; import {useTranslation} from "react-i18next"; -import type { TermixAlert } from '../../../types/index.js'; +import type {TermixAlert} from '../../../types/index.js'; interface AlertManagerProps { userId: string | null; @@ -49,7 +49,6 @@ export function HomepageAlertManager({userId, loggedIn}: AlertManagerProps): Rea setAlerts(sortedAlerts); setCurrentAlertIndex(0); } catch (err) { - console.error('Failed to fetch user alerts:', err); const {toast} = await import('sonner'); toast.error(t('homepage.failedToLoadAlerts')); setError(t('homepage.failedToLoadAlerts')); diff --git a/src/ui/Desktop/Homepage/HomepageAuth.tsx b/src/ui/Desktop/Homepage/HomepageAuth.tsx index 57c67f59..8f8daf85 100644 --- a/src/ui/Desktop/Homepage/HomepageAuth.tsx +++ b/src/ui/Desktop/Homepage/HomepageAuth.tsx @@ -1,5 +1,4 @@ import React, {useState, useEffect} from "react"; -import {Eye, EyeOff} from "lucide-react"; import {cn} from "@/lib/utils.ts"; import {Button} from "@/components/ui/button.tsx"; import {Input} from "@/components/ui/input.tsx"; @@ -24,9 +23,8 @@ import { getCookie, getServerConfig, isElectron, - type ServerConfig } from "../../main-axios.ts"; -import {ServerConfig as ServerConfigComponent} from "@/ui/Desktop/ElectronOnly/ServerConfig.tsx"; +import {ServerConfig as ServerConfigComponent} from "@/ui/Desktop/Electron Only/ServerConfig.tsx"; interface HomepageAuthProps extends React.ComponentProps<"div"> { setLoggedIn: (loggedIn: boolean) => void; @@ -61,14 +59,14 @@ export function HomepageAuth({ const [loading, setLoading] = useState(false); const [oidcLoading, setOidcLoading] = useState(false); const [visibility, setVisibility] = useState({ - password: false, - signupConfirm: false, - resetNew: false, - resetConfirm: false -}); + password: false, + signupConfirm: false, + resetNew: false, + resetConfirm: false + }); const toggleVisibility = (field: keyof typeof visibility) => { - setVisibility(prev => ({ ...prev, [field]: !prev[field] })); -}; + setVisibility(prev => ({...prev, [field]: !prev[field]})); + }; const [error, setError] = useState(null); const [internalLoggedIn, setInternalLoggedIn] = useState(false); @@ -83,7 +81,7 @@ export function HomepageAuth({ const [tempToken, setTempToken] = useState(""); const [resetLoading, setResetLoading] = useState(false); const [resetSuccess, setResetSuccess] = useState(false); - + const [totpRequired, setTotpRequired] = useState(false); const [totpCode, setTotpCode] = useState(""); const [totpTempToken, setTotpTempToken] = useState(""); @@ -159,23 +157,23 @@ export function HomepageAuth({ await registerUser(localUsername, password); res = await loginUser(localUsername, password); } - + if (res.requires_totp) { setTotpRequired(true); setTotpTempToken(res.temp_token); setLoading(false); return; } - + if (!res || !res.token) { throw new Error(t('errors.noTokenReceived')); } - + setCookie("jwt", res.token); [meRes] = await Promise.all([ getUserInfo(), ]); - + setInternalLoggedIn(true); setLoggedIn(true); setIsAdmin(!!meRes.is_admin); @@ -300,17 +298,17 @@ export function HomepageAuth({ setError(null); setTotpLoading(true); - + try { const res = await verifyTOTPLogin(totpTempToken, totpCode); - + if (!res || !res.token) { throw new Error(t('errors.noTokenReceived')); } - + setCookie("jwt", res.token); const meRes = await getUserInfo(); - + setInternalLoggedIn(true); setLoggedIn(true); setIsAdmin(!!meRes.is_admin); @@ -408,58 +406,51 @@ export function HomepageAuth({ ); - // Check if we need to show server config for Electron const [showServerConfig, setShowServerConfig] = useState(null); const [currentServerUrl, setCurrentServerUrl] = useState(''); - + useEffect(() => { const checkServerConfig = async () => { if (isElectron()) { try { const config = await getServerConfig(); - console.log('Desktop HomepageAuth - Server config check:', config); setCurrentServerUrl(config?.serverUrl || ''); setShowServerConfig(!config || !config.serverUrl); } catch (error) { - console.log('Desktop HomepageAuth - No server config found, showing config screen'); setShowServerConfig(true); } } else { setShowServerConfig(false); } }; - + checkServerConfig(); }, []); - + if (showServerConfig === null) { - // Still checking return (
-
+
); } - + if (showServerConfig) { - console.log('Desktop HomepageAuth - SHOWING SERVER CONFIG SCREEN'); return (
- { - console.log('Server configured, reloading page'); window.location.reload(); }} onCancel={() => { - console.log('Cancelled server config, going back to login'); setShowServerConfig(false); }} isFirstTime={!currentServerUrl} @@ -509,7 +500,7 @@ export function HomepageAuth({

{t('auth.twoFactorAuth')}

{t('auth.enterCode')}

- +
- + - +
)} - + {(!internalLoggedIn && (!authLoading || !getCookie("jwt")) && !totpRequired) && ( <>
@@ -758,29 +749,30 @@ export function HomepageAuth({
- - setNewPassword(e.target.value)} - disabled={resetLoading} - autoComplete="new-password" - /> -
-
- - setConfirmPassword(e.target.value)} - disabled={resetLoading} - autoComplete="new-password" - /> -
+ + setNewPassword(e.target.value)} + disabled={resetLoading} + autoComplete="new-password" + /> +
+
+ + setConfirmPassword(e.target.value)} + disabled={resetLoading} + autoComplete="new-password" + /> +
- - setPassword(e.target.value)} - disabled={loading || internalLoggedIn}/> -
- {tab === "signup" && ( -
- - setSignupConfirmPassword(e.target.value)} - disabled={loading || internalLoggedIn}/> -
+ + setPassword(e.target.value)} + disabled={loading || internalLoggedIn}/> +
+ {tab === "signup" && ( +
+ + setSignupConfirmPassword(e.target.value)} + disabled={loading || internalLoggedIn}/> +
)} {canSplit && ( diff --git a/src/ui/Desktop/Navigation/Tabs/TabContext.tsx b/src/ui/Desktop/Navigation/Tabs/TabContext.tsx index 22fd790d..752b9ab0 100644 --- a/src/ui/Desktop/Navigation/Tabs/TabContext.tsx +++ b/src/ui/Desktop/Navigation/Tabs/TabContext.tsx @@ -1,6 +1,6 @@ import React, {createContext, useContext, useState, useRef, type ReactNode} from 'react'; import {useTranslation} from 'react-i18next'; -import type { TabContextTab } from '../../../types/index.js'; +import type {TabContextTab} from '../../../types/index.js'; export type Tab = TabContextTab; diff --git a/src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx b/src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx index 961520de..b084d9ae 100644 --- a/src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx +++ b/src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx @@ -5,7 +5,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu.tsx"; -import { Button } from "@/components/ui/button.tsx"; +import {Button} from "@/components/ui/button.tsx"; import { ChevronDown, Home, @@ -16,31 +16,31 @@ import { Network as SshManagerIcon, User as UserIcon } from "lucide-react"; -import { useTabs, type Tab } from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx"; -import { useTranslation } from "react-i18next"; +import {useTabs, type Tab} from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx"; +import {useTranslation} from "react-i18next"; export function TabDropdown(): React.ReactElement { - const { tabs, currentTab, setCurrentTab } = useTabs(); - const { t } = useTranslation(); + const {tabs, currentTab, setCurrentTab} = useTabs(); + const {t} = useTranslation(); const getTabIcon = (tabType: Tab['type']) => { switch (tabType) { case 'home': - return ; + return ; case 'terminal': - return ; + return ; case 'server': - return ; + return ; case 'file_manager': - return ; + return ; case 'user_profile': - return ; + return ; case 'ssh_manager': - return ; + return ; case 'admin': - return ; + return ; default: - return ; + return ; } }; @@ -68,7 +68,6 @@ export function TabDropdown(): React.ReactElement { setCurrentTab(tabId); }; - // If only one tab (home), don't show dropdown if (tabs.length <= 1) { return null; } @@ -79,9 +78,9 @@ export function TabDropdown(): React.ReactElement { {isActive && ( -
+
)} ); diff --git a/src/ui/Desktop/Navigation/TopNavbar.tsx b/src/ui/Desktop/Navigation/TopNavbar.tsx index 201ba3b7..13e1e9e3 100644 --- a/src/ui/Desktop/Navigation/TopNavbar.tsx +++ b/src/ui/Desktop/Navigation/TopNavbar.tsx @@ -255,8 +255,8 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
- - + + - + {backupCodes.length > 0 && (
@@ -232,7 +232,7 @@ export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange }: TOTPSet variant="outline" onClick={downloadBackupCodes} > - + {t('auth.download')}
@@ -248,7 +248,7 @@ export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange }: TOTPSet {error && ( - + {t('common.error')} {error} @@ -269,9 +269,9 @@ export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange }: TOTPSet
- TOTP QR Code + TOTP QR Code
- +
@@ -285,14 +285,14 @@ export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange }: TOTPSet variant="outline" onClick={() => copyToClipboard(secret, "Secret key")} > - +

{t('auth.cannotScanQRText')}

- + @@ -323,15 +323,15 @@ export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange }: TOTPSet className="text-center text-2xl tracking-widest font-mono" />
- + {error && ( - + {t('common.error')} {error} )} - +
@@ -392,7 +392,7 @@ export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange }: TOTPSet ))}
- + @@ -405,14 +405,14 @@ export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange }: TOTPSet - + {t('auth.twoFactorTitle')}

{t('auth.addExtraSecurityLayer')}.

- - + {error && ( - + Error {error} diff --git a/src/ui/Desktop/User/UserProfile.tsx b/src/ui/Desktop/User/UserProfile.tsx index 0aed6050..de256bfc 100644 --- a/src/ui/Desktop/User/UserProfile.tsx +++ b/src/ui/Desktop/User/UserProfile.tsx @@ -1,7 +1,4 @@ import React, {useState, useEffect} from "react"; -import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card.tsx"; -import {Button} from "@/components/ui/button.tsx"; -import {Input} from "@/components/ui/input.tsx"; import {Label} from "@/components/ui/label.tsx"; import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx"; import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx"; @@ -10,13 +7,11 @@ import {User, Shield, Key, AlertCircle} from "lucide-react"; import {TOTPSetup} from "@/ui/Desktop/User/TOTPSetup.tsx"; import {getUserInfo} from "@/ui/main-axios.ts"; import {getVersionInfo} from "@/ui/main-axios.ts"; -import {toast} from "sonner"; import {PasswordReset} from "@/ui/Desktop/User/PasswordReset.tsx"; import {useTranslation} from "react-i18next"; import {LanguageSwitcher} from "@/ui/Desktop/User/LanguageSwitcher.tsx"; import {useSidebar} from "@/components/ui/sidebar.tsx"; - interface UserProfileProps { isTopbarOpen?: boolean; } @@ -45,7 +40,6 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) { const info = await getVersionInfo(); setVersionInfo({version: info.localVersion}); } catch (err) { - console.error("Failed to load version info", err); const {toast} = await import('sonner'); toast.error(t('user.failedToLoadVersionInfo')); } @@ -88,7 +82,8 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) { if (loading) { return ( -
+

{t('nav.userProfile')}

@@ -104,7 +99,8 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) { if (error || !userInfo) { return ( -
+

{t('nav.userProfile')}

@@ -114,7 +110,8 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) { {t('common.error')} - {error || t('errors.loadFailed')} + {error || t('errors.loadFailed')}
@@ -123,7 +120,8 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) { } return ( -
+

{t('nav.userProfile')}

@@ -133,12 +131,14 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
- + {t('nav.userProfile')} {!userInfo.is_oidc && ( - + {t('profile.security')} diff --git a/src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx b/src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx index c2242e66..9a6bbf49 100644 --- a/src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx +++ b/src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx @@ -5,7 +5,7 @@ import {ButtonGroup} from "@/components/ui/button-group.tsx"; import {Server, Terminal} from "lucide-react"; import {getServerStatusById} from "@/ui/main-axios.ts"; import {useTabs} from "@/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx"; -import type { HostProps } from '../../../../../types/index.js'; +import type {HostProps} from '../../../../../types/index.js'; export function Host({host, onHostConnect}: HostProps): React.ReactElement { const {addTab} = useTabs(); diff --git a/src/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx b/src/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx index 9d016020..85159b99 100644 --- a/src/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx +++ b/src/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx @@ -1,6 +1,6 @@ import React, {createContext, useContext, useState, useRef, type ReactNode} from 'react'; import {useTranslation} from 'react-i18next'; -import type { TabContextTab } from '../../../../types/index.js'; +import type {TabContextTab} from '../../../../types/index.js'; export type Tab = TabContextTab; diff --git a/src/ui/Mobile/Apps/Terminal/Terminal.tsx b/src/ui/Mobile/Apps/Terminal/Terminal.tsx index bfc21e9e..e703a40a 100644 --- a/src/ui/Mobile/Apps/Terminal/Terminal.tsx +++ b/src/ui/Mobile/Apps/Terminal/Terminal.tsx @@ -185,7 +185,8 @@ export const Terminal = forwardRef(function SSHTerminal( textarea.blur(); } - terminal.focus = () => {}; + terminal.focus = () => { + }; const resizeObserver = new ResizeObserver(() => { if (resizeTimeout.current) clearTimeout(resizeTimeout.current); @@ -221,11 +222,9 @@ export const Terminal = forwardRef(function SSHTerminal( ? 'ws://localhost:8082' : isElectron() ? (() => { - // Get configured server URL from window object (set by main-axios) const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081'; - // Convert HTTP/HTTPS to WS/WSS and use nginx reverse proxy path const wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://'; - const wsHost = baseUrl.replace(/^https?:\/\//, ''); // Keep the port + const wsHost = baseUrl.replace(/^https?:\/\//, ''); return `${wsProtocol}${wsHost}/ssh/websocket/`; })() : `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`; diff --git a/src/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx b/src/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx index 55822e9c..0c74f6eb 100644 --- a/src/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx +++ b/src/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx @@ -89,8 +89,6 @@ export function TerminalKeyboard({onSendInput, onLayoutChange}: TerminalKeyboard navigator.vibrate(20); } } catch (e) { - console.error("Vibration failed:", e); - // Don't show toast for vibration failure as it's not critical } onSendInput(input); diff --git a/src/ui/Mobile/Homepage/HomepageAuth.tsx b/src/ui/Mobile/Homepage/HomepageAuth.tsx index ff8d3057..3e8311b3 100644 --- a/src/ui/Mobile/Homepage/HomepageAuth.tsx +++ b/src/ui/Mobile/Homepage/HomepageAuth.tsx @@ -737,17 +737,17 @@ export function HomepageAuth({
setPassword(e.target.value)} - disabled={loading || internalLoggedIn}/> + value={password} onChange={e => setPassword(e.target.value)} + disabled={loading || internalLoggedIn}/>
{tab === "signup" && (
setSignupConfirmPassword(e.target.value)} - disabled={loading || internalLoggedIn}/> + className="h-11 text-base" + value={signupConfirmPassword} + onChange={e => setSignupConfirmPassword(e.target.value)} + disabled={loading || internalLoggedIn}/>
)}