Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/components/src/source_switcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const SWITCHER_CSS: &str = r#"
.ss-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
.ss-on{background:#3fb950}
.ss-off{background:#e5534b}
.ss-expired{background:#f5a623}
.ss-load{background:#d8a23a;animation:ss-pulse 1.1s ease-in-out infinite}
@keyframes ss-pulse{0%,100%{opacity:.4}50%{opacity:1}}
.ss-stk{flex:1;text-align:left;min-width:0}
Expand Down Expand Up @@ -153,7 +154,8 @@ pub fn SourceSwitcher(
class: match conn() {
ConnStatus::Online => "ss-dot ss-on",
ConnStatus::Connecting => "ss-dot ss-load",
_ => "ss-dot ss-off",
ConnStatus::Expired => "ss-dot ss-expired",
ConnStatus::Unreachable => "ss-dot ss-off",
},
}
i { class: "fa-solid fa-chevron-down ss-chev" }
Expand Down
40 changes: 33 additions & 7 deletions crates/hooks/src/source_switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ pub enum ConnStatus {
Connecting,
/// Verified and reachable.
Online,
/// Unreachable, or auth expired/invalid.
Offline,
/// Credentials are missing or rejected; the user needs to sign in again.
Expired,
/// The remote source could not be reached (network/server).
Unreachable,
}

/// Connection status of the active source: Local is always Online (no auth); a
/// server runs `validate()` on each switch — `Connecting` until it resolves to
/// `Online` (valid) or `Offline` (expired/unreachable).
/// `Online` (valid), `Expired` (auth missing/rejected), or `Unreachable`
/// (network/server).
pub fn use_connection_status() -> Memo<ConnStatus> {
let active_source = use_context::<Signal<ActiveSource>>();
let config = use_context::<Signal<AppConfig>>();
Expand All @@ -39,15 +42,23 @@ pub fn use_connection_status() -> Memo<ConnStatus> {
status.set(ConnStatus::Connecting);
spawn(async move {
let outcome = utils::offload(async move { src.validate().await }).await;
status.set(match outcome {
AuthOutcome::Valid => ConnStatus::Online,
AuthOutcome::Expired | AuthOutcome::Unreachable => ConnStatus::Offline,
});
status.set(status_for(outcome));
});
});
use_memo(move || *status.read())
}

/// Map a credential-validation outcome to the switcher's connection status.
/// `Expired` (missing/rejected creds) and `Unreachable` (network/server) are
/// kept distinct so the UI can prompt sign-in only when re-auth would help.
fn status_for(outcome: AuthOutcome) -> ConnStatus {
match outcome {
AuthOutcome::Valid => ConnStatus::Online,
AuthOutcome::Expired => ConnStatus::Expired,
AuthOutcome::Unreachable => ConnStatus::Unreachable,
}
}

/// Apply a source switch. For a server it loads the stored creds from the DB (so
/// the connection is the new server's, not a leftover one) and writes
/// `active_source` and `server` together; for Local it clears the server snapshot.
Expand Down Expand Up @@ -113,3 +124,18 @@ pub fn use_switch_source() -> impl Fn(Source) + Clone {
});
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn maps_auth_outcomes_to_connection_statuses() {
assert_eq!(status_for(AuthOutcome::Valid), ConnStatus::Online);
assert_eq!(status_for(AuthOutcome::Expired), ConnStatus::Expired);
assert_eq!(
status_for(AuthOutcome::Unreachable),
ConnStatus::Unreachable
);
}
}
Loading