Add electron server configurator
This commit is contained in:
@@ -172,7 +172,14 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
const wsUrl = isDev
|
||||
? 'ws://localhost:8082'
|
||||
: isElectron
|
||||
? 'ws://127.0.0.1:8082'
|
||||
? (() => {
|
||||
// Get configured server URL from window object (set by main-axios)
|
||||
const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081';
|
||||
// Convert HTTP/HTTPS to WS/WSS and use nginx reverse proxy path
|
||||
const wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://';
|
||||
const wsHost = baseUrl.replace(/^https?:\/\//, '').replace(/:\d+$/, ''); // Remove port if present
|
||||
return `${wsProtocol}${wsHost}/ssh/websocket/`;
|
||||
})()
|
||||
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
@@ -405,7 +412,14 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
const wsUrl = isDev
|
||||
? 'ws://localhost:8082'
|
||||
: isElectron
|
||||
? 'ws://127.0.0.1:8082'
|
||||
? (() => {
|
||||
// Get configured server URL from window object (set by main-axios)
|
||||
const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081';
|
||||
// Convert HTTP/HTTPS to WS/WSS and use nginx reverse proxy path
|
||||
const wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://';
|
||||
const wsHost = baseUrl.replace(/^https?:\/\//, '').replace(/:\d+$/, ''); // Remove port if present
|
||||
return `${wsProtocol}${wsHost}/ssh/websocket/`;
|
||||
})()
|
||||
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
||||
|
||||
connectToHost(cols, rows);
|
||||
|
||||
217
src/ui/Desktop/ElectronOnly/ServerConfig.tsx
Normal file
217
src/ui/Desktop/ElectronOnly/ServerConfig.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
import { getServerConfig, saveServerConfig, testServerConnection, type ServerConfig } from '@/ui/main-axios.ts';
|
||||
import { CheckCircle, XCircle, Server, Wifi } from 'lucide-react';
|
||||
|
||||
interface ServerConfigProps {
|
||||
onServerConfigured: (serverUrl: string) => void;
|
||||
onCancel?: () => void;
|
||||
isFirstTime?: boolean;
|
||||
}
|
||||
|
||||
export function ServerConfig({ onServerConfigured, onCancel, isFirstTime = false }: ServerConfigProps) {
|
||||
const { t } = useTranslation();
|
||||
const [serverUrl, setServerUrl] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [connectionStatus, setConnectionStatus] = useState<'unknown' | 'success' | 'error'>('unknown');
|
||||
|
||||
useEffect(() => {
|
||||
loadServerConfig();
|
||||
}, []);
|
||||
|
||||
const loadServerConfig = async () => {
|
||||
try {
|
||||
const config = await getServerConfig();
|
||||
if (config?.serverUrl) {
|
||||
setServerUrl(config.serverUrl);
|
||||
setConnectionStatus('success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load server config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!serverUrl.trim()) {
|
||||
setError(t('serverConfig.enterServerUrl'));
|
||||
return;
|
||||
}
|
||||
|
||||
setTesting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Normalize URL
|
||||
let normalizedUrl = serverUrl.trim();
|
||||
if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) {
|
||||
normalizedUrl = `http://${normalizedUrl}`;
|
||||
}
|
||||
|
||||
const result = await testServerConnection(normalizedUrl);
|
||||
|
||||
if (result.success) {
|
||||
setConnectionStatus('success');
|
||||
} else {
|
||||
setConnectionStatus('error');
|
||||
setError(result.error || t('serverConfig.connectionFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
setConnectionStatus('error');
|
||||
setError(t('serverConfig.connectionError'));
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
if (!serverUrl.trim()) {
|
||||
setError(t('serverConfig.enterServerUrl'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionStatus !== 'success') {
|
||||
setError(t('serverConfig.testConnectionFirst'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Normalize URL
|
||||
let normalizedUrl = serverUrl.trim();
|
||||
if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) {
|
||||
normalizedUrl = `http://${normalizedUrl}`;
|
||||
}
|
||||
|
||||
const config: ServerConfig = {
|
||||
serverUrl: normalizedUrl,
|
||||
lastUpdated: new Date().toISOString()
|
||||
};
|
||||
|
||||
const success = await saveServerConfig(config);
|
||||
|
||||
if (success) {
|
||||
onServerConfigured(normalizedUrl);
|
||||
} else {
|
||||
setError(t('serverConfig.saveFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
setError(t('serverConfig.saveError'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUrlChange = (value: string) => {
|
||||
setServerUrl(value);
|
||||
setConnectionStatus('unknown');
|
||||
setError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
|
||||
<Server className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold">{t('serverConfig.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{t('serverConfig.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-url">{t('serverConfig.serverUrl')}</Label>
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
id="server-url"
|
||||
type="text"
|
||||
placeholder="http://localhost:8081 or https://your-server.com"
|
||||
value={serverUrl}
|
||||
onChange={(e) => handleUrlChange(e.target.value)}
|
||||
className="flex-1 h-10"
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
disabled={testing || !serverUrl.trim() || loading}
|
||||
className="w-10 h-10 p-0 flex items-center justify-center"
|
||||
>
|
||||
{testing ? (
|
||||
<div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<Wifi className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{connectionStatus !== 'unknown' && (
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
{connectionStatus === 'success' ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span className="text-green-600">{t('serverConfig.connected')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="w-4 h-4 text-red-500" />
|
||||
<span className="text-red-600">{t('serverConfig.disconnected')}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex space-x-2">
|
||||
{onCancel && !isFirstTime && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={onCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
className={onCancel && !isFirstTime ? "flex-1" : "w-full"}
|
||||
onClick={handleSaveConfig}
|
||||
disabled={loading || testing || connectionStatus !== 'success'}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
<span>{t('serverConfig.saving')}</span>
|
||||
</div>
|
||||
) : (
|
||||
t('serverConfig.saveConfig')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{t('serverConfig.helpText')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,8 +20,11 @@ import {
|
||||
completePasswordReset,
|
||||
getOIDCAuthorizeUrl,
|
||||
verifyTOTPLogin,
|
||||
setCookie
|
||||
setCookie,
|
||||
getServerConfig,
|
||||
type ServerConfig
|
||||
} from "../../main-axios.ts";
|
||||
import {ServerConfig as ServerConfigComponent} from "@/ui/Desktop/ElectronOnly/ServerConfig.tsx";
|
||||
|
||||
function getCookie(name: string) {
|
||||
return document.cookie.split('; ').reduce((r, v) => {
|
||||
@@ -410,6 +413,66 @@ export function HomepageAuth({
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Check if we need to show server config for Electron
|
||||
const [showServerConfig, setShowServerConfig] = useState<boolean | null>(null);
|
||||
const [currentServerUrl, setCurrentServerUrl] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const checkServerConfig = async () => {
|
||||
if ((window as any).electronAPI) {
|
||||
try {
|
||||
const config = await getServerConfig();
|
||||
console.log('Desktop HomepageAuth - Server config check:', config);
|
||||
setCurrentServerUrl(config?.serverUrl || '');
|
||||
setShowServerConfig(!config || !config.serverUrl);
|
||||
} catch (error) {
|
||||
console.log('Desktop HomepageAuth - No server config found, showing config screen');
|
||||
setShowServerConfig(true);
|
||||
}
|
||||
} else {
|
||||
setShowServerConfig(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkServerConfig();
|
||||
}, []);
|
||||
|
||||
if (showServerConfig === null) {
|
||||
// Still checking
|
||||
return (
|
||||
<div
|
||||
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (showServerConfig) {
|
||||
console.log('Desktop HomepageAuth - SHOWING SERVER CONFIG SCREEN');
|
||||
return (
|
||||
<div
|
||||
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
<ServerConfigComponent
|
||||
onServerConfigured={() => {
|
||||
console.log('Server configured, reloading page');
|
||||
window.location.reload();
|
||||
}}
|
||||
onCancel={() => {
|
||||
console.log('Cancelled server config, going back to login');
|
||||
setShowServerConfig(false);
|
||||
}}
|
||||
isFirstTime={!currentServerUrl}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
||||
@@ -792,13 +855,32 @@ export function HomepageAuth({
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-dark-border">
|
||||
<div className="mt-6 pt-4 border-t border-dark-border space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm text-muted-foreground">{t('common.language')}</Label>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
{(window as any).electronAPI && currentServerUrl && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm text-muted-foreground">Server</Label>
|
||||
<div className="text-xs text-muted-foreground truncate max-w-[200px]">
|
||||
{currentServerUrl}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowServerConfig(true)}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user