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"
|
||||
ENABLE_SSL: "true"
|
||||
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:
|
||||
termix-dev-data:
|
||||
|
||||
@@ -186,12 +186,23 @@ http {
|
||||
}
|
||||
|
||||
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_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;
|
||||
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /health {
|
||||
|
||||
@@ -172,12 +172,23 @@ http {
|
||||
}
|
||||
|
||||
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_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;
|
||||
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /health {
|
||||
|
||||
@@ -97,6 +97,163 @@ ipcMain.handle("get-app-version", () => {
|
||||
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", () => {
|
||||
return process.platform;
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ const { contextBridge, ipcRenderer } = require("electron");
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
|
||||
getPlatform: () => ipcRenderer.invoke("get-platform"),
|
||||
checkElectronUpdate: () => ipcRenderer.invoke("check-electron-update"),
|
||||
|
||||
getServerConfig: () => ipcRenderer.invoke("get-server-config"),
|
||||
saveServerConfig: (config) =>
|
||||
|
||||
@@ -860,7 +860,22 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
||||
.json({ error: "File path, name, and content are required" });
|
||||
}
|
||||
|
||||
// Update last active time and extend keepalive for large file operations
|
||||
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("/")
|
||||
? filePath + fileName
|
||||
@@ -906,6 +921,13 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
||||
hasError = true;
|
||||
fileLogger.warn(
|
||||
`SFTP write failed, trying fallback method: ${streamErr.message}`,
|
||||
{
|
||||
operation: "file_upload",
|
||||
sessionId,
|
||||
fileName,
|
||||
fileSize: contentSize,
|
||||
error: streamErr.message,
|
||||
}
|
||||
);
|
||||
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",
|
||||
"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": {
|
||||
"close": "Close",
|
||||
"minimize": "Minimize",
|
||||
@@ -702,6 +715,7 @@
|
||||
"uploading": "Uploading...",
|
||||
"downloading": "Downloading...",
|
||||
"uploadingFile": "Uploading {{name}}...",
|
||||
"uploadingLargeFile": "Uploading large file {{name}} ({{size}})...",
|
||||
"downloadingFile": "Downloading {{name}}...",
|
||||
"creatingFile": "Creating {{name}}...",
|
||||
"creatingFolder": "Creating {{name}}...",
|
||||
|
||||
@@ -200,6 +200,19 @@
|
||||
"saveConfig": "保存配置",
|
||||
"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": {
|
||||
"close": "关闭",
|
||||
"minimize": "最小化",
|
||||
@@ -724,6 +737,7 @@
|
||||
"uploading": "上传中...",
|
||||
"downloading": "下载中...",
|
||||
"uploadingFile": "正在上传 {{name}}...",
|
||||
"uploadingLargeFile": "正在上传大文件 {{name}} ({{size}})...",
|
||||
"downloadingFile": "正在下载 {{name}}...",
|
||||
"creatingFile": "正在创建 {{name}}...",
|
||||
"creatingFolder": "正在创建 {{name}}...",
|
||||
|
||||
@@ -62,6 +62,24 @@ interface CreateIntent {
|
||||
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
|
||||
function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
const { openWindow } = useWindowManager();
|
||||
@@ -226,13 +244,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
|
||||
const handleFileDragEnd = useCallback(
|
||||
(e: DragEvent, draggedFiles: FileItem[]) => {
|
||||
// More conservative detection - only trigger download if clearly outside window
|
||||
const margin = 10; // Very small margin to reduce false positives
|
||||
// Allow dragging off screen on all sides - only check if completely outside window bounds
|
||||
const isOutside =
|
||||
e.clientX < margin ||
|
||||
e.clientX > window.innerWidth - margin ||
|
||||
e.clientY < margin ||
|
||||
e.clientY > window.innerHeight - margin;
|
||||
e.clientX < 0 ||
|
||||
e.clientX > window.innerWidth ||
|
||||
e.clientY < 0 ||
|
||||
e.clientY > window.innerHeight;
|
||||
|
||||
// Only trigger download if clearly outside the window bounds
|
||||
if (isOutside) {
|
||||
@@ -478,6 +495,20 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
async function handleUploadFile(file: File) {
|
||||
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 {
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
@@ -493,8 +524,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
file.type === "application/json" ||
|
||||
file.type === "application/javascript" ||
|
||||
file.type === "application/xml" ||
|
||||
file.type === "image/svg+xml" ||
|
||||
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) {
|
||||
@@ -532,11 +564,22 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
currentHost?.id,
|
||||
undefined, // userId - will be handled by backend
|
||||
);
|
||||
|
||||
// Dismiss progress toast if it exists
|
||||
if (progressToast) {
|
||||
toast.dismiss(progressToast);
|
||||
}
|
||||
|
||||
toast.success(
|
||||
t("fileManager.fileUploadedSuccessfully", { name: file.name }),
|
||||
);
|
||||
handleRefreshDirectory();
|
||||
} catch (error: any) {
|
||||
// Dismiss progress toast if it exists
|
||||
if (progressToast) {
|
||||
toast.dismiss(progressToast);
|
||||
}
|
||||
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
|
||||
@@ -1365,8 +1365,8 @@ export function FileManagerGrid({
|
||||
<div
|
||||
className="fixed pointer-events-none"
|
||||
style={{
|
||||
left: Math.min(Math.max(dragState.mousePosition.x + 40, 10), window.innerWidth - 300),
|
||||
top: Math.max(Math.min(dragState.mousePosition.y - 80, window.innerHeight - 100), 10),
|
||||
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), 0),
|
||||
zIndex: 999999,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -3,14 +3,16 @@ import { Button } from "@/components/ui/button.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
|
||||
import { VersionAlert } from "@/components/ui/version-alert.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
getServerConfig,
|
||||
saveServerConfig,
|
||||
testServerConnection,
|
||||
checkElectronUpdate,
|
||||
type ServerConfig,
|
||||
} from "@/ui/main-axios.ts";
|
||||
import { CheckCircle, XCircle, Server, Wifi } from "lucide-react";
|
||||
import { CheckCircle, XCircle, Server, Wifi, RefreshCw } from "lucide-react";
|
||||
|
||||
interface ServerConfigProps {
|
||||
onServerConfigured: (serverUrl: string) => void;
|
||||
@@ -31,9 +33,13 @@ export function ServerConfig({
|
||||
const [connectionStatus, setConnectionStatus] = useState<
|
||||
"unknown" | "success" | "error"
|
||||
>("unknown");
|
||||
const [versionInfo, setVersionInfo] = useState<any>(null);
|
||||
const [versionChecking, setVersionChecking] = useState(false);
|
||||
const [versionDismissed, setVersionDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadServerConfig();
|
||||
checkForUpdates();
|
||||
}, []);
|
||||
|
||||
const loadServerConfig = async () => {
|
||||
@@ -46,6 +52,29 @@ export function ServerConfig({
|
||||
} 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 () => {
|
||||
if (!serverUrl.trim()) {
|
||||
setError(t("serverConfig.enterServerUrl"));
|
||||
@@ -195,6 +224,34 @@ export function ServerConfig({
|
||||
</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">
|
||||
{onCancel && !isFirstTime && (
|
||||
<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()) {
|
||||
getServerConfig().then((config) => {
|
||||
if (config?.serverUrl) {
|
||||
|
||||
Reference in New Issue
Block a user