Files
fusion/packages/dashboard/app/components/NativeShellConnectionManager.tsx
Fusion d8f75202e9 feat(FN-3402): address shell connection feedback in NativeShellConnectionMa
Addressed shell connection feedback for FN-3402 in Step 6, updating the NativeShellConnectionManager component and its test file with expanded test coverage and refinements based on review comments.

Fusion-Task-Id: FN-3402
2026-05-06 14:11:32 -07:00

133 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from "react";
import type { FusionShellApi, ShellConnectionProfile, ShellConnectionState } from "../types/native-shell";
import "./NativeShellConnectionManager.css";
interface NativeShellConnectionManagerProps {
open: boolean;
shellApi: FusionShellApi;
shellState: ShellConnectionState;
onClose: () => void;
}
export function NativeShellConnectionManager({ open, shellApi, shellState, onClose }: NativeShellConnectionManagerProps) {
const activeProfile = useMemo(
() => shellState.profiles.find((profile) => profile.id === shellState.activeProfileId) ?? null,
[shellState.activeProfileId, shellState.profiles],
);
const [editingProfileId, setEditingProfileId] = useState<string | null>(null);
const [draft, setDraft] = useState<Partial<ShellConnectionProfile>>({});
const [error, setError] = useState<string | null>(null);
if (!open) return null;
const isAddingConnection = editingProfileId === "__new__";
const editingProfile = isAddingConnection
? null
: shellState.profiles.find((profile) => profile.id === editingProfileId) ?? activeProfile;
const workingName = draft.name ?? editingProfile?.name ?? "";
const workingUrl = draft.serverUrl ?? editingProfile?.serverUrl ?? "";
const workingToken = draft.authToken ?? editingProfile?.authToken ?? "";
const saveCurrent = async () => {
setError(null);
try {
// Early validation for user feedback before bridge persistence rejects.
const parsed = new URL(workingUrl.trim());
if (!/^https?:$/.test(parsed.protocol)) {
throw new Error("Server URL must use http or https");
}
const saved = await shellApi.saveProfile({
id: isAddingConnection ? undefined : (editingProfileId ?? editingProfile?.id),
name: workingName || "Remote Server",
serverUrl: workingUrl,
authToken: workingToken || null,
});
await shellApi.setActiveProfile(saved.id);
setEditingProfileId(saved.id);
setDraft({});
} catch (nextError) {
setError((nextError as Error).message);
}
};
return (
<div className="modal-overlay open">
<div className="modal native-shell-connection-manager" role="dialog" aria-label="Connection Manager">
<div className="modal-header">
<h2>Connection Manager</h2>
<button type="button" className="modal-close" onClick={onClose} aria-label="Close">
×
</button>
</div>
{shellState.host === "desktop-shell" && (
<div className="native-shell-connection-manager__mode-row">
<button type="button" className={`btn ${shellState.desktopMode === "local" ? "btn-primary" : ""}`} onClick={() => void shellApi.setDesktopMode("local")}>Local</button>
<button type="button" className={`btn ${shellState.desktopMode !== "local" ? "btn-primary" : ""}`} onClick={() => void shellApi.setDesktopMode("remote")}>Remote</button>
</div>
)}
<div className="native-shell-connection-manager__profiles">
{shellState.profiles.map((profile) => (
<div className="card native-shell-connection-manager__profile" key={profile.id}>
<div>
<strong>{profile.name}</strong>
<div className="settings-muted">{profile.serverUrl}</div>
</div>
<div className="native-shell-connection-manager__profile-actions">
<button
type="button"
className="btn btn-sm"
aria-label={`Edit ${profile.name}`}
onClick={() => {
setEditingProfileId(profile.id);
setDraft(profile);
}}
>
Edit
</button>
<button type="button" className="btn btn-sm" aria-label={`Use ${profile.name}`} onClick={() => void shellApi.setActiveProfile(profile.id)}>Use</button>
<button type="button" className="btn btn-sm btn-danger" aria-label={`Delete ${profile.name}`} onClick={() => void shellApi.deleteProfile(profile.id)}>Delete</button>
</div>
</div>
))}
</div>
<div className="native-shell-connection-manager__mode-row">
<button
type="button"
className="btn"
onClick={() => {
setEditingProfileId("__new__");
setDraft({ name: "", serverUrl: "", authToken: "" });
setError(null);
}}
>
Add connection
</button>
{shellState.host === "mobile-shell" && (
<button type="button" className="btn" onClick={() => void shellApi.startQrScan()}>
Scan QR
</button>
)}
</div>
<div className="form-group native-shell-connection-manager__editor">
<label htmlFor="native-shell-connection-manager-name">Name</label>
<input id="native-shell-connection-manager-name" className="input" value={workingName} onChange={(event) => setDraft((value) => ({ ...value, name: event.target.value }))} />
<label htmlFor="native-shell-connection-manager-url">Server URL</label>
<input id="native-shell-connection-manager-url" className="input" value={workingUrl} onChange={(event) => setDraft((value) => ({ ...value, serverUrl: event.target.value }))} />
<label htmlFor="native-shell-connection-manager-token">Auth token (optional)</label>
<input id="native-shell-connection-manager-token" className="input" type="password" value={workingToken ?? ""} onChange={(event) => setDraft((value) => ({ ...value, authToken: event.target.value }))} />
{error && <p className="form-error" role="alert">{error}</p>}
</div>
<div className="modal-actions">
<button type="button" className="btn" onClick={onClose}>Close</button>
<button type="button" className="btn btn-primary" onClick={() => void saveCurrent()} disabled={!workingUrl.trim()}>Save</button>
</div>
</div>
</div>
);
}