v1.6.0 #221
@@ -18,7 +18,7 @@ http {
|
|||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /users/ {
|
location ~ ^/users(/.*)?$ {
|
||||||
proxy_pass http://127.0.0.1:8081;
|
proxy_pass http://127.0.0.1:8081;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -27,7 +27,7 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /version/ {
|
location ~ ^/version(/.*)?$ {
|
||||||
proxy_pass http://127.0.0.1:8081;
|
proxy_pass http://127.0.0.1:8081;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -36,7 +36,7 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /releases/ {
|
location ~ ^/releases(/.*)?$ {
|
||||||
proxy_pass http://127.0.0.1:8081;
|
proxy_pass http://127.0.0.1:8081;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -45,7 +45,16 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /alerts/ {
|
location ~ ^/alerts(/.*)?$ {
|
||||||
|
proxy_pass http://127.0.0.1:8081;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/credentials(/.*)?$ {
|
||||||
proxy_pass http://127.0.0.1:8081;
|
proxy_pass http://127.0.0.1:8081;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -129,7 +138,7 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /status/ {
|
location ~ ^/status(/.*)?$ {
|
||||||
proxy_pass http://127.0.0.1:8085;
|
proxy_pass http://127.0.0.1:8085;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -138,7 +147,7 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /metrics/ {
|
location ~ ^/metrics(/.*)?$ {
|
||||||
proxy_pass http://127.0.0.1:8085;
|
proxy_pass http://127.0.0.1:8085;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ ipcMain.handle('save-server-config', (event, config) => {
|
|||||||
|
|
||||||
ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
||||||
try {
|
try {
|
||||||
const { default: fetch } = await import('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
// Try multiple endpoints to test the connection
|
// Try multiple endpoints to test the connection
|
||||||
const testUrls = [
|
const testUrls = [
|
||||||
|
|||||||
@@ -507,9 +507,11 @@
|
|||||||
"unknownError": "Unknown error occurred",
|
"unknownError": "Unknown error occurred",
|
||||||
"messageParseError": "Failed to parse server message",
|
"messageParseError": "Failed to parse server message",
|
||||||
"websocketError": "WebSocket connection error",
|
"websocketError": "WebSocket connection error",
|
||||||
|
"connecting": "Connecting...",
|
||||||
"reconnecting": "Reconnecting... ({{attempt}}/{{max}})",
|
"reconnecting": "Reconnecting... ({{attempt}}/{{max}})",
|
||||||
"reconnected": "Reconnected successfully",
|
"reconnected": "Reconnected successfully",
|
||||||
"maxReconnectAttemptsReached": "Maximum reconnection attempts reached"
|
"maxReconnectAttemptsReached": "Maximum reconnection attempts reached",
|
||||||
|
"connectionTimeout": "Connection timeout"
|
||||||
},
|
},
|
||||||
"fileManager": {
|
"fileManager": {
|
||||||
"title": "File Manager",
|
"title": "File Manager",
|
||||||
@@ -816,7 +818,6 @@
|
|||||||
"oidcAuthFailed": "OIDC authentication failed",
|
"oidcAuthFailed": "OIDC authentication failed",
|
||||||
"noTokenReceived": "No token received from login",
|
"noTokenReceived": "No token received from login",
|
||||||
"invalidAuthUrl": "Invalid authorization URL received from backend",
|
"invalidAuthUrl": "Invalid authorization URL received from backend",
|
||||||
"connectionTimeout": "Connection timeout",
|
|
||||||
"invalidInput": "Invalid input",
|
"invalidInput": "Invalid input",
|
||||||
"requiredField": "This field is required",
|
"requiredField": "This field is required",
|
||||||
"minLength": "Minimum length is {{min}}",
|
"minLength": "Minimum length is {{min}}",
|
||||||
|
|||||||
@@ -58,9 +58,8 @@
|
|||||||
"keyPassword": "密钥密码(可选)",
|
"keyPassword": "密钥密码(可选)",
|
||||||
"keyType": "密钥类型",
|
"keyType": "密钥类型",
|
||||||
"keyTypeRSA": "RSA",
|
"keyTypeRSA": "RSA",
|
||||||
"keyTypeECDSA": "ECDSA",
|
"keyTypeECDSA": "ECDSA",
|
||||||
"keyTypeEd25519": "Ed25519",
|
"keyTypeEd25519": "Ed25519",
|
||||||
"updateCredential": "更新凭据",
|
|
||||||
"basicInfo": "基本信息",
|
"basicInfo": "基本信息",
|
||||||
"authentication": "认证方式",
|
"authentication": "认证方式",
|
||||||
"organization": "组织管理",
|
"organization": "组织管理",
|
||||||
@@ -106,7 +105,6 @@
|
|||||||
"credentialSecuredDescription": "所有敏感数据均使用AES-256加密",
|
"credentialSecuredDescription": "所有敏感数据均使用AES-256加密",
|
||||||
"passwordAuthentication": "密码认证",
|
"passwordAuthentication": "密码认证",
|
||||||
"keyAuthentication": "密钥认证",
|
"keyAuthentication": "密钥认证",
|
||||||
"keyType": "密钥类型",
|
|
||||||
"securityReminder": "安全提醒",
|
"securityReminder": "安全提醒",
|
||||||
"securityReminderText": "请勿分享您的凭据。所有数据均已静态加密。",
|
"securityReminderText": "请勿分享您的凭据。所有数据均已静态加密。",
|
||||||
"hostsUsingCredential": "使用此凭据的主机",
|
"hostsUsingCredential": "使用此凭据的主机",
|
||||||
@@ -181,7 +179,7 @@
|
|||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"info": "信息",
|
"info": "信息",
|
||||||
"success": "成功",
|
"success": "成功",
|
||||||
"loading": "加载中",
|
"loading": "加载中...",
|
||||||
"required": "必填",
|
"required": "必填",
|
||||||
"optional": "可选",
|
"optional": "可选",
|
||||||
"clear": "清除",
|
"clear": "清除",
|
||||||
@@ -195,8 +193,7 @@
|
|||||||
"updateAvailable": "有可用更新",
|
"updateAvailable": "有可用更新",
|
||||||
"sshPath": "SSH 路径",
|
"sshPath": "SSH 路径",
|
||||||
"localPath": "本地路径",
|
"localPath": "本地路径",
|
||||||
"loading": "加载中...",
|
"noAuthCredentials": "此 SSH 主机没有可用的认证凭据",
|
||||||
"noAuthCredentials": "此 SSH 主机没有可用的身份验证凭据",
|
|
||||||
"noReleases": "没有发布版本",
|
"noReleases": "没有发布版本",
|
||||||
"updatesAndReleases": "更新与发布",
|
"updatesAndReleases": "更新与发布",
|
||||||
"newVersionAvailable": "有新版本 ({{version}}) 可用。",
|
"newVersionAvailable": "有新版本 ({{version}}) 可用。",
|
||||||
@@ -209,13 +206,10 @@
|
|||||||
"resetPassword": "重置密码",
|
"resetPassword": "重置密码",
|
||||||
"resetCode": "重置代码",
|
"resetCode": "重置代码",
|
||||||
"newPassword": "新密码",
|
"newPassword": "新密码",
|
||||||
"sshPath": "SSH 路径",
|
|
||||||
"localPath": "本地路径",
|
|
||||||
"folder": "文件夹",
|
"folder": "文件夹",
|
||||||
"file": "文件",
|
"file": "文件",
|
||||||
"renamedSuccessfully": "重命名成功",
|
"renamedSuccessfully": "重命名成功",
|
||||||
"deletedSuccessfully": "删除成功",
|
"deletedSuccessfully": "删除成功",
|
||||||
"noAuthCredentials": "此 SSH 主机没有可用的认证凭据",
|
|
||||||
"noTunnelConnections": "没有配置隧道连接",
|
"noTunnelConnections": "没有配置隧道连接",
|
||||||
"sshTools": "SSH 工具",
|
"sshTools": "SSH 工具",
|
||||||
"english": "英语",
|
"english": "英语",
|
||||||
@@ -236,22 +230,15 @@
|
|||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"loading": "加载中...",
|
|
||||||
"error": "错误",
|
|
||||||
"success": "成功",
|
|
||||||
"warning": "警告",
|
|
||||||
"info": "信息",
|
|
||||||
"confirm": "确认",
|
"confirm": "确认",
|
||||||
"yes": "是",
|
"yes": "是",
|
||||||
"no": "否",
|
"no": "否",
|
||||||
"ok": "确定",
|
"ok": "确定",
|
||||||
"close": "关闭",
|
|
||||||
"enabled": "已启用",
|
"enabled": "已启用",
|
||||||
"disabled": "已禁用",
|
"disabled": "已禁用",
|
||||||
"important": "重要",
|
"important": "重要",
|
||||||
"notEnabled": "未启用",
|
"notEnabled": "未启用",
|
||||||
"settingUp": "设置中...",
|
"settingUp": "设置中...",
|
||||||
"back": "返回",
|
|
||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
"previous": "上一步",
|
"previous": "上一步",
|
||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
@@ -297,7 +284,7 @@
|
|||||||
"userManagement": "用户管理",
|
"userManagement": "用户管理",
|
||||||
"makeAdmin": "设为管理员",
|
"makeAdmin": "设为管理员",
|
||||||
"removeAdmin": "移除管理员",
|
"removeAdmin": "移除管理员",
|
||||||
"deleteUser": "删除用户",
|
"deleteUser": "删除用户 {{username}} 吗?此操作无法撤销。",
|
||||||
"allowRegistration": "允许注册",
|
"allowRegistration": "允许注册",
|
||||||
"oidcSettings": "OIDC 设置",
|
"oidcSettings": "OIDC 设置",
|
||||||
"clientId": "客户端 ID",
|
"clientId": "客户端 ID",
|
||||||
@@ -346,7 +333,6 @@
|
|||||||
"removeAdminStatus": "移除 {{username}} 的管理员权限吗?",
|
"removeAdminStatus": "移除 {{username}} 的管理员权限吗?",
|
||||||
"adminStatusRemoved": "已移除 {{username}} 的管理员权限",
|
"adminStatusRemoved": "已移除 {{username}} 的管理员权限",
|
||||||
"failedToRemoveAdminStatus": "移除管理员权限失败",
|
"failedToRemoveAdminStatus": "移除管理员权限失败",
|
||||||
"deleteUser": "删除用户 {{username}} 吗?此操作无法撤销。",
|
|
||||||
"userDeletedSuccessfully": "用户 {{username}} 删除成功",
|
"userDeletedSuccessfully": "用户 {{username}} 删除成功",
|
||||||
"failedToDeleteUser": "删除用户失败",
|
"failedToDeleteUser": "删除用户失败",
|
||||||
"overrideUserInfoUrl": "覆盖用户信息 URL(非必填)"
|
"overrideUserInfoUrl": "覆盖用户信息 URL(非必填)"
|
||||||
@@ -380,7 +366,7 @@
|
|||||||
"importError": "导入错误",
|
"importError": "导入错误",
|
||||||
"failedToImportJson": "导入 JSON 文件失败",
|
"failedToImportJson": "导入 JSON 文件失败",
|
||||||
"connectionDetails": "连接详情",
|
"connectionDetails": "连接详情",
|
||||||
"organization": "组织",
|
"organization": "组织管理",
|
||||||
"ipAddress": "IP 地址",
|
"ipAddress": "IP 地址",
|
||||||
"port": "端口",
|
"port": "端口",
|
||||||
"name": "名称",
|
"name": "名称",
|
||||||
@@ -395,16 +381,11 @@
|
|||||||
"addHost": "添加主机",
|
"addHost": "添加主机",
|
||||||
"editHost": "编辑主机",
|
"editHost": "编辑主机",
|
||||||
"deleteHost": "删除主机",
|
"deleteHost": "删除主机",
|
||||||
"hostName": "主机名",
|
|
||||||
"ipAddress": "IP 地址",
|
|
||||||
"port": "端口",
|
|
||||||
"authType": "认证类型",
|
"authType": "认证类型",
|
||||||
"passwordAuth": "密码",
|
"passwordAuth": "密码",
|
||||||
"keyAuth": "SSH 密钥",
|
"keyAuth": "SSH 密钥",
|
||||||
"keyPassword": "密钥密码",
|
"keyPassword": "密钥密码",
|
||||||
"keyType": "密钥类型",
|
"keyType": "密钥类型",
|
||||||
"folder": "文件夹",
|
|
||||||
"tags": "标签",
|
|
||||||
"pin": "固定",
|
"pin": "固定",
|
||||||
"enableTerminal": "启用终端",
|
"enableTerminal": "启用终端",
|
||||||
"enableTunnel": "启用隧道",
|
"enableTunnel": "启用隧道",
|
||||||
@@ -418,8 +399,6 @@
|
|||||||
"connecting": "连接中...",
|
"connecting": "连接中...",
|
||||||
"connectionFailed": "连接失败",
|
"connectionFailed": "连接失败",
|
||||||
"connectionSuccess": "连接成功",
|
"connectionSuccess": "连接成功",
|
||||||
"connectionDetails": "连接详情",
|
|
||||||
"organization": "组织管理",
|
|
||||||
"addTags": "添加标签(空格添加)",
|
"addTags": "添加标签(空格添加)",
|
||||||
"sourcePort": "源端口",
|
"sourcePort": "源端口",
|
||||||
"sourcePortDesc": "(源指通用标签页中的当前连接详情)",
|
"sourcePortDesc": "(源指通用标签页中的当前连接详情)",
|
||||||
@@ -465,8 +444,6 @@
|
|||||||
"credentialRequired": "使用凭证认证时需要选择凭证",
|
"credentialRequired": "使用凭证认证时需要选择凭证",
|
||||||
"credentialDescription": "选择凭证将覆盖当前用户名并使用凭证的认证详细信息。",
|
"credentialDescription": "选择凭证将覆盖当前用户名并使用凭证的认证详细信息。",
|
||||||
"sshPrivateKey": "SSH 私钥",
|
"sshPrivateKey": "SSH 私钥",
|
||||||
"keyPassword": "密钥密码",
|
|
||||||
"keyType": "密钥类型",
|
|
||||||
"maxRetriesDescription": "隧道连接的最大重试次数。",
|
"maxRetriesDescription": "隧道连接的最大重试次数。",
|
||||||
"retryIntervalDescription": "重试尝试之间的等待时间。",
|
"retryIntervalDescription": "重试尝试之间的等待时间。",
|
||||||
"otherInstallMethods": "其他安装方法:",
|
"otherInstallMethods": "其他安装方法:",
|
||||||
@@ -533,7 +510,18 @@
|
|||||||
"error": "错误",
|
"error": "错误",
|
||||||
"disconnected": "已断开连接",
|
"disconnected": "已断开连接",
|
||||||
"connectionClosed": "连接已关闭",
|
"connectionClosed": "连接已关闭",
|
||||||
"connectionError": "连接错误"
|
"connectionError": "连接错误",
|
||||||
|
"connected": "已连接",
|
||||||
|
"sshConnected": "SSH 连接已建立",
|
||||||
|
"authError": "认证失败:{{message}}",
|
||||||
|
"unknownError": "发生未知错误",
|
||||||
|
"messageParseError": "解析服务器消息失败",
|
||||||
|
"websocketError": "WebSocket 连接错误",
|
||||||
|
"connecting": "连接中...",
|
||||||
|
"reconnecting": "重新连接中... ({{attempt}}/{{max}})",
|
||||||
|
"reconnected": "重新连接成功",
|
||||||
|
"maxReconnectAttemptsReached": "已达到最大重连尝试次数",
|
||||||
|
"connectionTimeout": "连接超时"
|
||||||
},
|
},
|
||||||
"fileManager": {
|
"fileManager": {
|
||||||
"title": "文件管理器",
|
"title": "文件管理器",
|
||||||
@@ -580,16 +568,11 @@
|
|||||||
"failedToRenameItem": "重命名项目失败",
|
"failedToRenameItem": "重命名项目失败",
|
||||||
"upload": "上传",
|
"upload": "上传",
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
"newFile": "新建文件",
|
|
||||||
"newFolder": "新建文件夹",
|
|
||||||
"rename": "重命名",
|
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"permissions": "权限",
|
"permissions": "权限",
|
||||||
"size": "大小",
|
"size": "大小",
|
||||||
"modified": "修改时间",
|
"modified": "修改时间",
|
||||||
"path": "路径",
|
"path": "路径",
|
||||||
"fileName": "文件名",
|
|
||||||
"folderName": "文件夹名",
|
|
||||||
"confirmDelete": "确定要删除 {{name}} 吗?",
|
"confirmDelete": "确定要删除 {{name}} 吗?",
|
||||||
"uploadSuccess": "文件上传成功",
|
"uploadSuccess": "文件上传成功",
|
||||||
"uploadFailed": "文件上传失败",
|
"uploadFailed": "文件上传失败",
|
||||||
@@ -609,10 +592,7 @@
|
|||||||
"fileSavedSuccessfully": "文件保存成功",
|
"fileSavedSuccessfully": "文件保存成功",
|
||||||
"saveTimeout": "保存操作超时。文件可能已成功保存,但操作用时过长。请检查 Docker 日志以确认。",
|
"saveTimeout": "保存操作超时。文件可能已成功保存,但操作用时过长。请检查 Docker 日志以确认。",
|
||||||
"failedToSaveFile": "保存文件失败",
|
"failedToSaveFile": "保存文件失败",
|
||||||
"folder": "文件夹",
|
|
||||||
"file": "文件",
|
|
||||||
"deletedSuccessfully": "删除成功",
|
"deletedSuccessfully": "删除成功",
|
||||||
"failedToDeleteItem": "删除项目失败",
|
|
||||||
"connectToServer": "连接到服务器",
|
"connectToServer": "连接到服务器",
|
||||||
"selectServerToEdit": "从侧边栏选择服务器以开始编辑文件",
|
"selectServerToEdit": "从侧边栏选择服务器以开始编辑文件",
|
||||||
"fileOperations": "文件操作",
|
"fileOperations": "文件操作",
|
||||||
@@ -640,11 +620,11 @@
|
|||||||
"tunnels": {
|
"tunnels": {
|
||||||
"title": "SSH 隧道",
|
"title": "SSH 隧道",
|
||||||
"noSshTunnels": "没有 SSH 隧道",
|
"noSshTunnels": "没有 SSH 隧道",
|
||||||
"createFirstTunnelMessage": "您还没有创建任何 SSH 隧道。在主机管理器中配置隧道连接以开始使用。",
|
"createFirstTunnelMessage": "创建您的第一个 SSH 隧道以开始使用。使用 SSH 管理器添加具有隧道连接的主机。",
|
||||||
"connected": "已连接",
|
"connected": "已连接",
|
||||||
"disconnected": "已断开",
|
"disconnected": "已断开连接",
|
||||||
"connecting": "连接中...",
|
"connecting": "连接中...",
|
||||||
"disconnecting": "断开中...",
|
"disconnecting": "断开连接中...",
|
||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"error": "错误",
|
"error": "错误",
|
||||||
"failed": "失败",
|
"failed": "失败",
|
||||||
@@ -680,17 +660,7 @@
|
|||||||
"local": "本地",
|
"local": "本地",
|
||||||
"remote": "远程",
|
"remote": "远程",
|
||||||
"dynamic": "动态",
|
"dynamic": "动态",
|
||||||
"noSshTunnels": "没有 SSH 隧道",
|
|
||||||
"createFirstTunnelMessage": "创建您的第一个 SSH 隧道以开始使用。使用 SSH 管理器添加具有隧道连接的主机。",
|
|
||||||
"unknown": "未知",
|
|
||||||
"connected": "已连接",
|
|
||||||
"connecting": "连接中...",
|
|
||||||
"disconnecting": "断开连接中...",
|
|
||||||
"disconnected": "已断开连接",
|
|
||||||
"portMapping": "端口 {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
|
"portMapping": "端口 {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
|
||||||
"disconnect": "断开连接",
|
|
||||||
"connect": "连接",
|
|
||||||
"canceling": "取消中...",
|
|
||||||
"endpointHostNotFound": "未找到端点主机"
|
"endpointHostNotFound": "未找到端点主机"
|
||||||
},
|
},
|
||||||
"serverStats": {
|
"serverStats": {
|
||||||
@@ -700,7 +670,7 @@
|
|||||||
"disk": "磁盘",
|
"disk": "磁盘",
|
||||||
"network": "网络",
|
"network": "网络",
|
||||||
"uptime": "运行时间",
|
"uptime": "运行时间",
|
||||||
"loadAverage": "平均负载",
|
"loadAverage": "平均: {{avg1}}, {{avg5}}, {{avg15}}",
|
||||||
"processes": "进程",
|
"processes": "进程",
|
||||||
"connections": "连接",
|
"connections": "连接",
|
||||||
"usage": "使用率",
|
"usage": "使用率",
|
||||||
@@ -716,20 +686,20 @@
|
|||||||
"cpuCores_one": "{{count}} 个 CPU",
|
"cpuCores_one": "{{count}} 个 CPU",
|
||||||
"cpuCores_other": "{{count}} 个 CPU",
|
"cpuCores_other": "{{count}} 个 CPU",
|
||||||
"naCpus": "N/A CPU",
|
"naCpus": "N/A CPU",
|
||||||
"loadAverage": "平均: {{avg1}}, {{avg5}}, {{avg15}}",
|
|
||||||
"loadAverageNA": "平均: N/A",
|
"loadAverageNA": "平均: N/A",
|
||||||
"cpuUsage": "CPU 使用率",
|
"cpuUsage": "CPU 使用率",
|
||||||
"memoryUsage": "内存使用率",
|
"memoryUsage": "内存使用率",
|
||||||
"rootStorageSpace": "根目录存储空间",
|
"rootStorageSpace": "根目录存储空间",
|
||||||
"of": "的",
|
"of": "的",
|
||||||
"feedbackMessage": "对服务器管理的下一步功能有想法?在这里分享吧",
|
"feedbackMessage": "对服务器管理的下一步功能有想法?在这里分享吧",
|
||||||
|
"failedToFetchHostConfig": "获取主机配置失败",
|
||||||
|
"failedToFetchStatus": "获取服务器状态失败",
|
||||||
|
"failedToFetchMetrics": "获取服务器指标失败",
|
||||||
"loadingMetrics": "正在加载指标...",
|
"loadingMetrics": "正在加载指标...",
|
||||||
"refreshing": "正在刷新...",
|
"refreshing": "正在刷新...",
|
||||||
"serverOffline": "服务器离线",
|
"serverOffline": "服务器离线",
|
||||||
"cannotFetchMetrics": "无法从离线服务器获取指标",
|
"cannotFetchMetrics": "无法从离线服务器获取指标",
|
||||||
"load": "负载",
|
"load": "负载"
|
||||||
"free": "空闲",
|
|
||||||
"available": "可用"
|
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"loginTitle": "登录 Termix",
|
"loginTitle": "登录 Termix",
|
||||||
@@ -831,7 +801,6 @@
|
|||||||
"oidcAuthFailed": "OIDC 认证失败",
|
"oidcAuthFailed": "OIDC 认证失败",
|
||||||
"noTokenReceived": "登录未收到令牌",
|
"noTokenReceived": "登录未收到令牌",
|
||||||
"invalidAuthUrl": "从后端收到无效的授权 URL",
|
"invalidAuthUrl": "从后端收到无效的授权 URL",
|
||||||
"connectionTimeout": "连接超时",
|
|
||||||
"invalidInput": "输入无效",
|
"invalidInput": "输入无效",
|
||||||
"requiredField": "此字段为必填项",
|
"requiredField": "此字段为必填项",
|
||||||
"minLength": "最小长度为 {{min}}",
|
"minLength": "最小长度为 {{min}}",
|
||||||
@@ -876,6 +845,9 @@
|
|||||||
"external": "外部 (OIDC)",
|
"external": "外部 (OIDC)",
|
||||||
"selectPreferredLanguage": "选择您的界面首选语言"
|
"selectPreferredLanguage": "选择您的界面首选语言"
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"failedToLoadVersionInfo": "加载版本信息失败"
|
||||||
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"enterCode": "000000",
|
"enterCode": "000000",
|
||||||
"ipAddress": "127.0.0.1",
|
"ipAddress": "127.0.0.1",
|
||||||
@@ -953,7 +925,6 @@
|
|||||||
"deleteItem": "删除项目",
|
"deleteItem": "删除项目",
|
||||||
"createNewFile": "创建新文件",
|
"createNewFile": "创建新文件",
|
||||||
"createNewFolder": "创建新文件夹",
|
"createNewFolder": "创建新文件夹",
|
||||||
"deleteItem": "删除项目",
|
|
||||||
"renameItem": "重命名项目",
|
"renameItem": "重命名项目",
|
||||||
"clickToSelectFile": "点击选择文件",
|
"clickToSelectFile": "点击选择文件",
|
||||||
"noSshHosts": "没有 SSH 主机",
|
"noSshHosts": "没有 SSH 主机",
|
||||||
@@ -1018,8 +989,6 @@
|
|||||||
"updateKey": "更新密钥",
|
"updateKey": "更新密钥",
|
||||||
"sshpassRequired": "密码认证需要 Sshpass",
|
"sshpassRequired": "密码认证需要 Sshpass",
|
||||||
"sshServerConfigRequired": "需要 SSH 服务器配置",
|
"sshServerConfigRequired": "需要 SSH 服务器配置",
|
||||||
"sshManagerAlreadyOpen": "SSH 管理器已打开",
|
|
||||||
"disabledDuringSplitScreen": "分屏期间禁用",
|
|
||||||
"productionFolder": "生产环境",
|
"productionFolder": "生产环境",
|
||||||
"databaseServer": "数据库服务器",
|
"databaseServer": "数据库服务器",
|
||||||
"unknownError": "未知错误",
|
"unknownError": "未知错误",
|
||||||
|
|||||||
@@ -306,9 +306,6 @@ export function Server({
|
|||||||
value={typeof metrics?.cpu?.percent === 'number' ? metrics!.cpu!.percent! : 0}
|
value={typeof metrics?.cpu?.percent === 'number' ? metrics!.cpu!.percent! : 0}
|
||||||
className="h-2"
|
className="h-2"
|
||||||
/>
|
/>
|
||||||
{typeof metrics?.cpu?.percent === 'number' && metrics.cpu.percent > 80 && (
|
|
||||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
@@ -347,9 +344,6 @@ export function Server({
|
|||||||
value={typeof metrics?.memory?.percent === 'number' ? metrics!.memory!.percent! : 0}
|
value={typeof metrics?.memory?.percent === 'number' ? metrics!.memory!.percent! : 0}
|
||||||
className="h-2"
|
className="h-2"
|
||||||
/>
|
/>
|
||||||
{typeof metrics?.memory?.percent === 'number' && metrics.memory.percent > 85 && (
|
|
||||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
@@ -390,9 +384,6 @@ export function Server({
|
|||||||
value={typeof metrics?.disk?.percent === 'number' ? metrics!.disk!.percent! : 0}
|
value={typeof metrics?.disk?.percent === 'number' ? metrics!.disk!.percent! : 0}
|
||||||
className="h-2"
|
className="h-2"
|
||||||
/>
|
/>
|
||||||
{typeof metrics?.disk?.percent === 'number' && metrics.disk.percent > 90 && (
|
|
||||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||||
const isVisibleRef = useRef<boolean>(false);
|
const isVisibleRef = useRef<boolean>(false);
|
||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
@@ -36,7 +37,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
const maxReconnectAttempts = 3;
|
const maxReconnectAttempts = 3;
|
||||||
const isUnmountingRef = useRef(false);
|
const isUnmountingRef = useRef(false);
|
||||||
const shouldNotReconnectRef = useRef(false);
|
const shouldNotReconnectRef = useRef(false);
|
||||||
|
const isReconnectingRef = useRef(false);
|
||||||
|
const connectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||||
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||||
@@ -76,6 +78,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
disconnect: () => {
|
disconnect: () => {
|
||||||
isUnmountingRef.current = true;
|
isUnmountingRef.current = true;
|
||||||
shouldNotReconnectRef.current = true;
|
shouldNotReconnectRef.current = true;
|
||||||
|
isReconnectingRef.current = false;
|
||||||
if (pingIntervalRef.current) {
|
if (pingIntervalRef.current) {
|
||||||
clearInterval(pingIntervalRef.current);
|
clearInterval(pingIntervalRef.current);
|
||||||
pingIntervalRef.current = null;
|
pingIntervalRef.current = null;
|
||||||
@@ -84,8 +87,13 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
clearTimeout(reconnectTimeoutRef.current);
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
reconnectTimeoutRef.current = null;
|
reconnectTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
webSocketRef.current?.close();
|
webSocketRef.current?.close();
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
|
setIsConnecting(false); // Clear connecting state
|
||||||
},
|
},
|
||||||
fit: () => {
|
fit: () => {
|
||||||
fitAddonRef.current?.fit();
|
fitAddonRef.current?.fit();
|
||||||
@@ -135,31 +143,54 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function attemptReconnection() {
|
function attemptReconnection() {
|
||||||
// Don't attempt reconnection if component is unmounting or if we shouldn't reconnect
|
// Don't attempt reconnection if component is unmounting, shouldn't reconnect, or already reconnecting
|
||||||
if (isUnmountingRef.current || shouldNotReconnectRef.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'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set reconnecting flag to prevent multiple simultaneous attempts
|
||||||
|
isReconnectingRef.current = true;
|
||||||
|
|
||||||
|
// Clear terminal immediately to prevent showing last line
|
||||||
|
if (terminal) {
|
||||||
|
terminal.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment attempt counter
|
||||||
reconnectAttempts.current++;
|
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
|
// Check again if component is still mounted and should reconnect
|
||||||
if (isUnmountingRef.current || shouldNotReconnectRef.current) {
|
if (isUnmountingRef.current || shouldNotReconnectRef.current) {
|
||||||
|
isReconnectingRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we haven't exceeded max attempts during the timeout
|
||||||
|
if (reconnectAttempts.current > maxReconnectAttempts) {
|
||||||
|
isReconnectingRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (terminal && hostConfig) {
|
if (terminal && hostConfig) {
|
||||||
// Clear terminal before reconnecting
|
// 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;
|
||||||
}, 2000 * reconnectAttempts.current); // Exponential backoff
|
}, 2000 * reconnectAttempts.current); // Exponential backoff
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +218,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
wasDisconnectedBySSH.current = false;
|
wasDisconnectedBySSH.current = false;
|
||||||
setConnectionError(null);
|
setConnectionError(null);
|
||||||
shouldNotReconnectRef.current = false; // Reset reconnection flag
|
shouldNotReconnectRef.current = false; // Reset reconnection flag
|
||||||
|
isReconnectingRef.current = false; // Reset reconnecting flag
|
||||||
|
setIsConnecting(true); // Set connecting state
|
||||||
|
|
||||||
setupWebSocketListeners(ws, cols, rows);
|
setupWebSocketListeners(ws, cols, rows);
|
||||||
}
|
}
|
||||||
@@ -195,12 +228,27 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
|
|
||||||
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
|
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
|
||||||
ws.addEventListener('open', () => {
|
ws.addEventListener('open', () => {
|
||||||
setIsConnected(true);
|
// Don't set isConnected to true here - wait for actual SSH connection
|
||||||
// Show reconnected toast if this was a reconnection attempt
|
// Don't show reconnected toast here - wait for actual connection confirmation
|
||||||
if (reconnectAttempts.current > 0) {
|
|
||||||
toast.success(t('terminal.reconnected'));
|
// Set a timeout for SSH connection establishment
|
||||||
}
|
connectionTimeoutRef.current = setTimeout(() => {
|
||||||
reconnectAttempts.current = 0; // Reset on successful connection
|
if (!isConnected) {
|
||||||
|
// SSH connection didn't establish within timeout
|
||||||
|
// Clear terminal immediately when connection times out
|
||||||
|
if (terminal) {
|
||||||
|
terminal.clear();
|
||||||
|
}
|
||||||
|
toast.error(t('terminal.connectionTimeout'));
|
||||||
|
if (webSocketRef.current) {
|
||||||
|
webSocketRef.current.close();
|
||||||
|
}
|
||||||
|
// Attempt reconnection if this was a reconnection attempt
|
||||||
|
if (reconnectAttempts.current > 0) {
|
||||||
|
attemptReconnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10000); // 10 second timeout for SSH connection
|
||||||
|
|
||||||
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) => {
|
||||||
@@ -250,6 +298,12 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
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) {
|
||||||
|
terminal.clear();
|
||||||
|
}
|
||||||
|
// Set connecting state immediately for reconnection
|
||||||
|
setIsConnecting(true);
|
||||||
attemptReconnection();
|
attemptReconnection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -258,9 +312,28 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
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
|
||||||
|
// Clear connection timeout since SSH connection is established
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
connectionTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
// Show reconnected toast if this was a reconnection attempt
|
||||||
|
if (reconnectAttempts.current > 0) {
|
||||||
|
toast.success(t('terminal.reconnected'));
|
||||||
|
}
|
||||||
|
// Reset reconnection counter and flags on successful connection
|
||||||
|
reconnectAttempts.current = 0;
|
||||||
|
isReconnectingRef.current = false;
|
||||||
} else if (msg.type === 'disconnected') {
|
} else if (msg.type === 'disconnected') {
|
||||||
wasDisconnectedBySSH.current = true;
|
wasDisconnectedBySSH.current = true;
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
|
// Clear terminal immediately when disconnected
|
||||||
|
if (terminal) {
|
||||||
|
terminal.clear();
|
||||||
|
}
|
||||||
|
// Set connecting state immediately for reconnection
|
||||||
|
setIsConnecting(true);
|
||||||
// Attempt reconnection for disconnections
|
// Attempt reconnection for disconnections
|
||||||
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||||
attemptReconnection();
|
attemptReconnection();
|
||||||
@@ -273,6 +346,12 @@ 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) {
|
||||||
|
terminal.clear();
|
||||||
|
}
|
||||||
|
// Set connecting state immediately for reconnection
|
||||||
|
setIsConnecting(true);
|
||||||
if (!wasDisconnectedBySSH.current && !isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
if (!wasDisconnectedBySSH.current && !isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||||
// Attempt reconnection for unexpected disconnections
|
// Attempt reconnection for unexpected disconnections
|
||||||
attemptReconnection();
|
attemptReconnection();
|
||||||
@@ -282,6 +361,12 @@ 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) {
|
||||||
|
terminal.clear();
|
||||||
|
}
|
||||||
|
// Set connecting state immediately for reconnection
|
||||||
|
setIsConnecting(true);
|
||||||
// Attempt reconnection for WebSocket errors
|
// Attempt reconnection for WebSocket errors
|
||||||
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||||
attemptReconnection();
|
attemptReconnection();
|
||||||
@@ -429,11 +514,14 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
return () => {
|
return () => {
|
||||||
isUnmountingRef.current = true;
|
isUnmountingRef.current = true;
|
||||||
shouldNotReconnectRef.current = true;
|
shouldNotReconnectRef.current = true;
|
||||||
|
isReconnectingRef.current = false;
|
||||||
|
setIsConnecting(false); // Clear connecting state
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
element?.removeEventListener('contextmenu', handleContextMenu);
|
element?.removeEventListener('contextmenu', handleContextMenu);
|
||||||
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||||
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||||
if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
|
if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
|
||||||
|
if (connectionTimeoutRef.current) clearTimeout(connectionTimeoutRef.current);
|
||||||
if (pingIntervalRef.current) {
|
if (pingIntervalRef.current) {
|
||||||
clearInterval(pingIntervalRef.current);
|
clearInterval(pingIntervalRef.current);
|
||||||
pingIntervalRef.current = null;
|
pingIntervalRef.current = null;
|
||||||
@@ -474,15 +562,28 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
}, [splitScreen, isVisible, terminal]);
|
}, [splitScreen, isVisible, terminal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="h-full w-full m-1 relative">
|
||||||
ref={xtermRef}
|
{/* Terminal */}
|
||||||
className={`h-full w-full m-1 transition-opacity duration-200 ${visible && isVisible ? 'opacity-100' : 'opacity-0'} overflow-hidden`}
|
<div
|
||||||
onClick={() => {
|
ref={xtermRef}
|
||||||
if (terminal && !splitScreen) {
|
className={`h-full w-full transition-opacity duration-200 ${visible && isVisible && !isConnecting ? 'opacity-100' : 'opacity-0'} overflow-hidden`}
|
||||||
terminal.focus();
|
onClick={() => {
|
||||||
}
|
if (terminal && !splitScreen) {
|
||||||
}}
|
terminal.focus();
|
||||||
/>
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Connecting State */}
|
||||||
|
{isConnecting && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-dark-bg">
|
||||||
|
<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>
|
||||||
|
<span className="text-gray-300">{t('terminal.connecting')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ function AppContent() {
|
|||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
authLoading={authLoading}
|
authLoading={authLoading}
|
||||||
onAuthSuccess={handleAuthSuccess}
|
onAuthSuccess={handleAuthSuccess}
|
||||||
isTopbarOpen={isTopbarOpen}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ interface HomepageProps {
|
|||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
authLoading: boolean;
|
authLoading: boolean;
|
||||||
onAuthSuccess: (authData: { isAdmin: boolean; username: string | null; userId: string | null }) => void;
|
onAuthSuccess: (authData: { isAdmin: boolean; username: string | null; userId: string | null }) => void;
|
||||||
isTopbarOpen?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCookie(name: string) {
|
function getCookie(name: string) {
|
||||||
@@ -30,8 +29,7 @@ export function Homepage({
|
|||||||
onSelectView,
|
onSelectView,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
authLoading,
|
authLoading,
|
||||||
onAuthSuccess,
|
onAuthSuccess
|
||||||
isTopbarOpen = true
|
|
||||||
}: HomepageProps): React.ReactElement {
|
}: HomepageProps): React.ReactElement {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [loggedIn, setLoggedIn] = useState(isAuthenticated);
|
const [loggedIn, setLoggedIn] = useState(isAuthenticated);
|
||||||
@@ -72,82 +70,64 @@ export function Homepage({
|
|||||||
}
|
}
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
const topOffset = isTopbarOpen ? 66 : 0;
|
|
||||||
const topPadding = isTopbarOpen ? 66 : 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
className="w-full min-h-svh relative transition-[padding-top] duration-300 ease-in-out"
|
|
||||||
style={{ paddingTop: `${topPadding}px` }}>
|
|
||||||
{!loggedIn ? (
|
{!loggedIn ? (
|
||||||
<div
|
<HomepageAuth
|
||||||
className="absolute left-0 w-full flex items-center justify-center"
|
setLoggedIn={setLoggedIn}
|
||||||
style={{
|
setIsAdmin={setIsAdmin}
|
||||||
top: `${topOffset}px`,
|
setUsername={setUsername}
|
||||||
height: `calc(100% - ${topOffset}px)`
|
setUserId={setUserId}
|
||||||
}}>
|
loggedIn={loggedIn}
|
||||||
<HomepageAuth
|
authLoading={authLoading}
|
||||||
setLoggedIn={setLoggedIn}
|
dbError={dbError}
|
||||||
setIsAdmin={setIsAdmin}
|
setDbError={setDbError}
|
||||||
setUsername={setUsername}
|
onAuthSuccess={onAuthSuccess}
|
||||||
setUserId={setUserId}
|
/>
|
||||||
loggedIn={loggedIn}
|
|
||||||
authLoading={authLoading}
|
|
||||||
dbError={dbError}
|
|
||||||
setDbError={setDbError}
|
|
||||||
onAuthSuccess={onAuthSuccess}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="flex flex-row items-center justify-center gap-8 relative z-10">
|
||||||
className="absolute left-0 w-full flex items-center justify-center"
|
<div className="flex flex-col items-center gap-6 w-[400px]">
|
||||||
style={{
|
<HomepageUpdateLog
|
||||||
top: `${topOffset}px`,
|
loggedIn={loggedIn}
|
||||||
height: `calc(100% - ${topOffset}px)`
|
/>
|
||||||
}}>
|
|
||||||
<div className="flex flex-row items-center justify-center gap-8 relative z-10">
|
|
||||||
<div className="flex flex-col items-center gap-6 w-[400px]">
|
|
||||||
<HomepageUpdateLog
|
|
||||||
loggedIn={loggedIn}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex flex-row items-center gap-3">
|
<div className="flex flex-row items-center gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||||
onClick={() => window.open('https://github.com/LukeGus/Termix', '_blank')}
|
onClick={() => window.open('https://github.com/LukeGus/Termix', '_blank')}
|
||||||
>
|
>
|
||||||
GitHub
|
GitHub
|
||||||
</Button>
|
</Button>
|
||||||
<div className="w-px h-4 bg-dark-border"></div>
|
<div className="w-px h-4 bg-dark-border"></div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||||
onClick={() => window.open('https://github.com/LukeGus/Termix/issues/new', '_blank')}
|
onClick={() => window.open('https://github.com/LukeGus/Termix/issues/new', '_blank')}
|
||||||
>
|
>
|
||||||
Feedback
|
Feedback
|
||||||
</Button>
|
</Button>
|
||||||
<div className="w-px h-4 bg-dark-border"></div>
|
<div className="w-px h-4 bg-dark-border"></div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||||
onClick={() => window.open('https://discord.com/invite/jVQGdvHDrf', '_blank')}
|
onClick={() => window.open('https://discord.com/invite/jVQGdvHDrf', '_blank')}
|
||||||
>
|
>
|
||||||
Discord
|
Discord
|
||||||
</Button>
|
</Button>
|
||||||
<div className="w-px h-4 bg-dark-border"></div>
|
<div className="w-px h-4 bg-dark-border"></div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||||
onClick={() => window.open('https://github.com/sponsors/LukeGus', '_blank')}
|
onClick={() => window.open('https://github.com/sponsors/LukeGus', '_blank')}
|
||||||
>
|
>
|
||||||
Donate
|
Donate
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -237,8 +237,13 @@ function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosI
|
|||||||
|
|
||||||
// Handle auth token clearing
|
// Handle auth token clearing
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true;
|
||||||
localStorage.removeItem('jwt');
|
if (isElectron) {
|
||||||
|
localStorage.removeItem('jwt');
|
||||||
|
} else {
|
||||||
|
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||||
|
localStorage.removeItem('jwt');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
|||||||
Reference in New Issue
Block a user