返回 DeepSeek-Reasonix
RemoteConnectionErrorDialog.tsx
根目录 / desktop / frontend / src / components / RemoteConnectionErrorDialog.tsx
1 import { useEffect, useRef } from "react";
2 import { createPortal } from "react-dom";
3
4 import { useT } from "../lib/i18n";
5 import { isRemoteHostKeyMismatch, remoteConnectionErrorSummaryKey } from "../lib/remoteErrors";
6 import type { RemoteConnectionStatus, RemoteHostView } from "../lib/types";
7
8 export function RemoteConnectionErrorDialog({
9 host,
10 status,
11 onClose,
12 onManage,
13 onRetry,
14 }: {
15 host: RemoteHostView;
16 status: RemoteConnectionStatus;
17 onClose: () => void;
18 onManage?: () => void;
19 onRetry?: () => void;
20 }) {
21 const t = useT();
22 const closeRef = useRef<HTMLButtonElement>(null);
23 const mismatch = isRemoteHostKeyMismatch(status);
24 const details = status.errorDetails;
25 const target = `${host.user ? `${host.user}@` : ""}${host.host}${host.port && host.port !== 22 ? `:${host.port}` : ""}`;
26
27 useEffect(() => {
28 closeRef.current?.focus();
29 }, []);
30
31 useEffect(() => {
32 const onKeyDown = (event: KeyboardEvent) => {
33 if (event.key !== "Escape") return;
34 event.preventDefault();
35 onClose();
36 };
37 window.addEventListener("keydown", onKeyDown);
38 return () => window.removeEventListener("keydown", onKeyDown);
39 }, [onClose]);
40
41 return createPortal(
42 <div className="remote-hostkey-overlay" role="dialog" aria-modal="true" aria-labelledby="remote-connection-error-title">
43 <div className="remote-hostkey-dialog remote-connection-error-dialog">
44 <h2 id="remote-connection-error-title" className="remote-hostkey-dialog__title">
45 {t(mismatch ? "remote.error.hostKeyMismatch.title" : "remote.error.dialog.title")}
46 </h2>
47 <p className="remote-connection-error-dialog__summary">
48 {t(remoteConnectionErrorSummaryKey(status), { host: host.label })}
49 </p>
50 <dl className="remote-hostkey-dialog__facts">
51 <dt>{t("remote.error.host")}</dt>
52 <dd>{target}</dd>
53 {details?.presentedSha256 && (
54 <>
55 <dt>{t("remote.error.presentedFingerprint")}</dt>
56 <dd className="remote-hostkey-dialog__fp">{details.presentedSha256}</dd>
57 </>
58 )}
59 </dl>
60 {details?.knownHostRecords && details.knownHostRecords.length > 0 && (
61 <div className="remote-connection-error-dialog__records">
62 <strong>{t("remote.error.knownHostRecords")}</strong>
63 <ul>
64 {details.knownHostRecords.map((record) => (
65 <li key={`${record.path}:${record.line}`}>
66 <code>{record.path}:{record.line}</code>
67 </li>
68 ))}
69 </ul>
70 </div>
71 )}
72 {status.error && (
73 <details className="remote-connection-error-dialog__technical">
74 <summary>{t("remote.error.technicalDetails")}</summary>
75 <code>{status.error}</code>
76 </details>
77 )}
78 <div className="remote-hostkey-dialog__actions">
79 <button ref={closeRef} className="btn" onClick={onClose}>
80 {t("remote.error.close")}
81 </button>
82 {onManage && (
83 <button className="btn" onClick={() => { onClose(); onManage(); }}>
84 {t("remote.error.manage")}
85 </button>
86 )}
87 {onRetry && (
88 <button className="btn btn--primary" onClick={() => { onClose(); onRetry(); }}>
89 {t(mismatch ? "remote.error.retryAfterFix" : "remote.error.retry")}
90 </button>
91 )}
92 </div>
93 </div>
94 </div>,
95 document.body,
96 );
97 }
98
98 lines Plain Text