2026-04-15 01:32:59 +08:00
|
|
|
import React from 'react';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
2026-05-13 13:26:15 +03:00
|
|
|
import { Icon } from "@/components/icon/Icon";
|
2026-04-26 14:03:39 +03:00
|
|
|
import { useI18n } from '@/lib/i18n';
|
2026-04-15 01:32:59 +08:00
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
import { redactSensitiveUrl } from '@/lib/desktopHosts';
|
|
|
|
|
import {
|
|
|
|
|
getDesktopRecoveryConfig,
|
|
|
|
|
type RecoveryVariant,
|
|
|
|
|
} from './desktopRecoveryConfig';
|
|
|
|
|
|
|
|
|
|
export type { RecoveryVariant } from './desktopRecoveryConfig';
|
|
|
|
|
|
|
|
|
|
export type DesktopConnectionRecoveryProps = {
|
|
|
|
|
variant: RecoveryVariant;
|
|
|
|
|
hostLabel?: string;
|
|
|
|
|
hostUrl?: string;
|
|
|
|
|
onRetry?: () => void;
|
|
|
|
|
onUseLocal?: () => void;
|
|
|
|
|
onUseRemote?: () => void;
|
|
|
|
|
isRetrying?: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Maps iconKey from config to actual icon component */
|
|
|
|
|
function getRecoveryIcon(iconKey: 'local' | 'remote'): React.ReactNode {
|
|
|
|
|
switch (iconKey) {
|
|
|
|
|
case 'local':
|
2026-05-13 13:26:15 +03:00
|
|
|
return <Icon name="macbook" className="h-8 w-8" />;
|
2026-04-15 01:32:59 +08:00
|
|
|
case 'remote':
|
2026-05-13 13:26:15 +03:00
|
|
|
return <Icon name="server" className="h-8 w-8" />;
|
2026-04-15 01:32:59 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function DesktopConnectionRecovery({
|
|
|
|
|
variant,
|
|
|
|
|
hostLabel,
|
|
|
|
|
hostUrl,
|
|
|
|
|
onRetry,
|
|
|
|
|
onUseLocal,
|
|
|
|
|
onUseRemote,
|
|
|
|
|
isRetrying = false,
|
|
|
|
|
}: DesktopConnectionRecoveryProps) {
|
2026-04-26 14:03:39 +03:00
|
|
|
const { t } = useI18n();
|
2026-04-15 01:32:59 +08:00
|
|
|
const config = getDesktopRecoveryConfig(variant, hostLabel, hostUrl);
|
2026-04-26 14:03:39 +03:00
|
|
|
const retryLabelKey = (config.retryLabelKey ?? 'onboarding.desktopRecovery.actions.retryConnection') as Parameters<typeof t>[0];
|
|
|
|
|
const descriptionParams = React.useMemo(() => {
|
|
|
|
|
if (config.descriptionParams?.host) {
|
|
|
|
|
return config.descriptionParams;
|
|
|
|
|
}
|
|
|
|
|
if (variant === 'remote-unreachable') {
|
|
|
|
|
return { host: t('onboarding.desktopRecovery.placeholders.remoteServer') };
|
|
|
|
|
}
|
2026-06-02 00:43:05 +03:00
|
|
|
if (variant === 'remote-wrong-service' || variant === 'remote-incompatible') {
|
2026-04-26 14:03:39 +03:00
|
|
|
return { host: t('onboarding.desktopRecovery.placeholders.unknownServer') };
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}, [config.descriptionParams, t, variant]);
|
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">
|
|
|
|
|
{/* Icon and title */}
|
|
|
|
|
<div className="flex flex-col items-center space-y-3 text-center">
|
|
|
|
|
<div
|
|
|
|
|
className="p-3 rounded-full"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: 'var(--status-warning)',
|
|
|
|
|
opacity: 0.15,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div style={{ color: 'var(--status-warning)' }}>
|
|
|
|
|
{getRecoveryIcon(config.iconKey)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<h1 className="typography-ui-header text-xl font-semibold text-foreground">
|
2026-04-26 14:03:39 +03:00
|
|
|
{t(config.titleKey as Parameters<typeof t>[0])}
|
2026-04-15 01:32:59 +08:00
|
|
|
</h1>
|
|
|
|
|
<p className="text-muted-foreground text-sm max-w-sm">
|
2026-04-26 14:03:39 +03:00
|
|
|
{t(
|
|
|
|
|
config.descriptionKey as Parameters<typeof t>[0],
|
|
|
|
|
descriptionParams
|
|
|
|
|
)}
|
2026-04-15 01:32:59 +08:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Host info if available */}
|
2026-06-02 00:43:05 +03:00
|
|
|
{hostUrl && (variant === 'remote-unreachable' || variant === 'remote-wrong-service' || variant === 'remote-incompatible') && (
|
2026-04-15 01:32:59 +08:00
|
|
|
<div className="rounded-lg border border-border bg-background/50 p-3">
|
2026-04-26 14:03:39 +03:00
|
|
|
<div className="text-xs text-muted-foreground mb-1">{t('onboarding.remoteConnection.field.serverAddress')}</div>
|
2026-04-15 01:32:59 +08:00
|
|
|
<div className="font-mono text-sm text-foreground truncate">{redactSensitiveUrl(hostUrl)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Action buttons */}
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
{config.showRetry && onRetry && (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={onRetry}
|
|
|
|
|
disabled={isRetrying}
|
|
|
|
|
className="w-full"
|
|
|
|
|
>
|
2026-05-13 13:26:15 +03:00
|
|
|
<Icon name="refresh" className={cn('h-4 w-4', isRetrying && 'animate-spin')} />
|
2026-04-26 14:03:39 +03:00
|
|
|
{isRetrying
|
|
|
|
|
? t('onboarding.desktopRecovery.actions.retrying')
|
|
|
|
|
: t(retryLabelKey)}
|
2026-04-15 01:32:59 +08:00
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
{config.showUseLocal && onUseLocal && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={onUseLocal}
|
|
|
|
|
disabled={isRetrying}
|
|
|
|
|
className="flex-1"
|
|
|
|
|
>
|
2026-05-13 13:26:15 +03:00
|
|
|
<Icon name="macbook" className="h-4 w-4" />
|
2026-04-26 14:03:39 +03:00
|
|
|
{t(config.useLocalLabelKey as Parameters<typeof t>[0])}
|
2026-04-15 01:32:59 +08:00
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{config.showUseRemote && onUseRemote && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={onUseRemote}
|
|
|
|
|
disabled={isRetrying}
|
|
|
|
|
className="flex-1"
|
|
|
|
|
>
|
2026-05-13 13:26:15 +03:00
|
|
|
<Icon name="server" className="h-4 w-4" />
|
2026-04-26 14:03:39 +03:00
|
|
|
{t(config.useRemoteLabelKey as Parameters<typeof t>[0])}
|
2026-04-15 01:32:59 +08:00
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|