Add versioning system to electron, update nginx configurations for file uploads, fix UI issues in file manager
This commit is contained in:
@@ -21,6 +21,15 @@ services:
|
|||||||
PORT: "4300"
|
PORT: "4300"
|
||||||
ENABLE_SSL: "true"
|
ENABLE_SSL: "true"
|
||||||
SSL_DOMAIN: "termix-dev.karmaa.site"
|
SSL_DOMAIN: "termix-dev.karmaa.site"
|
||||||
|
# Resource limits for better large file handling
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 2G
|
||||||
|
cpus: '1.0'
|
||||||
|
reservations:
|
||||||
|
memory: 512M
|
||||||
|
cpus: '0.5'
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
termix-dev-data:
|
termix-dev-data:
|
||||||
|
|||||||
@@ -186,12 +186,23 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /ssh/file_manager/ssh/ {
|
location /ssh/file_manager/ssh/ {
|
||||||
|
client_max_body_size 5G;
|
||||||
|
client_body_timeout 300s;
|
||||||
|
client_header_timeout 300s;
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:30004;
|
proxy_pass http://127.0.0.1:30004;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
|
||||||
|
proxy_request_buffering off;
|
||||||
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /health {
|
location /health {
|
||||||
|
|||||||
@@ -172,12 +172,23 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /ssh/file_manager/ssh/ {
|
location /ssh/file_manager/ssh/ {
|
||||||
|
client_max_body_size 5G;
|
||||||
|
client_body_timeout 300s;
|
||||||
|
client_header_timeout 300s;
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:30004;
|
proxy_pass http://127.0.0.1:30004;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
|
||||||
|
proxy_request_buffering off;
|
||||||
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /health {
|
location /health {
|
||||||
|
|||||||
@@ -97,6 +97,163 @@ ipcMain.handle("get-app-version", () => {
|
|||||||
return app.getVersion();
|
return app.getVersion();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GitHub API service for version checking
|
||||||
|
const GITHUB_API_BASE = "https://api.github.com";
|
||||||
|
const REPO_OWNER = "LukeGus";
|
||||||
|
const REPO_NAME = "Termix";
|
||||||
|
|
||||||
|
// Simple cache for GitHub API responses
|
||||||
|
const githubCache = new Map();
|
||||||
|
const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes
|
||||||
|
|
||||||
|
async function fetchGitHubAPI(endpoint, cacheKey) {
|
||||||
|
// Check cache first
|
||||||
|
const cached = githubCache.get(cacheKey);
|
||||||
|
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
|
||||||
|
return {
|
||||||
|
data: cached.data,
|
||||||
|
cached: true,
|
||||||
|
cache_age: Date.now() - cached.timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let fetch;
|
||||||
|
try {
|
||||||
|
fetch = globalThis.fetch || require("node-fetch");
|
||||||
|
} catch (e) {
|
||||||
|
const https = require("https");
|
||||||
|
const http = require("http");
|
||||||
|
const { URL } = require("url");
|
||||||
|
|
||||||
|
fetch = (url, options = {}) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const isHttps = urlObj.protocol === "https:";
|
||||||
|
const client = isHttps ? https : http;
|
||||||
|
|
||||||
|
const req = client.request(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
method: options.method || "GET",
|
||||||
|
headers: options.headers || {},
|
||||||
|
timeout: options.timeout || 10000,
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
let data = "";
|
||||||
|
res.on("data", (chunk) => (data += chunk));
|
||||||
|
res.on("end", () => {
|
||||||
|
resolve({
|
||||||
|
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||||
|
status: res.statusCode,
|
||||||
|
text: () => Promise.resolve(data),
|
||||||
|
json: () => Promise.resolve(JSON.parse(data)),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on("error", reject);
|
||||||
|
req.on("timeout", () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error("Request timeout"));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.body) {
|
||||||
|
req.write(options.body);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
"User-Agent": "TermixElectronUpdateChecker/1.0",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`GitHub API error: ${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Cache the response
|
||||||
|
githubCache.set(cacheKey, {
|
||||||
|
data,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
cached: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch from GitHub API:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Electron app updates
|
||||||
|
ipcMain.handle("check-electron-update", async () => {
|
||||||
|
try {
|
||||||
|
const localVersion = app.getVersion();
|
||||||
|
console.log(`Checking for updates. Local version: ${localVersion}`);
|
||||||
|
|
||||||
|
const releaseData = await fetchGitHubAPI(
|
||||||
|
`/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
|
||||||
|
"latest_release_electron"
|
||||||
|
);
|
||||||
|
|
||||||
|
const rawTag = releaseData.data.tag_name || releaseData.data.name || "";
|
||||||
|
const remoteVersionMatch = rawTag.match(/(\d+\.\d+(\.\d+)?)/);
|
||||||
|
const remoteVersion = remoteVersionMatch ? remoteVersionMatch[1] : null;
|
||||||
|
|
||||||
|
if (!remoteVersion) {
|
||||||
|
console.warn("Remote version not found in GitHub response:", rawTag);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Remote version not found",
|
||||||
|
localVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUpToDate = localVersion === remoteVersion;
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
success: true,
|
||||||
|
status: isUpToDate ? "up_to_date" : "requires_update",
|
||||||
|
localVersion: localVersion,
|
||||||
|
remoteVersion: remoteVersion,
|
||||||
|
latest_release: {
|
||||||
|
tag_name: releaseData.data.tag_name,
|
||||||
|
name: releaseData.data.name,
|
||||||
|
published_at: releaseData.data.published_at,
|
||||||
|
html_url: releaseData.data.html_url,
|
||||||
|
body: releaseData.data.body,
|
||||||
|
},
|
||||||
|
cached: releaseData.cached,
|
||||||
|
cache_age: releaseData.cache_age,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Version check result: ${result.status}`);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Version check failed:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
localVersion: app.getVersion(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-platform", () => {
|
ipcMain.handle("get-platform", () => {
|
||||||
return process.platform;
|
return process.platform;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const { contextBridge, ipcRenderer } = require("electron");
|
|||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
|
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
|
||||||
getPlatform: () => ipcRenderer.invoke("get-platform"),
|
getPlatform: () => ipcRenderer.invoke("get-platform"),
|
||||||
|
checkElectronUpdate: () => ipcRenderer.invoke("check-electron-update"),
|
||||||
|
|
||||||
getServerConfig: () => ipcRenderer.invoke("get-server-config"),
|
getServerConfig: () => ipcRenderer.invoke("get-server-config"),
|
||||||
saveServerConfig: (config) =>
|
saveServerConfig: (config) =>
|
||||||
|
|||||||
@@ -860,8 +860,23 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
|||||||
.json({ error: "File path, name, and content are required" });
|
.json({ error: "File path, name, and content are required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update last active time and extend keepalive for large file operations
|
||||||
sshConn.lastActive = Date.now();
|
sshConn.lastActive = Date.now();
|
||||||
|
|
||||||
|
// For large files, extend the keepalive interval to prevent connection drops
|
||||||
|
const contentSize = typeof content === 'string' ? Buffer.byteLength(content, 'utf8') : content.length;
|
||||||
|
if (contentSize > 10 * 1024 * 1024) { // 10MB threshold
|
||||||
|
fileLogger.info("Large file upload detected, extending SSH keepalive", {
|
||||||
|
operation: "file_upload",
|
||||||
|
sessionId,
|
||||||
|
fileName,
|
||||||
|
fileSize: contentSize,
|
||||||
|
});
|
||||||
|
// Extend keepalive interval for large files
|
||||||
|
// Note: SSH2 client doesn't expose config directly, but we can set keepalive
|
||||||
|
sshConn.client.setKeepAlive(true, 10000); // 10 seconds
|
||||||
|
}
|
||||||
|
|
||||||
const fullPath = filePath.endsWith("/")
|
const fullPath = filePath.endsWith("/")
|
||||||
? filePath + fileName
|
? filePath + fileName
|
||||||
: filePath + "/" + fileName;
|
: filePath + "/" + fileName;
|
||||||
@@ -906,6 +921,13 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
|||||||
hasError = true;
|
hasError = true;
|
||||||
fileLogger.warn(
|
fileLogger.warn(
|
||||||
`SFTP write failed, trying fallback method: ${streamErr.message}`,
|
`SFTP write failed, trying fallback method: ${streamErr.message}`,
|
||||||
|
{
|
||||||
|
operation: "file_upload",
|
||||||
|
sessionId,
|
||||||
|
fileName,
|
||||||
|
fileSize: contentSize,
|
||||||
|
error: streamErr.message,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
tryFallbackMethod();
|
tryFallbackMethod();
|
||||||
});
|
});
|
||||||
|
|||||||
122
src/components/ui/version-alert.tsx
Normal file
122
src/components/ui/version-alert.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
|
||||||
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
|
import { ExternalLink, Download, X, AlertTriangle } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface VersionAlertProps {
|
||||||
|
updateInfo: {
|
||||||
|
success: boolean;
|
||||||
|
status?: "up_to_date" | "requires_update";
|
||||||
|
localVersion?: string;
|
||||||
|
remoteVersion?: string;
|
||||||
|
latest_release?: {
|
||||||
|
tag_name: string;
|
||||||
|
name: string;
|
||||||
|
published_at: string;
|
||||||
|
html_url: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
cached?: boolean;
|
||||||
|
cache_age?: number;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
onDismiss?: () => void;
|
||||||
|
onDownload?: () => void;
|
||||||
|
showDismiss?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VersionAlert({
|
||||||
|
updateInfo,
|
||||||
|
onDismiss,
|
||||||
|
onDownload,
|
||||||
|
showDismiss = true,
|
||||||
|
}: VersionAlertProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!updateInfo.success) {
|
||||||
|
return (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertTitle>{t("versionCheck.error")}</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{updateInfo.error || t("versionCheck.checkFailed")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateInfo.status === "up_to_date") {
|
||||||
|
return (
|
||||||
|
<Alert>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
<AlertTitle>{t("versionCheck.upToDate")}</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{t("versionCheck.currentVersion", { version: updateInfo.localVersion })}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateInfo.status === "requires_update") {
|
||||||
|
return (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertTitle>{t("versionCheck.updateAvailable")}</AlertTitle>
|
||||||
|
<AlertDescription className="space-y-3">
|
||||||
|
<div>
|
||||||
|
{t("versionCheck.newVersionAvailable", {
|
||||||
|
current: updateInfo.localVersion,
|
||||||
|
latest: updateInfo.remoteVersion,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{updateInfo.latest_release && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<div className="font-medium">{updateInfo.latest_release.name}</div>
|
||||||
|
<div className="text-xs">
|
||||||
|
{t("versionCheck.releasedOn", {
|
||||||
|
date: new Date(updateInfo.latest_release.published_at).toLocaleDateString(),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
{updateInfo.latest_release?.html_url && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (onDownload) {
|
||||||
|
onDownload();
|
||||||
|
} else {
|
||||||
|
window.open(updateInfo.latest_release!.html_url, "_blank");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
{t("versionCheck.downloadUpdate")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDismiss && onDismiss && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onDismiss}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
{t("versionCheck.dismiss")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -202,6 +202,19 @@
|
|||||||
"saveConfig": "Save Configuration",
|
"saveConfig": "Save Configuration",
|
||||||
"helpText": "Enter the URL where your Termix server is running (e.g., http://localhost:30001 or https://your-server.com)"
|
"helpText": "Enter the URL where your Termix server is running (e.g., http://localhost:30001 or https://your-server.com)"
|
||||||
},
|
},
|
||||||
|
"versionCheck": {
|
||||||
|
"error": "Version Check Error",
|
||||||
|
"checkFailed": "Failed to check for updates",
|
||||||
|
"upToDate": "App is Up to Date",
|
||||||
|
"currentVersion": "You are running version {{version}}",
|
||||||
|
"updateAvailable": "Update Available",
|
||||||
|
"newVersionAvailable": "A new version is available! You are running {{current}}, but {{latest}} is available.",
|
||||||
|
"releasedOn": "Released on {{date}}",
|
||||||
|
"downloadUpdate": "Download Update",
|
||||||
|
"dismiss": "Dismiss",
|
||||||
|
"checking": "Checking for updates...",
|
||||||
|
"checkUpdates": "Check for Updates"
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"minimize": "Minimize",
|
"minimize": "Minimize",
|
||||||
@@ -702,6 +715,7 @@
|
|||||||
"uploading": "Uploading...",
|
"uploading": "Uploading...",
|
||||||
"downloading": "Downloading...",
|
"downloading": "Downloading...",
|
||||||
"uploadingFile": "Uploading {{name}}...",
|
"uploadingFile": "Uploading {{name}}...",
|
||||||
|
"uploadingLargeFile": "Uploading large file {{name}} ({{size}})...",
|
||||||
"downloadingFile": "Downloading {{name}}...",
|
"downloadingFile": "Downloading {{name}}...",
|
||||||
"creatingFile": "Creating {{name}}...",
|
"creatingFile": "Creating {{name}}...",
|
||||||
"creatingFolder": "Creating {{name}}...",
|
"creatingFolder": "Creating {{name}}...",
|
||||||
|
|||||||
@@ -200,6 +200,19 @@
|
|||||||
"saveConfig": "保存配置",
|
"saveConfig": "保存配置",
|
||||||
"helpText": "输入您的 Termix 服务器运行地址(例如:http://localhost:30001 或 https://your-server.com)"
|
"helpText": "输入您的 Termix 服务器运行地址(例如:http://localhost:30001 或 https://your-server.com)"
|
||||||
},
|
},
|
||||||
|
"versionCheck": {
|
||||||
|
"error": "版本检查错误",
|
||||||
|
"checkFailed": "检查更新失败",
|
||||||
|
"upToDate": "应用已是最新版本",
|
||||||
|
"currentVersion": "您正在运行版本 {{version}}",
|
||||||
|
"updateAvailable": "有可用更新",
|
||||||
|
"newVersionAvailable": "有新版本可用!您正在运行 {{current}},但 {{latest}} 已可用。",
|
||||||
|
"releasedOn": "发布于 {{date}}",
|
||||||
|
"downloadUpdate": "下载更新",
|
||||||
|
"dismiss": "忽略",
|
||||||
|
"checking": "正在检查更新...",
|
||||||
|
"checkUpdates": "检查更新"
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"minimize": "最小化",
|
"minimize": "最小化",
|
||||||
@@ -724,6 +737,7 @@
|
|||||||
"uploading": "上传中...",
|
"uploading": "上传中...",
|
||||||
"downloading": "下载中...",
|
"downloading": "下载中...",
|
||||||
"uploadingFile": "正在上传 {{name}}...",
|
"uploadingFile": "正在上传 {{name}}...",
|
||||||
|
"uploadingLargeFile": "正在上传大文件 {{name}} ({{size}})...",
|
||||||
"downloadingFile": "正在下载 {{name}}...",
|
"downloadingFile": "正在下载 {{name}}...",
|
||||||
"creatingFile": "正在创建 {{name}}...",
|
"creatingFile": "正在创建 {{name}}...",
|
||||||
"creatingFolder": "正在创建 {{name}}...",
|
"creatingFolder": "正在创建 {{name}}...",
|
||||||
|
|||||||
@@ -62,6 +62,24 @@ interface CreateIntent {
|
|||||||
currentName: string;
|
currentName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format file size helper function
|
||||||
|
function formatFileSize(bytes?: number): string {
|
||||||
|
if (bytes === undefined || bytes === null) return "-";
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
|
||||||
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
let size = bytes;
|
||||||
|
let unitIndex = 0;
|
||||||
|
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024;
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedSize = size < 10 && unitIndex > 0 ? size.toFixed(1) : Math.round(size).toString();
|
||||||
|
return `${formattedSize} ${units[unitIndex]}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Internal component, uses window manager
|
// Internal component, uses window manager
|
||||||
function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||||
const { openWindow } = useWindowManager();
|
const { openWindow } = useWindowManager();
|
||||||
@@ -226,13 +244,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
|
|
||||||
const handleFileDragEnd = useCallback(
|
const handleFileDragEnd = useCallback(
|
||||||
(e: DragEvent, draggedFiles: FileItem[]) => {
|
(e: DragEvent, draggedFiles: FileItem[]) => {
|
||||||
// More conservative detection - only trigger download if clearly outside window
|
// Allow dragging off screen on all sides - only check if completely outside window bounds
|
||||||
const margin = 10; // Very small margin to reduce false positives
|
|
||||||
const isOutside =
|
const isOutside =
|
||||||
e.clientX < margin ||
|
e.clientX < 0 ||
|
||||||
e.clientX > window.innerWidth - margin ||
|
e.clientX > window.innerWidth ||
|
||||||
e.clientY < margin ||
|
e.clientY < 0 ||
|
||||||
e.clientY > window.innerHeight - margin;
|
e.clientY > window.innerHeight;
|
||||||
|
|
||||||
// Only trigger download if clearly outside the window bounds
|
// Only trigger download if clearly outside the window bounds
|
||||||
if (isOutside) {
|
if (isOutside) {
|
||||||
@@ -478,6 +495,20 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
async function handleUploadFile(file: File) {
|
async function handleUploadFile(file: File) {
|
||||||
if (!sshSessionId) return;
|
if (!sshSessionId) return;
|
||||||
|
|
||||||
|
// Show progress for large files (>10MB)
|
||||||
|
const isLargeFile = file.size > 10 * 1024 * 1024;
|
||||||
|
let progressToast: any = null;
|
||||||
|
|
||||||
|
if (isLargeFile) {
|
||||||
|
progressToast = toast.loading(
|
||||||
|
t("fileManager.uploadingLargeFile", {
|
||||||
|
name: file.name,
|
||||||
|
size: formatFileSize(file.size)
|
||||||
|
}),
|
||||||
|
{ duration: Infinity }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure SSH connection is valid
|
// Ensure SSH connection is valid
|
||||||
await ensureSSHConnection();
|
await ensureSSHConnection();
|
||||||
@@ -493,8 +524,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
file.type === "application/json" ||
|
file.type === "application/json" ||
|
||||||
file.type === "application/javascript" ||
|
file.type === "application/javascript" ||
|
||||||
file.type === "application/xml" ||
|
file.type === "application/xml" ||
|
||||||
|
file.type === "image/svg+xml" ||
|
||||||
file.name.match(
|
file.name.match(
|
||||||
/\.(txt|json|js|ts|jsx|tsx|css|html|htm|xml|yaml|yml|md|py|java|c|cpp|h|sh|bat|ps1)$/i,
|
/\.(txt|json|js|ts|jsx|tsx|css|scss|less|html|htm|xml|svg|yaml|yml|md|markdown|mdown|mkdn|mdx|py|java|c|cpp|h|sh|bash|zsh|bat|ps1|toml|ini|conf|config|sql|vue|svelte)$/i,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isTextFile) {
|
if (isTextFile) {
|
||||||
@@ -532,11 +564,22 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
currentHost?.id,
|
currentHost?.id,
|
||||||
undefined, // userId - will be handled by backend
|
undefined, // userId - will be handled by backend
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Dismiss progress toast if it exists
|
||||||
|
if (progressToast) {
|
||||||
|
toast.dismiss(progressToast);
|
||||||
|
}
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
t("fileManager.fileUploadedSuccessfully", { name: file.name }),
|
t("fileManager.fileUploadedSuccessfully", { name: file.name }),
|
||||||
);
|
);
|
||||||
handleRefreshDirectory();
|
handleRefreshDirectory();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
// Dismiss progress toast if it exists
|
||||||
|
if (progressToast) {
|
||||||
|
toast.dismiss(progressToast);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
error.message?.includes("connection") ||
|
error.message?.includes("connection") ||
|
||||||
error.message?.includes("established")
|
error.message?.includes("established")
|
||||||
|
|||||||
@@ -1365,8 +1365,8 @@ export function FileManagerGrid({
|
|||||||
<div
|
<div
|
||||||
className="fixed pointer-events-none"
|
className="fixed pointer-events-none"
|
||||||
style={{
|
style={{
|
||||||
left: Math.min(Math.max(dragState.mousePosition.x + 40, 10), window.innerWidth - 300),
|
left: Math.min(Math.max(dragState.mousePosition.x + 40, 0), window.innerWidth - 300),
|
||||||
top: Math.max(Math.min(dragState.mousePosition.y - 80, window.innerHeight - 100), 10),
|
top: Math.max(Math.min(dragState.mousePosition.y - 80, window.innerHeight - 100), 0),
|
||||||
zIndex: 999999,
|
zIndex: 999999,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ import { Button } from "@/components/ui/button.tsx";
|
|||||||
import { Input } from "@/components/ui/input.tsx";
|
import { Input } from "@/components/ui/input.tsx";
|
||||||
import { Label } from "@/components/ui/label.tsx";
|
import { Label } from "@/components/ui/label.tsx";
|
||||||
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
|
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
|
||||||
|
import { VersionAlert } from "@/components/ui/version-alert.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
getServerConfig,
|
getServerConfig,
|
||||||
saveServerConfig,
|
saveServerConfig,
|
||||||
testServerConnection,
|
testServerConnection,
|
||||||
|
checkElectronUpdate,
|
||||||
type ServerConfig,
|
type ServerConfig,
|
||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
import { CheckCircle, XCircle, Server, Wifi } from "lucide-react";
|
import { CheckCircle, XCircle, Server, Wifi, RefreshCw } from "lucide-react";
|
||||||
|
|
||||||
interface ServerConfigProps {
|
interface ServerConfigProps {
|
||||||
onServerConfigured: (serverUrl: string) => void;
|
onServerConfigured: (serverUrl: string) => void;
|
||||||
@@ -31,9 +33,13 @@ export function ServerConfig({
|
|||||||
const [connectionStatus, setConnectionStatus] = useState<
|
const [connectionStatus, setConnectionStatus] = useState<
|
||||||
"unknown" | "success" | "error"
|
"unknown" | "success" | "error"
|
||||||
>("unknown");
|
>("unknown");
|
||||||
|
const [versionInfo, setVersionInfo] = useState<any>(null);
|
||||||
|
const [versionChecking, setVersionChecking] = useState(false);
|
||||||
|
const [versionDismissed, setVersionDismissed] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadServerConfig();
|
loadServerConfig();
|
||||||
|
checkForUpdates();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadServerConfig = async () => {
|
const loadServerConfig = async () => {
|
||||||
@@ -46,6 +52,29 @@ export function ServerConfig({
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkForUpdates = async () => {
|
||||||
|
setVersionChecking(true);
|
||||||
|
try {
|
||||||
|
const updateInfo = await checkElectronUpdate();
|
||||||
|
setVersionInfo(updateInfo);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check for updates:", error);
|
||||||
|
setVersionInfo({ success: false, error: "Check failed" });
|
||||||
|
} finally {
|
||||||
|
setVersionChecking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVersionDismiss = () => {
|
||||||
|
setVersionDismissed(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadUpdate = () => {
|
||||||
|
if (versionInfo?.latest_release?.html_url) {
|
||||||
|
window.open(versionInfo.latest_release.html_url, "_blank");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleTestConnection = async () => {
|
const handleTestConnection = async () => {
|
||||||
if (!serverUrl.trim()) {
|
if (!serverUrl.trim()) {
|
||||||
setError(t("serverConfig.enterServerUrl"));
|
setError(t("serverConfig.enterServerUrl"));
|
||||||
@@ -195,6 +224,34 @@ export function ServerConfig({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Version Check Section */}
|
||||||
|
{versionInfo && !versionDismissed && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium">{t("versionCheck.checkUpdates")}</h3>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={checkForUpdates}
|
||||||
|
disabled={versionChecking}
|
||||||
|
className="h-6 px-2"
|
||||||
|
>
|
||||||
|
{versionChecking ? (
|
||||||
|
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<RefreshCw className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<VersionAlert
|
||||||
|
updateInfo={versionInfo}
|
||||||
|
onDismiss={handleVersionDismiss}
|
||||||
|
onDownload={handleDownloadUpdate}
|
||||||
|
showDismiss={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
{onCancel && !isFirstTime && (
|
{onCancel && !isFirstTime && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -389,6 +389,36 @@ export async function testServerConnection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkElectronUpdate(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
status?: "up_to_date" | "requires_update";
|
||||||
|
localVersion?: string;
|
||||||
|
remoteVersion?: string;
|
||||||
|
latest_release?: {
|
||||||
|
tag_name: string;
|
||||||
|
name: string;
|
||||||
|
published_at: string;
|
||||||
|
html_url: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
cached?: boolean;
|
||||||
|
cache_age?: number;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
if (!isElectron())
|
||||||
|
return { success: false, error: "Not in Electron environment" };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await (window as any).electronAPI?.invoke(
|
||||||
|
"check-electron-update",
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check Electron update:", error);
|
||||||
|
return { success: false, error: "Update check failed" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
getServerConfig().then((config) => {
|
getServerConfig().then((config) => {
|
||||||
if (config?.serverUrl) {
|
if (config?.serverUrl) {
|
||||||
|
|||||||
Reference in New Issue
Block a user