feat(desktop): harden database switcher range

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Senior Frontend Engineer
2026-03-31 06:56:58 +00:00
parent 291f744e0f
commit 04da063abf
4 changed files with 139 additions and 4 deletions

View File

@@ -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<string | null>(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() {
<section>
<SectionEyebrow icon={<Database className="h-4 w-4" />} label="Database switcher" />
<div className="mt-3 grid gap-2">
{databases.map((database) => {
{databaseOptions.map((database) => {
const active = database === activeDb;
return (

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,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.