-
Notifications
You must be signed in to change notification settings - Fork 197
Phase 1: ConnectionModeSelect replaces Lazy toggle (issue netbirdio/netbird#5989) #627
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -34,6 +34,14 @@ import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon"; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import useGroupHelper from "@/modules/groups/useGroupHelper"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useGroups } from "@/contexts/GroupsProvider"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { SkeletonSettings } from "@components/skeletons/SkeletonSettings"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ConnectionModeValue, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_RELAY_TIMEOUT_SECONDS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MODE_META, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VISIBLE_MODE_OPTIONS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modeImpliesLegacyLazy, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolveLegacyLazyBool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@/modules/settings/connectionmode/modeOptions"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Props = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| account: Account; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -70,8 +78,16 @@ function ClientSettingsTabContent({ account }: Readonly<Props>) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { mutate } = useSWRConfig(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const saveRequest = useApiCall<Account>("/accounts/" + account.id, true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [lazyConnection, setLazyConnection] = useState( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| account.settings?.lazy_connection_enabled ?? false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Phase 1 of issue #5989: replaced the binary lazy-toggle with a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 2-value dropdown (p2p / p2p-lazy). The dashboard preserves the legacy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // lazy_connection_enabled boolean alongside the new connection_mode for | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // backwards-compat with older daemon versions. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [connectionMode, setConnectionMode] = useState<ConnectionModeValue>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (account.settings?.connection_mode as ConnectionModeValue | null | undefined) ?? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolveLegacyLazyBool(account.settings?.lazy_connection_enabled), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [relayTimeoutSeconds, setRelayTimeoutSeconds] = useState<number | null>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| account.settings?.relay_timeout_seconds ?? null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const autoUpdateSetting = account.settings?.auto_update_version; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -181,28 +197,55 @@ function ClientSettingsTabContent({ account }: Readonly<Props>) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const toggleLazyConnection = async (toggle: boolean) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Phase 1 (#5989): persist mode + timeout, AND mirror onto the legacy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // lazy_connection_enabled boolean so older daemon versions stay in sync. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const saveConnectionMode = async ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nextMode: ConnectionModeValue, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nextRelayTimeout: number | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setConnectionMode(nextMode); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setRelayTimeoutSeconds(nextRelayTimeout); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| notify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Lazy Connections", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: `Lazy Connections successfully ${ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toggle ? "enabled" : "disabled" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }.`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Connection Mode", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "Connection mode updated.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| promise: saveRequest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .put({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: account.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| settings: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...account.settings, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lazy_connection_enabled: toggle, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| connection_mode: nextMode, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| relay_timeout_seconds: nextRelayTimeout, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lazy_connection_enabled: modeImpliesLegacyLazy(nextMode), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLazyConnection(toggle); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mutate("/accounts"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadingMessage: "Updating Lazy Connections setting...", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadingMessage: "Updating connection mode...", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleModeChange = (next: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Mode-change preserves the persisted relay timeout (per spec | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // section 5.3): users only lose their entered value if they clear | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the input explicitly, not via mode-switch. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| saveConnectionMode(next as ConnectionModeValue, relayTimeoutSeconds); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleRelayTimeoutChange = (raw: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const trimmed = raw.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (trimmed === "") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| saveConnectionMode(connectionMode, null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = Number(trimmed); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!Number.isInteger(parsed) || parsed < 0) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| saveConnectionMode(connectionMode, parsed); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const currentMeta = MODE_META[connectionMode]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Tabs.Content value={"clients"}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className={"p-default py-6 max-w-2xl"}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -368,13 +411,17 @@ function ClientSettingsTabContent({ account }: Readonly<Props>) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <FlaskConicalIcon size={15} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Experimental | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Experimental: Connection Mode | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <HelpText> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Lazy connections are an experimental feature. Functionality and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| behavior may evolve. Instead of maintaining always-on connections, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NetBird activates them on-demand based on activity or signaling.{" "} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Choose how NetBird clients establish peer-to-peer connections.{" "} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className={"text-white font-medium"}>P2P</span> keeps | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| connections always on (best latency, more bandwidth).{" "} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className={"text-white font-medium"}>P2P Lazy</span>{" "} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opens connections on demand and tears them down after the relay | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout (much lower bandwidth on metered links like LTE). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Changes take effect after the client restarts.{" "} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <InlineLink | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| href={"https://docs.netbird.io/how-to/lazy-connection"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target={"_blank"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -383,25 +430,33 @@ function ClientSettingsTabContent({ account }: Readonly<Props>) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ExternalLinkIcon size={12} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </InlineLink> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </HelpText> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <FancyToggleSwitch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={"mt-2"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={lazyConnection} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={toggleLazyConnection} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label={ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ClockFadingIcon size={15} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Enable Lazy Connections | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| helpText={ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Allow to establish connections between peers only when | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required. This requires NetBird client v0.45 or higher. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Changes will only take effect after restarting the clients. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={!permission.settings.update} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className={"gap-4 items-center grid grid-cols-2 mt-2"}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SelectDropdown | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={connectionMode} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={handleModeChange} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options={VISIBLE_MODE_OPTIONS} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {currentMeta.showsRelayTimeout && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| relayTimeoutSeconds === null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : String(relayTimeoutSeconds) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| customPrefix={<ClockFadingIcon size={14} />} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder={String(DEFAULT_RELAY_TIMEOUT_SECONDS)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => handleRelayTimeoutChange(e.target.value)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={!permission.settings.update} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+439
to
+451
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add accessible label for the relay timeout input. The timeout input uses only an icon as ♿ Proposed fix to add aria-label <Input
value={
relayTimeoutSeconds === null
? ""
: String(relayTimeoutSeconds)
}
customPrefix={<ClockFadingIcon size={14} />}
placeholder={String(DEFAULT_RELAY_TIMEOUT_SECONDS)}
onChange={(e) => handleRelayTimeoutChange(e.target.value)}
disabled={!permission.settings.update}
+ aria-label="Relay timeout in seconds"
/>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {currentMeta.showsRelayTimeout && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <HelpText className={"mt-2"}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Relay timeout in seconds. Empty = use built-in default | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ({DEFAULT_RELAY_TIMEOUT_SECONDS}s = 5 min). Set to 0 to keep | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| the relay alive indefinitely. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </HelpText> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // Phase 1 (issue #5989) connection-mode options for the dashboard. | ||
| // Two modes are visible in Phase 1; relay-forced and p2p-dynamic remain | ||
| // admin-only (settable via API/CLI/env) and are kept out of the dropdown | ||
| // to avoid surprising the typical admin. | ||
|
|
||
| import { SelectOption } from "@components/select/SelectDropdown"; | ||
|
|
||
| export type ConnectionModeValue = | ||
| | "p2p" | ||
| | "p2p-lazy" | ||
| | "p2p-dynamic" | ||
| | "relay-forced"; | ||
|
|
||
| export interface ModeMeta { | ||
| value: ConnectionModeValue; | ||
| label: string; | ||
| visible: boolean; | ||
| showsRelayTimeout: boolean; | ||
| showsP2pTimeout: boolean; | ||
| } | ||
|
|
||
| export const MODE_META: Record<ConnectionModeValue, ModeMeta> = { | ||
| "p2p": { | ||
| value: "p2p", | ||
| label: "P2P (recommended)", | ||
| visible: true, | ||
| showsRelayTimeout: false, | ||
| showsP2pTimeout: false, | ||
| }, | ||
| "p2p-lazy": { | ||
| value: "p2p-lazy", | ||
| label: "P2P Lazy", | ||
| visible: true, | ||
| showsRelayTimeout: true, | ||
| showsP2pTimeout: false, | ||
| }, | ||
| "p2p-dynamic": { | ||
| value: "p2p-dynamic", | ||
| label: "P2P Dynamic", | ||
| visible: false, // Phase-1 hides; backend still accepts via API | ||
| showsRelayTimeout: true, | ||
| showsP2pTimeout: true, | ||
| }, | ||
| "relay-forced": { | ||
| value: "relay-forced", | ||
| label: "Relay Forced", | ||
| visible: false, // Phase-1 admin-only | ||
| showsRelayTimeout: false, | ||
| showsP2pTimeout: false, | ||
| }, | ||
| }; | ||
|
|
||
| export const VISIBLE_MODE_OPTIONS: SelectOption[] = Object.values(MODE_META) | ||
| .filter((m) => m.visible) | ||
| .map((m) => ({ label: m.label, value: m.value })); | ||
|
|
||
| // Defaults shown as placeholders when DB value is NULL. | ||
| export const DEFAULT_RELAY_TIMEOUT_SECONDS = 5 * 60; // 5 min | ||
| export const DEFAULT_P2P_TIMEOUT_SECONDS = 180 * 60; // 180 min | ||
|
|
||
| // resolveLegacyLazyBool mirrors the server-side fallback: if the new | ||
| // connection_mode field is null/undefined, derive the effective mode from | ||
| // the legacy lazy_connection_enabled boolean. Used to seed the dropdown | ||
| // state when a user opens an account that has never set the new field. | ||
| export function resolveLegacyLazyBool( | ||
| lazyEnabled: boolean | undefined, | ||
| ): ConnectionModeValue { | ||
| return lazyEnabled ? "p2p-lazy" : "p2p"; | ||
| } | ||
|
|
||
| // modeImpliesLegacyLazy is the inverse: when the user picks a mode in the | ||
| // dashboard, we ALSO write the legacy lazy_connection_enabled boolean to | ||
| // keep older daemon versions (which only understand the boolean) in sync. | ||
| // relay-forced and p2p-dynamic both map to false because the legacy boolean | ||
| // cannot express their semantics. | ||
| export function modeImpliesLegacyLazy(mode: ConnectionModeValue): boolean { | ||
| return mode === "p2p-lazy"; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optimistic state update without rollback on failure.
Lines 206-207 update local state before the API call completes. If
saveRequest.put()fails, the UI will show the new values while the server retains the old ones.Consider capturing the previous state and reverting on error:
🛡️ Proposed fix to add rollback on failure
const saveConnectionMode = async ( nextMode: ConnectionModeValue, nextRelayTimeout: number | null, ) => { + const prevMode = connectionMode; + const prevTimeout = relayTimeoutSeconds; setConnectionMode(nextMode); setRelayTimeoutSeconds(nextRelayTimeout); notify({ title: "Connection Mode", description: "Connection mode updated.", promise: saveRequest .put({ id: account.id, settings: { ...account.settings, connection_mode: nextMode, relay_timeout_seconds: nextRelayTimeout, lazy_connection_enabled: modeImpliesLegacyLazy(nextMode), }, }) .then(() => { mutate("/accounts"); + }) + .catch((err) => { + setConnectionMode(prevMode); + setRelayTimeoutSeconds(prevTimeout); + throw err; }), loadingMessage: "Updating connection mode...", }); };🤖 Prompt for AI Agents