Fix credentials UI
This commit is contained in:
@@ -77,12 +77,6 @@ const wss = new WebSocketServer({
|
||||
},
|
||||
});
|
||||
|
||||
sshLogger.success("SSH Terminal WebSocket server started with authentication", {
|
||||
operation: "server_start",
|
||||
port: 30002,
|
||||
features: ["JWT_auth", "connection_limits", "data_access_control"],
|
||||
});
|
||||
|
||||
wss.on("connection", async (ws: WebSocket, req) => {
|
||||
let userId: string | undefined;
|
||||
let userPayload: any;
|
||||
|
||||
@@ -88,6 +88,19 @@
|
||||
"leaveEmptyToKeepCurrent": "Leave empty to keep current value",
|
||||
"uploadKeyFile": "Upload Key File",
|
||||
"generateKeyPairButton": "Generate Key Pair",
|
||||
"generateKeyPair": "Generate Key Pair",
|
||||
"generateKeyPairDescription": "Generate a new SSH key pair. If you want to protect the key with a passphrase, enter it in the Key Password field below first.",
|
||||
"deploySSHKey": "Deploy SSH Key",
|
||||
"deploySSHKeyDescription": "Deploy public key to target server",
|
||||
"sourceCredential": "Source Credential",
|
||||
"targetHost": "Target Host",
|
||||
"deploymentProcess": "Deployment Process",
|
||||
"deploymentProcessDescription": "This will safely add the public key to the target host's ~/.ssh/authorized_keys file without overwriting existing keys. The operation is reversible.",
|
||||
"chooseHostToDeploy": "Choose a host to deploy to...",
|
||||
"deploying": "Deploying...",
|
||||
"name": "Name",
|
||||
"noHostsAvailable": "No hosts available",
|
||||
"noHostsMatchSearch": "No hosts match your search",
|
||||
"sshKeyGenerationNotImplemented": "SSH key generation feature coming soon",
|
||||
"connectionTestingNotImplemented": "Connection testing feature coming soon",
|
||||
"testConnection": "Test Connection",
|
||||
@@ -266,6 +279,9 @@
|
||||
"sshTools": "SSH Tools",
|
||||
"english": "English",
|
||||
"chinese": "Chinese",
|
||||
"cancel": "Cancel",
|
||||
"username": "Username",
|
||||
"name": "Name",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"register": "Register",
|
||||
@@ -966,6 +982,7 @@
|
||||
"connecting": "Connecting...",
|
||||
"disconnecting": "Disconnecting...",
|
||||
"unknownTunnelStatus": "Unknown",
|
||||
"unknown": "Unknown",
|
||||
"error": "Error",
|
||||
"failed": "Failed",
|
||||
"retrying": "Retrying",
|
||||
@@ -1011,7 +1028,10 @@
|
||||
"disconnect": "Disconnect",
|
||||
"connect": "Connect",
|
||||
"canceling": "Canceling...",
|
||||
"endpointHostNotFound": "Endpoint host not found"
|
||||
"endpointHostNotFound": "Endpoint host not found",
|
||||
"discord": "Discord",
|
||||
"githubIssue": "GitHub issue",
|
||||
"forHelp": "for help"
|
||||
},
|
||||
"serverStats": {
|
||||
"title": "Server Statistics",
|
||||
|
||||
@@ -88,6 +88,19 @@
|
||||
"leaveEmptyToKeepCurrent": "留空以保持当前值",
|
||||
"uploadKeyFile": "上传密钥文件",
|
||||
"generateKeyPairButton": "生成密钥对",
|
||||
"generateKeyPair": "生成密钥对",
|
||||
"generateKeyPairDescription": "生成新的SSH密钥对。如果您想用密码保护密钥,请先在下面的密钥密码字段中输入密码。",
|
||||
"deploySSHKey": "部署SSH密钥",
|
||||
"deploySSHKeyDescription": "将公钥部署到目标服务器",
|
||||
"sourceCredential": "源凭据",
|
||||
"targetHost": "目标主机",
|
||||
"deploymentProcess": "部署过程",
|
||||
"deploymentProcessDescription": "这将安全地将公钥添加到目标主机的~/.ssh/authorized_keys文件中,而不会覆盖现有密钥。此操作是可逆的。",
|
||||
"chooseHostToDeploy": "选择要部署到的主机...",
|
||||
"deploying": "部署中...",
|
||||
"name": "名称",
|
||||
"noHostsAvailable": "没有可用的主机",
|
||||
"noHostsMatchSearch": "没有匹配搜索的主机",
|
||||
"sshKeyGenerationNotImplemented": "SSH密钥生成功能即将推出",
|
||||
"connectionTestingNotImplemented": "连接测试功能即将推出",
|
||||
"testConnection": "测试连接",
|
||||
@@ -260,6 +273,9 @@
|
||||
"sshTools": "SSH 工具",
|
||||
"english": "英语",
|
||||
"chinese": "中文",
|
||||
"cancel": "取消",
|
||||
"username": "用户名",
|
||||
"name": "名称",
|
||||
"login": "登录",
|
||||
"logout": "登出",
|
||||
"register": "注册",
|
||||
@@ -438,7 +454,6 @@
|
||||
"verificationCompleted": "兼容性验证完成 - 未修改任何数据",
|
||||
"verificationInProgress": "验证完成",
|
||||
"dataMigrationCompleted": "数据迁移完成!",
|
||||
"migrationCompleted": "迁移完成",
|
||||
"verificationFailed": "兼容性验证失败",
|
||||
"migrationFailed": "迁移失败",
|
||||
"runningVerification": "正在进行兼容性验证...",
|
||||
@@ -760,7 +775,6 @@
|
||||
"renaming": "重命名中...",
|
||||
"fileUploadedSuccessfully": "文件 \"{{name}}\" 上传成功",
|
||||
"failedToUploadFile": "上传文件失败",
|
||||
"fileDownloadedSuccessfully": "文件 \"{{name}}\" 下载成功",
|
||||
"failedToDownloadFile": "下载文件失败",
|
||||
"noFileContent": "未收到文件内容",
|
||||
"filePath": "文件路径",
|
||||
@@ -786,12 +800,8 @@
|
||||
"noSSHConnection": "无SSH连接可用",
|
||||
"enterFolderName": "输入文件夹名称:",
|
||||
"enterFileName": "输入文件名称:",
|
||||
"copy": "复制",
|
||||
"cut": "剪切",
|
||||
"paste": "粘贴",
|
||||
"delete": "删除",
|
||||
"properties": "属性",
|
||||
"preview": "预览",
|
||||
"refresh": "刷新",
|
||||
"downloadFiles": "下载 {{count}} 个文件",
|
||||
"copyFiles": "复制 {{count}} 个项目",
|
||||
@@ -811,13 +821,6 @@
|
||||
"failedToDeleteItem": "删除项目失败",
|
||||
"itemRenamedSuccessfully": "{{type}}重命名成功",
|
||||
"failedToRenameItem": "重命名项目失败",
|
||||
"upload": "上传",
|
||||
"download": "下载",
|
||||
"delete": "删除",
|
||||
"permissions": "权限",
|
||||
"size": "大小",
|
||||
"modified": "修改时间",
|
||||
"path": "路径",
|
||||
"confirmDelete": "确定要删除 {{name}} 吗?",
|
||||
"uploadSuccess": "文件上传成功",
|
||||
"uploadFailed": "文件上传失败",
|
||||
@@ -834,7 +837,6 @@
|
||||
"noSshSessionId": "没有可用的 SSH 会话 ID",
|
||||
"noFilePath": "没有可用的文件路径",
|
||||
"noCurrentHost": "没有可用的当前主机",
|
||||
"fileSavedSuccessfully": "文件保存成功",
|
||||
"saveTimeout": "保存操作超时。文件可能已成功保存,但操作用时过长。请检查 Docker 日志以确认。",
|
||||
"failedToSaveFile": "保存文件失败",
|
||||
"deletedSuccessfully": "删除成功",
|
||||
@@ -889,10 +891,8 @@
|
||||
"unpinFile": "取消固定",
|
||||
"removeShortcut": "移除快捷方式",
|
||||
"saveFilesToSystem": "另存 {{count}} 个文件为...",
|
||||
"saveToSystem": "另存为...",
|
||||
"pinFile": "固定文件",
|
||||
"addToShortcuts": "添加到快捷方式",
|
||||
"selectLocationToSave": "选择位置保存",
|
||||
"downloadToDefaultLocation": "下载到默认位置",
|
||||
"pasteFailed": "粘贴失败",
|
||||
"noUndoableActions": "没有可撤销的操作",
|
||||
@@ -910,7 +910,6 @@
|
||||
"editPath": "编辑路径",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"folderName": "文件夹名",
|
||||
"find": "查找...",
|
||||
"replaceWith": "替换为...",
|
||||
"replace": "替换",
|
||||
@@ -936,15 +935,9 @@
|
||||
"outdent": "减少缩进",
|
||||
"autoComplete": "自动补全",
|
||||
"imageLoadError": "图片加载失败",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小",
|
||||
"rotate": "旋转",
|
||||
"originalSize": "原始大小",
|
||||
"startTyping": "开始输入...",
|
||||
"fileSavedSuccessfully": "文件保存成功",
|
||||
"autoSaveFailed": "自动保存失败",
|
||||
"fileAutoSaved": "文件已自动保存",
|
||||
"fileDownloadedSuccessfully": "文件下载成功",
|
||||
"moveFileFailed": "移动 {{name}} 失败",
|
||||
"moveOperationFailed": "移动操作失败",
|
||||
"canOnlyCompareFiles": "只能对比两个文件",
|
||||
@@ -980,6 +973,7 @@
|
||||
"connecting": "连接中...",
|
||||
"disconnecting": "断开连接中...",
|
||||
"unknownTunnelStatus": "未知",
|
||||
"unknown": "未知",
|
||||
"error": "错误",
|
||||
"failed": "失败",
|
||||
"retrying": "重试中",
|
||||
@@ -1015,7 +1009,10 @@
|
||||
"remote": "远程",
|
||||
"dynamic": "动态",
|
||||
"portMapping": "端口 {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
|
||||
"endpointHostNotFound": "未找到端点主机"
|
||||
"endpointHostNotFound": "未找到端点主机",
|
||||
"discord": "Discord",
|
||||
"githubIssue": "GitHub 问题",
|
||||
"forHelp": "寻求帮助"
|
||||
},
|
||||
"serverStats": {
|
||||
"title": "服务器统计",
|
||||
@@ -1283,101 +1280,30 @@
|
||||
"discord": "Discord",
|
||||
"connectToSshForOperations": "连接 SSH 以使用文件操作",
|
||||
"uploadFile": "上传文件",
|
||||
"newFile": "新建文件",
|
||||
"newFolder": "新建文件夹",
|
||||
"rename": "重命名",
|
||||
"deleteItem": "删除项目",
|
||||
"createNewFile": "创建新文件",
|
||||
"createNewFolder": "创建新文件夹",
|
||||
"renameItem": "重命名项目",
|
||||
"clickToSelectFile": "点击选择文件",
|
||||
"noSshHosts": "没有 SSH 主机",
|
||||
"sshHosts": "SSH 主机",
|
||||
"importSshHosts": "从 JSON 导入 SSH 主机",
|
||||
"clientId": "客户端 ID",
|
||||
"clientSecret": "客户端密钥",
|
||||
"error": "错误",
|
||||
"warning": "警告",
|
||||
"deleteAccount": "删除账户",
|
||||
"closeDeleteAccount": "关闭删除账户",
|
||||
"cannotDeleteAccount": "无法删除账户",
|
||||
"confirmPassword": "确认密码",
|
||||
"deleting": "删除中...",
|
||||
"externalAuth": "外部认证 (OIDC)",
|
||||
"configureExternalProvider": "配置外部身份提供者",
|
||||
"waitingForRetry": "等待重试",
|
||||
"retryingConnection": "重试连接中",
|
||||
"resetSplitSizes": "重置分屏大小",
|
||||
"sshManagerAlreadyOpen": "SSH 管理器已打开",
|
||||
"disabledDuringSplitScreen": "分屏期间禁用",
|
||||
"unknown": "未知",
|
||||
"connected": "已连接",
|
||||
"disconnected": "已断开连接",
|
||||
"maxRetriesExhausted": "已达到最大重试次数",
|
||||
"endpointHostNotFound": "未找到端点主机",
|
||||
"administrator": "管理员",
|
||||
"user": "用户",
|
||||
"external": "外部",
|
||||
"local": "本地",
|
||||
"saving": "保存中...",
|
||||
"saveConfiguration": "保存配置",
|
||||
"loading": "加载中...",
|
||||
"refresh": "刷新",
|
||||
"adding": "添加中...",
|
||||
"makeAdmin": "设为管理员",
|
||||
"verifying": "验证中...",
|
||||
"verifyAndEnable": "验证并启用",
|
||||
"secretKey": "密钥",
|
||||
"totpQrCode": "TOTP 二维码",
|
||||
"passwordRequired": "使用密码认证时需要密码",
|
||||
"sshKeyRequired": "使用密钥认证时需要 SSH 私钥",
|
||||
"keyTypeRequired": "使用密钥认证时需要密钥类型",
|
||||
"validSshConfigRequired": "必须从列表中选择有效的 SSH 配置",
|
||||
"updateHost": "更新主机",
|
||||
"addHost": "添加主机",
|
||||
"editHost": "编辑主机",
|
||||
"pinConnection": "固定连接",
|
||||
"authentication": "认证",
|
||||
"password": "密码",
|
||||
"key": "密钥",
|
||||
"sshPrivateKey": "SSH 私钥",
|
||||
"keyPassword": "密钥密码",
|
||||
"keyType": "密钥类型",
|
||||
"enableTerminal": "启用终端",
|
||||
"enableTunnel": "启用隧道",
|
||||
"enableFileManager": "启用文件管理器",
|
||||
"defaultPath": "默认路径",
|
||||
"tunnelConnections": "隧道连接",
|
||||
"maxRetries": "最大重试次数",
|
||||
"upload": "上传",
|
||||
"updateKey": "更新密钥",
|
||||
"sshpassRequired": "密码认证需要 Sshpass",
|
||||
"sshServerConfigRequired": "需要 SSH 服务器配置",
|
||||
"productionFolder": "生产环境",
|
||||
"databaseServer": "数据库服务器",
|
||||
"developmentServer": "开发服务器",
|
||||
"developmentFolder": "开发环境",
|
||||
"webServerProduction": "Web 服务器 - 生产环境",
|
||||
"unknownError": "未知错误",
|
||||
"failedToInitiatePasswordReset": "启动密码重置失败",
|
||||
"failedToVerifyResetCode": "验证重置代码失败",
|
||||
"failedToCompletePasswordReset": "完成密码重置失败",
|
||||
"invalidTotpCode": "无效的 TOTP 代码",
|
||||
"failedToStartOidcLogin": "启动 OIDC 登录失败",
|
||||
"failedToGetUserInfoAfterOidc": "OIDC 登录后获取用户信息失败",
|
||||
"loginWithExternalProvider": "使用外部提供者登录",
|
||||
"loginWithExternal": "使用外部提供者登录",
|
||||
"sendResetCode": "发送重置代码",
|
||||
"verifyCode": "验证代码",
|
||||
"resetPassword": "重置密码",
|
||||
"login": "登录",
|
||||
"signUp": "注册",
|
||||
"failedToUpdateOidcConfig": "更新 OIDC 配置失败",
|
||||
"failedToMakeUserAdmin": "设为管理员失败",
|
||||
"failedToStartTotpSetup": "启动 TOTP 设置失败",
|
||||
"invalidVerificationCode": "无效的验证码",
|
||||
"failedToDisableTotp": "禁用 TOTP 失败",
|
||||
"failedToGenerateBackupCodes": "生成备用码失败"
|
||||
"failedToStartTotpSetup": "启动 TOTP 设置失败"
|
||||
},
|
||||
"mobile": {
|
||||
"selectHostToStart": "选择一个主机以开始您的终端会话",
|
||||
|
||||
@@ -377,7 +377,6 @@ export function CredentialEditor({
|
||||
};
|
||||
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
const [keyGenerationPassphrase, setKeyGenerationPassphrase] = useState("");
|
||||
|
||||
const [folderDropdownOpen, setFolderDropdownOpen] = useState(false);
|
||||
const folderInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -440,10 +439,10 @@ export function CredentialEditor({
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="general" className="pt-2">
|
||||
<FormLabel className="mb-3 font-bold">
|
||||
<FormLabel className="mb-2 font-bold">
|
||||
{t("credentials.basicInformation")}
|
||||
</FormLabel>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -476,10 +475,10 @@ export function CredentialEditor({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormLabel className="mb-3 mt-3 font-bold">
|
||||
<FormLabel className="mb-2 mt-4 font-bold">
|
||||
{t("credentials.organization")}
|
||||
</FormLabel>
|
||||
<div className="grid grid-cols-26 gap-4">
|
||||
<div className="grid grid-cols-26 gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
@@ -623,7 +622,7 @@ export function CredentialEditor({
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="authentication">
|
||||
<FormLabel className="mb-3 font-bold">
|
||||
<FormLabel className="mb-2 font-bold">
|
||||
{t("credentials.authentication")}
|
||||
</FormLabel>
|
||||
<Tabs
|
||||
@@ -670,29 +669,15 @@ export function CredentialEditor({
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="key">
|
||||
<div className="mt-4">
|
||||
{/* Generate Key Pair Buttons */}
|
||||
<div className="mb-4 p-4 bg-muted/20 border border-muted rounded-md">
|
||||
<FormLabel className="mb-3 font-bold block">
|
||||
<div className="mt-2">
|
||||
<div className="mb-3 p-3 bg-muted/20 border border-muted rounded-md">
|
||||
<FormLabel className="mb-2 font-bold block">
|
||||
{t("credentials.generateKeyPair")}
|
||||
</FormLabel>
|
||||
|
||||
{/* Key Generation Passphrase Input */}
|
||||
<div className="mb-3">
|
||||
<FormLabel className="text-sm mb-2 block">
|
||||
{t("credentials.keyPassword")} (
|
||||
{t("credentials.optional")})
|
||||
</FormLabel>
|
||||
<PasswordInput
|
||||
placeholder={t("placeholders.keyPassword")}
|
||||
value={keyGenerationPassphrase}
|
||||
onChange={(e) =>
|
||||
setKeyGenerationPassphrase(e.target.value)
|
||||
}
|
||||
className="max-w-xs"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{t("credentials.keyPassphraseOptional")}
|
||||
<div className="mb-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("credentials.generateKeyPairDescription")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -703,24 +688,20 @@ export function CredentialEditor({
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const currentKeyPassword =
|
||||
form.watch("keyPassword");
|
||||
const result = await generateKeyPair(
|
||||
"ssh-ed25519",
|
||||
undefined,
|
||||
keyGenerationPassphrase,
|
||||
currentKeyPassword,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
form.setValue("key", result.privateKey);
|
||||
form.setValue("publicKey", result.publicKey);
|
||||
if (keyGenerationPassphrase) {
|
||||
form.setValue(
|
||||
"keyPassword",
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
}
|
||||
debouncedKeyDetection(
|
||||
result.privateKey,
|
||||
keyGenerationPassphrase,
|
||||
currentKeyPassword,
|
||||
);
|
||||
debouncedPublicKeyDetection(result.publicKey);
|
||||
toast.success(
|
||||
@@ -754,24 +735,20 @@ export function CredentialEditor({
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const currentKeyPassword =
|
||||
form.watch("keyPassword");
|
||||
const result = await generateKeyPair(
|
||||
"ecdsa-sha2-nistp256",
|
||||
undefined,
|
||||
keyGenerationPassphrase,
|
||||
currentKeyPassword,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
form.setValue("key", result.privateKey);
|
||||
form.setValue("publicKey", result.publicKey);
|
||||
if (keyGenerationPassphrase) {
|
||||
form.setValue(
|
||||
"keyPassword",
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
}
|
||||
debouncedKeyDetection(
|
||||
result.privateKey,
|
||||
keyGenerationPassphrase,
|
||||
currentKeyPassword,
|
||||
);
|
||||
debouncedPublicKeyDetection(result.publicKey);
|
||||
toast.success(
|
||||
@@ -805,24 +782,20 @@ export function CredentialEditor({
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const currentKeyPassword =
|
||||
form.watch("keyPassword");
|
||||
const result = await generateKeyPair(
|
||||
"ssh-rsa",
|
||||
2048,
|
||||
keyGenerationPassphrase,
|
||||
currentKeyPassword,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
form.setValue("key", result.privateKey);
|
||||
form.setValue("publicKey", result.publicKey);
|
||||
if (keyGenerationPassphrase) {
|
||||
form.setValue(
|
||||
"keyPassword",
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
}
|
||||
debouncedKeyDetection(
|
||||
result.privateKey,
|
||||
keyGenerationPassphrase,
|
||||
currentKeyPassword,
|
||||
);
|
||||
debouncedPublicKeyDetection(result.publicKey);
|
||||
toast.success(
|
||||
@@ -851,20 +824,17 @@ export function CredentialEditor({
|
||||
{t("credentials.generateRSA")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
{t("credentials.generateKeyPairNote")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 items-start">
|
||||
<div className="grid grid-cols-2 gap-3 items-start">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="key"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4 flex flex-col">
|
||||
<FormLabel className="mb-2 min-h-[20px]">
|
||||
<FormItem className="mb-3 flex flex-col">
|
||||
<FormLabel className="mb-1 min-h-[20px]">
|
||||
{t("credentials.sshPrivateKey")}
|
||||
</FormLabel>
|
||||
<div className="mb-2">
|
||||
<div className="mb-1">
|
||||
<div className="relative inline-block w-full">
|
||||
<input
|
||||
id="key-upload"
|
||||
@@ -968,12 +938,11 @@ export function CredentialEditor({
|
||||
control={form.control}
|
||||
name="publicKey"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4 flex flex-col">
|
||||
<FormLabel className="mb-2 min-h-[20px]">
|
||||
{t("credentials.sshPublicKey")} (
|
||||
{t("credentials.optional")})
|
||||
<FormItem className="mb-3 flex flex-col">
|
||||
<FormLabel className="mb-1 min-h-[20px]">
|
||||
{t("credentials.sshPublicKey")}
|
||||
</FormLabel>
|
||||
<div className="mb-2 flex gap-2">
|
||||
<div className="mb-1 flex gap-2">
|
||||
<div className="relative inline-block flex-1">
|
||||
<input
|
||||
id="public-key-upload"
|
||||
@@ -1100,9 +1069,6 @@ export function CredentialEditor({
|
||||
]}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{t("credentials.publicKeyNote")}
|
||||
</div>
|
||||
{detectedPublicKeyType && field.value && (
|
||||
<div className="text-sm mt-2">
|
||||
<span className="text-muted-foreground">
|
||||
@@ -1131,7 +1097,7 @@ export function CredentialEditor({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-8 gap-4 mt-4">
|
||||
<div className="grid grid-cols-8 gap-3 mt-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="keyPassword"
|
||||
|
||||
@@ -91,6 +91,9 @@ export function CredentialsManager({
|
||||
const [availableHosts, setAvailableHosts] = useState<any[]>([]);
|
||||
const [selectedHostId, setSelectedHostId] = useState<string>("");
|
||||
const [deployLoading, setDeployLoading] = useState(false);
|
||||
const [hostSearchQuery, setHostSearchQuery] = useState("");
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const dragCounter = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -98,6 +101,40 @@ export function CredentialsManager({
|
||||
fetchHosts();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showDeployDialog) {
|
||||
setDropdownOpen(false);
|
||||
setHostSearchQuery("");
|
||||
setTimeout(() => {
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement && activeElement.blur) {
|
||||
activeElement.blur();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [showDeployDialog]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (dropdownOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
} else {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [dropdownOpen]);
|
||||
|
||||
const fetchHosts = async () => {
|
||||
try {
|
||||
const hosts = await getSSHHosts();
|
||||
@@ -137,6 +174,8 @@ export function CredentialsManager({
|
||||
}
|
||||
setDeployingCredential(credential);
|
||||
setSelectedHostId("");
|
||||
setHostSearchQuery("");
|
||||
setDropdownOpen(false);
|
||||
setShowDeployDialog(true);
|
||||
};
|
||||
|
||||
@@ -789,145 +828,209 @@ export function CredentialsManager({
|
||||
|
||||
<Sheet open={showDeployDialog} onOpenChange={setShowDeployDialog}>
|
||||
<SheetContent className="w-[500px] max-w-[50vw] overflow-y-auto">
|
||||
<SheetHeader className="space-y-6 pb-8">
|
||||
<SheetTitle className="flex items-center space-x-4">
|
||||
<div className="p-2 rounded-lg bg-zinc-100 dark:bg-zinc-800">
|
||||
<Upload className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xl font-semibold">Deploy SSH Key</div>
|
||||
<div className="text-sm font-normal text-zinc-600 dark:text-zinc-400 mt-1">
|
||||
Deploy public key to target server
|
||||
<div className="px-4 py-4">
|
||||
<div className="space-y-3 pb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-900/30">
|
||||
<Upload className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{deployingCredential && (
|
||||
<div className="border border-zinc-200 dark:border-zinc-700 rounded-lg p-4 bg-zinc-50 dark:bg-zinc-900/50">
|
||||
<h4 className="text-sm font-semibold text-zinc-800 dark:text-zinc-200 mb-3 flex items-center">
|
||||
<Key className="h-4 w-4 mr-2 text-zinc-500" />
|
||||
Source Credential
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-1.5 rounded-md bg-zinc-100 dark:bg-zinc-800">
|
||||
<User className="h-3 w-3 text-zinc-500 dark:text-zinc-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Name
|
||||
</div>
|
||||
<div className="text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||
{deployingCredential.name ||
|
||||
deployingCredential.username}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-lg font-semibold">
|
||||
{t("credentials.deploySSHKey")}
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-1.5 rounded-md bg-zinc-100 dark:bg-zinc-800">
|
||||
<User className="h-3 w-3 text-zinc-500 dark:text-zinc-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Username
|
||||
</div>
|
||||
<div className="text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||
{deployingCredential.username}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-1.5 rounded-md bg-zinc-100 dark:bg-zinc-800">
|
||||
<Key className="h-3 w-3 text-zinc-500 dark:text-zinc-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Key Type
|
||||
</div>
|
||||
<div className="text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||
{deployingCredential.keyType || "SSH Key"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("credentials.deploySSHKeyDescription")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-semibold text-zinc-800 dark:text-zinc-200 flex items-center">
|
||||
<Server className="h-4 w-4 mr-2 text-zinc-500" />
|
||||
Target Host
|
||||
</label>
|
||||
<Select value={selectedHostId} onValueChange={setSelectedHostId}>
|
||||
<SelectTrigger className="h-12 border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900/50">
|
||||
<SelectValue placeholder="Choose a host to deploy to..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableHosts.map((host) => (
|
||||
<SelectItem key={host.id} value={host.id.toString()}>
|
||||
<div className="flex items-center gap-3 py-1">
|
||||
<div className="p-1.5 rounded-md bg-zinc-100 dark:bg-zinc-800">
|
||||
<Server className="h-3 w-3 text-zinc-500 dark:text-zinc-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-zinc-800 dark:text-zinc-200">
|
||||
{host.name || host.ip}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{host.username}@{host.ip}:{host.port}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="border border-blue-200 dark:border-blue-800 rounded-lg p-4 bg-blue-50 dark:bg-blue-900/20">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Info className="h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<p className="font-medium mb-1">Deployment Process</p>
|
||||
<p className="text-blue-700 dark:text-blue-300">
|
||||
This will safely add the public key to the target host's
|
||||
~/.ssh/authorized_keys file without overwriting existing
|
||||
keys. The operation is reversible.
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
{deployingCredential && (
|
||||
<div className="border rounded-lg p-3 bg-muted/20">
|
||||
<h4 className="text-sm font-semibold mb-2 flex items-center">
|
||||
<Key className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
{t("credentials.sourceCredential")}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3 px-2 py-1">
|
||||
<div className="p-1.5 rounded bg-muted">
|
||||
<User className="h-3 w-3 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("common.name")}
|
||||
</div>
|
||||
<div className="text-sm font-medium">
|
||||
{deployingCredential.name ||
|
||||
deployingCredential.username}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 px-2 py-1">
|
||||
<div className="p-1.5 rounded bg-muted">
|
||||
<User className="h-3 w-3 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("common.username")}
|
||||
</div>
|
||||
<div className="text-sm font-medium">
|
||||
{deployingCredential.username}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 px-2 py-1">
|
||||
<div className="p-1.5 rounded bg-muted">
|
||||
<Key className="h-3 w-3 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("credentials.keyType")}
|
||||
</div>
|
||||
<div className="text-sm font-medium">
|
||||
{deployingCredential.keyType || "SSH Key"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold flex items-center">
|
||||
<Server className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
{t("credentials.targetHost")}
|
||||
</label>
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<Input
|
||||
placeholder={t("credentials.chooseHostToDeploy")}
|
||||
value={hostSearchQuery}
|
||||
onChange={(e) => {
|
||||
setHostSearchQuery(e.target.value);
|
||||
if (e.target.value.trim() !== "") {
|
||||
setDropdownOpen(true);
|
||||
} else {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
}}
|
||||
onFocus={() => {
|
||||
setDropdownOpen(true);
|
||||
}}
|
||||
className="w-full"
|
||||
autoFocus={false}
|
||||
/>
|
||||
{dropdownOpen && (
|
||||
<div className="absolute top-full left-0 z-50 mt-1 w-full bg-card border border-border rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
||||
{availableHosts.length === 0 ? (
|
||||
<div className="p-3 text-sm text-muted-foreground text-center">
|
||||
{t("credentials.noHostsAvailable")}
|
||||
</div>
|
||||
) : availableHosts.filter(
|
||||
(host) =>
|
||||
!hostSearchQuery ||
|
||||
host.name
|
||||
?.toLowerCase()
|
||||
.includes(hostSearchQuery.toLowerCase()) ||
|
||||
host.ip
|
||||
?.toLowerCase()
|
||||
.includes(hostSearchQuery.toLowerCase()) ||
|
||||
host.username
|
||||
?.toLowerCase()
|
||||
.includes(hostSearchQuery.toLowerCase()),
|
||||
).length === 0 ? (
|
||||
<div className="p-3 text-sm text-muted-foreground text-center">
|
||||
{t("credentials.noHostsMatchSearch")}
|
||||
</div>
|
||||
) : (
|
||||
availableHosts
|
||||
.filter(
|
||||
(host) =>
|
||||
!hostSearchQuery ||
|
||||
host.name
|
||||
?.toLowerCase()
|
||||
.includes(hostSearchQuery.toLowerCase()) ||
|
||||
host.ip
|
||||
?.toLowerCase()
|
||||
.includes(hostSearchQuery.toLowerCase()) ||
|
||||
host.username
|
||||
?.toLowerCase()
|
||||
.includes(hostSearchQuery.toLowerCase()),
|
||||
)
|
||||
.map((host) => (
|
||||
<div
|
||||
key={host.id}
|
||||
className="flex items-center gap-3 py-2 px-3 hover:bg-muted cursor-pointer"
|
||||
onClick={() => {
|
||||
setSelectedHostId(host.id.toString());
|
||||
setHostSearchQuery(host.name || host.ip);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="p-1.5 rounded bg-muted">
|
||||
<Server className="h-3 w-3 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-foreground">
|
||||
{host.name || host.ip}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{host.username}@{host.ip}:{host.port}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-blue-200 dark:border-blue-800 rounded-lg p-3 bg-blue-50 dark:bg-blue-900/20">
|
||||
<div className="flex items-start space-x-2">
|
||||
<Info className="h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-blue-800 dark:text-blue-200 mb-1">
|
||||
{t("credentials.deploymentProcess")}
|
||||
</p>
|
||||
<p className="text-blue-700 dark:text-blue-300">
|
||||
{t("credentials.deploymentProcessDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDeployDialog(false)}
|
||||
disabled={deployLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={performDeploy}
|
||||
disabled={!selectedHostId || deployLoading}
|
||||
className="flex-1 bg-green-600 hover:bg-green-700 text-white"
|
||||
>
|
||||
{deployLoading ? (
|
||||
<div className="flex items-center">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent mr-2"></div>
|
||||
{t("credentials.deploying")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
{t("credentials.deploySSHKey")}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SheetFooter className="mt-8 flex space-x-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDeployDialog(false)}
|
||||
disabled={deployLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={performDeploy}
|
||||
disabled={!selectedHostId || deployLoading}
|
||||
className="flex-1 bg-green-600 hover:bg-green-700 text-white"
|
||||
>
|
||||
{deployLoading ? (
|
||||
<div className="flex items-center">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent mr-2"></div>
|
||||
Deploying...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
Deploy SSH Key
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
@@ -96,6 +96,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
||||
const [pinnedFiles, setPinnedFiles] = useState<Set<string>>(new Set());
|
||||
const [sidebarRefreshTrigger, setSidebarRefreshTrigger] = useState(0);
|
||||
const [isClosing, setIsClosing] = useState<boolean>(false);
|
||||
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
x: number;
|
||||
@@ -179,6 +180,18 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCloseWithError = useCallback(
|
||||
(errorMessage: string) => {
|
||||
if (isClosing) return; // Prevent duplicate calls
|
||||
setIsClosing(true);
|
||||
toast.error(errorMessage);
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[isClosing, onClose],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentHost) {
|
||||
initializeSSHConnection();
|
||||
@@ -286,12 +299,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("SSH connection failed:", error);
|
||||
toast.error(
|
||||
handleCloseWithError(
|
||||
t("fileManager.failedToConnect") + ": " + (error.message || error),
|
||||
);
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -342,9 +352,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("SSH")
|
||||
) {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
handleCloseWithError(
|
||||
t("fileManager.failedToLoadDirectory") +
|
||||
": " +
|
||||
(error.message || error),
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -1104,9 +1116,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
handleCloseWithError(
|
||||
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsReconnecting(false);
|
||||
|
||||
@@ -136,10 +136,8 @@ export function Server({
|
||||
fetchStatus();
|
||||
fetchMetrics();
|
||||
intervalId = window.setInterval(() => {
|
||||
if (isVisible) {
|
||||
fetchStatus();
|
||||
fetchMetrics();
|
||||
}
|
||||
fetchStatus();
|
||||
fetchMetrics();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
|
||||
@@ -617,9 +617,6 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
const jwtToken = getCookie("jwt");
|
||||
|
||||
if (!jwtToken || jwtToken.trim() === "") {
|
||||
console.warn(
|
||||
"WebSocket connection delayed - no authentication token",
|
||||
);
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false);
|
||||
setConnectionError("Authentication required");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TunnelViewer } from "@/ui/Desktop/Apps/Tunnel/TunnelViewer.tsx";
|
||||
import {
|
||||
getSSHHosts,
|
||||
@@ -15,6 +16,7 @@ import type {
|
||||
} from "../../../types/index.js";
|
||||
|
||||
export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const [allHosts, setAllHosts] = useState<SSHHost[]>([]);
|
||||
const [visibleHosts, setVisibleHosts] = useState<SSHHost[]>([]);
|
||||
const [tunnelStatuses, setTunnelStatuses] = useState<
|
||||
@@ -114,7 +116,7 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement {
|
||||
|
||||
useEffect(() => {
|
||||
fetchTunnelStatuses();
|
||||
const interval = setInterval(fetchTunnelStatuses, 500);
|
||||
const interval = setInterval(fetchTunnelStatuses, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchTunnelStatuses]);
|
||||
|
||||
@@ -137,7 +139,7 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement {
|
||||
);
|
||||
|
||||
if (!endpointHost) {
|
||||
throw new Error("Endpoint host not found");
|
||||
throw new Error(t("tunnels.endpointHostNotFound"));
|
||||
}
|
||||
|
||||
const tunnelConfig = {
|
||||
|
||||
@@ -237,7 +237,7 @@ export function TunnelObject({
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
Discord
|
||||
{t("tunnels.discord")}
|
||||
</a>{" "}
|
||||
or create a{" "}
|
||||
<a
|
||||
@@ -246,9 +246,9 @@ export function TunnelObject({
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
GitHub issue
|
||||
{t("tunnels.githubIssue")}
|
||||
</a>{" "}
|
||||
for help.
|
||||
{t("tunnels.forHelp")}.
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -471,7 +471,7 @@ export function TunnelObject({
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
Discord
|
||||
{t("tunnels.discord")}
|
||||
</a>{" "}
|
||||
or create a{" "}
|
||||
<a
|
||||
@@ -480,9 +480,9 @@ export function TunnelObject({
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
GitHub issue
|
||||
{t("tunnels.githubIssue")}
|
||||
</a>{" "}
|
||||
for help.
|
||||
{t("tunnels.forHelp")}.
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function Host({ host }: HostProps): React.ReactElement {
|
||||
|
||||
fetchStatus();
|
||||
|
||||
intervalId = window.setInterval(fetchStatus, 10000);
|
||||
intervalId = window.setInterval(fetchStatus, 30000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -46,7 +46,7 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
|
||||
fetchStatus();
|
||||
|
||||
intervalId = window.setInterval(fetchStatus, 10000);
|
||||
intervalId = window.setInterval(fetchStatus, 30000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -271,9 +271,6 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
|
||||
const jwtToken = getCookie("jwt");
|
||||
if (!jwtToken || jwtToken.trim() === "") {
|
||||
console.warn(
|
||||
"WebSocket connection delayed - no authentication token",
|
||||
);
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false);
|
||||
setConnectionError("Authentication required");
|
||||
|
||||
@@ -46,7 +46,7 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
|
||||
fetchStatus();
|
||||
|
||||
intervalId = window.setInterval(fetchStatus, 10000);
|
||||
intervalId = window.setInterval(fetchStatus, 30000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
Reference in New Issue
Block a user