Clean up frontend files and read me translations
This commit is contained in:
1
.env
1
.env
@@ -1,3 +1,2 @@
|
|||||||
VERSION=1.6.0
|
VERSION=1.6.0
|
||||||
VITE_API_HOST=localhost
|
VITE_API_HOST=localhost
|
||||||
CREDENTIAL_ENCRYPTION_KEY=98fbfabe84b125db7cbbb5168eb584aaecc2f3779a2aaa955c57bdd305071a84
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Contributing
|
_# Contributing
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ npm run dev
|
|||||||
npm run dev:backend
|
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/`.
|
This will start the backend and the frontend Vite server. You can access Termix by going to `http://localhost:5174/`.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
@@ -59,8 +60,9 @@ This will start the backend and the frontend Vite server. You can access Termix
|
|||||||
## Color Scheme
|
## Color Scheme
|
||||||
|
|
||||||
### Background Colors
|
### Background Colors
|
||||||
|
|
||||||
| CSS Variable | Color Value | Usage | Description |
|
| CSS Variable | Color Value | Usage | Description |
|
||||||
|--------------|-------------|-------|-------------|
|
|-------------------------------|-------------|-----------------------------|------------------------------------------|
|
||||||
| `--color-dark-bg` | `#18181b` | Main dark background | Primary dark background color |
|
| `--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-darker` | `#0e0e10` | Darker backgrounds | Darker variant for panels and containers |
|
||||||
| `--color-dark-bg-darkest` | `#09090b` | Darkest backgrounds | Darkest background (terminal) |
|
| `--color-dark-bg-darkest` | `#09090b` | Darkest backgrounds | Darkest background (terminal) |
|
||||||
@@ -70,16 +72,18 @@ This will start the backend and the frontend Vite server. You can access Termix
|
|||||||
| `--color-dark-bg-panel-hover` | `#232327` | Panel hover states | Background for panels on hover |
|
| `--color-dark-bg-panel-hover` | `#232327` | Panel hover states | Background for panels on hover |
|
||||||
|
|
||||||
### Element-Specific Backgrounds
|
### Element-Specific Backgrounds
|
||||||
|
|
||||||
| CSS Variable | Color Value | Usage | Description |
|
| CSS Variable | Color Value | Usage | Description |
|
||||||
|--------------|-------------|-------|-------------|
|
|--------------------------|-------------|--------------------|-----------------------------------------------|
|
||||||
| `--color-dark-bg-input` | `#222225` | Input fields | Background for input fields and form elements |
|
| `--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-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-active` | `#1d1d1f` | Active states | Background for active/selected elements |
|
||||||
| `--color-dark-bg-header` | `#131316` | Header backgrounds | Background for headers and navigation bars |
|
| `--color-dark-bg-header` | `#131316` | Header backgrounds | Background for headers and navigation bars |
|
||||||
|
|
||||||
### Border Colors
|
### Border Colors
|
||||||
|
|
||||||
| CSS Variable | Color Value | Usage | Description |
|
| CSS Variable | Color Value | Usage | Description |
|
||||||
|--------------|-------------|-------|-------------|
|
|------------------------------|-------------|-----------------|------------------------------------------|
|
||||||
| `--color-dark-border` | `#303032` | Default borders | Standard border color |
|
| `--color-dark-border` | `#303032` | Default borders | Standard border color |
|
||||||
| `--color-dark-border-active` | `#2d2d30` | Active borders | Border color for active elements |
|
| `--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-hover` | `#434345` | Hover borders | Border color on hover states |
|
||||||
@@ -88,14 +92,16 @@ This will start the backend and the frontend Vite server. You can access Termix
|
|||||||
| `--color-dark-border-panel` | `#222224` | Panel borders | Border color for panels and cards |
|
| `--color-dark-border-panel` | `#222224` | Panel borders | Border color for panels and cards |
|
||||||
|
|
||||||
### Interactive States
|
### Interactive States
|
||||||
|
|
||||||
| CSS Variable | Color Value | Usage | Description |
|
| CSS Variable | Color Value | Usage | Description |
|
||||||
|--------------|-------------|-------|-------------|
|
|--------------------------|-------------|-------------------|-----------------------------------------------|
|
||||||
| `--color-dark-hover` | `#2d2d30` | Hover states | Background color for hover effects |
|
| `--color-dark-hover` | `#2d2d30` | Hover states | Background color for hover effects |
|
||||||
| `--color-dark-active` | `#2a2a2c` | Active states | Background color for active elements |
|
| `--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-pressed` | `#1a1a1c` | Pressed states | Background color for pressed/clicked elements |
|
||||||
| `--color-dark-hover-alt` | `#2a2a2d` | Alternative hover | Alternative hover state color |
|
| `--color-dark-hover-alt` | `#2a2a2d` | Alternative hover | Alternative hover state color |
|
||||||
|
|
||||||
|
|
||||||
## Support
|
## 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.
|
||||||
35
README-CN.md
35
README-CN.md
@@ -1,7 +1,7 @@
|
|||||||
# Repo Stats
|
# 仓库统计
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> |
|
<a href="README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> 英文</a> |
|
||||||
<img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文
|
<img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -9,7 +9,9 @@
|
|||||||

|

|
||||||

|

