2026-07-21 21:11:13 +03:00
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
2026-04-15 01:32:59 +08:00
|
|
|
import {
|
|
|
|
|
desktopHostsGet,
|
|
|
|
|
desktopHostsSet,
|
|
|
|
|
desktopHostProbe,
|
2026-06-02 00:43:05 +03:00
|
|
|
resolveDesktopHostUrl,
|
2026-07-21 21:11:13 +03:00
|
|
|
importDesktopHostPairing,
|
|
|
|
|
type DesktopHost,
|
2026-04-15 01:32:59 +08:00
|
|
|
type HostProbeResult,
|
|
|
|
|
} from '@/lib/desktopHosts';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
2026-06-03 02:42:00 +03:00
|
|
|
import { isDesktopShell, restartDesktopApp } from '@/lib/desktop';
|
2026-04-26 14:03:39 +03:00
|
|
|
import { useI18n } from '@/lib/i18n';
|
2026-04-15 01:32:59 +08:00
|
|
|
|
|
|
|
|
type ConnectionState = 'idle' | 'testing' | 'success' | 'error';
|
|
|
|
|
|
|
|
|
|
export interface RemoteConnectionFormProps {
|
|
|
|
|
onBack: () => void;
|
|
|
|
|
/** Optional: show the back button (default: true) */
|
|
|
|
|
showBackButton?: boolean;
|
|
|
|
|
/** Optional: initial URL to pre-populate */
|
|
|
|
|
initialUrl?: string;
|
|
|
|
|
/** Optional: initial label to pre-populate */
|
|
|
|
|
initialLabel?: string;
|
|
|
|
|
/** Optional: show recovery mode styling/behavior */
|
|
|
|
|
isRecoveryMode?: boolean;
|
|
|
|
|
/** Optional: callback when successfully connected */
|
|
|
|
|
onConnect?: () => void;
|
|
|
|
|
/** Optional: callback when user wants to switch to local setup */
|
|
|
|
|
onSwitchToLocal?: () => void;
|
2026-07-21 21:11:13 +03:00
|
|
|
showInstancePicker?: boolean;
|
2026-04-15 01:32:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ProbeStatus = HostProbeResult['status'] | null;
|
|
|
|
|
|
2026-04-26 14:03:39 +03:00
|
|
|
function getProbeStatusMessageKey(status: ProbeStatus): string | null {
|
2026-04-15 01:32:59 +08:00
|
|
|
switch (status) {
|
|
|
|
|
case 'ok':
|
|
|
|
|
return null; // Success is shown separately
|
|
|
|
|
case 'auth':
|
2026-04-26 14:03:39 +03:00
|
|
|
return 'onboarding.remoteConnection.probe.authMessage';
|
2026-06-02 00:43:05 +03:00
|
|
|
case 'update-recommended':
|
|
|
|
|
return 'onboarding.remoteConnection.probe.updateRecommendedMessage';
|
|
|
|
|
case 'incompatible':
|
|
|
|
|
return 'onboarding.remoteConnection.probe.incompatibleMessage';
|
2026-04-15 01:32:59 +08:00
|
|
|
case 'wrong-service':
|
2026-04-26 14:03:39 +03:00
|
|
|
return 'onboarding.remoteConnection.probe.wrongServiceMessage';
|
2026-04-15 01:32:59 +08:00
|
|
|
case 'unreachable':
|
2026-04-26 14:03:39 +03:00
|
|
|
return 'onboarding.remoteConnection.probe.unreachableMessage';
|
2026-04-15 01:32:59 +08:00
|
|
|
default:
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isBlockingStatus(status: ProbeStatus): boolean {
|
2026-06-02 00:43:05 +03:00
|
|
|
return status === 'wrong-service' || status === 'unreachable' || status === 'incompatible';
|
2026-04-15 01:32:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function RemoteConnectionForm({
|
|
|
|
|
onBack,
|
|
|
|
|
showBackButton = true,
|
|
|
|
|
initialUrl = '',
|
|
|
|
|
initialLabel = '',
|
|
|
|
|
isRecoveryMode = false,
|
|
|
|
|
onConnect,
|
|
|
|
|
onSwitchToLocal,
|
2026-07-21 21:11:13 +03:00
|
|
|
showInstancePicker = false,
|
2026-04-15 01:32:59 +08:00
|
|
|
}: RemoteConnectionFormProps) {
|
2026-04-26 14:03:39 +03:00
|
|
|
const { t } = useI18n();
|
2026-04-15 01:32:59 +08:00
|
|
|
const [url, setUrl] = useState(initialUrl);
|
|
|
|
|
const [label, setLabel] = useState(initialLabel);
|
|
|
|
|
const [state, setState] = useState<ConnectionState>('idle');
|
|
|
|
|
const [probeResult, setProbeResult] = useState<HostProbeResult | null>(null);
|
|
|
|
|
const [error, setError] = useState('');
|
2026-07-21 21:11:13 +03:00
|
|
|
const [hosts, setHosts] = useState<DesktopHost[]>([]);
|
|
|
|
|
const [view, setView] = useState<'instances' | 'add' | 'import'>(() => showInstancePicker ? 'instances' : 'add');
|
|
|
|
|
const [connectLink, setConnectLink] = useState('');
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!showInstancePicker) return;
|
|
|
|
|
void desktopHostsGet().then((config) => setHosts(config.hosts)).catch((err) => {
|
|
|
|
|
setError(err instanceof Error ? err.message : String(err));
|
|
|
|
|
});
|
|
|
|
|
}, [showInstancePicker]);
|
2026-04-15 01:32:59 +08:00
|
|
|
|
2026-06-02 00:43:05 +03:00
|
|
|
const resolvedUrl = resolveDesktopHostUrl(url);
|
|
|
|
|
const normalizedUrl = resolvedUrl?.persistedUrl ?? null;
|
2026-04-15 01:32:59 +08:00
|
|
|
|
|
|
|
|
const handleUrlChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
setUrl(e.target.value);
|
|
|
|
|
setState('idle');
|
|
|
|
|
setProbeResult(null);
|
|
|
|
|
setError('');
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleLabelChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
setLabel(e.target.value);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleTest = useCallback(async () => {
|
|
|
|
|
if (!normalizedUrl) return;
|
|
|
|
|
|
|
|
|
|
setState('testing');
|
|
|
|
|
setProbeResult(null);
|
|
|
|
|
setError('');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await desktopHostProbe(normalizedUrl);
|
|
|
|
|
setProbeResult(result);
|
2026-06-02 00:43:05 +03:00
|
|
|
setState(result.status === 'ok' || result.status === 'update-recommended' ? 'success' : 'error');
|
2026-04-15 01:32:59 +08:00
|
|
|
} catch (err) {
|
2026-04-26 14:03:39 +03:00
|
|
|
setError(err instanceof Error ? err.message : t('onboarding.remoteConnection.errors.connectionTestFailed'));
|
2026-04-15 01:32:59 +08:00
|
|
|
setState('error');
|
|
|
|
|
}
|
2026-04-26 14:03:39 +03:00
|
|
|
}, [normalizedUrl, t]);
|
2026-04-15 01:32:59 +08:00
|
|
|
|
|
|
|
|
const handleConnect = useCallback(async () => {
|
2026-06-02 00:43:05 +03:00
|
|
|
if (!resolvedUrl) return;
|
|
|
|
|
const targetUrl = resolvedUrl.persistedUrl;
|
2026-04-15 01:32:59 +08:00
|
|
|
|
|
|
|
|
setState('testing');
|
|
|
|
|
setProbeResult(null);
|
|
|
|
|
setError('');
|
|
|
|
|
|
|
|
|
|
try {
|
2026-06-02 00:43:05 +03:00
|
|
|
const probe = await desktopHostProbe(targetUrl);
|
2026-04-15 01:32:59 +08:00
|
|
|
setProbeResult(probe);
|
|
|
|
|
|
|
|
|
|
// Block connection on wrong-service or unreachable
|
|
|
|
|
if (isBlockingStatus(probe.status)) {
|
|
|
|
|
setState('error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const config = await desktopHostsGet();
|
2026-06-02 00:43:05 +03:00
|
|
|
const hostLabel = label.trim() || targetUrl;
|
2026-04-15 01:32:59 +08:00
|
|
|
|
|
|
|
|
const existingHost = config.hosts.find(
|
2026-06-02 00:43:05 +03:00
|
|
|
(h) => h.url === targetUrl
|
2026-04-15 01:32:59 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const hostId = existingHost ? existingHost.id : `host-${Date.now().toString(16)}`;
|
|
|
|
|
|
|
|
|
|
const newHost = {
|
|
|
|
|
id: hostId,
|
|
|
|
|
label: hostLabel,
|
2026-06-02 00:43:05 +03:00
|
|
|
url: targetUrl,
|
|
|
|
|
apiUrl: targetUrl,
|
2026-04-15 01:32:59 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updatedHosts = existingHost
|
|
|
|
|
? config.hosts.map((h) => (h.id === hostId ? newHost : h))
|
|
|
|
|
: [...config.hosts, newHost];
|
|
|
|
|
|
|
|
|
|
// Set as default and mark initial choice completed
|
|
|
|
|
await desktopHostsSet({
|
|
|
|
|
hosts: updatedHosts,
|
|
|
|
|
defaultHostId: hostId,
|
|
|
|
|
initialHostChoiceCompleted: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onConnect?.();
|
|
|
|
|
|
2026-06-02 00:43:05 +03:00
|
|
|
if (resolvedUrl.redeemUrl) {
|
|
|
|
|
window.location.assign(resolvedUrl.redeemUrl);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 02:42:00 +03:00
|
|
|
if (isDesktopShell()) {
|
|
|
|
|
await restartDesktopApp();
|
2026-04-15 01:32:59 +08:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2026-04-26 14:03:39 +03:00
|
|
|
setError(err instanceof Error ? err.message : t('onboarding.remoteConnection.errors.failedToSaveConnection'));
|
2026-04-15 01:32:59 +08:00
|
|
|
setState('error');
|
|
|
|
|
}
|
2026-06-02 00:43:05 +03:00
|
|
|
}, [resolvedUrl, label, onConnect, t]);
|
2026-04-15 01:32:59 +08:00
|
|
|
|
2026-07-21 21:11:13 +03:00
|
|
|
const selectHost = useCallback(async (hostId: string) => {
|
|
|
|
|
const config = await desktopHostsGet();
|
|
|
|
|
await desktopHostsSet({
|
|
|
|
|
hosts: config.hosts,
|
|
|
|
|
defaultHostId: hostId,
|
|
|
|
|
initialHostChoiceCompleted: true,
|
|
|
|
|
});
|
|
|
|
|
await restartDesktopApp();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleImport = useCallback(async () => {
|
|
|
|
|
setState('testing');
|
|
|
|
|
setError('');
|
|
|
|
|
try {
|
|
|
|
|
const config = await desktopHostsGet();
|
|
|
|
|
const imported = await importDesktopHostPairing(connectLink, config.hosts);
|
|
|
|
|
await desktopHostsSet({
|
|
|
|
|
hosts: imported.hosts,
|
|
|
|
|
defaultHostId: imported.hostId,
|
|
|
|
|
initialHostChoiceCompleted: true,
|
|
|
|
|
});
|
|
|
|
|
await restartDesktopApp();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(
|
|
|
|
|
err instanceof Error && err.message === 'invalid-connect-link'
|
|
|
|
|
? t('settings.remoteInstances.direct.error.invalidConnectLink')
|
|
|
|
|
: t('onboarding.remoteConnection.errors.failedToSaveConnection'),
|
|
|
|
|
);
|
|
|
|
|
setState('error');
|
|
|
|
|
}
|
|
|
|
|
}, [connectLink, t]);
|
|
|
|
|
|
2026-04-15 01:32:59 +08:00
|
|
|
const isTesting = state === 'testing';
|
|
|
|
|
const canTest = normalizedUrl !== null && !isTesting;
|
|
|
|
|
const canConnect = normalizedUrl !== null && !isTesting && !isBlockingStatus(probeResult?.status ?? null);
|
|
|
|
|
|
2026-04-26 14:03:39 +03:00
|
|
|
const probeMessageKey = getProbeStatusMessageKey(probeResult?.status ?? null);
|
2026-04-15 01:32:59 +08:00
|
|
|
const isSuccess = probeResult?.status === 'ok';
|
2026-06-02 00:43:05 +03:00
|
|
|
const isUpdateRecommended = probeResult?.status === 'update-recommended';
|
2026-04-15 01:32:59 +08:00
|
|
|
const isAuth = probeResult?.status === 'auth';
|
|
|
|
|
const isBlocking = isBlockingStatus(probeResult?.status ?? null);
|
|
|
|
|
|
2026-07-21 21:11:13 +03:00
|
|
|
if (showInstancePicker && view === 'instances') {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col items-center justify-center h-full p-8">
|
|
|
|
|
<div className="w-full max-w-md space-y-6">
|
|
|
|
|
<div className="space-y-2 text-center">
|
|
|
|
|
<h1 className="typography-ui-header text-xl font-semibold text-foreground">
|
|
|
|
|
{t('desktopHostSwitcher.actions.switchInstance')}
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-muted-foreground text-sm">{t('settings.remoteInstances.direct.description')}</p>
|
|
|
|
|
</div>
|
|
|
|
|
{error ? <div className="text-sm text-[var(--status-error)]">{error}</div> : null}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{hosts.length === 0 ? (
|
|
|
|
|
<div className="py-4 text-center text-sm text-muted-foreground">
|
|
|
|
|
{t('settings.remoteInstances.direct.state.empty')}
|
|
|
|
|
</div>
|
|
|
|
|
) : hosts.map((host) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={host.id}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="w-full justify-start"
|
|
|
|
|
onClick={() => void selectHost(host.id)}
|
|
|
|
|
>
|
|
|
|
|
<span className="min-w-0 truncate">{host.label}</span>
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button variant="outline" className="flex-1" onClick={() => setView('import')}>
|
|
|
|
|
{t('settings.remoteInstances.direct.import.action')}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button className="flex-1" onClick={() => setView('add')}>
|
|
|
|
|
{t('settings.remoteInstances.direct.actions.add')}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (showInstancePicker && view === 'import') {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col items-center justify-center h-full p-8">
|
|
|
|
|
<div className="w-full max-w-md space-y-6">
|
|
|
|
|
<Button variant="ghost" onClick={() => setView('instances')} className="p-0 text-muted-foreground">
|
|
|
|
|
{t('onboarding.common.actions.back')}
|
|
|
|
|
</Button>
|
|
|
|
|
<div className="space-y-2 text-center">
|
|
|
|
|
<h1 className="typography-ui-header text-xl font-semibold text-foreground">
|
|
|
|
|
{t('settings.remoteInstances.direct.import.action')}
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-muted-foreground text-sm">{t('settings.remoteInstances.direct.import.description')}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Input
|
|
|
|
|
value={connectLink}
|
|
|
|
|
onChange={(event) => setConnectLink(event.target.value)}
|
|
|
|
|
placeholder={t('settings.remoteInstances.direct.import.placeholder')}
|
|
|
|
|
disabled={isTesting}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
{error ? <div className="text-sm text-[var(--status-error)]">{error}</div> : null}
|
|
|
|
|
<Button onClick={() => void handleImport()} disabled={isTesting || !connectLink.trim()}>
|
|
|
|
|
{t('settings.remoteInstances.direct.import.action')}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 01:32:59 +08:00
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col items-center justify-center h-full p-8">
|
|
|
|
|
<div className="w-full max-w-md space-y-6">
|
2026-07-21 21:11:13 +03:00
|
|
|
{(showBackButton || showInstancePicker) && (
|
2026-04-15 01:32:59 +08:00
|
|
|
<div className="flex items-center">
|
2026-07-21 21:11:13 +03:00
|
|
|
<Button variant="ghost" onClick={showInstancePicker ? () => setView('instances') : onBack} className="p-0 text-muted-foreground hover:text-foreground">
|
2026-04-26 14:03:39 +03:00
|
|
|
{t('onboarding.common.actions.back')}
|
2026-04-15 01:32:59 +08:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2 text-center">
|
2026-04-26 14:03:39 +03:00
|
|
|
<h1 className="typography-ui-header text-xl font-semibold text-foreground">
|
2026-04-15 01:32:59 +08:00
|
|
|
{isRecoveryMode
|
2026-04-26 14:03:39 +03:00
|
|
|
? t('onboarding.remoteConnection.titleRecovery')
|
|
|
|
|
: t('onboarding.remoteConnection.title')}
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-muted-foreground text-sm">
|
|
|
|
|
{t('onboarding.remoteConnection.description')}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-04-15 01:32:59 +08:00
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label htmlFor="remote-url" className="text-sm text-foreground">
|
2026-04-26 14:03:39 +03:00
|
|
|
{t('onboarding.remoteConnection.field.serverAddress')}
|
2026-04-15 01:32:59 +08:00
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="remote-url"
|
|
|
|
|
type="url"
|
|
|
|
|
value={url}
|
|
|
|
|
onChange={handleUrlChange}
|
2026-04-26 14:03:39 +03:00
|
|
|
placeholder={t('onboarding.remoteConnection.field.serverAddressPlaceholder')}
|
2026-04-15 01:32:59 +08:00
|
|
|
disabled={isTesting}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label htmlFor="remote-label" className="text-sm text-foreground">
|
2026-04-26 14:03:39 +03:00
|
|
|
{t('onboarding.remoteConnection.field.nameOptional')}
|
2026-04-15 01:32:59 +08:00
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="remote-label"
|
|
|
|
|
type="text"
|
|
|
|
|
value={label}
|
|
|
|
|
onChange={handleLabelChange}
|
2026-04-26 14:03:39 +03:00
|
|
|
placeholder={t('onboarding.remoteConnection.field.namePlaceholder')}
|
2026-04-15 01:32:59 +08:00
|
|
|
disabled={isTesting}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Success message */}
|
|
|
|
|
{probeResult && isSuccess && (
|
|
|
|
|
<div
|
|
|
|
|
className="rounded-lg border p-3 text-sm"
|
|
|
|
|
style={{
|
|
|
|
|
borderColor: 'var(--status-success)',
|
|
|
|
|
color: 'var(--status-success)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-04-26 14:03:39 +03:00
|
|
|
{t('onboarding.remoteConnection.status.connectedSuccessfully', { latencyMs: probeResult.latencyMs })}
|
2026-04-15 01:32:59 +08:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Auth warning (non-blocking) */}
|
|
|
|
|
{probeResult && isAuth && (
|
|
|
|
|
<div
|
|
|
|
|
className="rounded-lg border p-3 text-sm"
|
|
|
|
|
style={{
|
|
|
|
|
borderColor: 'var(--status-warning)',
|
|
|
|
|
color: 'var(--status-warning)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-04-26 14:03:39 +03:00
|
|
|
{t('onboarding.remoteConnection.status.authWarning')}
|
2026-04-15 01:32:59 +08:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-06-02 00:43:05 +03:00
|
|
|
{probeResult && isUpdateRecommended && (
|
|
|
|
|
<div
|
|
|
|
|
className="rounded-lg border p-3 text-sm"
|
|
|
|
|
style={{
|
|
|
|
|
borderColor: 'var(--status-warning)',
|
|
|
|
|
color: 'var(--status-warning)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{probeMessageKey ? t(probeMessageKey as Parameters<typeof t>[0]) : null}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-04-15 01:32:59 +08:00
|
|
|
{/* Blocking errors */}
|
|
|
|
|
{probeResult && isBlocking && (
|
|
|
|
|
<div
|
|
|
|
|
className="rounded-lg border p-3 text-sm space-y-3"
|
|
|
|
|
style={{
|
|
|
|
|
borderColor: 'var(--status-error)',
|
|
|
|
|
color: 'var(--status-error)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div>
|
2026-04-26 14:03:39 +03:00
|
|
|
<div className="font-semibold mb-1">{t('onboarding.remoteConnection.status.connectionFailed')}</div>
|
|
|
|
|
<div className="opacity-90">{probeMessageKey ? t(probeMessageKey as Parameters<typeof t>[0]) : null}</div>
|
2026-04-15 01:32:59 +08:00
|
|
|
</div>
|
|
|
|
|
<div className="text-xs opacity-80">
|
|
|
|
|
{probeResult.status === 'unreachable'
|
2026-04-26 14:03:39 +03:00
|
|
|
? t('onboarding.remoteConnection.status.suggestionsUnreachable')
|
|
|
|
|
: t('onboarding.remoteConnection.status.suggestionsWrongService')}
|
2026-04-15 01:32:59 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Generic error */}
|
|
|
|
|
{error && (
|
|
|
|
|
<div
|
|
|
|
|
className="rounded-lg border p-3 text-sm"
|
|
|
|
|
style={{
|
|
|
|
|
borderColor: 'var(--status-error)',
|
|
|
|
|
color: 'var(--status-error)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{error}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={handleTest}
|
|
|
|
|
disabled={!canTest}
|
|
|
|
|
>
|
2026-04-26 14:03:39 +03:00
|
|
|
{isTesting ? t('onboarding.remoteConnection.actions.testing') : t('onboarding.remoteConnection.actions.testConnection')}
|
2026-04-15 01:32:59 +08:00
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleConnect}
|
|
|
|
|
disabled={!canConnect}
|
|
|
|
|
>
|
2026-04-26 14:03:39 +03:00
|
|
|
{t('onboarding.remoteConnection.actions.connectAndRestart')}
|
2026-04-15 01:32:59 +08:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Suggested actions when connection is blocked */}
|
|
|
|
|
{isBlocking && (
|
|
|
|
|
<div className="flex flex-col gap-2 pt-2 border-t border-border">
|
2026-04-26 14:03:39 +03:00
|
|
|
<div className="text-xs text-muted-foreground text-center">{t('onboarding.remoteConnection.actions.whatToDo')}</div>
|
2026-04-15 01:32:59 +08:00
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={onBack}
|
|
|
|
|
className="flex-1"
|
|
|
|
|
>
|
2026-04-26 14:03:39 +03:00
|
|
|
{t('onboarding.remoteConnection.actions.chooseDifferentServer')}
|
2026-04-15 01:32:59 +08:00
|
|
|
</Button>
|
|
|
|
|
{!isRecoveryMode && onSwitchToLocal && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={onSwitchToLocal}
|
|
|
|
|
className="flex-1"
|
|
|
|
|
>
|
2026-04-26 14:03:39 +03:00
|
|
|
{t('onboarding.remoteConnection.actions.useLocalInstead')}
|
2026-04-15 01:32:59 +08:00
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|