refactor: wrap tunnel connect/disconnect operations in spawn_blocking and add pending state UI feedback
Move tunnel_manager::connect and disconnect calls into spawn_blocking tasks to prevent blocking async runtime. Clone app handle and profile path before spawning. Add map_err for task join failures. Add tunnelActionPending state to track in-progress tunnel operations. Pass busy prop to AppHeader and disable sync/logout/connect buttons during tunnel actions. Update connect button text to show "
This commit is contained in:
@@ -69,6 +69,7 @@ export function App() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [selectingProfile, setSelectingProfile] = useState(false);
|
||||
const [tunnelActionPending, setTunnelActionPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [state, setState] = useState<EnrollmentState | null>(null);
|
||||
@@ -187,6 +188,7 @@ export function App() {
|
||||
|
||||
async function toggleConnection() {
|
||||
const command = connected ? "disconnect_tunnel" : "connect_tunnel";
|
||||
setTunnelActionPending(true);
|
||||
try {
|
||||
if (!connected) {
|
||||
const syncedState = await invoke<EnrollmentState>("connect_tunnel");
|
||||
@@ -213,6 +215,8 @@ export function App() {
|
||||
}
|
||||
}
|
||||
setError(formatInvokeError(err, "Tunnel action failed"));
|
||||
} finally {
|
||||
setTunnelActionPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +235,7 @@ export function App() {
|
||||
<div className="client-shell">
|
||||
<div className="app-shell">
|
||||
<AppHeader
|
||||
busy={tunnelActionPending}
|
||||
connected={connected}
|
||||
enrolled={Boolean(state)}
|
||||
onLogout={resetEnrollment}
|
||||
@@ -290,7 +295,7 @@ export function App() {
|
||||
profiles={state?.availableProfiles ?? []}
|
||||
resources={state?.resources ?? []}
|
||||
selectedProfileId={state?.selectedProfileId ?? null}
|
||||
selectingProfile={selectingProfile}
|
||||
selectingProfile={selectingProfile || tunnelActionPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ActionButton } from "./ActionButton";
|
||||
|
||||
type AppHeaderProps = {
|
||||
busy: boolean;
|
||||
enrolled: boolean;
|
||||
connected: boolean;
|
||||
syncing: boolean;
|
||||
@@ -10,6 +11,7 @@ type AppHeaderProps = {
|
||||
};
|
||||
|
||||
export function AppHeader({
|
||||
busy,
|
||||
enrolled,
|
||||
connected,
|
||||
syncing,
|
||||
@@ -36,10 +38,10 @@ export function AppHeader({
|
||||
<div className="header-actions-secondary">
|
||||
{enrolled ? (
|
||||
<>
|
||||
<ActionButton disabled={syncing} onClick={onSync} variant="secondary">
|
||||
<ActionButton disabled={busy || syncing} onClick={onSync} variant="secondary">
|
||||
{syncing ? "Syncing..." : "Sync"}
|
||||
</ActionButton>
|
||||
<ActionButton onClick={onLogout} variant="ghost">
|
||||
<ActionButton disabled={busy || syncing} onClick={onLogout} variant="ghost">
|
||||
Logout
|
||||
</ActionButton>
|
||||
</>
|
||||
@@ -47,11 +49,11 @@ export function AppHeader({
|
||||
</div>
|
||||
<div className="header-actions-primary">
|
||||
<ActionButton
|
||||
disabled={!enrolled}
|
||||
disabled={!enrolled || busy || syncing}
|
||||
onClick={onToggleConnection}
|
||||
variant={connected ? "danger" : "primary"}
|
||||
>
|
||||
{!enrolled ? "Provision first" : connected ? "Disconnect" : "Connect"}
|
||||
{!enrolled ? "Provision first" : busy ? connected ? "Disconnecting..." : "Connecting..." : connected ? "Disconnect" : "Connect"}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user