|
||||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||||
#### Top Technologies
|
|
||||||
|
#### 核心技术
|
||||||
|
|
||||||
[](#)
|
[](#)
|
||||||
[](#)
|
[](#)
|
||||||
[](#)
|
[](#)
|
||||||
@@ -28,16 +30,18 @@
|
|||||||
如果你愿意,可以在这里支持这个项目!\
|
如果你愿意,可以在这里支持这个项目!\
|
||||||
[](https://github.com/sponsors/LukeGus)
|
[](https://github.com/sponsors/LukeGus)
|
||||||
|
|
||||||
# Overview
|
# 概览
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/LukeGus/Termix">
|
<a href="https://github.com/LukeGus/Termix">
|
||||||
<img alt="Termix Banner" src=./public/icon.svg style="width: 250px; height: 250px;"> </a>
|
<img alt="Termix Banner" src=./public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix 提供 SSH 终端访问、SSH 隧道功能以及远程文件编辑,还会陆续添加更多工具。
|
Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix
|
||||||
|
提供 SSH 终端访问、SSH 隧道功能以及远程文件编辑,还会陆续添加更多工具。
|
||||||
|
|
||||||
|
# 功能
|
||||||
|
|
||||||
# Features
|
|
||||||
- **SSH 终端访问** - 功能完整的终端,支持分屏(最多 4 个面板)和标签系统
|
- **SSH 终端访问** - 功能完整的终端,支持分屏(最多 4 个面板)和标签系统
|
||||||
- **SSH 隧道管理** - 创建和管理 SSH 隧道,支持自动重连和健康监控
|
- **SSH 隧道管理** - 创建和管理 SSH 隧道,支持自动重连和健康监控
|
||||||
- **远程文件编辑器** - 直接在远程服务器编辑文件,支持语法高亮和文件管理功能(上传、删除、重命名等)
|
- **远程文件编辑器** - 直接在远程服务器编辑文件,支持语法高亮和文件管理功能(上传、删除、重命名等)
|
||||||
@@ -47,14 +51,17 @@ Termix 是一个开源、永久免费、自托管的一体化服务器管理平
|
|||||||
- **现代化界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁界面
|
- **现代化界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁界面
|
||||||
- **语言支持** - 内置中英文支持
|
- **语言支持** - 内置中英文支持
|
||||||
|
|
||||||
# Planned Features
|
# 计划功能
|
||||||
|
|
||||||
- **增强管理员控制** - 提供更精细的用户和管理员权限控制、共享主机等功能
|
- **增强管理员控制** - 提供更精细的用户和管理员权限控制、共享主机等功能
|
||||||
- **主题定制** - 修改所有工具的主题风格
|
- **主题定制** - 修改所有工具的主题风格
|
||||||
- **增强终端支持** - 添加更多终端协议,如 VNC 和 RDP(有类似 Apache Guacamole 的 RDP 集成经验者请通过创建 issue 联系我)
|
- **增强终端支持** - 添加更多终端协议,如 VNC 和 RDP(有类似 Apache Guacamole 的 RDP 集成经验者请通过创建 issue 联系我)
|
||||||
- **移动端支持** - 支持移动应用或 Termix 网站移动版,让你在手机上管理服务器
|
- **移动端支持** - 支持移动应用或 Termix 网站移动版,让你在手机上管理服务器
|
||||||
|
|
||||||
# Installation
|
# 安装
|
||||||
|
|
||||||
访问 Termix [文档](https://docs.termix.site/install) 获取安装信息。或者可以参考以下示例 docker-compose 文件:
|
访问 Termix [文档](https://docs.termix.site/install) 获取安装信息。或者可以参考以下示例 docker-compose 文件:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
termix:
|
termix:
|
||||||
@@ -73,10 +80,12 @@ volumes:
|
|||||||
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。
|
||||||
|
|
||||||
|
# 展示
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
|
<img src="./repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
|
||||||
@@ -95,6 +104,6 @@ volumes:
|
|||||||
</video>
|
</video>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# License
|
# 许可证
|
||||||
根据 Apache 2.0 许可证发布。更多信息请参见 LICENSE。
|
|
||||||
|
|
||||||
|
根据 Apache 2.0 许可证发布。更多信息请参见 LICENSE。
|
||||||
34
README.md
34
README.md
@@ -1,4 +1,5 @@
|
|||||||
# Repo Stats
|
# Repo Stats
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English |
|
<img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English |
|
||||||
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a>
|
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a>
|
||||||
@@ -9,7 +10,9 @@
|
|||||||

|

|
||||||

|

|
||||||
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
|
||||||
|
|
||||||
#### Top Technologies
|
#### Top Technologies
|
||||||
|
|
||||||
[](#)
|
[](#)
|
||||||
[](#)
|
[](#)
|
||||||
[](#)
|
[](#)
|
||||||
@@ -35,24 +38,34 @@ If you would like, you can support the project here!\
|
|||||||
<img alt="Termix Banner" src=./public/icon.svg style="width: 250px; height: 250px;"> </a>
|
<img alt="Termix Banner" src=./public/icon.svg style="width: 250px; height: 250px;"> </a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
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
|
# Features
|
||||||
|
|
||||||
- **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) and tab system
|
- **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
|
- **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
|
- **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
|
- **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
|
- **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
|
- **Modern UI** - Clean mobile friendly (in progress) interface built with React, Tailwind CSS, and Shadcn
|
||||||
- **Languages** - Built-in support for English and Chinese
|
- **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
|
# 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
|
# 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
|
```yaml
|
||||||
services:
|
services:
|
||||||
termix:
|
termix:
|
||||||
@@ -70,10 +83,16 @@ volumes:
|
|||||||
termix-data:
|
termix-data:
|
||||||
driver: local
|
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
|
# 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
|
# Show-off
|
||||||
|
|
||||||
@@ -95,4 +114,5 @@ If you need help with Termix, you can join the [Discord](https://discord.gg/jVQG
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
Distributed under the Apache License Version 2.0. See LICENSE for more information.
|
Distributed under the Apache License Version 2.0. See LICENSE for more information.
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export function useConfirmation() {
|
|||||||
setOnConfirm(null);
|
setOnConfirm(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// For simple confirmations, we can use a toast with action
|
|
||||||
const confirmWithToast = (message: string, callback: () => void, variant: 'default' | 'destructive' = 'default') => {
|
const confirmWithToast = (message: string, callback: () => void, variant: 'default' | 'destructive' = 'default') => {
|
||||||
const actionText = variant === 'destructive' ? 'Delete' : 'Confirm';
|
const actionText = variant === 'destructive' ? 'Delete' : 'Confirm';
|
||||||
const cancelText = 'Cancel';
|
const cancelText = 'Cancel';
|
||||||
@@ -47,9 +46,10 @@ export function useConfirmation() {
|
|||||||
},
|
},
|
||||||
cancel: {
|
cancel: {
|
||||||
label: cancelText,
|
label: cancelText,
|
||||||
onClick: () => {}
|
onClick: () => {
|
||||||
|
}
|
||||||
},
|
},
|
||||||
duration: 10000, // Longer duration for confirmations
|
duration: 10000,
|
||||||
className: variant === 'destructive' ? 'border-red-500' : ''
|
className: variant === 'destructive' ? 'border-red-500' : ''
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +1,26 @@
|
|||||||
// i18n configuration for multi-language support
|
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import {initReactI18next} from 'react-i18next';
|
import {initReactI18next} from 'react-i18next';
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
|
||||||
// Import translation files directly
|
|
||||||
import enTranslation from '../locales/en/translation.json';
|
import enTranslation from '../locales/en/translation.json';
|
||||||
import zhTranslation from '../locales/zh/translation.json';
|
import zhTranslation from '../locales/zh/translation.json';
|
||||||
|
|
||||||
// Initialize i18n
|
|
||||||
i18n
|
i18n
|
||||||
.use(LanguageDetector) // Detect user language
|
.use(LanguageDetector)
|
||||||
.use(initReactI18next) // Pass i18n instance to react-i18next
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
supportedLngs: ['en', 'zh'], // Supported languages
|
supportedLngs: ['en', 'zh'],
|
||||||
fallbackLng: 'en', // Fallback language
|
fallbackLng: 'en',
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|
||||||
// Detection options - disabled to always use English by default
|
|
||||||
detection: {
|
detection: {
|
||||||
order: ['localStorage', 'cookie'], // Only check user's saved preference
|
order: ['localStorage', 'cookie'],
|
||||||
caches: ['localStorage', 'cookie'],
|
caches: ['localStorage', 'cookie'],
|
||||||
lookupLocalStorage: 'i18nextLng',
|
lookupLocalStorage: 'i18nextLng',
|
||||||
lookupCookie: 'i18nextLng',
|
lookupCookie: 'i18nextLng',
|
||||||
checkWhitelist: true,
|
checkWhitelist: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Resources - load translations directly
|
|
||||||
resources: {
|
resources: {
|
||||||
en: {
|
en: {
|
||||||
translation: enTranslation
|
translation: enTranslation
|
||||||
@@ -36,11 +31,11 @@ i18n
|
|||||||
},
|
},
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // React already escapes values
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
react: {
|
react: {
|
||||||
useSuspense: false, // Disable suspense for SSR compatibility
|
useSuspense: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success';
|
||||||
|
|
||||||
export interface LogContext {
|
export interface LogContext {
|
||||||
@@ -21,6 +16,7 @@ export interface LogContext {
|
|||||||
retryCount?: number;
|
retryCount?: number;
|
||||||
errorCode?: string;
|
errorCode?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +132,6 @@ class FrontendLogger {
|
|||||||
this.log('success', message, context);
|
this.log('success', message, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience methods for common operations
|
|
||||||
api(message: string, context?: LogContext): void {
|
api(message: string, context?: LogContext): void {
|
||||||
this.info(`API: ${message}`, {...context, operation: 'api'});
|
this.info(`API: ${message}`, {...context, operation: 'api'});
|
||||||
}
|
}
|
||||||
@@ -185,7 +180,6 @@ class FrontendLogger {
|
|||||||
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 {
|
requestStart(method: string, url: string, context?: LogContext): void {
|
||||||
const cleanUrl = this.sanitizeUrl(url);
|
const cleanUrl = this.sanitizeUrl(url);
|
||||||
const shortUrl = this.getShortUrl(cleanUrl);
|
const shortUrl = this.getShortUrl(cleanUrl);
|
||||||
@@ -269,12 +263,10 @@ class FrontendLogger {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced logging for API operations
|
|
||||||
apiOperation(operation: string, details: string, context?: LogContext): void {
|
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 {
|
requestSummary(method: string, url: string, status: number, responseTime: number, context?: LogContext): void {
|
||||||
const cleanUrl = this.sanitizeUrl(url);
|
const cleanUrl = this.sanitizeUrl(url);
|
||||||
const shortUrl = this.getShortUrl(cleanUrl);
|
const shortUrl = this.getShortUrl(cleanUrl);
|
||||||
@@ -287,7 +279,6 @@ class FrontendLogger {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// New helper methods for better formatting
|
|
||||||
private getShortUrl(url: string): string {
|
private getShortUrl(url: string): string {
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
@@ -316,10 +307,8 @@ class FrontendLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sanitizeUrl(url: string): string {
|
private sanitizeUrl(url: string): string {
|
||||||
// Remove sensitive information from URLs for logging
|
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
// Remove query parameters that might contain sensitive data
|
|
||||||
if (urlObj.searchParams.has('password') || urlObj.searchParams.has('token')) {
|
if (urlObj.searchParams.has('password') || urlObj.searchParams.has('token')) {
|
||||||
urlObj.search = '';
|
urlObj.search = '';
|
||||||
}
|
}
|
||||||
@@ -330,7 +319,6 @@ class FrontendLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service-specific loggers
|
|
||||||
export const apiLogger = new FrontendLogger('API', '🌐', '#3b82f6');
|
export const apiLogger = new FrontendLogger('API', '🌐', '#3b82f6');
|
||||||
export const authLogger = new FrontendLogger('AUTH', '🔐', '#dc2626');
|
export const authLogger = new FrontendLogger('AUTH', '🔐', '#dc2626');
|
||||||
export const sshLogger = new FrontendLogger('SSH', '🖥️', '#1e3a8a');
|
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 statsLogger = new FrontendLogger('STATS', '📊', '#22c55e');
|
||||||
export const systemLogger = new FrontendLogger('SYSTEM', '🚀', '#1e3a8a');
|
export const systemLogger = new FrontendLogger('SYSTEM', '🚀', '#1e3a8a');
|
||||||
|
|
||||||
// Default logger for general use
|
|
||||||
export const logger = systemLogger;
|
export const logger = systemLogger;
|
||||||
|
|||||||
20
src/types/electron.d.ts
vendored
20
src/types/electron.d.ts
vendored
@@ -1,20 +0,0 @@
|
|||||||
interface ElectronAPI {
|
|
||||||
getAppVersion: () => Promise<string>;
|
|
||||||
getPlatform: () => Promise<string>;
|
|
||||||
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<any>;
|
|
||||||
showOpenDialog: (options: any) => Promise<any>;
|
|
||||||
onUpdateAvailable: (callback: () => void) => void;
|
|
||||||
onUpdateDownloaded: (callback: () => void) => void;
|
|
||||||
removeAllListeners: (channel: string) => void;
|
|
||||||
isElectron: boolean;
|
|
||||||
isDev: boolean;
|
|
||||||
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Window {
|
|
||||||
electronAPI?: ElectronAPI;
|
|
||||||
IS_ELECTRON?: boolean;
|
|
||||||
}
|
|
||||||
@@ -75,12 +75,9 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
const jwt = getCookie("jwt");
|
const jwt = getCookie("jwt");
|
||||||
if (!jwt) return;
|
if (!jwt) return;
|
||||||
|
|
||||||
// Check if we're in Electron and have a server configured
|
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
// In Electron, check if we have a configured server
|
|
||||||
const serverUrl = (window as any).configuredServerUrl;
|
const serverUrl = (window as any).configuredServerUrl;
|
||||||
if (!serverUrl) {
|
if (!serverUrl) {
|
||||||
console.log('No server configured in Electron, skipping API calls');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,8 +87,6 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
if (res) setOidcConfig(res);
|
if (res) setOidcConfig(res);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.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')) {
|
if (!err.message?.includes('No server configured')) {
|
||||||
toast.error(t('admin.failedToFetchOidcConfig'));
|
toast.error(t('admin.failedToFetchOidcConfig'));
|
||||||
}
|
}
|
||||||
@@ -100,11 +95,9 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Check if we're in Electron and have a server configured
|
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
const serverUrl = (window as any).configuredServerUrl;
|
const serverUrl = (window as any).configuredServerUrl;
|
||||||
if (!serverUrl) {
|
if (!serverUrl) {
|
||||||
console.log('No server configured in Electron, skipping registration status check');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,8 +109,6 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.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')) {
|
if (!err.message?.includes('No server configured')) {
|
||||||
toast.error(t('admin.failedToFetchRegistrationStatus'));
|
toast.error(t('admin.failedToFetchRegistrationStatus'));
|
||||||
}
|
}
|
||||||
@@ -128,11 +119,9 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
const jwt = getCookie("jwt");
|
const jwt = getCookie("jwt");
|
||||||
if (!jwt) return;
|
if (!jwt) return;
|
||||||
|
|
||||||
// Check if we're in Electron and have a server configured
|
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
const serverUrl = (window as any).configuredServerUrl;
|
const serverUrl = (window as any).configuredServerUrl;
|
||||||
if (!serverUrl) {
|
if (!serverUrl) {
|
||||||
console.log('No server configured in Electron, skipping user fetch');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,8 +131,6 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
const response = await getUserList();
|
const response = await getUserList();
|
||||||
setUsers(response.users);
|
setUsers(response.users);
|
||||||
} catch (err) {
|
} 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')) {
|
if (!err.message?.includes('No server configured')) {
|
||||||
toast.error(t('admin.failedToFetchUsers'));
|
toast.error(t('admin.failedToFetchUsers'));
|
||||||
}
|
}
|
||||||
@@ -219,7 +206,6 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
toast.success(t('admin.adminStatusRemoved', {username}));
|
toast.success(t('admin.adminStatusRemoved', {username}));
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to remove admin status:', err);
|
|
||||||
toast.error(t('admin.failedToRemoveAdminStatus'));
|
toast.error(t('admin.failedToRemoveAdminStatus'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +222,6 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
toast.success(t('admin.userDeletedSuccessfully', {username}));
|
toast.success(t('admin.userDeletedSuccessfully', {username}));
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to delete user:', err);
|
|
||||||
toast.error(t('admin.failedToDeleteUser'));
|
toast.error(t('admin.failedToDeleteUser'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -414,7 +399,8 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
size="sm">{usersLoading ? t('admin.loading') : t('admin.refresh')}</Button>
|
size="sm">{usersLoading ? t('admin.loading') : t('admin.refresh')}</Button>
|
||||||
</div>
|
</div>
|
||||||
{usersLoading ? (
|
{usersLoading ? (
|
||||||
<div className="text-center py-8 text-muted-foreground">{t('admin.loadingUsers')}</div>
|
<div
|
||||||
|
className="text-center py-8 text-muted-foreground">{t('admin.loadingUsers')}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border rounded-md overflow-hidden">
|
<div className="border rounded-md overflow-hidden">
|
||||||
<Table>
|
<Table>
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import { Button } from "@/components/ui/button"
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form"
|
} from "@/components/ui/form"
|
||||||
import {Input} from "@/components/ui/input"
|
import {Input} from "@/components/ui/input"
|
||||||
import {PasswordInput} from "@/components/ui/password-input"
|
import {PasswordInput} from "@/components/ui/password-input"
|
||||||
@@ -18,7 +16,6 @@ import { ScrollArea } from "@/components/ui/scroll-area"
|
|||||||
import {Separator} from "@/components/ui/separator"
|
import {Separator} from "@/components/ui/separator"
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
|
||||||
import React, {useEffect, useRef, useState} from "react"
|
import React, {useEffect, useRef, useState} from "react"
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
|
||||||
import {toast} from "sonner"
|
import {toast} from "sonner"
|
||||||
import {createCredential, updateCredential, getCredentials, getCredentialDetails} from '@/ui/main-axios'
|
import {createCredential, updateCredential, getCredentials, getCredentialDetails} from '@/ui/main-axios'
|
||||||
import {useTranslation} from "react-i18next"
|
import {useTranslation} from "react-i18next"
|
||||||
@@ -64,7 +61,6 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
|||||||
const fullDetails = await getCredentialDetails(editingCredential.id);
|
const fullDetails = await getCredentialDetails(editingCredential.id);
|
||||||
setFullCredentialDetails(fullDetails);
|
setFullCredentialDetails(fullDetails);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch credential details:', error);
|
|
||||||
toast.error(t('credentials.failedToFetchCredentialDetails'));
|
toast.error(t('credentials.failedToFetchCredentialDetails'));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -139,7 +135,6 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
|||||||
const defaultAuthType = fullCredentialDetails.authType;
|
const defaultAuthType = fullCredentialDetails.authType;
|
||||||
setAuthTab(defaultAuthType);
|
setAuthTab(defaultAuthType);
|
||||||
|
|
||||||
// Force form reset with a small delay to ensure proper rendering
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const formData = {
|
const formData = {
|
||||||
name: fullCredentialDetails.name || "",
|
name: fullCredentialDetails.name || "",
|
||||||
@@ -154,11 +149,10 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
|||||||
keyType: "auto" as const,
|
keyType: "auto" as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only set the relevant authentication fields based on authType
|
|
||||||
if (defaultAuthType === 'password') {
|
if (defaultAuthType === 'password') {
|
||||||
formData.password = fullCredentialDetails.password || "";
|
formData.password = fullCredentialDetails.password || "";
|
||||||
} else if (defaultAuthType === 'key') {
|
} else if (defaultAuthType === 'key') {
|
||||||
formData.key = "existing_key"; // Placeholder to indicate existing key
|
formData.key = "existing_key";
|
||||||
formData.keyPassword = fullCredentialDetails.keyPassword || "";
|
formData.keyPassword = fullCredentialDetails.keyPassword || "";
|
||||||
formData.keyType = (fullCredentialDetails.keyType as any) || "auto" as const;
|
formData.keyType = (fullCredentialDetails.keyType as any) || "auto" as const;
|
||||||
}
|
}
|
||||||
@@ -234,7 +228,6 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
|||||||
|
|
||||||
window.dispatchEvent(new CustomEvent('credentials:changed'));
|
window.dispatchEvent(new CustomEvent('credentials:changed'));
|
||||||
|
|
||||||
// Reset form after successful submission
|
|
||||||
form.reset();
|
form.reset();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t('credentials.failedToSaveCredential'));
|
toast.error(t('credentials.failedToSaveCredential'));
|
||||||
@@ -483,17 +476,13 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
|||||||
setAuthTab(newAuthType);
|
setAuthTab(newAuthType);
|
||||||
form.setValue('authType', newAuthType);
|
form.setValue('authType', newAuthType);
|
||||||
|
|
||||||
// Clear ALL authentication fields first
|
|
||||||
form.setValue('password', '');
|
form.setValue('password', '');
|
||||||
form.setValue('key', null);
|
form.setValue('key', null);
|
||||||
form.setValue('keyPassword', '');
|
form.setValue('keyPassword', '');
|
||||||
form.setValue('keyType', 'auto');
|
form.setValue('keyType', 'auto');
|
||||||
|
|
||||||
// Then set only the relevant fields based on auth type
|
|
||||||
if (newAuthType === 'password') {
|
if (newAuthType === 'password') {
|
||||||
// Password fields will be filled by user
|
|
||||||
} else if (newAuthType === 'key') {
|
} else if (newAuthType === 'key') {
|
||||||
// Key fields will be filled by user
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="flex-1 flex flex-col h-full min-h-0"
|
className="flex-1 flex flex-col h-full min-h-0"
|
||||||
@@ -510,7 +499,8 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('credentials.password')}</FormLabel>
|
<FormLabel>{t('credentials.password')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<PasswordInput placeholder={t('placeholders.password')} {...field} />
|
<PasswordInput
|
||||||
|
placeholder={t('placeholders.password')} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -521,7 +511,6 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
|||||||
value={keyInputMethod}
|
value={keyInputMethod}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setKeyInputMethod(value as 'upload' | 'paste');
|
setKeyInputMethod(value as 'upload' | 'paste');
|
||||||
// Clear the other field when switching
|
|
||||||
if (value === 'upload') {
|
if (value === 'upload') {
|
||||||
form.setValue('key', null);
|
form.setValue('key', null);
|
||||||
} else {
|
} else {
|
||||||
@@ -530,7 +519,8 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
|||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<TabsList className="inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground">
|
<TabsList
|
||||||
|
className="inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground">
|
||||||
<TabsTrigger value="upload">{t('hosts.uploadFile')}</TabsTrigger>
|
<TabsTrigger value="upload">{t('hosts.uploadFile')}</TabsTrigger>
|
||||||
<TabsTrigger value="paste">{t('hosts.pasteKey')}</TabsTrigger>
|
<TabsTrigger value="paste">{t('hosts.pasteKey')}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -607,7 +597,8 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
|||||||
ref={keyTypeDropdownRef}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 gap-1 p-0">
|
<div
|
||||||
|
className="grid grid-cols-1 gap-1 p-0">
|
||||||
{keyTypeOptions.map((opt) => (
|
{keyTypeOptions.map((opt) => (
|
||||||
<Button
|
<Button
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
@@ -689,7 +680,8 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
|||||||
ref={keyTypeDropdownRef}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 gap-1 p-0">
|
<div
|
||||||
|
className="grid grid-cols-1 gap-1 p-0">
|
||||||
{keyTypeOptions.map((opt) => (
|
{keyTypeOptions.map((opt) => (
|
||||||
<Button
|
<Button
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export function CredentialSelector({ value, onValueChange, onCredentialSelect }:
|
|||||||
const credentialsArray = Array.isArray(data) ? data : (data.credentials || data.data || []);
|
const credentialsArray = Array.isArray(data) ? data : (data.credentials || data.data || []);
|
||||||
setCredentials(credentialsArray);
|
setCredentials(credentialsArray);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch credentials:', error);
|
|
||||||
const {toast} = await import('sonner');
|
const {toast} = await import('sonner');
|
||||||
toast.error(t('credentials.failedToFetchCredentials'));
|
toast.error(t('credentials.failedToFetchCredentials'));
|
||||||
setCredentials([]);
|
setCredentials([]);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {Separator} from "@/components/ui/separator";
|
import {Separator} from "@/components/ui/separator";
|
||||||
import {ScrollArea} from "@/components/ui/scroll-area";
|
import {ScrollArea} from "@/components/ui/scroll-area";
|
||||||
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
import {Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle} from "@/components/ui/sheet";
|
||||||
import {
|
import {
|
||||||
Key,
|
Key,
|
||||||
User,
|
User,
|
||||||
@@ -13,13 +13,11 @@ import {
|
|||||||
Folder,
|
Folder,
|
||||||
Edit3,
|
Edit3,
|
||||||
Copy,
|
Copy,
|
||||||
Settings,
|
|
||||||
Shield,
|
Shield,
|
||||||
Clock,
|
Clock,
|
||||||
Server,
|
Server,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
ExternalLink,
|
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
FileText
|
FileText
|
||||||
@@ -47,7 +45,6 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
const response = await getCredentialDetails(credential.id);
|
const response = await getCredentialDetails(credential.id);
|
||||||
setCredentialDetails(response);
|
setCredentialDetails(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch credential details:', error);
|
|
||||||
toast.error(t('credentials.failedToFetchCredentialDetails'));
|
toast.error(t('credentials.failedToFetchCredentialDetails'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -57,7 +54,6 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
const response = await getCredentialHosts(credential.id);
|
const response = await getCredentialHosts(credential.id);
|
||||||
setHostsUsing(response);
|
setHostsUsing(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch hosts using credential:', error);
|
|
||||||
toast.error(t('credentials.failedToFetchHostsUsing'));
|
toast.error(t('credentials.failedToFetchHostsUsing'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -127,7 +123,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
</div>
|
</div>
|
||||||
<div className={`p-3 rounded-md bg-zinc-800 dark:bg-zinc-800 ${isMultiline ? '' : 'min-h-[2.5rem]'}`}>
|
<div className={`p-3 rounded-md bg-zinc-800 dark:bg-zinc-800 ${isMultiline ? '' : 'min-h-[2.5rem]'}`}>
|
||||||
{isVisible ? (
|
{isVisible ? (
|
||||||
<pre className={`text-sm ${isMultiline ? 'whitespace-pre-wrap' : 'whitespace-nowrap'} font-mono`}>
|
<pre
|
||||||
|
className={`text-sm ${isMultiline ? 'whitespace-pre-wrap' : 'whitespace-nowrap'} font-mono`}>
|
||||||
{value}
|
{value}
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
@@ -167,11 +164,13 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Badge variant="outline" className="border-zinc-300 dark:border-zinc-600 text-zinc-600 dark:text-zinc-400">
|
<Badge variant="outline"
|
||||||
|
className="border-zinc-300 dark:border-zinc-600 text-zinc-600 dark:text-zinc-400">
|
||||||
{credentialDetails.authType}
|
{credentialDetails.authType}
|
||||||
</Badge>
|
</Badge>
|
||||||
{credentialDetails.keyType && (
|
{credentialDetails.keyType && (
|
||||||
<Badge variant="secondary" className="bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300">
|
<Badge variant="secondary"
|
||||||
|
className="bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300">
|
||||||
{credentialDetails.keyType}
|
{credentialDetails.keyType}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -181,7 +180,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
|
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
{/* Tab Navigation */}
|
{/* Tab Navigation */}
|
||||||
<div className="flex space-x-2 p-2 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg">
|
<div
|
||||||
|
className="flex space-x-2 p-2 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg">
|
||||||
<Button
|
<Button
|
||||||
variant={activeTab === 'overview' ? 'default' : 'ghost'}
|
variant={activeTab === 'overview' ? 'default' : 'ghost'}
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -216,7 +216,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
<div className="grid gap-10 lg:grid-cols-2">
|
<div className="grid gap-10 lg:grid-cols-2">
|
||||||
<Card className="border-zinc-200 dark:border-zinc-700">
|
<Card className="border-zinc-200 dark:border-zinc-700">
|
||||||
<CardHeader className="pb-8">
|
<CardHeader className="pb-8">
|
||||||
<CardTitle className="text-lg font-semibold">{t('credentials.basicInformation')}</CardTitle>
|
<CardTitle
|
||||||
|
className="text-lg font-semibold">{t('credentials.basicInformation')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-8">
|
<CardContent className="space-y-8">
|
||||||
<div className="flex items-center space-x-5">
|
<div className="flex items-center space-x-5">
|
||||||
@@ -224,8 +225,10 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
<User className="h-4 w-4 text-zinc-500 dark:text-zinc-400"/>
|
<User className="h-4 w-4 text-zinc-500 dark:text-zinc-400"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('common.username')}</div>
|
<div
|
||||||
<div className="font-medium text-zinc-800 dark:text-zinc-200">{credentialDetails.username}</div>
|
className="text-sm text-zinc-500 dark:text-zinc-400">{t('common.username')}</div>
|
||||||
|
<div
|
||||||
|
className="font-medium text-zinc-800 dark:text-zinc-200">{credentialDetails.username}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -233,7 +236,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<Folder className="h-4 w-4 text-zinc-500 dark:text-zinc-400"/>
|
<Folder className="h-4 w-4 text-zinc-500 dark:text-zinc-400"/>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('common.folder')}</div>
|
<div
|
||||||
|
className="text-sm text-zinc-500 dark:text-zinc-400">{t('common.folder')}</div>
|
||||||
<div className="font-medium">{credentialDetails.folder}</div>
|
<div className="font-medium">{credentialDetails.folder}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,7 +247,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4">
|
||||||
<Hash className="h-4 w-4 text-zinc-500 dark:text-zinc-400 mt-1"/>
|
<Hash className="h-4 w-4 text-zinc-500 dark:text-zinc-400 mt-1"/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-sm text-zinc-500 dark:text-zinc-400 mb-3">{t('hosts.tags')}</div>
|
<div
|
||||||
|
className="text-sm text-zinc-500 dark:text-zinc-400 mb-3">{t('hosts.tags')}</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{credentialDetails.tags.map((tag, index) => (
|
{credentialDetails.tags.map((tag, index) => (
|
||||||
<Badge key={index} variant="outline" className="text-xs">
|
<Badge key={index} variant="outline" className="text-xs">
|
||||||
@@ -260,7 +265,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<Calendar className="h-4 w-4 text-zinc-500 dark:text-zinc-400"/>
|
<Calendar className="h-4 w-4 text-zinc-500 dark:text-zinc-400"/>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.created')}</div>
|
<div
|
||||||
|
className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.created')}</div>
|
||||||
<div className="font-medium">{formatDate(credentialDetails.createdAt)}</div>
|
<div className="font-medium">{formatDate(credentialDetails.createdAt)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,7 +274,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<Calendar className="h-4 w-4 text-zinc-500 dark:text-zinc-400"/>
|
<Calendar className="h-4 w-4 text-zinc-500 dark:text-zinc-400"/>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.lastModified')}</div>
|
<div
|
||||||
|
className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.lastModified')}</div>
|
||||||
<div className="font-medium">{formatDate(credentialDetails.updatedAt)}</div>
|
<div className="font-medium">{formatDate(credentialDetails.updatedAt)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,19 +297,24 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{credentialDetails.lastUsed && (
|
{credentialDetails.lastUsed && (
|
||||||
<div className="flex items-center space-x-4 p-4 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
<div
|
||||||
|
className="flex items-center space-x-4 p-4 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
||||||
<Clock className="h-5 w-5 text-zinc-600 dark:text-zinc-400"/>
|
<Clock className="h-5 w-5 text-zinc-600 dark:text-zinc-400"/>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.lastUsed')}</div>
|
<div
|
||||||
<div className="font-medium">{formatDate(credentialDetails.lastUsed)}</div>
|
className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.lastUsed')}</div>
|
||||||
|
<div
|
||||||
|
className="font-medium">{formatDate(credentialDetails.lastUsed)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 p-4 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
<div
|
||||||
|
className="flex items-center space-x-4 p-4 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
||||||
<Server className="h-5 w-5 text-zinc-600 dark:text-zinc-400"/>
|
<Server className="h-5 w-5 text-zinc-600 dark:text-zinc-400"/>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.connectedHosts')}</div>
|
<div
|
||||||
|
className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.connectedHosts')}</div>
|
||||||
<div className="font-medium">{hostsUsing.length}</div>
|
<div className="font-medium">{hostsUsing.length}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -323,7 +335,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
<div className="flex items-center space-x-4 p-6 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
<div
|
||||||
|
className="flex items-center space-x-4 p-6 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
||||||
<CheckCircle className="h-6 w-6 text-zinc-600 dark:text-zinc-400"/>
|
<CheckCircle className="h-6 w-6 text-zinc-600 dark:text-zinc-400"/>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-zinc-800 dark:text-zinc-200">
|
<div className="font-medium text-zinc-800 dark:text-zinc-200">
|
||||||
@@ -348,7 +361,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-3">
|
<div
|
||||||
|
className="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-3">
|
||||||
{t('credentials.keyType')}
|
{t('credentials.keyType')}
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" className="text-sm">
|
<Badge variant="outline" className="text-sm">
|
||||||
@@ -367,7 +381,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-start space-x-4 p-6 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
<div
|
||||||
|
className="flex items-start space-x-4 p-6 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
||||||
<AlertTriangle className="h-5 w-5 text-zinc-600 dark:text-zinc-400 mt-0.5"/>
|
<AlertTriangle className="h-5 w-5 text-zinc-600 dark:text-zinc-400 mt-0.5"/>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<div className="font-medium text-zinc-800 dark:text-zinc-200 mb-2">
|
<div className="font-medium text-zinc-800 dark:text-zinc-200 mb-2">
|
||||||
@@ -407,7 +422,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="p-2 bg-zinc-100 dark:bg-zinc-800 rounded">
|
<div className="p-2 bg-zinc-100 dark:bg-zinc-800 rounded">
|
||||||
<Server className="h-4 w-4 text-zinc-600 dark:text-zinc-400" />
|
<Server
|
||||||
|
className="h-4 w-4 text-zinc-600 dark:text-zinc-400"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
@@ -418,7 +434,8 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-sm text-zinc-500 dark:text-zinc-400">
|
<div
|
||||||
|
className="text-right text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
{formatDate(host.createdAt)}
|
{formatDate(host.createdAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleEdit = (credential: Credential) => {
|
const handleEdit = (credential: Credential) => {
|
||||||
if (onEditCredential) {
|
if (onEditCredential) {
|
||||||
onEditCredential(credential);
|
onEditCredential(credential);
|
||||||
@@ -93,7 +92,10 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps
|
|||||||
|
|
||||||
const handleRemoveFromFolder = async (credential: Credential) => {
|
const handleRemoveFromFolder = async (credential: Credential) => {
|
||||||
confirmWithToast(
|
confirmWithToast(
|
||||||
t('credentials.confirmRemoveFromFolder', { name: credential.name || credential.username, folder: credential.folder }),
|
t('credentials.confirmRemoveFromFolder', {
|
||||||
|
name: credential.name || credential.username,
|
||||||
|
folder: credential.folder
|
||||||
|
}),
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
setOperationLoading(true);
|
setOperationLoading(true);
|
||||||
@@ -143,11 +145,10 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps
|
|||||||
setEditingFolderName('');
|
setEditingFolderName('');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Drag and drop handlers
|
|
||||||
const handleDragStart = (e: React.DragEvent, credential: Credential) => {
|
const handleDragStart = (e: React.DragEvent, credential: Credential) => {
|
||||||
setDraggedCredential(credential);
|
setDraggedCredential(credential);
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
e.dataTransfer.setData('text/plain', ''); // Required for Firefox
|
e.dataTransfer.setData('text/plain', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragEnd = () => {
|
const handleDragEnd = () => {
|
||||||
@@ -356,7 +357,8 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps
|
|||||||
<div className="flex items-center gap-2 flex-1">
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<Folder className="h-4 w-4"/>
|
<Folder className="h-4 w-4"/>
|
||||||
{editingFolder === folder ? (
|
{editingFolder === folder ? (
|
||||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
<div className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.stopPropagation()}>
|
||||||
<Input
|
<Input
|
||||||
value={editingFolderName}
|
value={editingFolderName}
|
||||||
onChange={(e) => setEditingFolderName(e.target.value)}
|
onChange={(e) => setEditingFolderName(e.target.value)}
|
||||||
@@ -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"
|
className="h-5 w-5 p-0 text-orange-500 hover:text-orange-700 hover:bg-orange-500/10"
|
||||||
disabled={operationLoading}
|
disabled={operationLoading}
|
||||||
>
|
>
|
||||||
<FolderMinus className="h-3 w-3"/>
|
<FolderMinus
|
||||||
|
className="h-3 w-3"/>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Remove from folder "{credential.folder}"</p>
|
<p>Remove from folder
|
||||||
|
"{credential.folder}"</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -538,7 +542,8 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
<Badge variant="outline" className="text-xs px-1 py-0">
|
<Badge variant="outline"
|
||||||
|
className="text-xs px-1 py-0">
|
||||||
{credential.authType === 'password' ? (
|
{credential.authType === 'password' ? (
|
||||||
<Key className="h-2 w-2 mr-0.5"/>
|
<Key className="h-2 w-2 mr-0.5"/>
|
||||||
) : (
|
) : (
|
||||||
@@ -547,7 +552,8 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps
|
|||||||
{credential.authType}
|
{credential.authType}
|
||||||
</Badge>
|
</Badge>
|
||||||
{credential.authType === 'key' && credential.keyType && (
|
{credential.authType === 'key' && credential.keyType && (
|
||||||
<Badge variant="outline" className="text-xs px-1 py-0">
|
<Badge variant="outline"
|
||||||
|
className="text-xs px-1 py-0">
|
||||||
{credential.keyType}
|
{credential.keyType}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -558,7 +564,8 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps
|
|||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="font-medium">Click to edit credential</p>
|
<p className="font-medium">Click to edit credential</p>
|
||||||
<p className="text-xs text-muted-foreground">Drag to move between folders</p>
|
<p className="text-xs text-muted-foreground">Drag to
|
||||||
|
move between folders</p>
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, {useState, useEffect, useRef} from "react";
|
import React, {useState, useEffect, useRef} from "react";
|
||||||
import {FileManagerLeftSidebar} from "@/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx";
|
import {FileManagerLeftSidebar} from "@/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx";
|
||||||
import {FileManagerTabList} from "@/ui/Desktop/Apps/File Manager/FileManagerTabList.tsx";
|
|
||||||
import {FileManagerHomeView} from "@/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx";
|
import {FileManagerHomeView} from "@/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx";
|
||||||
import {FileManagerFileEditor} from "@/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx";
|
import {FileManagerFileEditor} from "@/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx";
|
||||||
import {FileManagerOperations} from "@/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx";
|
import {FileManagerOperations} from "@/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx";
|
||||||
@@ -8,7 +7,6 @@ import {Button} from '@/components/ui/button.tsx';
|
|||||||
import {FIleManagerTopNavbar} from "@/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx";
|
import {FIleManagerTopNavbar} from "@/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx";
|
||||||
import {cn} from '@/lib/utils.ts';
|
import {cn} from '@/lib/utils.ts';
|
||||||
import {Save, RefreshCw, Settings, Trash2} from 'lucide-react';
|
import {Save, RefreshCw, Settings, Trash2} from 'lucide-react';
|
||||||
import {Separator} from '@/components/ui/separator.tsx';
|
|
||||||
import {toast} from 'sonner';
|
import {toast} from 'sonner';
|
||||||
import {useTranslation} from 'react-i18next';
|
import {useTranslation} from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
@@ -26,9 +24,9 @@ import {
|
|||||||
getSSHStatus,
|
getSSHStatus,
|
||||||
connectSSH
|
connectSSH
|
||||||
} from '@/ui/main-axios.ts';
|
} from '@/ui/main-axios.ts';
|
||||||
import type { SSHHost, Tab, FileManagerProps } from '../../../types/index.js';
|
import type {SSHHost, Tab} from '../../../types/index.js';
|
||||||
|
|
||||||
export function FileManager({onSelectView, embedded = false, initialHost = null, onClose}: {
|
export function FileManager({onSelectView, initialHost = null, onClose}: {
|
||||||
onSelectView?: (view: string) => void,
|
onSelectView?: (view: string) => void,
|
||||||
embedded?: boolean,
|
embedded?: boolean,
|
||||||
initialHost?: SSHHost | null,
|
initialHost?: SSHHost | null,
|
||||||
@@ -122,10 +120,9 @@ export function FileManager({onSelectView, embedded = false, initialHost = null,
|
|||||||
type: 'directory'
|
type: 'directory'
|
||||||
})));
|
})));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch home data:', err);
|
|
||||||
const {toast} = await import('sonner');
|
const {toast} = await import('sonner');
|
||||||
toast.error(t('fileManager.failedToFetchHomeData'));
|
toast.error(t('fileManager.failedToFetchHomeData'));
|
||||||
// Close the file manager tab on connection failure
|
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
@@ -371,7 +368,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null,
|
|||||||
loading: false
|
loading: false
|
||||||
} : t));
|
} : t));
|
||||||
|
|
||||||
// Handle toast notification from backend
|
|
||||||
if (result?.toast) {
|
if (result?.toast) {
|
||||||
toast[result.toast.type](result.toast.message);
|
toast[result.toast.type](result.toast.message);
|
||||||
} else {
|
} else {
|
||||||
@@ -389,7 +385,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null,
|
|||||||
hostId: currentHost.id
|
hostId: currentHost.id
|
||||||
});
|
});
|
||||||
} catch (recentErr) {
|
} catch (recentErr) {
|
||||||
console.error('Failed to add recent file:', recentErr);
|
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
@@ -444,7 +439,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null,
|
|||||||
const {deleteSSHItem} = await import('@/ui/main-axios.ts');
|
const {deleteSSHItem} = await import('@/ui/main-axios.ts');
|
||||||
const response = await deleteSSHItem(currentHost.id.toString(), item.path, item.type === 'directory');
|
const response = await deleteSSHItem(currentHost.id.toString(), item.path, item.type === 'directory');
|
||||||
|
|
||||||
// Handle toast notification from backend
|
|
||||||
if (response?.toast) {
|
if (response?.toast) {
|
||||||
toast[response.toast.type](response.toast.message);
|
toast[response.toast.type](response.toast.message);
|
||||||
} else {
|
} else {
|
||||||
@@ -475,7 +469,8 @@ export function FileManager({onSelectView, embedded = false, initialHost = null,
|
|||||||
onPathChange={updateCurrentPath}
|
onPathChange={updateCurrentPath}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-0 left-64 right-0 bottom-0 flex items-center justify-center bg-dark-bg-darkest">
|
<div
|
||||||
|
className="absolute top-0 left-64 right-0 bottom-0 flex items-center justify-center bg-dark-bg-darkest">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-xl font-semibold text-white mb-2">{t('fileManager.connectToServer')}</h2>
|
<h2 className="text-xl font-semibold text-white mb-2">{t('fileManager.connectToServer')}</h2>
|
||||||
<p className="text-muted-foreground">{t('fileManager.selectServerToEdit')}</p>
|
<p className="text-muted-foreground">{t('fileManager.selectServerToEdit')}</p>
|
||||||
@@ -546,7 +541,8 @@ export function FileManager({onSelectView, embedded = false, initialHost = null,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-[44px] left-64 right-0 bottom-0 overflow-hidden z-[10] bg-dark-bg-very-light flex flex-col">
|
<div
|
||||||
|
className="absolute top-[44px] left-64 right-0 bottom-0 overflow-hidden z-[10] bg-dark-bg-very-light flex flex-col">
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{activeTab === 'home' ? (
|
{activeTab === 'home' ? (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {useState, useEffect} from "react";
|
import React, {useEffect} from "react";
|
||||||
import CodeMirror from "@uiw/react-codemirror";
|
import CodeMirror from "@uiw/react-codemirror";
|
||||||
import {loadLanguage} from '@uiw/codemirror-extensions-langs';
|
import {loadLanguage} from '@uiw/codemirror-extensions-langs';
|
||||||
import {hyperLink} from '@uiw/codemirror-extensions-hyper-link';
|
import {hyperLink} from '@uiw/codemirror-extensions-hyper-link';
|
||||||
|
|||||||
@@ -111,9 +111,12 @@ export function FileManagerHomeView({
|
|||||||
<div className="p-4 flex flex-col gap-4 h-full bg-dark-bg-darkest">
|
<div className="p-4 flex flex-col gap-4 h-full bg-dark-bg-darkest">
|
||||||
<Tabs value={tab} onValueChange={v => setTab(v as 'recent' | 'pinned' | 'shortcuts')} className="w-full">
|
<Tabs value={tab} onValueChange={v => setTab(v as 'recent' | 'pinned' | 'shortcuts')} className="w-full">
|
||||||
<TabsList className="mb-4 bg-dark-bg border-2 border-dark-border">
|
<TabsList className="mb-4 bg-dark-bg border-2 border-dark-border">
|
||||||
<TabsTrigger value="recent" className="data-[state=active]:bg-dark-bg-button">{t('fileManager.recent')}</TabsTrigger>
|
<TabsTrigger value="recent"
|
||||||
<TabsTrigger value="pinned" className="data-[state=active]:bg-dark-bg-button">{t('fileManager.pinned')}</TabsTrigger>
|
className="data-[state=active]:bg-dark-bg-button">{t('fileManager.recent')}</TabsTrigger>
|
||||||
<TabsTrigger value="shortcuts" className="data-[state=active]:bg-dark-bg-button">{t('fileManager.folderShortcuts')}</TabsTrigger>
|
<TabsTrigger value="pinned"
|
||||||
|
className="data-[state=active]:bg-dark-bg-button">{t('fileManager.pinned')}</TabsTrigger>
|
||||||
|
<TabsTrigger value="shortcuts"
|
||||||
|
className="data-[state=active]:bg-dark-bg-button">{t('fileManager.folderShortcuts')}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="recent" className="mt-0">
|
<TabsContent value="recent" className="mt-0">
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useState, useRef, forwardRef, useImperativeHandle} from 'react';
|
import React, {useEffect, useState, useRef, forwardRef, useImperativeHandle} from 'react';
|
||||||
import {Separator} from '@/components/ui/separator.tsx';
|
import {Folder, File, ArrowUp, Pin, MoreVertical, Trash2, Edit3} from 'lucide-react';
|
||||||
import {CornerDownLeft, Folder, File, Server, ArrowUp, Pin, MoreVertical, Trash2, Edit3} from 'lucide-react';
|
|
||||||
import {ScrollArea} from '@/components/ui/scroll-area.tsx';
|
import {ScrollArea} from '@/components/ui/scroll-area.tsx';
|
||||||
import {cn} from '@/lib/utils.ts';
|
import {cn} from '@/lib/utils.ts';
|
||||||
import {Input} from '@/components/ui/input.tsx';
|
import {Input} from '@/components/ui/input.tsx';
|
||||||
@@ -11,18 +10,16 @@ import {
|
|||||||
listSSHFiles,
|
listSSHFiles,
|
||||||
renameSSHItem,
|
renameSSHItem,
|
||||||
deleteSSHItem,
|
deleteSSHItem,
|
||||||
getFileManagerRecent,
|
|
||||||
getFileManagerPinned,
|
getFileManagerPinned,
|
||||||
addFileManagerPinned,
|
addFileManagerPinned,
|
||||||
removeFileManagerPinned,
|
removeFileManagerPinned,
|
||||||
readSSHFile,
|
|
||||||
getSSHStatus,
|
getSSHStatus,
|
||||||
connectSSH
|
connectSSH
|
||||||
} from '@/ui/main-axios.ts';
|
} 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(
|
const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||||
{onSelectView, onOpenFile, tabs, host, onOperationComplete, onError, onSuccess, onPathChange, onDeleteItem}: {
|
{onOpenFile, tabs, host, onOperationComplete, onPathChange, onDeleteItem}: {
|
||||||
onSelectView?: (view: string) => void;
|
onSelectView?: (view: string) => void;
|
||||||
onOpenFile: (file: any) => void;
|
onOpenFile: (file: any) => void;
|
||||||
tabs: any[];
|
tabs: any[];
|
||||||
@@ -55,7 +52,6 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
|||||||
|
|
||||||
const [sshSessionId, setSshSessionId] = useState<string | null>(null);
|
const [sshSessionId, setSshSessionId] = useState<string | null>(null);
|
||||||
const [filesLoading, setFilesLoading] = useState(false);
|
const [filesLoading, setFilesLoading] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [connectingSSH, setConnectingSSH] = useState(false);
|
const [connectingSSH, setConnectingSSH] = useState(false);
|
||||||
const [connectionCache, setConnectionCache] = useState<Record<string, {
|
const [connectionCache, setConnectionCache] = useState<Record<string, {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -320,22 +316,6 @@ 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) => {
|
const startRename = (item: any) => {
|
||||||
setRenamingItem({item, newName: item.name});
|
setRenamingItem({item, newName: item.name});
|
||||||
closeContextMenu();
|
closeContextMenu();
|
||||||
@@ -360,10 +340,12 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-[256px] max-w-[256px]">
|
<div className="flex flex-col h-full w-[256px] max-w-[256px]">
|
||||||
<div className="flex flex-col flex-grow min-h-0">
|
<div className="flex flex-col flex-grow min-h-0">
|
||||||
<div className="flex-1 w-full h-full flex flex-col bg-dark-bg-darkest border-r-2 border-dark-border overflow-hidden p-0 relative min-h-0">
|
<div
|
||||||
|
className="flex-1 w-full h-full flex flex-col bg-dark-bg-darkest border-r-2 border-dark-border overflow-hidden p-0 relative min-h-0">
|
||||||
{host && (
|
{host && (
|
||||||
<div className="flex flex-col h-full w-full max-w-[260px]">
|
<div className="flex flex-col h-full w-full max-w-[260px]">
|
||||||
<div className="flex items-center gap-2 px-2 py-1.5 border-b-2 border-dark-border bg-dark-bg z-20 max-w-[260px]">
|
<div
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 border-b-2 border-dark-border bg-dark-bg z-20 max-w-[260px]">
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -405,7 +387,8 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
|||||||
{connectingSSH || filesLoading ? (
|
{connectingSSH || filesLoading ? (
|
||||||
<div className="text-xs text-muted-foreground">{t('common.loading')}</div>
|
<div className="text-xs text-muted-foreground">{t('common.loading')}</div>
|
||||||
) : filteredFiles.length === 0 ? (
|
) : filteredFiles.length === 0 ? (
|
||||||
<div className="text-xs text-muted-foreground">{t('fileManager.noFilesOrFoldersFound')}</div>
|
<div
|
||||||
|
className="text-xs text-muted-foreground">{t('fileManager.noFilesOrFoldersFound')}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{filteredFiles.map((item: any) => {
|
{filteredFiles.map((item: any) => {
|
||||||
@@ -425,11 +408,16 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
|||||||
{isRenaming ? (
|
{isRenaming ? (
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
{item.type === 'directory' ?
|
{item.type === 'directory' ?
|
||||||
<Folder className="w-4 h-4 text-blue-400 flex-shrink-0"/> :
|
<Folder
|
||||||
<File className="w-4 h-4 text-muted-foreground flex-shrink-0"/>}
|
className="w-4 h-4 text-blue-400 flex-shrink-0"/> :
|
||||||
|
<File
|
||||||
|
className="w-4 h-4 text-muted-foreground flex-shrink-0"/>}
|
||||||
<Input
|
<Input
|
||||||
value={renamingItem.newName}
|
value={renamingItem.newName}
|
||||||
onChange={(e) => 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"
|
className="flex-1 h-6 text-sm bg-dark-bg-button border border-dark-border-hover text-white"
|
||||||
autoFocus
|
autoFocus
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
@@ -454,13 +442,17 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
|||||||
}))}
|
}))}
|
||||||
>
|
>
|
||||||
{item.type === 'directory' ?
|
{item.type === 'directory' ?
|
||||||
<Folder className="w-4 h-4 text-blue-400 flex-shrink-0"/> :
|
<Folder
|
||||||
<File className="w-4 h-4 text-muted-foreground flex-shrink-0"/>}
|
className="w-4 h-4 text-blue-400 flex-shrink-0"/> :
|
||||||
<span className="text-sm text-white truncate flex-1 min-w-0">{item.name}</span>
|
<File
|
||||||
|
className="w-4 h-4 text-muted-foreground flex-shrink-0"/>}
|
||||||
|
<span
|
||||||
|
className="text-sm text-white truncate flex-1 min-w-0">{item.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{item.type === 'file' && (
|
{item.type === 'file' && (
|
||||||
<Button size="icon" variant="ghost" className="h-7 w-7"
|
<Button size="icon" variant="ghost"
|
||||||
|
className="h-7 w-7"
|
||||||
disabled={isOpen}
|
disabled={isOpen}
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -474,7 +466,10 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
|||||||
sshSessionId: host?.id.toString()
|
sshSessionId: host?.id.toString()
|
||||||
});
|
});
|
||||||
setFiles(files.map(f =>
|
setFiles(files.map(f =>
|
||||||
f.path === item.path ? { ...f, isPinned: false } : f
|
f.path === item.path ? {
|
||||||
|
...f,
|
||||||
|
isPinned: false
|
||||||
|
} : f
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
await addFileManagerPinned({
|
await addFileManagerPinned({
|
||||||
@@ -485,14 +480,18 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
|||||||
sshSessionId: host?.id.toString()
|
sshSessionId: host?.id.toString()
|
||||||
});
|
});
|
||||||
setFiles(files.map(f =>
|
setFiles(files.map(f =>
|
||||||
f.path === item.path ? { ...f, isPinned: true } : f
|
f.path === item.path ? {
|
||||||
|
...f,
|
||||||
|
isPinned: true
|
||||||
|
} : f
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pin className={`w-1 h-1 ${item.isPinned ? 'text-yellow-400 fill-current' : 'text-muted-foreground'}`}/>
|
<Pin
|
||||||
|
className={`w-1 h-1 ${item.isPinned ? 'text-yellow-400 fill-current' : 'text-muted-foreground'}`}/>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!isOpen && (
|
{!isOpen && (
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Button} from '@/components/ui/button.tsx';
|
import {Button} from '@/components/ui/button.tsx';
|
||||||
import {Card} from '@/components/ui/card.tsx';
|
import {Card} from '@/components/ui/card.tsx';
|
||||||
import {Separator} from '@/components/ui/separator.tsx';
|
import {Folder, File, Trash2, Pin} from 'lucide-react';
|
||||||
import {Plus, Folder, File, Star, Trash2, Edit, Link2, Server, Pin} from 'lucide-react';
|
|
||||||
import {useTranslation} from 'react-i18next';
|
import {useTranslation} from 'react-i18next';
|
||||||
|
|
||||||
interface SSHConnection {
|
interface SSHConnection {
|
||||||
@@ -43,12 +42,6 @@ interface FileManagerLeftSidebarVileViewerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FileManagerLeftSidebarFileViewer({
|
export function FileManagerLeftSidebarFileViewer({
|
||||||
sshConnections,
|
|
||||||
onAddSSH,
|
|
||||||
onConnectSSH,
|
|
||||||
onEditSSH,
|
|
||||||
onDeleteSSH,
|
|
||||||
onPinSSH,
|
|
||||||
currentPath,
|
currentPath,
|
||||||
files,
|
files,
|
||||||
onOpenFile,
|
onOpenFile,
|
||||||
@@ -58,9 +51,6 @@ export function FileManagerLeftSidebarFileViewer({
|
|||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isSSHMode,
|
isSSHMode,
|
||||||
onSwitchToLocal,
|
|
||||||
onSwitchToSSH,
|
|
||||||
currentSSH,
|
|
||||||
}: FileManagerLeftSidebarVileViewerProps) {
|
}: FileManagerLeftSidebarVileViewerProps) {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Edit3,
|
Edit3,
|
||||||
X,
|
X,
|
||||||
Check,
|
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
FileText,
|
FileText,
|
||||||
Folder
|
Folder
|
||||||
@@ -72,7 +71,6 @@ export function FileManagerOperations({
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Show loading toast
|
|
||||||
const {toast} = await import('sonner');
|
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}));
|
||||||
|
|
||||||
@@ -82,10 +80,8 @@ export function FileManagerOperations({
|
|||||||
|
|
||||||
const response = await uploadSSHFile(sshSessionId, currentPath, uploadFile.name, content);
|
const response = await uploadSSHFile(sshSessionId, currentPath, uploadFile.name, content);
|
||||||
|
|
||||||
// Dismiss loading toast and show success
|
|
||||||
toast.dismiss(loadingToast);
|
toast.dismiss(loadingToast);
|
||||||
|
|
||||||
// Handle toast notification from backend
|
|
||||||
if (response?.toast) {
|
if (response?.toast) {
|
||||||
toast[response.toast.type](response.toast.message);
|
toast[response.toast.type](response.toast.message);
|
||||||
} else {
|
} else {
|
||||||
@@ -96,7 +92,6 @@ export function FileManagerOperations({
|
|||||||
setUploadFile(null);
|
setUploadFile(null);
|
||||||
onOperationComplete();
|
onOperationComplete();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Dismiss loading toast and show error
|
|
||||||
toast.dismiss(loadingToast);
|
toast.dismiss(loadingToast);
|
||||||
onError(error?.response?.data?.error || t('fileManager.failedToUploadFile'));
|
onError(error?.response?.data?.error || t('fileManager.failedToUploadFile'));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -109,7 +104,6 @@ export function FileManagerOperations({
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Show loading toast
|
|
||||||
const {toast} = await import('sonner');
|
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()}));
|
||||||
|
|
||||||
@@ -118,10 +112,8 @@ export function FileManagerOperations({
|
|||||||
|
|
||||||
const response = await createSSHFile(sshSessionId, currentPath, newFileName.trim());
|
const response = await createSSHFile(sshSessionId, currentPath, newFileName.trim());
|
||||||
|
|
||||||
// Dismiss loading toast
|
|
||||||
toast.dismiss(loadingToast);
|
toast.dismiss(loadingToast);
|
||||||
|
|
||||||
// Handle toast notification from backend
|
|
||||||
if (response?.toast) {
|
if (response?.toast) {
|
||||||
toast[response.toast.type](response.toast.message);
|
toast[response.toast.type](response.toast.message);
|
||||||
} else {
|
} else {
|
||||||
@@ -132,7 +124,6 @@ export function FileManagerOperations({
|
|||||||
setNewFileName('');
|
setNewFileName('');
|
||||||
onOperationComplete();
|
onOperationComplete();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Dismiss loading toast and show error
|
|
||||||
toast.dismiss(loadingToast);
|
toast.dismiss(loadingToast);
|
||||||
onError(error?.response?.data?.error || t('fileManager.failedToCreateFile'));
|
onError(error?.response?.data?.error || t('fileManager.failedToCreateFile'));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -145,7 +136,6 @@ export function FileManagerOperations({
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Show loading toast
|
|
||||||
const {toast} = await import('sonner');
|
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()}));
|
||||||
|
|
||||||
@@ -154,10 +144,8 @@ export function FileManagerOperations({
|
|||||||
|
|
||||||
const response = await createSSHFolder(sshSessionId, currentPath, newFolderName.trim());
|
const response = await createSSHFolder(sshSessionId, currentPath, newFolderName.trim());
|
||||||
|
|
||||||
// Dismiss loading toast
|
|
||||||
toast.dismiss(loadingToast);
|
toast.dismiss(loadingToast);
|
||||||
|
|
||||||
// Handle toast notification from backend
|
|
||||||
if (response?.toast) {
|
if (response?.toast) {
|
||||||
toast[response.toast.type](response.toast.message);
|
toast[response.toast.type](response.toast.message);
|
||||||
} else {
|
} else {
|
||||||
@@ -168,7 +156,6 @@ export function FileManagerOperations({
|
|||||||
setNewFolderName('');
|
setNewFolderName('');
|
||||||
onOperationComplete();
|
onOperationComplete();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Dismiss loading toast and show error
|
|
||||||
toast.dismiss(loadingToast);
|
toast.dismiss(loadingToast);
|
||||||
onError(error?.response?.data?.error || t('fileManager.failedToCreateFolder'));
|
onError(error?.response?.data?.error || t('fileManager.failedToCreateFolder'));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -181,7 +168,6 @@ export function FileManagerOperations({
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Show loading toast
|
|
||||||
const {toast} = await import('sonner');
|
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'),
|
type: deleteIsDirectory ? t('fileManager.folder') : t('fileManager.file'),
|
||||||
@@ -193,10 +179,8 @@ export function FileManagerOperations({
|
|||||||
|
|
||||||
const response = await deleteSSHItem(sshSessionId, deletePath, deleteIsDirectory);
|
const response = await deleteSSHItem(sshSessionId, deletePath, deleteIsDirectory);
|
||||||
|
|
||||||
// Dismiss loading toast
|
|
||||||
toast.dismiss(loadingToast);
|
toast.dismiss(loadingToast);
|
||||||
|
|
||||||
// Handle toast notification from backend
|
|
||||||
if (response?.toast) {
|
if (response?.toast) {
|
||||||
toast[response.toast.type](response.toast.message);
|
toast[response.toast.type](response.toast.message);
|
||||||
} else {
|
} else {
|
||||||
@@ -208,7 +192,6 @@ export function FileManagerOperations({
|
|||||||
setDeleteIsDirectory(false);
|
setDeleteIsDirectory(false);
|
||||||
onOperationComplete();
|
onOperationComplete();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Dismiss loading toast and show error
|
|
||||||
toast.dismiss(loadingToast);
|
toast.dismiss(loadingToast);
|
||||||
onError(error?.response?.data?.error || t('fileManager.failedToDeleteItem'));
|
onError(error?.response?.data?.error || t('fileManager.failedToDeleteItem'));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -221,7 +204,6 @@ export function FileManagerOperations({
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Show loading toast
|
|
||||||
const {toast} = await import('sonner');
|
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'),
|
type: renameIsDirectory ? t('fileManager.folder') : t('fileManager.file'),
|
||||||
@@ -234,10 +216,8 @@ export function FileManagerOperations({
|
|||||||
|
|
||||||
const response = await renameSSHItem(sshSessionId, renamePath, newName.trim());
|
const response = await renameSSHItem(sshSessionId, renamePath, newName.trim());
|
||||||
|
|
||||||
// Dismiss loading toast
|
|
||||||
toast.dismiss(loadingToast);
|
toast.dismiss(loadingToast);
|
||||||
|
|
||||||
// Handle toast notification from backend
|
|
||||||
if (response?.toast) {
|
if (response?.toast) {
|
||||||
toast[response.toast.type](response.toast.message);
|
toast[response.toast.type](response.toast.message);
|
||||||
} else {
|
} else {
|
||||||
@@ -250,7 +230,6 @@ export function FileManagerOperations({
|
|||||||
setNewName('');
|
setNewName('');
|
||||||
onOperationComplete();
|
onOperationComplete();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Dismiss loading toast and show error
|
|
||||||
toast.dismiss(loadingToast);
|
toast.dismiss(loadingToast);
|
||||||
onError(error?.response?.data?.error || t('fileManager.failedToRenameItem'));
|
onError(error?.response?.data?.error || t('fileManager.failedToRenameItem'));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -577,7 +556,8 @@ export function FileManagerOperations({
|
|||||||
<div className="bg-red-900/20 border border-red-500/30 rounded-lg p-3">
|
<div className="bg-red-900/20 border border-red-500/30 rounded-lg p-3">
|
||||||
<div className="flex items-start gap-2 text-red-300">
|
<div className="flex items-start gap-2 text-red-300">
|
||||||
<AlertCircle className="w-5 h-5 flex-shrink-0"/>
|
<AlertCircle className="w-5 h-5 flex-shrink-0"/>
|
||||||
<span className="text-sm font-medium break-words">{t('fileManager.warningCannotUndo')}</span>
|
<span
|
||||||
|
className="text-sm font-medium break-words">{t('fileManager.warningCannotUndo')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export function HostManager({onSelectView, isTopbarOpen}: HostManagerProps): Rea
|
|||||||
|
|
||||||
const handleTabChange = (value: string) => {
|
const handleTabChange = (value: string) => {
|
||||||
setActiveTab(value);
|
setActiveTab(value);
|
||||||
// Reset editing states when switching away from edit tabs
|
|
||||||
if (value !== "add_host") {
|
if (value !== "add_host") {
|
||||||
setEditingHost(null);
|
setEditingHost(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,7 @@ import {Controller, useForm} from "react-hook-form"
|
|||||||
import {z} from "zod"
|
import {z} from "zod"
|
||||||
|
|
||||||
import {Button} from "@/components/ui/button.tsx"
|
import {Button} from "@/components/ui/button.tsx"
|
||||||
import {
|
import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel,} from "@/components/ui/form.tsx";
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form.tsx";
|
|
||||||
import {Input} from "@/components/ui/input.tsx";
|
import {Input} from "@/components/ui/input.tsx";
|
||||||
import {PasswordInput} from "@/components/ui/password-input.tsx";
|
import {PasswordInput} from "@/components/ui/password-input.tsx";
|
||||||
import {ScrollArea} from "@/components/ui/scroll-area.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 {Switch} from "@/components/ui/switch.tsx";
|
||||||
import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
|
import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
|
||||||
import {toast} from "sonner";
|
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 {useTranslation} from "react-i18next";
|
||||||
import {CredentialSelector} from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx";
|
import {CredentialSelector} from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx";
|
||||||
|
|
||||||
@@ -66,7 +58,6 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
const [keyInputMethod, setKeyInputMethod] = useState<'upload' | 'paste'>('upload');
|
const [keyInputMethod, setKeyInputMethod] = useState<'upload' | 'paste'>('upload');
|
||||||
const isSubmittingRef = useRef(false);
|
const isSubmittingRef = useRef(false);
|
||||||
|
|
||||||
// Ref for the IP address input to manage focus
|
|
||||||
const ipInputRef = useRef<HTMLInputElement>(null);
|
const ipInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -103,7 +94,6 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Listen for credential changes to refresh the credential list
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleCredentialChange = async () => {
|
const handleCredentialChange = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -126,7 +116,6 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
setFolders(uniqueFolders);
|
setFolders(uniqueFolders);
|
||||||
setSshConfigurations(uniqueConfigurations);
|
setSshConfigurations(uniqueConfigurations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle error silently
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -247,7 +236,6 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update username when switching to credential tab and a credential is selected
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authTab === 'credential') {
|
if (authTab === 'credential') {
|
||||||
const currentCredentialId = form.getValues('credentialId');
|
const currentCredentialId = form.getValues('credentialId');
|
||||||
@@ -297,11 +285,10 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
tunnelConnections: cleanedHost.tunnelConnections || [],
|
tunnelConnections: cleanedHost.tunnelConnections || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only set the relevant authentication fields based on authType
|
|
||||||
if (defaultAuthType === 'password') {
|
if (defaultAuthType === 'password') {
|
||||||
formData.password = cleanedHost.password || "";
|
formData.password = cleanedHost.password || "";
|
||||||
} else if (defaultAuthType === 'key') {
|
} else if (defaultAuthType === 'key') {
|
||||||
formData.key = "existing_key"; // Placeholder to indicate existing key
|
formData.key = "existing_key";
|
||||||
formData.keyPassword = cleanedHost.keyPassword || "";
|
formData.keyPassword = cleanedHost.keyPassword || "";
|
||||||
formData.keyType = (cleanedHost.keyType as any) || "auto";
|
formData.keyType = (cleanedHost.keyType as any) || "auto";
|
||||||
} else if (defaultAuthType === 'credential') {
|
} else if (defaultAuthType === 'credential') {
|
||||||
@@ -415,7 +402,6 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
|
|
||||||
window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
|
window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
|
||||||
|
|
||||||
// Reset form after successful submission
|
|
||||||
form.reset();
|
form.reset();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t('hosts.failedToSaveHost'));
|
toast.error(t('hosts.failedToSaveHost'));
|
||||||
@@ -746,7 +732,6 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
setAuthTab(newAuthType);
|
setAuthTab(newAuthType);
|
||||||
form.setValue('authType', newAuthType);
|
form.setValue('authType', newAuthType);
|
||||||
|
|
||||||
// Clear authentication fields based on what we're switching away from
|
|
||||||
if (newAuthType === 'password') {
|
if (newAuthType === 'password') {
|
||||||
form.setValue('key', null);
|
form.setValue('key', null);
|
||||||
form.setValue('keyPassword', '');
|
form.setValue('keyPassword', '');
|
||||||
@@ -777,7 +762,8 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('hosts.password')}</FormLabel>
|
<FormLabel>{t('hosts.password')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<PasswordInput placeholder={t('placeholders.password')} {...field} />
|
<PasswordInput
|
||||||
|
placeholder={t('placeholders.password')} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -788,7 +774,6 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
value={keyInputMethod}
|
value={keyInputMethod}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setKeyInputMethod(value as 'upload' | 'paste');
|
setKeyInputMethod(value as 'upload' | 'paste');
|
||||||
// Clear the other field when switching
|
|
||||||
if (value === 'upload') {
|
if (value === 'upload') {
|
||||||
form.setValue('key', null);
|
form.setValue('key', null);
|
||||||
} else {
|
} else {
|
||||||
@@ -797,7 +782,8 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<TabsList className="inline-flex items-center justify-center rounded-md bg-muted p-1 text-muted-foreground">
|
<TabsList
|
||||||
|
className="inline-flex items-center justify-center rounded-md bg-muted p-1 text-muted-foreground">
|
||||||
<TabsTrigger value="upload">{t('hosts.uploadFile')}</TabsTrigger>
|
<TabsTrigger value="upload">{t('hosts.uploadFile')}</TabsTrigger>
|
||||||
<TabsTrigger value="paste">{t('hosts.pasteKey')}</TabsTrigger>
|
<TabsTrigger value="paste">{t('hosts.pasteKey')}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -932,7 +918,6 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
onCredentialSelect={(credential) => {
|
onCredentialSelect={(credential) => {
|
||||||
if (credential) {
|
if (credential) {
|
||||||
// Update username when credential is selected
|
|
||||||
form.setValue('username', credential.username);
|
form.setValue('username', credential.username);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -1002,7 +987,8 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
|||||||
sshpass</code> or <code
|
sshpass</code> or <code
|
||||||
className="bg-muted px-1 rounded inline">sudo dnf install
|
className="bg-muted px-1 rounded inline">sudo dnf install
|
||||||
sshpass</code></div>
|
sshpass</code></div>
|
||||||
<div>• {t('hosts.macos')} <code className="bg-muted px-1 rounded inline">brew
|
<div>• {t('hosts.macos')} <code
|
||||||
|
className="bg-muted px-1 rounded inline">brew
|
||||||
install hudochenkov/sshpass/sshpass</code></div>
|
install hudochenkov/sshpass/sshpass</code></div>
|
||||||
<div>• {t('hosts.windows')}</div>
|
<div>• {t('hosts.windows')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, {useState, useEffect, useMemo, useRef} from "react";
|
import React, {useState, useEffect, useMemo, useRef} from "react";
|
||||||
import {Card, CardContent} from "@/components/ui/card.tsx";
|
|
||||||
import {Button} from "@/components/ui/button.tsx";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import {Badge} from "@/components/ui/badge.tsx";
|
import {Badge} from "@/components/ui/badge.tsx";
|
||||||
import {ScrollArea} from "@/components/ui/scroll-area.tsx";
|
import {ScrollArea} from "@/components/ui/scroll-area.tsx";
|
||||||
@@ -22,13 +21,11 @@ import {
|
|||||||
FileEdit,
|
FileEdit,
|
||||||
Search,
|
Search,
|
||||||
Upload,
|
Upload,
|
||||||
Info,
|
|
||||||
X,
|
X,
|
||||||
Check,
|
Check,
|
||||||
Pencil,
|
Pencil,
|
||||||
FolderMinus
|
FolderMinus
|
||||||
} from "lucide-react";
|
} 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) {
|
export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
||||||
@@ -49,7 +46,6 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchHosts();
|
fetchHosts();
|
||||||
|
|
||||||
// Listen for refresh events from other components
|
|
||||||
const handleHostsRefresh = () => {
|
const handleHostsRefresh = () => {
|
||||||
fetchHosts();
|
fetchHosts();
|
||||||
};
|
};
|
||||||
@@ -116,7 +112,6 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
const handleExport = (host: SSHHost) => {
|
const handleExport = (host: SSHHost) => {
|
||||||
const actualAuthType = host.credentialId ? 'credential' : (host.key ? 'key' : 'password');
|
const actualAuthType = host.credentialId ? 'credential' : (host.key ? 'key' : 'password');
|
||||||
|
|
||||||
// Check if host uses sensitive authentication data
|
|
||||||
if (actualAuthType === 'credential') {
|
if (actualAuthType === 'credential') {
|
||||||
const confirmMessage = t('hosts.exportCredentialWarning', {
|
const confirmMessage = t('hosts.exportCredentialWarning', {
|
||||||
name: host.name || `${host.username}@${host.ip}`
|
name: host.name || `${host.username}@${host.ip}`
|
||||||
@@ -137,19 +132,17 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No sensitive data, proceed directly
|
|
||||||
performExport(host, actualAuthType);
|
performExport(host, actualAuthType);
|
||||||
};
|
};
|
||||||
|
|
||||||
const performExport = (host: SSHHost, actualAuthType: string) => {
|
const performExport = (host: SSHHost, actualAuthType: string) => {
|
||||||
|
|
||||||
// Create export data with sensitive fields excluded
|
|
||||||
const exportData: any = {
|
const exportData: any = {
|
||||||
name: host.name,
|
name: host.name,
|
||||||
ip: host.ip,
|
ip: host.ip,
|
||||||
port: host.port,
|
port: host.port,
|
||||||
username: host.username,
|
username: host.username,
|
||||||
authType: actualAuthType, // Use the determined authType, not the stored one
|
authType: actualAuthType,
|
||||||
folder: host.folder,
|
folder: host.folder,
|
||||||
tags: host.tags,
|
tags: host.tags,
|
||||||
pin: host.pin,
|
pin: host.pin,
|
||||||
@@ -160,12 +153,10 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
tunnelConnections: host.tunnelConnections,
|
tunnelConnections: host.tunnelConnections,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only include credentialId if actualAuthType is credential, but set it to null for security
|
|
||||||
if (actualAuthType === 'credential') {
|
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(
|
const cleanExportData = Object.fromEntries(
|
||||||
Object.entries(exportData).filter(([_, value]) => value !== undefined)
|
Object.entries(exportData).filter(([_, value]) => value !== undefined)
|
||||||
);
|
);
|
||||||
@@ -243,11 +234,10 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
setEditingFolderName('');
|
setEditingFolderName('');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Drag and drop handlers
|
|
||||||
const handleDragStart = (e: React.DragEvent, host: SSHHost) => {
|
const handleDragStart = (e: React.DragEvent, host: SSHHost) => {
|
||||||
setDraggedHost(host);
|
setDraggedHost(host);
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
e.dataTransfer.setData('text/plain', ''); // Required for Firefox
|
e.dataTransfer.setData('text/plain', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragEnd = () => {
|
const handleDragEnd = () => {
|
||||||
@@ -755,7 +745,8 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
<div className="flex items-center gap-2 flex-1">
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<Folder className="h-4 w-4"/>
|
<Folder className="h-4 w-4"/>
|
||||||
{editingFolder === folder ? (
|
{editingFolder === folder ? (
|
||||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
<div className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.stopPropagation()}>
|
||||||
<Input
|
<Input
|
||||||
value={editingFolderName}
|
value={editingFolderName}
|
||||||
onChange={(e) => setEditingFolderName(e.target.value)}
|
onChange={(e) => setEditingFolderName(e.target.value)}
|
||||||
@@ -872,11 +863,13 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
className="h-5 w-5 p-0 text-orange-500 hover:text-orange-700 hover:bg-orange-500/10"
|
className="h-5 w-5 p-0 text-orange-500 hover:text-orange-700 hover:bg-orange-500/10"
|
||||||
disabled={operationLoading}
|
disabled={operationLoading}
|
||||||
>
|
>
|
||||||
<FolderMinus className="h-3 w-3"/>
|
<FolderMinus
|
||||||
|
className="h-3 w-3"/>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Remove from folder "{host.folder}"</p>
|
<p>Remove from folder
|
||||||
|
"{host.folder}"</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -959,13 +952,15 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
|
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{host.enableTerminal && (
|
{host.enableTerminal && (
|
||||||
<Badge variant="outline" className="text-xs px-1 py-0">
|
<Badge variant="outline"
|
||||||
|
className="text-xs px-1 py-0">
|
||||||
<Terminal className="h-2 w-2 mr-0.5"/>
|
<Terminal className="h-2 w-2 mr-0.5"/>
|
||||||
{t('hosts.terminalBadge')}
|
{t('hosts.terminalBadge')}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{host.enableTunnel && (
|
{host.enableTunnel && (
|
||||||
<Badge variant="outline" className="text-xs px-1 py-0">
|
<Badge variant="outline"
|
||||||
|
className="text-xs px-1 py-0">
|
||||||
<Network className="h-2 w-2 mr-0.5"/>
|
<Network className="h-2 w-2 mr-0.5"/>
|
||||||
{t('hosts.tunnelBadge')}
|
{t('hosts.tunnelBadge')}
|
||||||
{host.tunnelConnections && host.tunnelConnections.length > 0 && (
|
{host.tunnelConnections && host.tunnelConnections.length > 0 && (
|
||||||
@@ -975,7 +970,8 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{host.enableFileManager && (
|
{host.enableFileManager && (
|
||||||
<Badge variant="outline" className="text-xs px-1 py-0">
|
<Badge variant="outline"
|
||||||
|
className="text-xs px-1 py-0">
|
||||||
<FileEdit className="h-2 w-2 mr-0.5"/>
|
<FileEdit className="h-2 w-2 mr-0.5"/>
|
||||||
{t('hosts.fileManagerBadge')}
|
{t('hosts.fileManagerBadge')}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -987,7 +983,8 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="font-medium">Click to edit host</p>
|
<p className="font-medium">Click to edit host</p>
|
||||||
<p className="text-xs text-muted-foreground">Drag to move between folders</p>
|
<p className="text-xs text-muted-foreground">Drag to
|
||||||
|
move between folders</p>
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export function Server({
|
|||||||
setCurrentHostConfig(updatedHost);
|
setCurrentHostConfig(updatedHost);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch latest host config:', error);
|
|
||||||
toast.error(t('serverStats.failedToFetchHostConfig'));
|
toast.error(t('serverStats.failedToFetchHostConfig'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +67,6 @@ export function Server({
|
|||||||
setCurrentHostConfig(updatedHost);
|
setCurrentHostConfig(updatedHost);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch updated host config:', error);
|
|
||||||
toast.error(t('serverStats.failedToFetchHostConfig'));
|
toast.error(t('serverStats.failedToFetchHostConfig'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,20 +87,14 @@ export function Server({
|
|||||||
setServerStatus(res?.status === 'online' ? 'online' : 'offline');
|
setServerStatus(res?.status === 'online' ? 'online' : 'offline');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to fetch server status:', error);
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
// Handle different error types from the new backend
|
|
||||||
if (error?.response?.status === 503) {
|
if (error?.response?.status === 503) {
|
||||||
// Server is offline
|
|
||||||
setServerStatus('offline');
|
setServerStatus('offline');
|
||||||
} else if (error?.response?.status === 504) {
|
} else if (error?.response?.status === 504) {
|
||||||
// Timeout - treat as degraded
|
|
||||||
setServerStatus('offline');
|
setServerStatus('offline');
|
||||||
} else if (error?.response?.status === 404) {
|
} else if (error?.response?.status === 404) {
|
||||||
// Host not found
|
|
||||||
setServerStatus('offline');
|
setServerStatus('offline');
|
||||||
} else {
|
} else {
|
||||||
// Other errors - treat as offline
|
|
||||||
setServerStatus('offline');
|
setServerStatus('offline');
|
||||||
}
|
}
|
||||||
toast.error(t('serverStats.failedToFetchStatus'));
|
toast.error(t('serverStats.failedToFetchStatus'));
|
||||||
@@ -119,7 +111,6 @@ export function Server({
|
|||||||
setMetrics(data);
|
setMetrics(data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch server metrics:', error);
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setMetrics(null);
|
setMetrics(null);
|
||||||
toast.error(t('serverStats.failedToFetchMetrics'));
|
toast.error(t('serverStats.failedToFetchMetrics'));
|
||||||
@@ -204,18 +195,13 @@ export function Server({
|
|||||||
const data = await getServerMetricsById(currentHostConfig.id);
|
const data = await getServerMetricsById(currentHostConfig.id);
|
||||||
setMetrics(data);
|
setMetrics(data);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Handle different error types from the new backend
|
|
||||||
if (error?.response?.status === 503) {
|
if (error?.response?.status === 503) {
|
||||||
// Server is offline
|
|
||||||
setServerStatus('offline');
|
setServerStatus('offline');
|
||||||
} else if (error?.response?.status === 504) {
|
} else if (error?.response?.status === 504) {
|
||||||
// Timeout - treat as offline
|
|
||||||
setServerStatus('offline');
|
setServerStatus('offline');
|
||||||
} else if (error?.response?.status === 404) {
|
} else if (error?.response?.status === 404) {
|
||||||
// Host not found
|
|
||||||
setServerStatus('offline');
|
setServerStatus('offline');
|
||||||
} else {
|
} else {
|
||||||
// Other errors - treat as offline
|
|
||||||
setServerStatus('offline');
|
setServerStatus('offline');
|
||||||
}
|
}
|
||||||
setMetrics(null);
|
setMetrics(null);
|
||||||
@@ -228,7 +214,8 @@ export function Server({
|
|||||||
>
|
>
|
||||||
{isRefreshing ? (
|
{isRefreshing ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-4 border-2 border-gray-300 border-t-transparent rounded-full animate-spin"></div>
|
<div
|
||||||
|
className="w-4 h-4 border-2 border-gray-300 border-t-transparent rounded-full animate-spin"></div>
|
||||||
{t('serverStats.refreshing')}
|
{t('serverStats.refreshing')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -265,14 +252,16 @@ export function Server({
|
|||||||
{isLoadingMetrics && !metrics ? (
|
{isLoadingMetrics && !metrics ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-6 h-6 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></div>
|
<div
|
||||||
|
className="w-6 h-6 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></div>
|
||||||
<span className="text-gray-300">{t('serverStats.loadingMetrics')}</span>
|
<span className="text-gray-300">{t('serverStats.loadingMetrics')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : !metrics && serverStatus === 'offline' ? (
|
) : !metrics && serverStatus === 'offline' ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="w-12 h-12 mx-auto mb-3 rounded-full bg-red-500/20 flex items-center justify-center">
|
<div
|
||||||
|
className="w-12 h-12 mx-auto mb-3 rounded-full bg-red-500/20 flex items-center justify-center">
|
||||||
<div className="w-6 h-6 border-2 border-red-400 rounded-full"></div>
|
<div className="w-6 h-6 border-2 border-red-400 rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-300 mb-1">{t('serverStats.serverOffline')}</p>
|
<p className="text-gray-300 mb-1">{t('serverStats.serverOffline')}</p>
|
||||||
@@ -282,7 +271,8 @@ export function Server({
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 lg:gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 lg:gap-6">
|
||||||
{/* CPU Stats */}
|
{/* CPU Stats */}
|
||||||
<div className="space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200">
|
<div
|
||||||
|
className="space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<Cpu className="h-5 w-5 text-blue-400"/>
|
<Cpu className="h-5 w-5 text-blue-400"/>
|
||||||
<h3 className="font-semibold text-lg text-white">{t('serverStats.cpuUsage')}</h3>
|
<h3 className="font-semibold text-lg text-white">{t('serverStats.cpuUsage')}</h3>
|
||||||
@@ -318,7 +308,8 @@ export function Server({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Memory Stats */}
|
{/* Memory Stats */}
|
||||||
<div className="space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200">
|
<div
|
||||||
|
className="space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<MemoryStick className="h-5 w-5 text-green-400"/>
|
<MemoryStick className="h-5 w-5 text-green-400"/>
|
||||||
<h3 className="font-semibold text-lg text-white">{t('serverStats.memoryUsage')}</h3>
|
<h3 className="font-semibold text-lg text-white">{t('serverStats.memoryUsage')}</h3>
|
||||||
@@ -358,7 +349,8 @@ export function Server({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Disk Stats */}
|
{/* Disk Stats */}
|
||||||
<div className="space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200">
|
<div
|
||||||
|
className="space-y-3 p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<HardDrive className="h-5 w-5 text-orange-400"/>
|
<HardDrive className="h-5 w-5 text-orange-400"/>
|
||||||
<h3 className="font-semibold text-lg text-white">{t('serverStats.rootStorageSpace')}</h3>
|
<h3 className="font-semibold text-lg text-white">{t('serverStats.rootStorageSpace')}</h3>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
}
|
}
|
||||||
webSocketRef.current?.close();
|
webSocketRef.current?.close();
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setIsConnecting(false); // Clear connecting state
|
setIsConnecting(false);
|
||||||
},
|
},
|
||||||
fit: () => {
|
fit: () => {
|
||||||
fitAddonRef.current?.fit();
|
fitAddonRef.current?.fit();
|
||||||
@@ -138,59 +138,48 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function attemptReconnection() {
|
function attemptReconnection() {
|
||||||
// Don't attempt reconnection if component is unmounting, shouldn't reconnect, or already reconnecting
|
|
||||||
if (isUnmountingRef.current || shouldNotReconnectRef.current || isReconnectingRef.current) {
|
if (isUnmountingRef.current || shouldNotReconnectRef.current || isReconnectingRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we've already reached max attempts
|
|
||||||
if (reconnectAttempts.current >= maxReconnectAttempts) {
|
if (reconnectAttempts.current >= maxReconnectAttempts) {
|
||||||
toast.error(t('terminal.maxReconnectAttemptsReached'));
|
toast.error(t('terminal.maxReconnectAttemptsReached'));
|
||||||
// Close the terminal tab when max attempts reached
|
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set reconnecting flag to prevent multiple simultaneous attempts
|
|
||||||
isReconnectingRef.current = true;
|
isReconnectingRef.current = true;
|
||||||
|
|
||||||
// Clear terminal immediately to prevent showing last line
|
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
terminal.clear();
|
terminal.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment attempt counter
|
|
||||||
reconnectAttempts.current++;
|
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(() => {
|
reconnectTimeoutRef.current = setTimeout(() => {
|
||||||
// Check again if component is still mounted and should reconnect
|
|
||||||
if (isUnmountingRef.current || shouldNotReconnectRef.current) {
|
if (isUnmountingRef.current || shouldNotReconnectRef.current) {
|
||||||
isReconnectingRef.current = false;
|
isReconnectingRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we haven't exceeded max attempts during the timeout
|
|
||||||
if (reconnectAttempts.current > maxReconnectAttempts) {
|
if (reconnectAttempts.current > maxReconnectAttempts) {
|
||||||
isReconnectingRef.current = false;
|
isReconnectingRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (terminal && hostConfig) {
|
if (terminal && hostConfig) {
|
||||||
// Ensure terminal is clear before reconnecting
|
|
||||||
terminal.clear();
|
terminal.clear();
|
||||||
const cols = terminal.cols;
|
const cols = terminal.cols;
|
||||||
const rows = terminal.rows;
|
const rows = terminal.rows;
|
||||||
connectToHost(cols, rows);
|
connectToHost(cols, rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset reconnecting flag after attempting connection
|
|
||||||
isReconnectingRef.current = false;
|
isReconnectingRef.current = false;
|
||||||
}, 2000 * reconnectAttempts.current); // Exponential backoff
|
}, 2000 * reconnectAttempts.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectToHost(cols: number, rows: number) {
|
function connectToHost(cols: number, rows: number) {
|
||||||
@@ -201,11 +190,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
? 'ws://localhost:8082'
|
? 'ws://localhost:8082'
|
||||||
: isElectron
|
: isElectron
|
||||||
? (() => {
|
? (() => {
|
||||||
// Get configured server URL from window object (set by main-axios)
|
|
||||||
const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081';
|
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 wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://';
|
||||||
const wsHost = baseUrl.replace(/^https?:\/\//, ''); // Keep the port
|
const wsHost = baseUrl.replace(/^https?:\/\//, '');
|
||||||
return `${wsProtocol}${wsHost}/ssh/websocket/`;
|
return `${wsProtocol}${wsHost}/ssh/websocket/`;
|
||||||
})()
|
})()
|
||||||
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
||||||
@@ -214,25 +201,18 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
webSocketRef.current = ws;
|
webSocketRef.current = ws;
|
||||||
wasDisconnectedBySSH.current = false;
|
wasDisconnectedBySSH.current = false;
|
||||||
setConnectionError(null);
|
setConnectionError(null);
|
||||||
shouldNotReconnectRef.current = false; // Reset reconnection flag
|
shouldNotReconnectRef.current = false;
|
||||||
isReconnectingRef.current = false; // Reset reconnecting flag
|
isReconnectingRef.current = false;
|
||||||
setIsConnecting(true); // Set connecting state
|
setIsConnecting(true);
|
||||||
|
|
||||||
setupWebSocketListeners(ws, cols, rows);
|
setupWebSocketListeners(ws, cols, rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
|
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
|
||||||
ws.addEventListener('open', () => {
|
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(() => {
|
connectionTimeoutRef.current = setTimeout(() => {
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
// SSH connection didn't establish within timeout
|
|
||||||
// Clear terminal immediately when connection times out
|
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
terminal.clear();
|
terminal.clear();
|
||||||
}
|
}
|
||||||
@@ -240,12 +220,11 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
if (webSocketRef.current) {
|
if (webSocketRef.current) {
|
||||||
webSocketRef.current.close();
|
webSocketRef.current.close();
|
||||||
}
|
}
|
||||||
// Attempt reconnection if this was a reconnection attempt
|
|
||||||
if (reconnectAttempts.current > 0) {
|
if (reconnectAttempts.current > 0) {
|
||||||
attemptReconnection();
|
attemptReconnection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 10000); // 10 second timeout for SSH connection
|
}, 10000);
|
||||||
|
|
||||||
ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}}));
|
ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}}));
|
||||||
terminal.onData((data) => {
|
terminal.onData((data) => {
|
||||||
@@ -265,10 +244,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
if (msg.type === 'data') {
|
if (msg.type === 'data') {
|
||||||
terminal.write(msg.data);
|
terminal.write(msg.data);
|
||||||
} else if (msg.type === 'error') {
|
} else if (msg.type === 'error') {
|
||||||
// Handle different types of errors
|
|
||||||
const errorMessage = msg.message || t('terminal.unknownError');
|
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('password') ||
|
||||||
errorMessage.toLowerCase().includes('permission') ||
|
errorMessage.toLowerCase().includes('permission') ||
|
||||||
@@ -277,61 +254,49 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
errorMessage.toLowerCase().includes('failed') ||
|
errorMessage.toLowerCase().includes('failed') ||
|
||||||
errorMessage.toLowerCase().includes('incorrect')) {
|
errorMessage.toLowerCase().includes('incorrect')) {
|
||||||
toast.error(t('terminal.authError', {message: errorMessage}));
|
toast.error(t('terminal.authError', {message: errorMessage}));
|
||||||
shouldNotReconnectRef.current = true; // Don't reconnect on auth errors
|
shouldNotReconnectRef.current = true;
|
||||||
// Close terminal on auth errors
|
|
||||||
if (webSocketRef.current) {
|
if (webSocketRef.current) {
|
||||||
webSocketRef.current.close();
|
webSocketRef.current.close();
|
||||||
}
|
}
|
||||||
// Close the terminal tab immediately
|
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a connection error that should trigger reconnection
|
|
||||||
if (errorMessage.toLowerCase().includes('connection') ||
|
if (errorMessage.toLowerCase().includes('connection') ||
|
||||||
errorMessage.toLowerCase().includes('timeout') ||
|
errorMessage.toLowerCase().includes('timeout') ||
|
||||||
errorMessage.toLowerCase().includes('network')) {
|
errorMessage.toLowerCase().includes('network')) {
|
||||||
toast.error(t('terminal.connectionError', {message: errorMessage}));
|
toast.error(t('terminal.connectionError', {message: errorMessage}));
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
// Clear terminal immediately when connection error occurs
|
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
terminal.clear();
|
terminal.clear();
|
||||||
}
|
}
|
||||||
// Set connecting state immediately for reconnection
|
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
attemptReconnection();
|
attemptReconnection();
|
||||||
return;
|
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') {
|
} else if (msg.type === 'connected') {
|
||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
setIsConnecting(false); // Clear connecting state
|
setIsConnecting(false);
|
||||||
// Clear connection timeout since SSH connection is established
|
|
||||||
if (connectionTimeoutRef.current) {
|
if (connectionTimeoutRef.current) {
|
||||||
clearTimeout(connectionTimeoutRef.current);
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
connectionTimeoutRef.current = null;
|
connectionTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
// Show reconnected toast if this was a reconnection attempt
|
|
||||||
if (reconnectAttempts.current > 0) {
|
if (reconnectAttempts.current > 0) {
|
||||||
toast.success(t('terminal.reconnected'));
|
toast.success(t('terminal.reconnected'));
|
||||||
}
|
}
|
||||||
// Reset reconnection counter and flags on successful connection
|
|
||||||
reconnectAttempts.current = 0;
|
reconnectAttempts.current = 0;
|
||||||
isReconnectingRef.current = false;
|
isReconnectingRef.current = false;
|
||||||
} else if (msg.type === 'disconnected') {
|
} else if (msg.type === 'disconnected') {
|
||||||
wasDisconnectedBySSH.current = true;
|
wasDisconnectedBySSH.current = true;
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
// Clear terminal immediately when disconnected
|
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
terminal.clear();
|
terminal.clear();
|
||||||
}
|
}
|
||||||
// Set connecting state immediately for reconnection
|
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
// Attempt reconnection for disconnections
|
|
||||||
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||||
attemptReconnection();
|
attemptReconnection();
|
||||||
}
|
}
|
||||||
@@ -343,14 +308,11 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
|
|
||||||
ws.addEventListener('close', (event) => {
|
ws.addEventListener('close', (event) => {
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
// Clear terminal immediately when connection closes
|
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
terminal.clear();
|
terminal.clear();
|
||||||
}
|
}
|
||||||
// Set connecting state immediately for reconnection
|
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
if (!wasDisconnectedBySSH.current && !isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
if (!wasDisconnectedBySSH.current && !isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||||
// Attempt reconnection for unexpected disconnections
|
|
||||||
attemptReconnection();
|
attemptReconnection();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -358,13 +320,10 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
ws.addEventListener('error', (event) => {
|
ws.addEventListener('error', (event) => {
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setConnectionError(t('terminal.websocketError'));
|
setConnectionError(t('terminal.websocketError'));
|
||||||
// Clear terminal immediately when WebSocket error occurs
|
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
terminal.clear();
|
terminal.clear();
|
||||||
}
|
}
|
||||||
// Set connecting state immediately for reconnection
|
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
// Attempt reconnection for WebSocket errors
|
|
||||||
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||||
attemptReconnection();
|
attemptReconnection();
|
||||||
}
|
}
|
||||||
@@ -486,23 +445,6 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
const cols = terminal.cols;
|
const cols = terminal.cols;
|
||||||
const rows = terminal.rows;
|
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);
|
connectToHost(cols, rows);
|
||||||
}, 300);
|
}, 300);
|
||||||
});
|
});
|
||||||
@@ -511,7 +453,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
isUnmountingRef.current = true;
|
isUnmountingRef.current = true;
|
||||||
shouldNotReconnectRef.current = true;
|
shouldNotReconnectRef.current = true;
|
||||||
isReconnectingRef.current = false;
|
isReconnectingRef.current = false;
|
||||||
setIsConnecting(false); // Clear connecting state
|
setIsConnecting(false);
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
element?.removeEventListener('contextmenu', handleContextMenu);
|
element?.removeEventListener('contextmenu', handleContextMenu);
|
||||||
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||||
@@ -574,7 +516,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
{isConnecting && (
|
{isConnecting && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-dark-bg">
|
<div className="absolute inset-0 flex items-center justify-center bg-dark-bg">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-6 h-6 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></div>
|
<div
|
||||||
|
className="w-6 h-6 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></div>
|
||||||
<span className="text-gray-300">{t('terminal.connecting')}</span>
|
<span className="text-gray-300">{t('terminal.connecting')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button} from "@/components/ui/button.tsx";
|
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 {Separator} from "@/components/ui/separator.tsx";
|
||||||
import {useTranslation} from 'react-i18next';
|
import {useTranslation} from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Loader2,
|
Loader2,
|
||||||
Pin,
|
Pin,
|
||||||
Terminal,
|
|
||||||
Network,
|
Network,
|
||||||
FileEdit,
|
|
||||||
Tag,
|
Tag,
|
||||||
Play,
|
Play,
|
||||||
Square,
|
Square,
|
||||||
@@ -16,11 +14,10 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Wifi,
|
Wifi,
|
||||||
WifiOff,
|
WifiOff,
|
||||||
Zap,
|
|
||||||
X
|
X
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {Badge} from "@/components/ui/badge.tsx";
|
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({
|
export function TunnelObject({
|
||||||
host,
|
host,
|
||||||
@@ -227,7 +224,10 @@ export function TunnelObject({
|
|||||||
{statusValue === 'WAITING' ? t('tunnels.waitingForRetry') : t('tunnels.retryingConnection')}
|
{statusValue === 'WAITING' ? t('tunnels.waitingForRetry') : t('tunnels.retryingConnection')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{t('tunnels.attempt', { current: status.retryCount, max: status.maxRetries })}
|
{t('tunnels.attempt', {
|
||||||
|
current: status.retryCount,
|
||||||
|
max: status.maxRetries
|
||||||
|
})}
|
||||||
{status.nextRetryIn && (
|
{status.nextRetryIn && (
|
||||||
<span> • {t('tunnels.nextRetryIn', {seconds: status.nextRetryIn})}</span>
|
<span> • {t('tunnels.nextRetryIn', {seconds: status.nextRetryIn})}</span>
|
||||||
)}
|
)}
|
||||||
@@ -408,7 +408,10 @@ export function TunnelObject({
|
|||||||
{statusValue === 'WAITING' ? t('tunnels.waitingForRetry') : t('tunnels.retryingConnection')}
|
{statusValue === 'WAITING' ? t('tunnels.waitingForRetry') : t('tunnels.retryingConnection')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{t('tunnels.attempt', { current: status.retryCount, max: status.maxRetries })}
|
{t('tunnels.attempt', {
|
||||||
|
current: status.retryCount,
|
||||||
|
max: status.maxRetries
|
||||||
|
})}
|
||||||
{status.nextRetryIn && (
|
{status.nextRetryIn && (
|
||||||
<span> • {t('tunnels.nextRetryIn', {seconds: status.nextRetryIn})}</span>
|
<span> • {t('tunnels.nextRetryIn', {seconds: status.nextRetryIn})}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {TopNavbar} from "@/ui/Desktop/Navigation/TopNavbar.tsx";
|
|||||||
import {AdminSettings} from "@/ui/Desktop/Admin/AdminSettings.tsx";
|
import {AdminSettings} from "@/ui/Desktop/Admin/AdminSettings.tsx";
|
||||||
import {UserProfile} from "@/ui/Desktop/User/UserProfile.tsx";
|
import {UserProfile} from "@/ui/Desktop/User/UserProfile.tsx";
|
||||||
import {Toaster} from "@/components/ui/sonner.tsx";
|
import {Toaster} from "@/components/ui/sonner.tsx";
|
||||||
import { getUserInfo, getCookie, setCookie } from "@/ui/main-axios.ts";
|
import {getUserInfo, getCookie} from "@/ui/main-axios.ts";
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
const [view, setView] = useState<string>("homepage")
|
const [view, setView] = useState<string>("homepage")
|
||||||
|
|||||||
216
src/ui/Desktop/Electron Only/ServerConfig.tsx
Normal file
216
src/ui/Desktop/Electron Only/ServerConfig.tsx
Normal file
@@ -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<string | null>(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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mx-auto mb-4 w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
|
||||||
|
<Server className="w-6 h-6 text-primary"/>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-semibold">{t('serverConfig.title')}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
{t('serverConfig.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="server-url">{t('serverConfig.serverUrl')}</Label>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Input
|
||||||
|
id="server-url"
|
||||||
|
type="text"
|
||||||
|
placeholder="http://localhost:8081 or https://your-server.com"
|
||||||
|
value={serverUrl}
|
||||||
|
onChange={(e) => handleUrlChange(e.target.value)}
|
||||||
|
className="flex-1 h-10"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleTestConnection}
|
||||||
|
disabled={testing || !serverUrl.trim() || loading}
|
||||||
|
className="w-10 h-10 p-0 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{testing ? (
|
||||||
|
<div
|
||||||
|
className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin"/>
|
||||||
|
) : (
|
||||||
|
<Wifi className="w-4 h-4"/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{connectionStatus !== 'unknown' && (
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
{connectionStatus === 'success' ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-500"/>
|
||||||
|
<span className="text-green-600">{t('serverConfig.connected')}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<XCircle className="w-4 h-4 text-red-500"/>
|
||||||
|
<span className="text-red-600">{t('serverConfig.disconnected')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{onCancel && !isFirstTime && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className={onCancel && !isFirstTime ? "flex-1" : "w-full"}
|
||||||
|
onClick={handleSaveConfig}
|
||||||
|
disabled={loading || testing || connectionStatus !== 'success'}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"/>
|
||||||
|
<span>{t('serverConfig.saving')}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t('serverConfig.saveConfig')
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
|
{t('serverConfig.helpText')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<string | null>(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 (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="mx-auto mb-4 w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
|
|
||||||
<Server className="w-6 h-6 text-primary" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-xl font-semibold">{t('serverConfig.title')}</h2>
|
|
||||||
<p className="text-sm text-muted-foreground mt-2">
|
|
||||||
{t('serverConfig.description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="server-url">{t('serverConfig.serverUrl')}</Label>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Input
|
|
||||||
id="server-url"
|
|
||||||
type="text"
|
|
||||||
placeholder="http://localhost:8081 or https://your-server.com"
|
|
||||||
value={serverUrl}
|
|
||||||
onChange={(e) => handleUrlChange(e.target.value)}
|
|
||||||
className="flex-1 h-10"
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleTestConnection}
|
|
||||||
disabled={testing || !serverUrl.trim() || loading}
|
|
||||||
className="w-10 h-10 p-0 flex items-center justify-center"
|
|
||||||
>
|
|
||||||
{testing ? (
|
|
||||||
<div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Wifi className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{connectionStatus !== 'unknown' && (
|
|
||||||
<div className="flex items-center space-x-2 text-sm">
|
|
||||||
{connectionStatus === 'success' ? (
|
|
||||||
<>
|
|
||||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
|
||||||
<span className="text-green-600">{t('serverConfig.connected')}</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<XCircle className="w-4 h-4 text-red-500" />
|
|
||||||
<span className="text-red-600">{t('serverConfig.disconnected')}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
{onCancel && !isFirstTime && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="flex-1"
|
|
||||||
onClick={onCancel}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className={onCancel && !isFirstTime ? "flex-1" : "w-full"}
|
|
||||||
onClick={handleSaveConfig}
|
|
||||||
disabled={loading || testing || connectionStatus !== 'success'}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
||||||
<span>{t('serverConfig.saving')}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
t('serverConfig.saveConfig')
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-xs text-muted-foreground text-center">
|
|
||||||
{t('serverConfig.helpText')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {HomepageAuth} from "@/ui/Desktop/Homepage/HomepageAuth.tsx";
|
import {HomepageAuth} from "@/ui/Desktop/Homepage/HomepageAuth.tsx";
|
||||||
import {HomepageUpdateLog} from "@/ui/Desktop/Homepage/HompageUpdateLog.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 {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";
|
import {useTranslation} from "react-i18next";
|
||||||
|
|
||||||
interface HomepageProps {
|
interface HomepageProps {
|
||||||
@@ -15,22 +14,19 @@ interface HomepageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Homepage({
|
export function Homepage({
|
||||||
onSelectView,
|
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
authLoading,
|
authLoading,
|
||||||
onAuthSuccess,
|
onAuthSuccess,
|
||||||
isTopbarOpen
|
isTopbarOpen
|
||||||
}: HomepageProps): React.ReactElement {
|
}: HomepageProps): React.ReactElement {
|
||||||
const {t} = useTranslation();
|
|
||||||
const [loggedIn, setLoggedIn] = useState(isAuthenticated);
|
const [loggedIn, setLoggedIn] = useState(isAuthenticated);
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [username, setUsername] = useState<string | null>(null);
|
const [username, setUsername] = useState<string | null>(null);
|
||||||
const [userId, setUserId] = useState<string | null>(null);
|
const [userId, setUserId] = useState<string | null>(null);
|
||||||
const [dbError, setDbError] = useState<string | null>(null);
|
const [dbError, setDbError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Calculate margins based on topbar state (same logic as AppView.tsx)
|
|
||||||
const topMarginPx = isTopbarOpen ? 74 : 26;
|
const topMarginPx = isTopbarOpen ? 74 : 26;
|
||||||
const leftMarginPx = 26; // Assuming sidebar is collapsed for homepage
|
const leftMarginPx = 26;
|
||||||
const bottomMarginPx = 8;
|
const bottomMarginPx = 8;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export function HomepageAlertManager({userId, loggedIn}: AlertManagerProps): Rea
|
|||||||
setAlerts(sortedAlerts);
|
setAlerts(sortedAlerts);
|
||||||
setCurrentAlertIndex(0);
|
setCurrentAlertIndex(0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch user alerts:', err);
|
|
||||||
const {toast} = await import('sonner');
|
const {toast} = await import('sonner');
|
||||||
toast.error(t('homepage.failedToLoadAlerts'));
|
toast.error(t('homepage.failedToLoadAlerts'));
|
||||||
setError(t('homepage.failedToLoadAlerts'));
|
setError(t('homepage.failedToLoadAlerts'));
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, {useState, useEffect} from "react";
|
import React, {useState, useEffect} from "react";
|
||||||
import {Eye, EyeOff} from "lucide-react";
|
|
||||||
import {cn} from "@/lib/utils.ts";
|
import {cn} from "@/lib/utils.ts";
|
||||||
import {Button} from "@/components/ui/button.tsx";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import {Input} from "@/components/ui/input.tsx";
|
import {Input} from "@/components/ui/input.tsx";
|
||||||
@@ -24,7 +23,6 @@ import {
|
|||||||
getCookie,
|
getCookie,
|
||||||
getServerConfig,
|
getServerConfig,
|
||||||
isElectron,
|
isElectron,
|
||||||
type ServerConfig
|
|
||||||
} from "../../main-axios.ts";
|
} from "../../main-axios.ts";
|
||||||
import {ServerConfig as ServerConfigComponent} from "@/ui/Desktop/Electron Only/ServerConfig.tsx";
|
import {ServerConfig as ServerConfigComponent} from "@/ui/Desktop/Electron Only/ServerConfig.tsx";
|
||||||
|
|
||||||
@@ -408,7 +406,6 @@ export function HomepageAuth({
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if we need to show server config for Electron
|
|
||||||
const [showServerConfig, setShowServerConfig] = useState<boolean | null>(null);
|
const [showServerConfig, setShowServerConfig] = useState<boolean | null>(null);
|
||||||
const [currentServerUrl, setCurrentServerUrl] = useState<string>('');
|
const [currentServerUrl, setCurrentServerUrl] = useState<string>('');
|
||||||
|
|
||||||
@@ -417,11 +414,9 @@ export function HomepageAuth({
|
|||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
try {
|
try {
|
||||||
const config = await getServerConfig();
|
const config = await getServerConfig();
|
||||||
console.log('Desktop HomepageAuth - Server config check:', config);
|
|
||||||
setCurrentServerUrl(config?.serverUrl || '');
|
setCurrentServerUrl(config?.serverUrl || '');
|
||||||
setShowServerConfig(!config || !config.serverUrl);
|
setShowServerConfig(!config || !config.serverUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Desktop HomepageAuth - No server config found, showing config screen');
|
|
||||||
setShowServerConfig(true);
|
setShowServerConfig(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -433,7 +428,6 @@ export function HomepageAuth({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (showServerConfig === null) {
|
if (showServerConfig === null) {
|
||||||
// Still checking
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
||||||
@@ -447,7 +441,6 @@ export function HomepageAuth({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showServerConfig) {
|
if (showServerConfig) {
|
||||||
console.log('Desktop HomepageAuth - SHOWING SERVER CONFIG SCREEN');
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
||||||
@@ -455,11 +448,9 @@ export function HomepageAuth({
|
|||||||
>
|
>
|
||||||
<ServerConfigComponent
|
<ServerConfigComponent
|
||||||
onServerConfigured={() => {
|
onServerConfigured={() => {
|
||||||
console.log('Server configured, reloading page');
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
console.log('Cancelled server config, going back to login');
|
|
||||||
setShowServerConfig(false);
|
setShowServerConfig(false);
|
||||||
}}
|
}}
|
||||||
isFirstTime={!currentServerUrl}
|
isFirstTime={!currentServerUrl}
|
||||||
@@ -770,7 +761,8 @@ export function HomepageAuth({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="confirm-password">{t('auth.confirmNewPassword')}</Label>
|
<Label
|
||||||
|
htmlFor="confirm-password">{t('auth.confirmNewPassword')}</Label>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
id="confirm-password"
|
id="confirm-password"
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
|
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
|
||||||
import {Button} from "@/components/ui/button.tsx";
|
|
||||||
import {Separator} from "@/components/ui/separator.tsx";
|
import {Separator} from "@/components/ui/separator.tsx";
|
||||||
import {getReleasesRSS, getVersionInfo} from "@/ui/main-axios.ts";
|
import {getReleasesRSS, getVersionInfo} from "@/ui/main-axios.ts";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
@@ -90,7 +89,8 @@ export function HomepageUpdateLog({loggedIn}: HomepageUpdateLogProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[400px] h-[600px] flex flex-col border-2 border-dark-border rounded-lg bg-dark-bg p-4 shadow-lg">
|
<div
|
||||||
|
className="w-[400px] h-[600px] flex flex-col border-2 border-dark-border rounded-lg bg-dark-bg p-4 shadow-lg">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold mb-3 text-white">{t('common.updatesAndReleases')}</h3>
|
<h3 className="text-lg font-bold mb-3 text-white">{t('common.updatesAndReleases')}</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,11 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
|||||||
const layoutTabs = [mainTab, ...splitTabs.filter((t: any) => t && t.id !== (mainTab && (mainTab as any).id))].filter(Boolean) as any[];
|
const layoutTabs = [mainTab, ...splitTabs.filter((t: any) => t && t.id !== (mainTab && (mainTab as any).id))].filter(Boolean) as any[];
|
||||||
if (allSplitScreenTab.length === 0) return null;
|
if (allSplitScreenTab.length === 0) return null;
|
||||||
|
|
||||||
const handleStyle = {pointerEvents: 'auto', zIndex: 12, background: 'var(--color-dark-border)'} as React.CSSProperties;
|
const handleStyle = {
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
zIndex: 12,
|
||||||
|
background: 'var(--color-dark-border)'
|
||||||
|
} as React.CSSProperties;
|
||||||
const commonGroupProps = {onLayout: scheduleMeasureAndFit, onResize: scheduleMeasureAndFit} as any;
|
const commonGroupProps = {onLayout: scheduleMeasureAndFit, onResize: scheduleMeasureAndFit} as any;
|
||||||
|
|
||||||
if (layoutTabs.length === 2) {
|
if (layoutTabs.length === 2) {
|
||||||
@@ -226,7 +230,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
|||||||
<div ref={el => {
|
<div ref={el => {
|
||||||
panelRefs.current[String(a.id)] = el;
|
panelRefs.current[String(a.id)] = el;
|
||||||
}} className="h-full w-full flex flex-col bg-transparent relative">
|
}} className="h-full w-full flex flex-col bg-transparent relative">
|
||||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{a.title}</div>
|
<div
|
||||||
|
className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{a.title}</div>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle style={handleStyle}/>
|
<ResizableHandle style={handleStyle}/>
|
||||||
@@ -235,7 +240,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
|||||||
<div ref={el => {
|
<div ref={el => {
|
||||||
panelRefs.current[String(b.id)] = el;
|
panelRefs.current[String(b.id)] = el;
|
||||||
}} className="h-full w-full flex flex-col bg-transparent relative">
|
}} className="h-full w-full flex flex-col bg-transparent relative">
|
||||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
|
<div
|
||||||
|
className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
|
||||||
{b.title}
|
{b.title}
|
||||||
<ResetButton onClick={handleReset}/>
|
<ResetButton onClick={handleReset}/>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,7 +266,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
|||||||
<div ref={el => {
|
<div ref={el => {
|
||||||
panelRefs.current[String(a.id)] = el;
|
panelRefs.current[String(a.id)] = el;
|
||||||
}} className="h-full w-full flex flex-col relative">
|
}} className="h-full w-full flex flex-col relative">
|
||||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{a.title}</div>
|
<div
|
||||||
|
className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{a.title}</div>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle style={handleStyle}/>
|
<ResizableHandle style={handleStyle}/>
|
||||||
@@ -269,7 +276,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
|||||||
<div ref={el => {
|
<div ref={el => {
|
||||||
panelRefs.current[String(b.id)] = el;
|
panelRefs.current[String(b.id)] = el;
|
||||||
}} className="h-full w-full flex flex-col relative">
|
}} className="h-full w-full flex flex-col relative">
|
||||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
|
<div
|
||||||
|
className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
|
||||||
{b.title}
|
{b.title}
|
||||||
<ResetButton onClick={handleReset}/>
|
<ResetButton onClick={handleReset}/>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,7 +291,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
|||||||
<div ref={el => {
|
<div ref={el => {
|
||||||
panelRefs.current[String(c.id)] = el;
|
panelRefs.current[String(c.id)] = el;
|
||||||
}} className="h-full w-full flex flex-col relative">
|
}} className="h-full w-full flex flex-col relative">
|
||||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{c.title}</div>
|
<div
|
||||||
|
className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{c.title}</div>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePrimitive.PanelGroup>
|
</ResizablePrimitive.PanelGroup>
|
||||||
@@ -305,7 +314,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
|||||||
<div ref={el => {
|
<div ref={el => {
|
||||||
panelRefs.current[String(a.id)] = el;
|
panelRefs.current[String(a.id)] = el;
|
||||||
}} className="h-full w-full flex flex-col relative">
|
}} className="h-full w-full flex flex-col relative">
|
||||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{a.title}</div>
|
<div
|
||||||
|
className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{a.title}</div>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle style={handleStyle}/>
|
<ResizableHandle style={handleStyle}/>
|
||||||
@@ -314,7 +324,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
|||||||
<div ref={el => {
|
<div ref={el => {
|
||||||
panelRefs.current[String(b.id)] = el;
|
panelRefs.current[String(b.id)] = el;
|
||||||
}} className="h-full w-full flex flex-col relative">
|
}} className="h-full w-full flex flex-col relative">
|
||||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
|
<div
|
||||||
|
className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
|
||||||
{b.title}
|
{b.title}
|
||||||
<ResetButton onClick={handleReset}/>
|
<ResetButton onClick={handleReset}/>
|
||||||
</div>
|
</div>
|
||||||
@@ -332,7 +343,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
|||||||
<div ref={el => {
|
<div ref={el => {
|
||||||
panelRefs.current[String(c.id)] = el;
|
panelRefs.current[String(c.id)] = el;
|
||||||
}} className="h-full w-full flex flex-col relative">
|
}} className="h-full w-full flex flex-col relative">
|
||||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{c.title}</div>
|
<div
|
||||||
|
className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{c.title}</div>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle style={handleStyle}/>
|
<ResizableHandle style={handleStyle}/>
|
||||||
@@ -341,7 +353,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
|||||||
<div ref={el => {
|
<div ref={el => {
|
||||||
panelRefs.current[String(d.id)] = el;
|
panelRefs.current[String(d.id)] = el;
|
||||||
}} className="h-full w-full flex flex-col relative">
|
}} className="h-full w-full flex flex-col relative">
|
||||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{d.title}</div>
|
<div
|
||||||
|
className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{d.title}</div>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ interface FolderCardProps {
|
|||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FolderCard({folderName, hosts, isFirst, isLast}: FolderCardProps): React.ReactElement {
|
export function FolderCard({folderName, hosts}: FolderCardProps): React.ReactElement {
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
|
|
||||||
const toggleExpanded = () => {
|
const toggleExpanded = () => {
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ function handleLogout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function LeftSidebar({
|
export function LeftSidebar({
|
||||||
onSelectView,
|
onSelectView,
|
||||||
getView,
|
getView,
|
||||||
@@ -128,7 +127,6 @@ export function LeftSidebar({
|
|||||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const fetchHosts = React.useCallback(async () => {
|
const fetchHosts = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const newHosts = await getSSHHosts();
|
const newHosts = await getSSHHosts();
|
||||||
@@ -180,7 +178,6 @@ export function LeftSidebar({
|
|||||||
setHosts(newHosts);
|
setHosts(newHosts);
|
||||||
prevHostsRef.current = newHosts;
|
prevHostsRef.current = newHosts;
|
||||||
|
|
||||||
// Update hostConfig in existing tabs
|
|
||||||
newHosts.forEach(newHost => {
|
newHosts.forEach(newHost => {
|
||||||
updateHostConfig(newHost.id, newHost);
|
updateHostConfig(newHost.id, newHost);
|
||||||
});
|
});
|
||||||
@@ -193,7 +190,7 @@ export function LeftSidebar({
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
fetchHosts();
|
fetchHosts();
|
||||||
const interval = setInterval(fetchHosts, 300000); // 5 minutes instead of 10 seconds
|
const interval = setInterval(fetchHosts, 300000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [fetchHosts]);
|
}, [fetchHosts]);
|
||||||
|
|
||||||
@@ -302,7 +299,8 @@ export function LeftSidebar({
|
|||||||
<Separator className="p-0.25"/>
|
<Separator className="p-0.25"/>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup className="!m-0 !p-0 !-mb-2">
|
<SidebarGroup className="!m-0 !p-0 !-mb-2">
|
||||||
<Button className="m-2 flex flex-row font-semibold border-2 !border-dark-border" variant="outline"
|
<Button className="m-2 flex flex-row font-semibold border-2 !border-dark-border"
|
||||||
|
variant="outline"
|
||||||
onClick={openSshManagerTab} disabled={!!sshManagerTab || isSplitScreenActive}
|
onClick={openSshManagerTab} disabled={!!sshManagerTab || isSplitScreenActive}
|
||||||
title={sshManagerTab ? t('interface.sshManagerAlreadyOpen') : isSplitScreenActive ? t('interface.disabledDuringSplitScreen') : undefined}>
|
title={sshManagerTab ? t('interface.sshManagerAlreadyOpen') : isSplitScreenActive ? t('interface.disabledDuringSplitScreen') : undefined}>
|
||||||
<HardDrive strokeWidth="2.5"/>
|
<HardDrive strokeWidth="2.5"/>
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ export function TabDropdown(): React.ReactElement {
|
|||||||
setCurrentTab(tabId);
|
setCurrentTab(tabId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// If only one tab (home), don't show dropdown
|
|
||||||
if (tabs.length <= 1) {
|
if (tabs.length <= 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,7 +348,8 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
|||||||
{isRecording && (
|
{isRecording && (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-white">{t('sshTools.selectTerminals')}</label>
|
<label
|
||||||
|
className="text-sm font-medium text-white">{t('sshTools.selectTerminals')}</label>
|
||||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto mt-2">
|
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto mt-2">
|
||||||
{terminalTabs.map(tab => (
|
{terminalTabs.map(tab => (
|
||||||
<Button
|
<Button
|
||||||
@@ -370,7 +371,8 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-white">{t('sshTools.typeCommands')}</label>
|
<label
|
||||||
|
className="text-sm font-medium text-white">{t('sshTools.typeCommands')}</label>
|
||||||
<Input
|
<Input
|
||||||
id="ssh-tools-input"
|
id="ssh-tools-input"
|
||||||
placeholder={t('placeholders.typeHere')}
|
placeholder={t('placeholders.typeHere')}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Language switcher component for changing UI language
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {useTranslation} from 'react-i18next';
|
import {useTranslation} from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
@@ -20,7 +19,6 @@ export function LanguageSwitcher() {
|
|||||||
|
|
||||||
const handleLanguageChange = (value: string) => {
|
const handleLanguageChange = (value: string) => {
|
||||||
i18n.changeLanguage(value);
|
i18n.changeLanguage(value);
|
||||||
// Save to localStorage for persistence
|
|
||||||
localStorage.setItem('i18nextLng', value);
|
localStorage.setItem('i18nextLng', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React, {useState, useEffect} from "react";
|
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 {Label} from "@/components/ui/label.tsx";
|
||||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
|
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.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 {TOTPSetup} from "@/ui/Desktop/User/TOTPSetup.tsx";
|
||||||
import {getUserInfo} from "@/ui/main-axios.ts";
|
import {getUserInfo} from "@/ui/main-axios.ts";
|
||||||
import {getVersionInfo} 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 {PasswordReset} from "@/ui/Desktop/User/PasswordReset.tsx";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {LanguageSwitcher} from "@/ui/Desktop/User/LanguageSwitcher.tsx";
|
import {LanguageSwitcher} from "@/ui/Desktop/User/LanguageSwitcher.tsx";
|
||||||
import {useSidebar} from "@/components/ui/sidebar.tsx";
|
import {useSidebar} from "@/components/ui/sidebar.tsx";
|
||||||
|
|
||||||
|
|
||||||
interface UserProfileProps {
|
interface UserProfileProps {
|
||||||
isTopbarOpen?: boolean;
|
isTopbarOpen?: boolean;
|
||||||
}
|
}
|
||||||
@@ -45,7 +40,6 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
|
|||||||
const info = await getVersionInfo();
|
const info = await getVersionInfo();
|
||||||
setVersionInfo({version: info.localVersion});
|
setVersionInfo({version: info.localVersion});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load version info", err);
|
|
||||||
const {toast} = await import('sonner');
|
const {toast} = await import('sonner');
|
||||||
toast.error(t('user.failedToLoadVersionInfo'));
|
toast.error(t('user.failedToLoadVersionInfo'));
|
||||||
}
|
}
|
||||||
@@ -88,7 +82,8 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div style={wrapperStyle} className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
|
<div style={wrapperStyle}
|
||||||
|
className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
|
||||||
<div className="h-full w-full flex flex-col">
|
<div className="h-full w-full flex flex-col">
|
||||||
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
||||||
<h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
|
<h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
|
||||||
@@ -104,7 +99,8 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
|
|||||||
|
|
||||||
if (error || !userInfo) {
|
if (error || !userInfo) {
|
||||||
return (
|
return (
|
||||||
<div style={wrapperStyle} className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
|
<div style={wrapperStyle}
|
||||||
|
className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
|
||||||
<div className="h-full w-full flex flex-col">
|
<div className="h-full w-full flex flex-col">
|
||||||
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
||||||
<h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
|
<h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
|
||||||
@@ -114,7 +110,8 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
|
|||||||
<Alert variant="destructive" className="bg-red-900/20 border-red-500/50">
|
<Alert variant="destructive" className="bg-red-900/20 border-red-500/50">
|
||||||
<AlertCircle className="h-4 w-4"/>
|
<AlertCircle className="h-4 w-4"/>
|
||||||
<AlertTitle className="text-red-400">{t('common.error')}</AlertTitle>
|
<AlertTitle className="text-red-400">{t('common.error')}</AlertTitle>
|
||||||
<AlertDescription className="text-red-300">{error || t('errors.loadFailed')}</AlertDescription>
|
<AlertDescription
|
||||||
|
className="text-red-300">{error || t('errors.loadFailed')}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,7 +120,8 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={wrapperStyle} className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
|
<div style={wrapperStyle}
|
||||||
|
className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
|
||||||
<div className="h-full w-full flex flex-col">
|
<div className="h-full w-full flex flex-col">
|
||||||
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
||||||
<h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
|
<h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
|
||||||
@@ -133,12 +131,14 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
|
|||||||
<div className="px-6 py-4 overflow-auto flex-1">
|
<div className="px-6 py-4 overflow-auto flex-1">
|
||||||
<Tabs defaultValue="profile" className="w-full">
|
<Tabs defaultValue="profile" className="w-full">
|
||||||
<TabsList className="mb-4 bg-dark-bg border-2 border-dark-border">
|
<TabsList className="mb-4 bg-dark-bg border-2 border-dark-border">
|
||||||
<TabsTrigger value="profile" className="flex items-center gap-2 data-[state=active]:bg-dark-bg-button">
|
<TabsTrigger value="profile"
|
||||||
|
className="flex items-center gap-2 data-[state=active]:bg-dark-bg-button">
|
||||||
<User className="w-4 h-4"/>
|
<User className="w-4 h-4"/>
|
||||||
{t('nav.userProfile')}
|
{t('nav.userProfile')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
{!userInfo.is_oidc && (
|
{!userInfo.is_oidc && (
|
||||||
<TabsTrigger value="security" className="flex items-center gap-2 data-[state=active]:bg-dark-bg-button">
|
<TabsTrigger value="security"
|
||||||
|
className="flex items-center gap-2 data-[state=active]:bg-dark-bg-button">
|
||||||
<Shield className="w-4 h-4"/>
|
<Shield className="w-4 h-4"/>
|
||||||
{t('profile.security')}
|
{t('profile.security')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
|||||||
@@ -185,7 +185,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
textarea.blur();
|
textarea.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
terminal.focus = () => {};
|
terminal.focus = () => {
|
||||||
|
};
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||||
@@ -221,11 +222,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
? 'ws://localhost:8082'
|
? 'ws://localhost:8082'
|
||||||
: isElectron()
|
: isElectron()
|
||||||
? (() => {
|
? (() => {
|
||||||
// Get configured server URL from window object (set by main-axios)
|
|
||||||
const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081';
|
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 wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://';
|
||||||
const wsHost = baseUrl.replace(/^https?:\/\//, ''); // Keep the port
|
const wsHost = baseUrl.replace(/^https?:\/\//, '');
|
||||||
return `${wsProtocol}${wsHost}/ssh/websocket/`;
|
return `${wsProtocol}${wsHost}/ssh/websocket/`;
|
||||||
})()
|
})()
|
||||||
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
||||||
|
|||||||
@@ -89,8 +89,6 @@ export function TerminalKeyboard({onSendInput, onLayoutChange}: TerminalKeyboard
|
|||||||
navigator.vibrate(20);
|
navigator.vibrate(20);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Vibration failed:", e);
|
|
||||||
// Don't show toast for vibration failure as it's not critical
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSendInput(input);
|
onSendInput(input);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {useRef, FC, useState, useEffect} from "react";
|
import React, {useState, useEffect} from "react";
|
||||||
import {Terminal} from "@/ui/Mobile/Apps/Terminal/Terminal.tsx";
|
import {Terminal} from "@/ui/Mobile/Apps/Terminal/Terminal.tsx";
|
||||||
import {TerminalKeyboard} from "@/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx";
|
import {TerminalKeyboard} from "@/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx";
|
||||||
import {BottomNavbar} from "@/ui/Mobile/Navigation/BottomNavbar.tsx";
|
import {BottomNavbar} from "@/ui/Mobile/Navigation/BottomNavbar.tsx";
|
||||||
@@ -128,7 +128,8 @@ const AppContent: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-screen flex flex-col bg-dark-bg-darkest overflow-y-hidden overflow-x-hidden relative">
|
<div
|
||||||
|
className="h-screen w-screen flex flex-col bg-dark-bg-darkest overflow-y-hidden overflow-x-hidden relative">
|
||||||
<div className="flex-1 min-h-0 relative">
|
<div className="flex-1 min-h-0 relative">
|
||||||
{tabs.map(tab => (
|
{tabs.map(tab => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -11,7 +11,16 @@ import type {
|
|||||||
FileManagerFile,
|
FileManagerFile,
|
||||||
FileManagerShortcut
|
FileManagerShortcut
|
||||||
} from '../types/index.js';
|
} from '../types/index.js';
|
||||||
import { apiLogger, authLogger, sshLogger, tunnelLogger, fileLogger, statsLogger, systemLogger, type LogContext } from '../lib/frontend-logger.js';
|
import {
|
||||||
|
apiLogger,
|
||||||
|
authLogger,
|
||||||
|
sshLogger,
|
||||||
|
tunnelLogger,
|
||||||
|
fileLogger,
|
||||||
|
statsLogger,
|
||||||
|
systemLogger,
|
||||||
|
type LogContext
|
||||||
|
} from '../lib/frontend-logger.js';
|
||||||
|
|
||||||
interface FileManagerOperation {
|
interface FileManagerOperation {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -123,12 +132,10 @@ function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosI
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request interceptor with enhanced logging
|
|
||||||
instance.interceptors.request.use((config) => {
|
instance.interceptors.request.use((config) => {
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
// Store timing and request ID for response logging
|
|
||||||
(config as any).startTime = startTime;
|
(config as any).startTime = startTime;
|
||||||
(config as any).requestId = requestId;
|
(config as any).requestId = requestId;
|
||||||
|
|
||||||
@@ -144,10 +151,8 @@ function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosI
|
|||||||
operation: 'request_start'
|
operation: 'request_start'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the appropriate logger for this service
|
|
||||||
const logger = getLoggerForService(serviceName);
|
const logger = getLoggerForService(serviceName);
|
||||||
|
|
||||||
// Log request start with grouping
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
logger.requestStart(method, fullUrl, context);
|
logger.requestStart(method, fullUrl, context);
|
||||||
}
|
}
|
||||||
@@ -158,7 +163,6 @@ function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosI
|
|||||||
authLogger.warn('No JWT token found, request will be unauthenticated', context);
|
authLogger.warn('No JWT token found, request will be unauthenticated', context);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Electron-specific headers for OIDC and other backend detection
|
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
config.headers['X-Electron-App'] = 'true';
|
config.headers['X-Electron-App'] = 'true';
|
||||||
config.headers['User-Agent'] = 'Termix-Electron/1.6.0';
|
config.headers['User-Agent'] = 'Termix-Electron/1.6.0';
|
||||||
@@ -167,7 +171,6 @@ function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosI
|
|||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Response interceptor with comprehensive logging
|
|
||||||
instance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
@@ -189,15 +192,12 @@ function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosI
|
|||||||
operation: 'request_success'
|
operation: 'request_success'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the appropriate logger for this service
|
|
||||||
const logger = getLoggerForService(serviceName);
|
const logger = getLoggerForService(serviceName);
|
||||||
|
|
||||||
// Log successful requests in development
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
logger.requestSuccess(method, fullUrl, response.status, responseTime, context);
|
logger.requestSuccess(method, fullUrl, response.status, responseTime, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Performance logging for slow requests
|
|
||||||
if (responseTime > 3000) {
|
if (responseTime > 3000) {
|
||||||
logger.warn(`🐌 Slow request: ${responseTime}ms`, context);
|
logger.warn(`🐌 Slow request: ${responseTime}ms`, context);
|
||||||
}
|
}
|
||||||
@@ -228,10 +228,8 @@ function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosI
|
|||||||
operation: 'request_error'
|
operation: 'request_error'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the appropriate logger for this service
|
|
||||||
const logger = getLoggerForService(serviceName);
|
const logger = getLoggerForService(serviceName);
|
||||||
|
|
||||||
// Log errors with appropriate method based on error type
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
logger.authError(method, fullUrl, context);
|
logger.authError(method, fullUrl, context);
|
||||||
@@ -242,7 +240,6 @@ function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle auth token clearing
|
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
localStorage.removeItem('jwt');
|
localStorage.removeItem('jwt');
|
||||||
@@ -275,7 +272,6 @@ if (isElectron) {
|
|||||||
apiPort = 8081;
|
apiPort = 8081;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server configuration management for Electron
|
|
||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
lastUpdated: string;
|
lastUpdated: string;
|
||||||
@@ -322,7 +318,6 @@ export async function testServerConnection(serverUrl: string): Promise<{ success
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize server configuration on load
|
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
getServerConfig().then(config => {
|
getServerConfig().then(config => {
|
||||||
if (config?.serverUrl) {
|
if (config?.serverUrl) {
|
||||||
@@ -335,12 +330,9 @@ if (isElectron) {
|
|||||||
function getApiUrl(path: string, defaultPort: number): string {
|
function getApiUrl(path: string, defaultPort: number): string {
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
if (configuredServerUrl) {
|
if (configuredServerUrl) {
|
||||||
// In Electron with configured server, all requests go through nginx reverse proxy
|
|
||||||
// Use the same base URL for all services (nginx routes to correct backend port)
|
|
||||||
const baseUrl = configuredServerUrl.replace(/\/$/, '');
|
const baseUrl = configuredServerUrl.replace(/\/$/, '');
|
||||||
return `${baseUrl}${path}`;
|
return `${baseUrl}${path}`;
|
||||||
}
|
}
|
||||||
// In Electron without configured server, return a placeholder that will cause requests to fail gracefully
|
|
||||||
return 'http://no-server-configured';
|
return 'http://no-server-configured';
|
||||||
} else if (isDev) {
|
} else if (isDev) {
|
||||||
return `http://${apiHost}:${defaultPort}${path}`;
|
return `http://${apiHost}:${defaultPort}${path}`;
|
||||||
@@ -349,7 +341,6 @@ function getApiUrl(path: string, defaultPort: number): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi-port backend architecture (original design)
|
|
||||||
// SSH Host Management API (port 8081)
|
// SSH Host Management API (port 8081)
|
||||||
export let sshHostApi = createApiInstance(
|
export let sshHostApi = createApiInstance(
|
||||||
getApiUrl('/ssh', 8081),
|
getApiUrl('/ssh', 8081),
|
||||||
@@ -362,7 +353,7 @@ export let tunnelApi = createApiInstance(
|
|||||||
'TUNNEL'
|
'TUNNEL'
|
||||||
);
|
);
|
||||||
|
|
||||||
// File Manager Operations API (port 8084) - SSH file operations
|
// File Manager Operations API (port 8084)
|
||||||
export let fileManagerApi = createApiInstance(
|
export let fileManagerApi = createApiInstance(
|
||||||
getApiUrl('/ssh/file_manager', 8084),
|
getApiUrl('/ssh/file_manager', 8084),
|
||||||
'FILE_MANAGER'
|
'FILE_MANAGER'
|
||||||
@@ -374,13 +365,12 @@ export let statsApi = createApiInstance(
|
|||||||
'STATS'
|
'STATS'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Authentication API (port 8081) - includes users, alerts, version, releases
|
// Authentication API (port 8081)
|
||||||
export let authApi = createApiInstance(
|
export let authApi = createApiInstance(
|
||||||
getApiUrl('', 8081),
|
getApiUrl('', 8081),
|
||||||
'AUTH'
|
'AUTH'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Function to update API instances with new server configuration
|
|
||||||
function updateApiInstances() {
|
function updateApiInstances() {
|
||||||
systemLogger.info('Updating API instances with new server configuration', {
|
systemLogger.info('Updating API instances with new server configuration', {
|
||||||
operation: 'api_instance_update',
|
operation: 'api_instance_update',
|
||||||
@@ -402,17 +392,6 @@ function updateApiInstances() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to update API instances with new port (for Electron) - kept for backward compatibility
|
|
||||||
function updateApiPorts(port: number) {
|
|
||||||
systemLogger.info('Updating API instances with new port', {
|
|
||||||
operation: 'api_port_update',
|
|
||||||
newPort: port
|
|
||||||
});
|
|
||||||
|
|
||||||
apiPort = port;
|
|
||||||
updateApiInstances();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ERROR HANDLING
|
// ERROR HANDLING
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -450,7 +429,6 @@ function handleApiError(error: unknown, operation: string): never {
|
|||||||
errorMessage: message
|
errorMessage: message
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enhanced error logging with appropriate logger
|
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
authLogger.warn(`Auth failed: ${method} ${url} - ${message}`, errorContext);
|
authLogger.warn(`Auth failed: ${method} ${url} - ${message}`, errorContext);
|
||||||
throw new ApiError('Authentication required. Please log in again.', 401, 'AUTH_REQUIRED');
|
throw new ApiError('Authentication required. Please log in again.', 401, 'AUTH_REQUIRED');
|
||||||
@@ -1015,7 +993,6 @@ export async function getOIDCConfig(): Promise<any> {
|
|||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.warn('Failed to fetch OIDC config:', error.response?.data?.error || error.message);
|
console.warn('Failed to fetch OIDC config:', error.response?.data?.error || error.message);
|
||||||
// Don't show toast for OIDC config as it's optional
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user