feat(desktop): harden connection session lifecycle
- add a visible connection session summary for ready/testing/demo/retry states - prevent stale live connection tests from overwriting a newer active DB selection - include the missing frontend helpers already referenced by App.tsx so the branch builds cleanly Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
startTransition,
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ChangeEvent,
|
||||
type FormEvent,
|
||||
@@ -82,6 +83,14 @@ import {
|
||||
updateDemoStringValue,
|
||||
} from "./lib/demo-workspace";
|
||||
import { buildDatabaseSwitcherLabels } from "./lib/database-switcher";
|
||||
import {
|
||||
applyConnectionTestFailure,
|
||||
applyConnectionTestSuccess,
|
||||
buildConnectionSessionSummary,
|
||||
formatConnectionCardDetailLabel,
|
||||
formatConnectionStatusLabel,
|
||||
markConnectionTesting,
|
||||
} from "./lib/connection-session";
|
||||
import {
|
||||
createDemoKeyRecord,
|
||||
findDuplicateKeyName,
|
||||
@@ -493,7 +502,9 @@ function App() {
|
||||
const [addKeyDialogOpen, setAddKeyDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [pendingMutation, setPendingMutation] = useState<PendingMutationGuard | null>(null);
|
||||
const [isConnectionTestRunning, setIsConnectionTestRunning] = useState(false);
|
||||
const [connectionTestState, setConnectionTestState] = useState<{ connectionId: string } | null>(
|
||||
null,
|
||||
);
|
||||
const [isCommandRunning, setIsCommandRunning] = useState(false);
|
||||
const [isBrowseLoading, setIsBrowseLoading] = useState(false);
|
||||
const [isInspectorLoading, setIsInspectorLoading] = useState(false);
|
||||
@@ -528,6 +539,9 @@ function App() {
|
||||
const activeConnection =
|
||||
connections.find((connection) => connection.id === activeConnectionId) ??
|
||||
connections[0];
|
||||
const activeConnectionIdRef = useRef(activeConnectionId);
|
||||
const activeConnectionNameRef = useRef(activeConnection.name);
|
||||
const activeDbRef = useRef(activeDb);
|
||||
const databaseOptions = buildDatabaseSwitcherLabels(parseDatabaseLabel(activeDb));
|
||||
const searchState = resolveKeyBrowserSearchState(search);
|
||||
const deferredSearchState = resolveKeyBrowserSearchState(deferredSearch);
|
||||
@@ -591,6 +605,20 @@ function App() {
|
||||
ttlWriteSupported,
|
||||
});
|
||||
const localSmokeFixtureActive = isLocalSmokeFixtureConnection(activeConnection);
|
||||
const isConnectionTestRunning = connectionTestState !== null;
|
||||
const activeConnectionTestRunning = connectionTestState?.connectionId === activeConnection.id;
|
||||
const otherTestingConnectionName =
|
||||
connectionTestState !== null && connectionTestState.connectionId !== activeConnection.id
|
||||
? connections.find((connection) => connection.id === connectionTestState.connectionId)?.name ??
|
||||
"another connection"
|
||||
: null;
|
||||
const connectionSessionSummary = buildConnectionSessionSummary({
|
||||
connection: activeConnection,
|
||||
databaseLabel: activeDb,
|
||||
tauriAvailable,
|
||||
activeTestRunning: activeConnectionTestRunning,
|
||||
otherTestingConnectionName,
|
||||
});
|
||||
const localSmokeFilterActive = search.trim() === localSmokeFixtureSearchText;
|
||||
const ttlDraftError = ttlDraft.trim() ? validateTtlDraft(ttlDraft) : null;
|
||||
const nextTtlMillis =
|
||||
@@ -660,6 +688,12 @@ function App() {
|
||||
bannerMessage: banner.message,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
activeConnectionIdRef.current = activeConnectionId;
|
||||
activeConnectionNameRef.current = activeConnection.name;
|
||||
activeDbRef.current = activeDb;
|
||||
}, [activeConnectionId, activeConnection.name, activeDb]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tauriAvailable) {
|
||||
startTransition(() => {
|
||||
@@ -1538,23 +1572,14 @@ function App() {
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
const connection = activeConnection;
|
||||
const request = toRedisConnectionRequest(connection, activeDb);
|
||||
const databaseLabel = activeDb;
|
||||
const request = toRedisConnectionRequest(connection, databaseLabel);
|
||||
|
||||
startTransition(() => {
|
||||
setIsConnectionTestRunning(true);
|
||||
setConnections((current) =>
|
||||
current.map((item) =>
|
||||
item.id === connection.id
|
||||
? {
|
||||
...item,
|
||||
status: "checking",
|
||||
detail: "Live test in progress",
|
||||
}
|
||||
: item,
|
||||
),
|
||||
);
|
||||
setConnectionTestState({ connectionId: connection.id });
|
||||
setConnections((current) => markConnectionTesting(current, connection.id));
|
||||
showNeutralBanner(
|
||||
`Testing ${connection.name} against ${activeDb} using the current desktop contract.`,
|
||||
`Testing ${connection.name} against ${databaseLabel} using the current desktop contract.`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1574,7 +1599,7 @@ function App() {
|
||||
showNeutralBanner(
|
||||
`Tauri runtime is unavailable in browser dev mode, so connection testing stayed in demo fallback for ${connection.name}.`,
|
||||
);
|
||||
setIsConnectionTestRunning(false);
|
||||
setConnectionTestState(null);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1586,46 +1611,46 @@ function App() {
|
||||
|
||||
startTransition(() => {
|
||||
setConnections((current) =>
|
||||
current.map((item) =>
|
||||
item.id === connection.id
|
||||
? {
|
||||
...item,
|
||||
status: "healthy",
|
||||
detail: result.round_trip_status,
|
||||
target: {
|
||||
...item.target,
|
||||
database: result.selected_database,
|
||||
username: result.authenticated_as ?? item.target.username,
|
||||
},
|
||||
}
|
||||
: item,
|
||||
),
|
||||
applyConnectionTestSuccess(current, connection.id, {
|
||||
roundTripStatus: result.round_trip_status,
|
||||
selectedDatabase: result.selected_database,
|
||||
authenticatedAs: result.authenticated_as,
|
||||
}),
|
||||
);
|
||||
setActiveDb(formatDatabaseLabel(result.selected_database));
|
||||
showAccentBanner(
|
||||
`Live Redis test passed for ${connection.name} on ${formatDatabaseLabel(result.selected_database)}. Round-trip status: ${result.round_trip_status}.`,
|
||||
setConnectionTestState(null);
|
||||
|
||||
if (activeConnectionIdRef.current === connection.id) {
|
||||
setActiveDb(formatDatabaseLabel(result.selected_database));
|
||||
setSelectedKeyId(null);
|
||||
showAccentBanner(
|
||||
`Live Redis test passed for ${connection.name} on ${formatDatabaseLabel(result.selected_database)}. Round-trip status: ${result.round_trip_status}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showNeutralBanner(
|
||||
`Live Redis test passed for ${connection.name}; current workspace stayed on ${activeConnectionNameRef.current} / ${activeDbRef.current}.`,
|
||||
);
|
||||
setIsConnectionTestRunning(false);
|
||||
});
|
||||
} catch (error) {
|
||||
const errorCode = formatBackendErrorCodeLabel(toBackendError(error).code);
|
||||
|
||||
startTransition(() => {
|
||||
setConnections((current) =>
|
||||
current.map((item) =>
|
||||
item.id === connection.id
|
||||
? {
|
||||
...item,
|
||||
status: "error",
|
||||
detail: formatBackendErrorCodeLabel(toBackendError(error).code),
|
||||
}
|
||||
: item,
|
||||
),
|
||||
setConnections((current) => applyConnectionTestFailure(current, connection.id, errorCode));
|
||||
setConnectionTestState(null);
|
||||
|
||||
if (activeConnectionIdRef.current === connection.id) {
|
||||
showBackendErrorBanner(error, {
|
||||
operation: "connection_test",
|
||||
connectionName: connection.name,
|
||||
databaseLabel,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
showNeutralBanner(
|
||||
`Live Redis test failed for ${connection.name}. The current workspace stayed on ${activeConnectionNameRef.current} / ${activeDbRef.current}; reopen ${connection.name} to retry after reviewing its card status.`,
|
||||
);
|
||||
showBackendErrorBanner(error, {
|
||||
operation: "connection_test",
|
||||
connectionName: connection.name,
|
||||
databaseLabel: activeDb,
|
||||
});
|
||||
setIsConnectionTestRunning(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1778,7 +1803,7 @@ function App() {
|
||||
<StatusPill
|
||||
icon={<Activity className="h-4 w-4" />}
|
||||
label="Status"
|
||||
value={formatConnectionStatus(activeConnection)}
|
||||
value={formatConnectionStatusLabel(activeConnection)}
|
||||
/>
|
||||
<StatusPill
|
||||
icon={<PanelLeft className="h-4 w-4" />}
|
||||
@@ -1838,7 +1863,7 @@ function App() {
|
||||
<p className="text-sm font-medium text-[var(--ink-0)]">{connection.name}</p>
|
||||
<p className="mt-1 text-sm text-[var(--ink-soft)]">{formatEndpoint(connection.target)}</p>
|
||||
<p className="mt-2 font-mono text-xs uppercase tracking-[0.14em] text-[var(--ink-muted)]">
|
||||
{formatConnectionCardDetail(connection)}
|
||||
{formatConnectionCardDetailLabel(connection)}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={active ? "accent" : "neutral"}>{connection.environment}</Badge>
|
||||
@@ -1958,9 +1983,32 @@ function App() {
|
||||
onClick={handleTestConnection}
|
||||
disabled={isConnectionTestRunning}
|
||||
>
|
||||
{isConnectionTestRunning ? "Testing..." : tauriAvailable ? "Test live" : "Test demo"}
|
||||
{activeConnectionTestRunning
|
||||
? "Testing..."
|
||||
: isConnectionTestRunning
|
||||
? "Test running elsewhere"
|
||||
: tauriAvailable
|
||||
? "Test live"
|
||||
: "Test demo"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-[22px] border border-[var(--line-soft)] bg-[var(--surface-base)] px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--ink-0)]">Connection session</p>
|
||||
<p className="mt-1 text-sm text-[var(--ink-soft)]">
|
||||
{connectionSessionSummary.summary}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={connectionSessionSummary.badgeVariant}>
|
||||
{connectionSessionSummary.badgeLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-[var(--ink-soft)]">
|
||||
{connectionSessionSummary.hint}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@@ -2888,38 +2936,6 @@ function formatEndpoint(target: ConnectionProfile["target"]) {
|
||||
return `${target.host}:${target.port}`;
|
||||
}
|
||||
|
||||
function formatConnectionStatus(connection: ConnectionProfile) {
|
||||
if (connection.status === "healthy") {
|
||||
return `Healthy · ${connection.detail}`;
|
||||
}
|
||||
|
||||
if (connection.status === "checking") {
|
||||
return "Testing live connection";
|
||||
}
|
||||
|
||||
if (connection.status === "error") {
|
||||
return `Error · ${connection.detail}`;
|
||||
}
|
||||
|
||||
return connection.detail;
|
||||
}
|
||||
|
||||
function formatConnectionCardDetail(connection: ConnectionProfile) {
|
||||
if (connection.status === "checking") {
|
||||
return "live test in progress";
|
||||
}
|
||||
|
||||
if (connection.status === "healthy") {
|
||||
return `checked ${connection.detail}`;
|
||||
}
|
||||
|
||||
if (connection.status === "error") {
|
||||
return `failed ${connection.detail}`;
|
||||
}
|
||||
|
||||
return connection.detail;
|
||||
}
|
||||
|
||||
function formatBackendErrorCodeLabel(code: string) {
|
||||
return code.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user