diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 6fd4d47..ba8d429 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -70,6 +70,7 @@ import { updateDemoKeyTtl, updateDemoStringValue, } from "./lib/demo-workspace"; +import { buildDatabaseSwitcherLabels } from "./lib/database-switcher"; import { cn } from "./lib/utils"; declare const __APP_VERSION__: string; @@ -357,8 +358,6 @@ const initialDemoKeys: KeyRecord[] = [ }, ]; -const databases = ["db0", "db1", "db2", "db5"]; - const fallbackBootstrap: BackendBootstrap = { app_name: "Redis GUI Foundation", surface: "desktop", @@ -396,7 +395,7 @@ function App() { const [connections, setConnections] = useState(initialConnections); const [demoKeys, setDemoKeys] = useState(initialDemoKeys); const [activeConnectionId, setActiveConnectionId] = useState(initialConnections[0].id); - const [activeDb, setActiveDb] = useState(databases[0]); + const [activeDb, setActiveDb] = useState("db0"); const [search, setSearch] = useState(""); const [selectedKeyId, setSelectedKeyId] = useState(initialDemoKeys[1].id); const [draftValue, setDraftValue] = useState(initialDemoKeys[1].value); @@ -443,6 +442,7 @@ function App() { const activeConnection = connections.find((connection) => connection.id === activeConnectionId) ?? connections[0]; + const databaseOptions = buildDatabaseSwitcherLabels(parseDatabaseLabel(activeDb)); const filteredDemoKeys = filterWorkspaceKeys(demoKeys, deferredSearch); const filteredKeys = tauriAvailable ? liveKeys : filteredDemoKeys; @@ -1583,7 +1583,7 @@ function App() {
} label="Database switcher" />
- {databases.map((database) => { + {databaseOptions.map((database) => { const active = database === activeDb; return ( diff --git a/apps/desktop/src/lib/database-switcher.test.ts b/apps/desktop/src/lib/database-switcher.test.ts new file mode 100644 index 0000000..81c27e5 --- /dev/null +++ b/apps/desktop/src/lib/database-switcher.test.ts @@ -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", + ]); + }); +}); diff --git a/apps/desktop/src/lib/database-switcher.ts b/apps/desktop/src/lib/database-switcher.ts new file mode 100644 index 0000000..b3f2fd4 --- /dev/null +++ b/apps/desktop/src/lib/database-switcher.ts @@ -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(); + + 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}`; +} diff --git a/plans/2026-03-31-hac-22-db-switcher-hardening.md b/plans/2026-03-31-hac-22-db-switcher-hardening.md new file mode 100644 index 0000000..398eb3f --- /dev/null +++ b/plans/2026-03-31-hac-22-db-switcher-hardening.md @@ -0,0 +1,32 @@ +# HAC-22 DB Switcher Hardening + +Date: 2026-03-31 UTC +Owner: Senior Frontend Engineer +Issue: HAC-22 +Scope: `desktop-v1` user-visible frontend hardening only + +## Context + +- `apps/desktop` already ships a visible DB switcher, but the current UI only renders four fixed choices: `db0`, `db1`, `db2`, and `db5`. +- Current product scope already includes one active standalone Redis connection, explicit DB context, and DB-scoped browse / inspect / command flows. +- The hardcoded four-item list is weaker than the current product boundary and makes shell-level verification less representative for operators who need to move across common DB indexes. + +## Decision + +- Do not expand product scope or alter backend contracts. +- Keep the existing rail placement and interaction model for the DB switcher. +- Widen the visible DB switcher to a predictable default operator range and ensure the currently active DB remains selectable even when it falls outside the default range. + +## Implemented Slice + +1. Added a shared frontend helper that returns DB labels for the switcher. +2. Defaulted the visible range to `db0` through `db15`. +3. Preserved the currently active DB when it falls outside that range and kept the options numerically ordered. +4. Covered the helper with focused Vitest cases. + +## Acceptance + +- The visible DB switcher no longer depends on a four-item hardcoded list. +- Operators can select common DB indexes from `db0` through `db15` without editing connection drafts. +- An active DB outside the default range still appears in the switcher and stays selectable. +- `pnpm --filter @redis-gui/desktop test` and `pnpm run desktop:build` remain green.