Add versioning system to electron, update nginx configurations for file uploads, fix UI issues in file manager
This commit is contained in:
@@ -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