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:
@@ -393,20 +393,31 @@ async fn select_access_profile(app: AppHandle, profile_id: String) -> Result<Enr
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn connect_tunnel(app: AppHandle) -> Result<EnrollmentResult, String> {
|
async fn connect_tunnel(app: AppHandle) -> Result<EnrollmentResult, String> {
|
||||||
let session_state = sync_current_session(&app).await?;
|
let session_state = sync_current_session(&app).await?;
|
||||||
let result = tunnel_manager::connect(&app, std::path::Path::new(&session_state.profile_path));
|
let app_handle = app.clone();
|
||||||
|
let profile_path = session_state.profile_path.clone();
|
||||||
|
let result = tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
tunnel_manager::connect(&app_handle, std::path::Path::new(&profile_path))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("Unable to join tunnel connect task: {err}"))?;
|
||||||
refresh_tray_menu(&app);
|
refresh_tray_menu(&app);
|
||||||
result?;
|
result?;
|
||||||
Ok(session_state.enrollment)
|
Ok(session_state.enrollment)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn disconnect_tunnel(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
|
async fn disconnect_tunnel(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
|
||||||
let profile_path = {
|
let profile_path = {
|
||||||
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
|
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
|
||||||
let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
|
let session = session.as_ref().ok_or_else(|| "No active session is available".to_string())?;
|
||||||
session.profile_path.clone()
|
session.profile_path.clone()
|
||||||
};
|
};
|
||||||
let result = tunnel_manager::disconnect(&app, std::path::Path::new(&profile_path));
|
let app_handle = app.clone();
|
||||||
|
let result = tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
tunnel_manager::disconnect(&app_handle, std::path::Path::new(&profile_path))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("Unable to join tunnel disconnect task: {err}"))?;
|
||||||
refresh_tray_menu(&app);
|
refresh_tray_menu(&app);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export function App() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [syncing, setSyncing] = useState(false);
|
const [syncing, setSyncing] = useState(false);
|
||||||
const [selectingProfile, setSelectingProfile] = useState(false);
|
const [selectingProfile, setSelectingProfile] = useState(false);
|
||||||
|
const [tunnelActionPending, setTunnelActionPending] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
const [state, setState] = useState<EnrollmentState | null>(null);
|
const [state, setState] = useState<EnrollmentState | null>(null);
|
||||||
@@ -187,6 +188,7 @@ export function App() {
|
|||||||
|
|
||||||
async function toggleConnection() {
|
async function toggleConnection() {
|
||||||
const command = connected ? "disconnect_tunnel" : "connect_tunnel";
|
const command = connected ? "disconnect_tunnel" : "connect_tunnel";
|
||||||
|
setTunnelActionPending(true);
|
||||||
try {
|
try {
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
const syncedState = await invoke<EnrollmentState>("connect_tunnel");
|
const syncedState = await invoke<EnrollmentState>("connect_tunnel");
|
||||||
@@ -213,6 +215,8 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setError(formatInvokeError(err, "Tunnel action failed"));
|
setError(formatInvokeError(err, "Tunnel action failed"));
|
||||||
|
} finally {
|
||||||
|
setTunnelActionPending(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +235,7 @@ export function App() {
|
|||||||
<div className="client-shell">
|
<div className="client-shell">
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<AppHeader
|
<AppHeader
|
||||||
|
busy={tunnelActionPending}
|
||||||
connected={connected}
|
connected={connected}
|
||||||
enrolled={Boolean(state)}
|
enrolled={Boolean(state)}
|
||||||
onLogout={resetEnrollment}
|
onLogout={resetEnrollment}
|
||||||
@@ -290,7 +295,7 @@ export function App() {
|
|||||||
profiles={state?.availableProfiles ?? []}
|
profiles={state?.availableProfiles ?? []}
|
||||||
resources={state?.resources ?? []}
|
resources={state?.resources ?? []}
|
||||||
selectedProfileId={state?.selectedProfileId ?? null}
|
selectedProfileId={state?.selectedProfileId ?? null}
|
||||||
selectingProfile={selectingProfile}
|
selectingProfile={selectingProfile || tunnelActionPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ActionButton } from "./ActionButton";
|
import { ActionButton } from "./ActionButton";
|
||||||
|
|
||||||
type AppHeaderProps = {
|
type AppHeaderProps = {
|
||||||
|
busy: boolean;
|
||||||
enrolled: boolean;
|
enrolled: boolean;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
syncing: boolean;
|
syncing: boolean;
|
||||||
@@ -10,6 +11,7 @@ type AppHeaderProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function AppHeader({
|
export function AppHeader({
|
||||||
|
busy,
|
||||||
enrolled,
|
enrolled,
|
||||||
connected,
|
connected,
|
||||||
syncing,
|
syncing,
|
||||||
@@ -36,10 +38,10 @@ export function AppHeader({
|
|||||||
<div className="header-actions-secondary">
|
<div className="header-actions-secondary">
|
||||||
{enrolled ? (
|
{enrolled ? (
|
||||||
<>
|
<>
|
||||||
<ActionButton disabled={syncing} onClick={onSync} variant="secondary">
|
<ActionButton disabled={busy || syncing} onClick={onSync} variant="secondary">
|
||||||
{syncing ? "Syncing..." : "Sync"}
|
{syncing ? "Syncing..." : "Sync"}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton onClick={onLogout} variant="ghost">
|
<ActionButton disabled={busy || syncing} onClick={onLogout} variant="ghost">
|
||||||
Logout
|
Logout
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</>
|
</>
|
||||||
@@ -47,11 +49,11 @@ export function AppHeader({
|
|||||||
</div>
|
</div>
|
||||||
<div className="header-actions-primary">
|
<div className="header-actions-primary">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
disabled={!enrolled}
|
disabled={!enrolled || busy || syncing}
|
||||||
onClick={onToggleConnection}
|
onClick={onToggleConnection}
|
||||||
variant={connected ? "danger" : "primary"}
|
variant={connected ? "danger" : "primary"}
|
||||||
>
|
>
|
||||||
{!enrolled ? "Provision first" : connected ? "Disconnect" : "Connect"}
|
{!enrolled ? "Provision first" : busy ? connected ? "Disconnecting..." : "Connecting..." : connected ? "Disconnect" : "Connect"}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user