feat(desktop): stabilize connection workspace state

Absorb the local workspace drift for connection session handling, database switching, inspector readability helpers, and the bounded add-key preview flow.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Senior Frontend Engineer
2026-03-31 10:14:13 +00:00
parent 35040ef0c9
commit 8b6689c445
9 changed files with 1194 additions and 89 deletions

View File

@@ -3,6 +3,7 @@ import {
startTransition, startTransition,
useDeferredValue, useDeferredValue,
useEffect, useEffect,
useRef,
useState, useState,
type ChangeEvent, type ChangeEvent,
type FormEvent, type FormEvent,
@@ -82,6 +83,14 @@ import {
updateDemoStringValue, updateDemoStringValue,
} from "./lib/demo-workspace"; } from "./lib/demo-workspace";
import { buildDatabaseSwitcherLabels } from "./lib/database-switcher"; import { buildDatabaseSwitcherLabels } from "./lib/database-switcher";
import {
applyConnectionTestFailure,
applyConnectionTestSuccess,
buildConnectionSessionSummary,
formatConnectionCardDetailLabel,
formatConnectionStatusLabel,
markConnectionTesting,
} from "./lib/connection-session";
import { import {
createDemoKeyRecord, createDemoKeyRecord,
findDuplicateKeyName, findDuplicateKeyName,
@@ -493,7 +502,9 @@ function App() {
const [addKeyDialogOpen, setAddKeyDialogOpen] = useState(false); const [addKeyDialogOpen, setAddKeyDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [pendingMutation, setPendingMutation] = useState<PendingMutationGuard | null>(null); 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 [isCommandRunning, setIsCommandRunning] = useState(false);
const [isBrowseLoading, setIsBrowseLoading] = useState(false); const [isBrowseLoading, setIsBrowseLoading] = useState(false);
const [isInspectorLoading, setIsInspectorLoading] = useState(false); const [isInspectorLoading, setIsInspectorLoading] = useState(false);
@@ -528,6 +539,9 @@ function App() {
const activeConnection = const activeConnection =
connections.find((connection) => connection.id === activeConnectionId) ?? connections.find((connection) => connection.id === activeConnectionId) ??
connections[0]; connections[0];
const activeConnectionIdRef = useRef(activeConnectionId);
const activeConnectionNameRef = useRef(activeConnection.name);
const activeDbRef = useRef(activeDb);
const databaseOptions = buildDatabaseSwitcherLabels(parseDatabaseLabel(activeDb)); const databaseOptions = buildDatabaseSwitcherLabels(parseDatabaseLabel(activeDb));
const searchState = resolveKeyBrowserSearchState(search); const searchState = resolveKeyBrowserSearchState(search);
const deferredSearchState = resolveKeyBrowserSearchState(deferredSearch); const deferredSearchState = resolveKeyBrowserSearchState(deferredSearch);
@@ -591,6 +605,20 @@ function App() {
ttlWriteSupported, ttlWriteSupported,
}); });
const localSmokeFixtureActive = isLocalSmokeFixtureConnection(activeConnection); 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 localSmokeFilterActive = search.trim() === localSmokeFixtureSearchText;
const ttlDraftError = ttlDraft.trim() ? validateTtlDraft(ttlDraft) : null; const ttlDraftError = ttlDraft.trim() ? validateTtlDraft(ttlDraft) : null;
const nextTtlMillis = const nextTtlMillis =
@@ -660,6 +688,12 @@ function App() {
bannerMessage: banner.message, bannerMessage: banner.message,
}); });
useEffect(() => {
activeConnectionIdRef.current = activeConnectionId;
activeConnectionNameRef.current = activeConnection.name;
activeDbRef.current = activeDb;
}, [activeConnectionId, activeConnection.name, activeDb]);
useEffect(() => { useEffect(() => {
if (!tauriAvailable) { if (!tauriAvailable) {
startTransition(() => { startTransition(() => {
@@ -1538,23 +1572,14 @@ function App() {
const handleTestConnection = async () => { const handleTestConnection = async () => {
const connection = activeConnection; const connection = activeConnection;
const request = toRedisConnectionRequest(connection, activeDb); const databaseLabel = activeDb;
const request = toRedisConnectionRequest(connection, databaseLabel);
startTransition(() => { startTransition(() => {
setIsConnectionTestRunning(true); setConnectionTestState({ connectionId: connection.id });
setConnections((current) => setConnections((current) => markConnectionTesting(current, connection.id));
current.map((item) =>
item.id === connection.id
? {
...item,
status: "checking",
detail: "Live test in progress",
}
: item,
),
);
showNeutralBanner( 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( showNeutralBanner(
`Tauri runtime is unavailable in browser dev mode, so connection testing stayed in demo fallback for ${connection.name}.`, `Tauri runtime is unavailable in browser dev mode, so connection testing stayed in demo fallback for ${connection.name}.`,
); );
setIsConnectionTestRunning(false); setConnectionTestState(null);
}); });
return; return;
} }
@@ -1586,46 +1611,46 @@ function App() {
startTransition(() => { startTransition(() => {
setConnections((current) => setConnections((current) =>
current.map((item) => applyConnectionTestSuccess(current, connection.id, {
item.id === connection.id roundTripStatus: result.round_trip_status,
? { selectedDatabase: result.selected_database,
...item, authenticatedAs: result.authenticated_as,
status: "healthy", }),
detail: result.round_trip_status,
target: {
...item.target,
database: result.selected_database,
username: result.authenticated_as ?? item.target.username,
},
}
: item,
),
); );
setActiveDb(formatDatabaseLabel(result.selected_database)); setConnectionTestState(null);
showAccentBanner(
`Live Redis test passed for ${connection.name} on ${formatDatabaseLabel(result.selected_database)}. Round-trip status: ${result.round_trip_status}.`, 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) { } catch (error) {
const errorCode = formatBackendErrorCodeLabel(toBackendError(error).code);
startTransition(() => { startTransition(() => {
setConnections((current) => setConnections((current) => applyConnectionTestFailure(current, connection.id, errorCode));
current.map((item) => setConnectionTestState(null);
item.id === connection.id
? { if (activeConnectionIdRef.current === connection.id) {
...item, showBackendErrorBanner(error, {
status: "error", operation: "connection_test",
detail: formatBackendErrorCodeLabel(toBackendError(error).code), connectionName: connection.name,
} databaseLabel,
: item, });
), 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 <StatusPill
icon={<Activity className="h-4 w-4" />} icon={<Activity className="h-4 w-4" />}
label="Status" label="Status"
value={formatConnectionStatus(activeConnection)} value={formatConnectionStatusLabel(activeConnection)}
/> />
<StatusPill <StatusPill
icon={<PanelLeft className="h-4 w-4" />} 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="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-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)]"> <p className="mt-2 font-mono text-xs uppercase tracking-[0.14em] text-[var(--ink-muted)]">
{formatConnectionCardDetail(connection)} {formatConnectionCardDetailLabel(connection)}
</p> </p>
</div> </div>
<Badge variant={active ? "accent" : "neutral"}>{connection.environment}</Badge> <Badge variant={active ? "accent" : "neutral"}>{connection.environment}</Badge>
@@ -1958,9 +1983,32 @@ function App() {
onClick={handleTestConnection} onClick={handleTestConnection}
disabled={isConnectionTestRunning} disabled={isConnectionTestRunning}
> >
{isConnectionTestRunning ? "Testing..." : tauriAvailable ? "Test live" : "Test demo"} {activeConnectionTestRunning
? "Testing..."
: isConnectionTestRunning
? "Test running elsewhere"
: tauriAvailable
? "Test live"
: "Test demo"}
</Button> </Button>
</div> </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>
<section> <section>
@@ -2106,6 +2154,7 @@ function App() {
? `${filteredKeys.length}${liveHasMore ? "+" : ""} visible in ${activeDb}` ? `${filteredKeys.length}${liveHasMore ? "+" : ""} visible in ${activeDb}`
: `${filteredKeys.length} of ${demoKeys.length} keys in ${activeDb}`} : `${filteredKeys.length} of ${demoKeys.length} keys in ${activeDb}`}
</Badge> </Badge>
<Badge variant="accent">Preview</Badge>
<Dialog open={addKeyDialogOpen} onOpenChange={setAddKeyDialogOpen}> <Dialog open={addKeyDialogOpen} onOpenChange={setAddKeyDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm"> <Button size="sm">
@@ -2115,12 +2164,15 @@ function App() {
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<p className="font-mono text-xs uppercase tracking-[0.22em] text-[var(--ink-muted)]"> <div className="flex flex-wrap items-center gap-2">
Add key <p className="font-mono text-xs uppercase tracking-[0.22em] text-[var(--ink-muted)]">
</p> Add key
</p>
<Badge variant="accent">Preview</Badge>
</div>
<DialogTitle>Create one bounded string key</DialogTitle> <DialogTitle>Create one bounded string key</DialogTitle>
<DialogDescription> <DialogDescription>
The create flow stays scoped to {activeConnection.name} / {activeDb}, only supports string values, and never overwrites an existing key name. The create flow stays scoped to {activeConnection.name} / {activeDb}, only supports string values, never overwrites an existing key name, and remains separate from the current desktop-v1 acceptance baseline.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -2888,38 +2940,6 @@ function formatEndpoint(target: ConnectionProfile["target"]) {
return `${target.host}:${target.port}`; 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) { function formatBackendErrorCodeLabel(code: string) {
return code.replace(/_/g, " "); return code.replace(/_/g, " ");
} }

View File

@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import {
createDemoKeyRecord,
findDuplicateKeyName,
parseOptionalTtlMillis,
validateAddKeyDraft,
} from "./add-key-draft";
describe("add key draft helpers", () => {
it("validates required key name and optional TTL shape", () => {
expect(
validateAddKeyDraft({
name: " ",
value: "hello world",
ttlMillis: "0",
}),
).toEqual({
name: "Redis key name must not be empty.",
ttlMillis: "TTL must be greater than zero when provided.",
});
expect(
validateAddKeyDraft({
name: "draft:key",
value: "",
ttlMillis: "soon",
}),
).toEqual({
ttlMillis: "TTL must be an integer number of milliseconds when provided.",
});
});
it("parses optional TTL input and preserves blank as persistent", () => {
expect(parseOptionalTtlMillis("")).toBeNull();
expect(parseOptionalTtlMillis(" 60000 ")).toBe(60000);
});
it("detects duplicate key names with Redis-style case-sensitive matching", () => {
expect(findDuplicateKeyName([{ name: "draft:key" }], "draft:key")).toBe(true);
expect(findDuplicateKeyName([{ name: "draft:key" }], "Draft:Key")).toBe(false);
});
it("builds a demo string key record with optional TTL formatting", () => {
expect(
createDemoKeyRecord({
name: " draft:key ",
value: "hello world",
ttlMillis: "60000",
}),
).toEqual({
id: "draft:key",
name: "draft:key",
type: "string",
ttl: "1m",
preview: "hello world",
editable: true,
value: "hello world",
summary:
"String-only Add Key flow stays aligned to the bounded create contract and keeps the new key visible in the current workspace scope.",
});
});
});

View File

@@ -0,0 +1,67 @@
import {
buildDemoStringPreview,
formatDemoDurationFromMillis,
type DemoWorkspaceKey,
} from "./demo-workspace";
export type AddKeyDraft = {
name: string;
value: string;
ttlMillis: string;
};
export type AddKeyDraftErrors = Partial<Record<"name" | "ttlMillis", string>>;
export function validateAddKeyDraft(draft: AddKeyDraft): AddKeyDraftErrors {
const errors: AddKeyDraftErrors = {};
if (!draft.name.trim()) {
errors.name = "Redis key name must not be empty.";
}
const ttlValue = draft.ttlMillis.trim();
if (ttlValue) {
if (!/^\d+$/.test(ttlValue)) {
errors.ttlMillis = "TTL must be an integer number of milliseconds when provided.";
} else if (Number.parseInt(ttlValue, 10) <= 0) {
errors.ttlMillis = "TTL must be greater than zero when provided.";
}
}
return errors;
}
export function parseOptionalTtlMillis(value: string) {
const normalized = value.trim();
if (!normalized) {
return null;
}
const parsed = Number.parseInt(normalized, 10);
return Number.isFinite(parsed) ? parsed : null;
}
export function findDuplicateKeyName<T extends { name: string }>(
keys: T[],
keyName: string,
) {
const normalized = keyName.trim();
return keys.some((key) => key.name === normalized);
}
export function createDemoKeyRecord(draft: AddKeyDraft): DemoWorkspaceKey {
const keyName = draft.name.trim();
const ttlMillis = parseOptionalTtlMillis(draft.ttlMillis);
return {
id: keyName,
name: keyName,
type: "string",
ttl: ttlMillis === null ? "persistent" : formatDemoDurationFromMillis(ttlMillis),
preview: buildDemoStringPreview(draft.value),
editable: true,
value: draft.value,
summary:
"String-only Add Key flow stays aligned to the bounded create contract and keeps the new key visible in the current workspace scope.",
};
}

View File

@@ -0,0 +1,151 @@
import { describe, expect, it } from "vitest";
import {
applyConnectionTestFailure,
applyConnectionTestSuccess,
buildConnectionSessionSummary,
formatConnectionCardDetailLabel,
formatConnectionStatusLabel,
markConnectionTesting,
} from "./connection-session";
const baseConnection = {
id: "redis-local",
name: "redis-local",
detail: "Draft profile",
status: "idle" as const,
target: {
database: 0,
username: null,
},
};
describe("connection session helpers", () => {
it("keeps connection test transitions in one place", () => {
const checking = markConnectionTesting([baseConnection], baseConnection.id);
expect(checking[0]).toMatchObject({
status: "checking",
detail: "Live test in progress",
});
const healthy = applyConnectionTestSuccess(checking, baseConnection.id, {
roundTripStatus: "PONG in 11ms",
selectedDatabase: 4,
authenticatedAs: "readonly",
});
expect(healthy[0]).toMatchObject({
status: "healthy",
detail: "PONG in 11ms",
target: {
database: 4,
username: "readonly",
},
});
const failed = applyConnectionTestFailure(healthy, baseConnection.id, "authentication_failed");
expect(failed[0]).toMatchObject({
status: "error",
detail: "authentication_failed",
});
});
it("formats consistent status and card copy", () => {
expect(formatConnectionStatusLabel(baseConnection)).toBe("Idle · Draft profile");
expect(
formatConnectionStatusLabel({
...baseConnection,
status: "healthy",
detail: "PONG in 11ms",
}),
).toBe("Ready · PONG in 11ms");
expect(
formatConnectionCardDetailLabel({
...baseConnection,
status: "error",
detail: "authentication_failed",
}),
).toBe("retry authentication_failed");
});
it("builds session summaries for demo, testing, ready, and retry states", () => {
expect(
buildConnectionSessionSummary({
connection: baseConnection,
databaseLabel: "db0",
tauriAvailable: false,
activeTestRunning: false,
otherTestingConnectionName: null,
}),
).toMatchObject({
badgeLabel: "Demo fallback",
badgeVariant: "neutral",
});
expect(
buildConnectionSessionSummary({
connection: baseConnection,
databaseLabel: "db0",
tauriAvailable: true,
activeTestRunning: true,
otherTestingConnectionName: null,
}),
).toMatchObject({
badgeLabel: "Testing",
badgeVariant: "accent",
});
expect(
buildConnectionSessionSummary({
connection: {
...baseConnection,
status: "healthy",
detail: "PONG in 11ms",
},
databaseLabel: "db4",
tauriAvailable: true,
activeTestRunning: false,
otherTestingConnectionName: null,
}),
).toMatchObject({
badgeLabel: "Ready",
badgeVariant: "accent",
});
expect(
buildConnectionSessionSummary({
connection: {
...baseConnection,
status: "error",
detail: "authentication_failed",
},
databaseLabel: "db0",
tauriAvailable: true,
activeTestRunning: false,
otherTestingConnectionName: null,
}),
).toMatchObject({
badgeLabel: "Retry needed",
badgeVariant: "danger",
});
});
it("keeps the active session copy clear when another connection is testing", () => {
expect(
buildConnectionSessionSummary({
connection: {
...baseConnection,
status: "healthy",
detail: "PONG in 11ms",
},
databaseLabel: "db0",
tauriAvailable: true,
activeTestRunning: false,
otherTestingConnectionName: "redis-staging",
}),
).toMatchObject({
badgeLabel: "Ready",
badgeVariant: "neutral",
summary:
"redis-local remains selected on db0 while redis-staging finishes a separate live test.",
});
});
});

View File

@@ -0,0 +1,179 @@
export type SessionConnection = {
id: string;
name: string;
detail: string;
status: "healthy" | "idle" | "checking" | "error";
target: {
database: number;
username: string | null;
};
};
export type ConnectionTestState = {
connectionId: string;
};
export type ConnectionSessionSummary = {
badgeLabel: string;
badgeVariant: "neutral" | "accent" | "danger";
summary: string;
hint: string;
};
export type ConnectionTestSuccessResult = {
roundTripStatus: string;
selectedDatabase: number;
authenticatedAs: string | null;
};
export function formatConnectionStatusLabel(connection: SessionConnection) {
if (connection.status === "healthy") {
return `Ready · ${connection.detail}`;
}
if (connection.status === "checking") {
return "Testing · Live contract probe";
}
if (connection.status === "error") {
return `Attention · ${connection.detail}`;
}
return `Idle · ${connection.detail}`;
}
export function formatConnectionCardDetailLabel(connection: SessionConnection) {
if (connection.status === "checking") {
return "live test in progress";
}
if (connection.status === "healthy") {
return `ready ${connection.detail}`;
}
if (connection.status === "error") {
return `retry ${connection.detail}`;
}
return connection.detail;
}
export function markConnectionTesting<T extends SessionConnection>(
connections: T[],
connectionId: string,
) {
return connections.map((connection) =>
connection.id === connectionId
? {
...connection,
status: "checking" as const,
detail: "Live test in progress",
}
: connection,
);
}
export function applyConnectionTestSuccess<T extends SessionConnection>(
connections: T[],
connectionId: string,
result: ConnectionTestSuccessResult,
) {
return connections.map((connection) =>
connection.id === connectionId
? {
...connection,
status: "healthy" as const,
detail: result.roundTripStatus,
target: {
...connection.target,
database: result.selectedDatabase,
username: result.authenticatedAs ?? connection.target.username,
},
}
: connection,
);
}
export function applyConnectionTestFailure<T extends SessionConnection>(
connections: T[],
connectionId: string,
detail: string,
) {
return connections.map((connection) =>
connection.id === connectionId
? {
...connection,
status: "error" as const,
detail,
}
: connection,
);
}
export function buildConnectionSessionSummary(args: {
connection: SessionConnection;
databaseLabel: string;
tauriAvailable: boolean;
activeTestRunning: boolean;
otherTestingConnectionName: string | null;
}): ConnectionSessionSummary {
const {
connection,
databaseLabel,
tauriAvailable,
activeTestRunning,
otherTestingConnectionName,
} = args;
if (!tauriAvailable) {
return {
badgeLabel: "Demo fallback",
badgeVariant: "neutral",
summary: `${connection.name} stays scoped to ${databaseLabel} in browser mode without contacting Redis.`,
hint: "Use Test demo to verify shell flow only, then switch to the Tauri desktop runtime for a live connection retry.",
};
}
if (activeTestRunning) {
return {
badgeLabel: "Testing",
badgeVariant: "accent",
summary: `Running a live connection probe for ${connection.name} on ${databaseLabel}.`,
hint: "Keep this scope selected if you want the tested database and retry messaging to update in place.",
};
}
if (otherTestingConnectionName) {
return {
badgeLabel: "Ready",
badgeVariant: "neutral",
summary: `${connection.name} remains selected on ${databaseLabel} while ${otherTestingConnectionName} finishes a separate live test.`,
hint: "The current workspace stays stable. Switch back to the testing connection after the probe completes if you need to review its result.",
};
}
if (connection.status === "healthy") {
return {
badgeLabel: "Ready",
badgeVariant: "accent",
summary: `${connection.name} last passed a live connection check on ${databaseLabel}.`,
hint: "Re-run Test live after changing host, DB, credentials, or after recovering from a Redis outage.",
};
}
if (connection.status === "error") {
return {
badgeLabel: "Retry needed",
badgeVariant: "danger",
summary: `${connection.name} last failed a live connection check on ${databaseLabel}.`,
hint: "Review the banner and connection card detail, adjust the profile if needed, then retry Test live from this same scope.",
};
}
return {
badgeLabel: "Ready to test",
badgeVariant: "neutral",
summary: `${connection.name} is selected on ${databaseLabel} but has not completed a live check in the current session.`,
hint: "Run Test live before treating browse, inspect, or command results as current Redis evidence.",
};
}

View File

@@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest";
import { buildDatabaseSwitcherLabels } from "./database-switcher";
describe("database switcher helper", () => {
it("returns the default operator range from db0 through db15", () => {
expect(buildDatabaseSwitcherLabels(0)).toEqual([
"db0",
"db1",
"db2",
"db3",
"db4",
"db5",
"db6",
"db7",
"db8",
"db9",
"db10",
"db11",
"db12",
"db13",
"db14",
"db15",
]);
});
it("keeps an active out-of-range database visible and numerically ordered", () => {
expect(buildDatabaseSwitcherLabels(23)).toEqual([
"db0",
"db1",
"db2",
"db3",
"db4",
"db5",
"db6",
"db7",
"db8",
"db9",
"db10",
"db11",
"db12",
"db13",
"db14",
"db15",
"db23",
]);
});
it("normalizes negative or fractional inputs back to safe labels", () => {
expect(buildDatabaseSwitcherLabels(-3)).toEqual([
"db0",
"db1",
"db2",
"db3",
"db4",
"db5",
"db6",
"db7",
"db8",
"db9",
"db10",
"db11",
"db12",
"db13",
"db14",
"db15",
]);
expect(buildDatabaseSwitcherLabels(4.8, 2)).toEqual([
"db0",
"db1",
"db2",
"db4",
]);
});
});

View File

@@ -0,0 +1,29 @@
export function buildDatabaseSwitcherLabels(
activeDatabase: number,
maxDefaultDatabase = 15,
) {
const normalizedDefaultMax = Math.max(0, Math.floor(maxDefaultDatabase));
const normalizedActive = Math.max(0, Math.floor(activeDatabase));
const labels = new Set<string>();
for (let database = 0; database <= normalizedDefaultMax; database += 1) {
labels.add(formatDatabaseLabel(database));
}
labels.add(formatDatabaseLabel(normalizedActive));
return [...labels].sort(compareDatabaseLabels);
}
function compareDatabaseLabels(left: string, right: string) {
return parseDatabaseLabel(left) - parseDatabaseLabel(right);
}
function parseDatabaseLabel(value: string) {
const parsed = Number.parseInt(value.replace(/^db/i, ""), 10);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
}
function formatDatabaseLabel(database: number) {
return `db${database}`;
}

View File

@@ -0,0 +1,183 @@
import { describe, expect, it } from "vitest";
import { buildInspectorReadabilitySummary } from "./inspector-readability";
describe("inspector readability summary", () => {
it("guides the operator when no key is selected", () => {
expect(
buildInspectorReadabilitySummary({
databaseLabel: "db4",
runtimeMode: "live",
selectedKey: null,
record: null,
isLoading: false,
isEditable: false,
ttlWriteSupported: false,
}),
).toEqual(
expect.objectContaining({
status: "awaiting selection",
surfaceTitle: "No key selected",
actionTitle: "Next step",
facts: expect.arrayContaining([
{ label: "Scope", value: "db4" },
{ label: "Type", value: "unknown" },
]),
}),
);
});
it("describes editable live strings with explicit write semantics", () => {
expect(
buildInspectorReadabilitySummary({
databaseLabel: "db0",
runtimeMode: "live",
selectedKey: {
name: "cache:homepage",
type: "string",
ttl: "48s",
},
record: {
metadata: {
key_type: "string",
ttl: { kind: "expires_in_millis", value: 48_000 },
},
data: {
kind: "string",
value: { kind: "string", value: "payload" },
},
},
isLoading: false,
isEditable: true,
ttlWriteSupported: true,
}),
).toEqual({
status: "editable string",
surfaceTitle: "String value",
surfaceDescription:
"The full live string payload is loaded in the inspector. Saving replaces the entire string value and preserves the active TTL boundary.",
actionTitle: "Operator path",
actionDescription:
"Review the full payload, make bounded edits, and use Save string value when you are ready to replace the current value.",
facts: [
{ label: "Scope", value: "db0" },
{ label: "Type", value: "string" },
{ label: "TTL state", value: "48s" },
{ label: "Value shape", value: "string payload" },
{ label: "Write path", value: "replace full string" },
{ label: "TTL control", value: "available" },
],
});
});
it("describes structured live values as readable inspection-only surfaces", () => {
expect(
buildInspectorReadabilitySummary({
databaseLabel: "db0",
runtimeMode: "live",
selectedKey: {
name: "session:42",
type: "hash",
ttl: "13m",
},
record: {
metadata: {
key_type: "hash",
ttl: { kind: "expires_in_millis", value: 780_000 },
},
data: {
kind: "hash",
entries: [
{
field: { kind: "string", value: "region" },
value: { kind: "string", value: "eu-west-1" },
},
{
field: { kind: "string", value: "role" },
value: { kind: "string", value: "editor" },
},
],
},
},
isLoading: false,
isEditable: false,
ttlWriteSupported: true,
}),
).toEqual(
expect.objectContaining({
status: "structured read only",
surfaceTitle: "Hash fields",
facts: [
{ label: "Scope", value: "db0" },
{ label: "Type", value: "hash" },
{ label: "TTL state", value: "13m" },
{ label: "Value shape", value: "2 fields" },
{ label: "Write path", value: "inspection only" },
{ label: "TTL control", value: "available" },
],
}),
);
});
it("keeps demo mode explicit so shell review is not confused with live evidence", () => {
expect(
buildInspectorReadabilitySummary({
databaseLabel: "db0",
runtimeMode: "demo",
selectedKey: {
name: "cache:homepage",
type: "string",
ttl: "48s",
},
record: null,
isLoading: false,
isEditable: true,
ttlWriteSupported: true,
}),
).toEqual(
expect.objectContaining({
status: "demo editable",
surfaceTitle: "String value",
facts: expect.arrayContaining([
{ label: "Write path", value: "session-local string save" },
{ label: "TTL control", value: "available" },
]),
}),
);
});
it("makes stale browse-to-read gaps explicit when the key disappears", () => {
expect(
buildInspectorReadabilitySummary({
databaseLabel: "db2",
runtimeMode: "live",
selectedKey: {
name: "jobs:failed",
type: "list",
ttl: "persistent",
},
record: {
metadata: {
key_type: "list",
ttl: { kind: "missing" },
},
data: {
kind: "missing",
},
},
isLoading: false,
isEditable: false,
ttlWriteSupported: false,
}),
).toEqual(
expect.objectContaining({
status: "missing key",
surfaceTitle: "Key disappeared after browse",
facts: expect.arrayContaining([
{ label: "Scope", value: "db2" },
{ label: "TTL state", value: "missing" },
{ label: "Write path", value: "not available" },
]),
}),
);
});
});

View File

@@ -0,0 +1,340 @@
type RedisKeyTtl =
| { kind: "persistent" }
| { kind: "missing" }
| { kind: "expires_in_millis"; value: number };
type RedisValueContent =
| { kind: "string"; value: string }
| { kind: "binary"; bytes: number[] };
type RedisFieldValueEntry = {
field: RedisValueContent;
value: RedisValueContent;
};
type RedisSortedSetEntry = {
member: RedisValueContent;
score: string;
};
type RedisStreamEntry = {
id: string;
fields: RedisFieldValueEntry[];
};
type RedisValueData =
| { kind: "missing" }
| { kind: "string"; value: RedisValueContent }
| { kind: "hash"; entries: RedisFieldValueEntry[] }
| { kind: "list"; items: RedisValueContent[] }
| { kind: "set"; members: RedisValueContent[] }
| { kind: "sorted_set"; entries: RedisSortedSetEntry[] }
| { kind: "stream"; entries: RedisStreamEntry[] };
type RedisValueRecord = {
metadata: {
key_type: string;
ttl: RedisKeyTtl;
};
data: RedisValueData;
};
type SelectedInspectorKey = {
name: string;
type: string;
ttl: string;
};
export type InspectorFact = {
label: string;
value: string;
};
export type InspectorReadabilitySummary = {
status: string;
surfaceTitle: string;
surfaceDescription: string;
actionTitle: string;
actionDescription: string;
facts: InspectorFact[];
};
export type InspectorReadabilityContext = {
databaseLabel: string;
runtimeMode: "live" | "demo";
selectedKey: SelectedInspectorKey | null;
record: RedisValueRecord | null;
isLoading: boolean;
isEditable: boolean;
ttlWriteSupported: boolean;
};
export function buildInspectorReadabilitySummary(
context: InspectorReadabilityContext,
): InspectorReadabilitySummary {
const keyType = resolveKeyType(context);
const ttlState = resolveTtlState(context);
const valueShape = describeValueShape(context);
const writePath = describeWritePath(context);
const ttlPath = context.selectedKey
? context.ttlWriteSupported
? "available"
: "not available"
: "awaiting selection";
const facts: InspectorFact[] = [
{ label: "Scope", value: context.databaseLabel },
{ label: "Type", value: formatRedisKeyTypeLabel(keyType) },
{ label: "TTL state", value: ttlState },
{ label: "Value shape", value: valueShape },
{ label: "Write path", value: writePath },
{ label: "TTL control", value: ttlPath },
];
if (!context.selectedKey) {
return {
status: "awaiting selection",
surfaceTitle: "No key selected",
surfaceDescription:
"Pick a key from the browser to load its metadata, value shape, and TTL state into the inspector.",
actionTitle: "Next step",
actionDescription:
"If the browser is empty, clear or adjust the current search so the inspector has a live target again.",
facts,
};
}
if (context.runtimeMode === "demo") {
return {
status: context.isEditable ? "demo editable" : "demo read only",
surfaceTitle: buildSurfaceTitle(context.selectedKey.type, false),
surfaceDescription: context.isEditable
? "Browser dev mode is showing a session-local string payload. Layout review is safe here, but no live Redis mutation occurs."
: `${formatRedisKeyTypeLabel(context.selectedKey.type)} payloads stay on the demo inspection path in browser mode so the UI can be reviewed without pretending a live backend exists.`,
actionTitle: "Operator path",
actionDescription: context.isEditable
? "Use Save string value only for session-local shell review. Switch to the desktop runtime before treating the result as Redis evidence."
: "Review the current structure here, then use the desktop runtime when you need live Redis data or verification.",
facts,
};
}
if (context.isLoading) {
return {
status: "loading",
surfaceTitle: `Loading ${formatRedisKeyTypeLabel(keyType)} value`,
surfaceDescription:
"The desktop bridge is reading the current key so the inspector can show live metadata, value shape, and write capability together.",
actionTitle: "Operator path",
actionDescription:
"Wait for the read to finish before editing or relying on the TTL state shown in the inspector.",
facts,
};
}
if (!context.record) {
return {
status: "metadata only",
surfaceTitle: `${formatRedisKeyTypeLabel(keyType)} metadata ready`,
surfaceDescription:
"The browser still has metadata for the selected key, but the live value surface is not currently loaded in the inspector.",
actionTitle: "Operator path",
actionDescription:
"Use Refresh metadata to retry the live read, or return to the browser if the key may have changed upstream.",
facts,
};
}
if (context.record.data.kind === "missing") {
return {
status: "missing key",
surfaceTitle: "Key disappeared after browse",
surfaceDescription:
"The key was visible during browse but no longer existed when the inspector requested the live value.",
actionTitle: "Operator path",
actionDescription:
"Refresh metadata or reload the browser list to clear the stale selection before taking further action.",
facts,
};
}
if (context.isEditable) {
return {
status: "editable string",
surfaceTitle: "String value",
surfaceDescription:
"The full live string payload is loaded in the inspector. Saving replaces the entire string value and preserves the active TTL boundary.",
actionTitle: "Operator path",
actionDescription:
"Review the full payload, make bounded edits, and use Save string value when you are ready to replace the current value.",
facts,
};
}
return {
status: "structured read only",
surfaceTitle: buildSurfaceTitle(context.record.data.kind, true),
surfaceDescription: buildStructuredDescription(context.record.data.kind),
actionTitle: "Operator path",
actionDescription:
"Use the structured panels to scan the live payload quickly. Mutations for this key type remain outside the current desktop contract.",
facts,
};
}
function resolveKeyType(context: InspectorReadabilityContext) {
return context.record?.metadata.key_type ?? context.selectedKey?.type ?? "unknown";
}
function resolveTtlState(context: InspectorReadabilityContext) {
if (context.record) {
return formatRedisKeyTtl(context.record.metadata.ttl);
}
return context.selectedKey?.ttl ?? "awaiting selection";
}
function describeValueShape(context: InspectorReadabilityContext) {
if (!context.selectedKey) {
return "no active key";
}
if (context.runtimeMode === "demo") {
return context.isEditable
? "session-local string payload"
: `${formatRedisKeyTypeLabel(context.selectedKey.type)} demo preview`;
}
if (context.isLoading) {
return "live value request in flight";
}
if (!context.record) {
return "metadata loaded, value unavailable";
}
switch (context.record.data.kind) {
case "missing":
return "key missing";
case "string":
return "string payload";
case "hash":
return `${context.record.data.entries.length} field${context.record.data.entries.length === 1 ? "" : "s"}`;
case "list":
return `${context.record.data.items.length} item${context.record.data.items.length === 1 ? "" : "s"}`;
case "set":
return `${context.record.data.members.length} member${context.record.data.members.length === 1 ? "" : "s"}`;
case "sorted_set":
return `${context.record.data.entries.length} scored member${context.record.data.entries.length === 1 ? "" : "s"}`;
case "stream":
return `${context.record.data.entries.length} entr${context.record.data.entries.length === 1 ? "y" : "ies"}`;
default:
return "unknown";
}
}
function describeWritePath(context: InspectorReadabilityContext) {
if (!context.selectedKey) {
return "awaiting selection";
}
if (context.runtimeMode === "demo") {
return context.isEditable ? "session-local string save" : "inspection only";
}
if (context.isLoading) {
return "pending live capability";
}
if (!context.record) {
return "value unavailable";
}
if (context.record.data.kind === "missing") {
return "not available";
}
return context.isEditable ? "replace full string" : "inspection only";
}
function buildSurfaceTitle(kind: string, liveStructured: boolean) {
switch (kind) {
case "hash":
return "Hash fields";
case "list":
return "List items";
case "set":
return liveStructured ? "Set members" : "Set preview";
case "sorted_set":
case "zset":
return "Sorted set members";
case "stream":
return "Stream entries";
case "string":
return "String value";
default:
return "Value surface";
}
}
function buildStructuredDescription(kind: string) {
switch (kind) {
case "hash":
return "Live HGETALL output is rendered as field/value rows so operators do not need to parse raw JSON during inspection.";
case "list":
return "Live LRANGE output is rendered in list order so operators can scan indexed items without leaving the inspector.";
case "set":
return "Live SMEMBERS output is rendered as a structured member list so the inspector stays readable even when the payload is dense.";
case "sorted_set":
return "Live ZRANGE output is rendered as member and score pairs so ordering semantics stay visible in the inspector.";
case "stream":
return "Live XRANGE output is rendered as entry rows with inline field payloads so stream structure is visible at a glance.";
default:
return "The live value is available on a structured read-only path in the current desktop contract.";
}
}
function formatRedisKeyTtl(ttl: RedisKeyTtl) {
switch (ttl.kind) {
case "persistent":
return "persistent";
case "missing":
return "missing";
case "expires_in_millis":
return formatDurationFromMillis(ttl.value);
default:
return "unknown";
}
}
function formatDurationFromMillis(value: number) {
if (value < 1_000) {
return `${value}ms`;
}
const totalSeconds = Math.floor(value / 1_000);
if (totalSeconds < 60) {
return `${totalSeconds}s`;
}
const totalMinutes = Math.floor(totalSeconds / 60);
if (totalMinutes < 60) {
return `${totalMinutes}m`;
}
const totalHours = Math.floor(totalMinutes / 60);
if (totalHours < 24) {
return `${totalHours}h`;
}
const totalDays = Math.floor(totalHours / 24);
return `${totalDays}d`;
}
function formatRedisKeyTypeLabel(value: string) {
switch (value) {
case "zset":
return "sorted set";
default:
return value || "unknown";
}
}