Compare commits
1 Commits
cmp-22-db-
...
cmp-29-bro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf6f8bc0bf |
@@ -64,13 +64,18 @@ import {
|
|||||||
resolveSelectedWorkspaceKey,
|
resolveSelectedWorkspaceKey,
|
||||||
resolveSelectedWorkspaceKeyId,
|
resolveSelectedWorkspaceKeyId,
|
||||||
} from "./lib/key-workspace-state";
|
} from "./lib/key-workspace-state";
|
||||||
|
import {
|
||||||
|
buildInspectorEmptyStateMessage,
|
||||||
|
buildKeyBrowserEmptyStateMessage,
|
||||||
|
buildKeyBrowserStatusMessage,
|
||||||
|
resolveKeyBrowserSearchState,
|
||||||
|
} from "./lib/key-browser-search";
|
||||||
import {
|
import {
|
||||||
parseDemoTtlToSeconds,
|
parseDemoTtlToSeconds,
|
||||||
persistDemoKeyTtl,
|
persistDemoKeyTtl,
|
||||||
updateDemoKeyTtl,
|
updateDemoKeyTtl,
|
||||||
updateDemoStringValue,
|
updateDemoStringValue,
|
||||||
} from "./lib/demo-workspace";
|
} from "./lib/demo-workspace";
|
||||||
import { buildDatabaseSwitcherLabels } from "./lib/database-switcher";
|
|
||||||
import { cn } from "./lib/utils";
|
import { cn } from "./lib/utils";
|
||||||
|
|
||||||
declare const __APP_VERSION__: string;
|
declare const __APP_VERSION__: string;
|
||||||
@@ -358,6 +363,8 @@ const initialDemoKeys: KeyRecord[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const databases = ["db0", "db1", "db2", "db5"];
|
||||||
|
|
||||||
const fallbackBootstrap: BackendBootstrap = {
|
const fallbackBootstrap: BackendBootstrap = {
|
||||||
app_name: "Redis GUI Foundation",
|
app_name: "Redis GUI Foundation",
|
||||||
surface: "desktop",
|
surface: "desktop",
|
||||||
@@ -395,7 +402,7 @@ function App() {
|
|||||||
const [connections, setConnections] = useState(initialConnections);
|
const [connections, setConnections] = useState(initialConnections);
|
||||||
const [demoKeys, setDemoKeys] = useState(initialDemoKeys);
|
const [demoKeys, setDemoKeys] = useState(initialDemoKeys);
|
||||||
const [activeConnectionId, setActiveConnectionId] = useState(initialConnections[0].id);
|
const [activeConnectionId, setActiveConnectionId] = useState(initialConnections[0].id);
|
||||||
const [activeDb, setActiveDb] = useState("db0");
|
const [activeDb, setActiveDb] = useState(databases[0]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(initialDemoKeys[1].id);
|
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(initialDemoKeys[1].id);
|
||||||
const [draftValue, setDraftValue] = useState(initialDemoKeys[1].value);
|
const [draftValue, setDraftValue] = useState(initialDemoKeys[1].value);
|
||||||
@@ -442,9 +449,10 @@ function App() {
|
|||||||
const activeConnection =
|
const activeConnection =
|
||||||
connections.find((connection) => connection.id === activeConnectionId) ??
|
connections.find((connection) => connection.id === activeConnectionId) ??
|
||||||
connections[0];
|
connections[0];
|
||||||
const databaseOptions = buildDatabaseSwitcherLabels(parseDatabaseLabel(activeDb));
|
|
||||||
|
|
||||||
const filteredDemoKeys = filterWorkspaceKeys(demoKeys, deferredSearch);
|
const searchState = resolveKeyBrowserSearchState(search);
|
||||||
|
const deferredSearchState = resolveKeyBrowserSearchState(deferredSearch);
|
||||||
|
const filteredDemoKeys = filterWorkspaceKeys(demoKeys, deferredSearchState.trimmedQuery);
|
||||||
const filteredKeys = tauriAvailable ? liveKeys : filteredDemoKeys;
|
const filteredKeys = tauriAvailable ? liveKeys : filteredDemoKeys;
|
||||||
const draftErrors = validateConnectionDraft(connectionDraft, connections);
|
const draftErrors = validateConnectionDraft(connectionDraft, connections);
|
||||||
const draftErrorList = Object.values(draftErrors);
|
const draftErrorList = Object.values(draftErrors);
|
||||||
@@ -492,6 +500,22 @@ function App() {
|
|||||||
? selectedValueRecord?.metadata.ttl.kind !== "persistent"
|
? selectedValueRecord?.metadata.ttl.kind !== "persistent"
|
||||||
: selectedKey?.ttl !== "persistent");
|
: selectedKey?.ttl !== "persistent");
|
||||||
const latestConsoleEntry = consoleHistory[0] ?? null;
|
const latestConsoleEntry = consoleHistory[0] ?? null;
|
||||||
|
const keyBrowserStatusMessage = buildKeyBrowserStatusMessage({
|
||||||
|
databaseLabel: activeDb,
|
||||||
|
visibleCount: filteredKeys.length,
|
||||||
|
hasMore: tauriAvailable && liveHasMore,
|
||||||
|
isLoading: isBrowseLoading,
|
||||||
|
search: deferredSearchState,
|
||||||
|
});
|
||||||
|
const keyBrowserEmptyStateMessage = buildKeyBrowserEmptyStateMessage({
|
||||||
|
databaseLabel: activeDb,
|
||||||
|
isLoading: isBrowseLoading,
|
||||||
|
search: deferredSearchState,
|
||||||
|
});
|
||||||
|
const inspectorEmptyStateMessage = buildInspectorEmptyStateMessage(
|
||||||
|
activeDb,
|
||||||
|
deferredSearchState,
|
||||||
|
);
|
||||||
const qaSnapshot = buildQaSnapshot({
|
const qaSnapshot = buildQaSnapshot({
|
||||||
appName: bootstrap.app_name,
|
appName: bootstrap.app_name,
|
||||||
appVersion,
|
appVersion,
|
||||||
@@ -564,7 +588,7 @@ function App() {
|
|||||||
const request: RedisKeyBrowseRequest = {
|
const request: RedisKeyBrowseRequest = {
|
||||||
connection: toRedisConnectionRequest(activeConnection, activeDb),
|
connection: toRedisConnectionRequest(activeConnection, activeDb),
|
||||||
cursor: "0",
|
cursor: "0",
|
||||||
pattern: toRedisSearchPattern(deferredSearch),
|
pattern: deferredSearchState.requestPattern,
|
||||||
page_size: browsePageSize,
|
page_size: browsePageSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -618,7 +642,7 @@ function App() {
|
|||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}, [activeConnectionId, activeDb, deferredSearch, tauriAvailable]);
|
}, [activeConnectionId, activeDb, deferredSearchState.requestPattern, tauriAvailable]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tauriAvailable) {
|
if (!tauriAvailable) {
|
||||||
@@ -1129,7 +1153,7 @@ function App() {
|
|||||||
request: {
|
request: {
|
||||||
connection: toRedisConnectionRequest(activeConnection, activeDb),
|
connection: toRedisConnectionRequest(activeConnection, activeDb),
|
||||||
cursor: liveNextCursor,
|
cursor: liveNextCursor,
|
||||||
pattern: toRedisSearchPattern(deferredSearch),
|
pattern: deferredSearchState.requestPattern,
|
||||||
page_size: browsePageSize,
|
page_size: browsePageSize,
|
||||||
} satisfies RedisKeyBrowseRequest,
|
} satisfies RedisKeyBrowseRequest,
|
||||||
});
|
});
|
||||||
@@ -1583,7 +1607,7 @@ function App() {
|
|||||||
<section>
|
<section>
|
||||||
<SectionEyebrow icon={<Database className="h-4 w-4" />} label="Database switcher" />
|
<SectionEyebrow icon={<Database className="h-4 w-4" />} label="Database switcher" />
|
||||||
<div className="mt-3 grid gap-2">
|
<div className="mt-3 grid gap-2">
|
||||||
{databaseOptions.map((database) => {
|
{databases.map((database) => {
|
||||||
const active = database === activeDb;
|
const active = database === activeDb;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1732,7 +1756,25 @@ function App() {
|
|||||||
className="w-full bg-transparent text-sm text-[var(--ink-0)] outline-none placeholder:text-[var(--ink-muted)]"
|
className="w-full bg-transparent text-sm text-[var(--ink-0)] outline-none placeholder:text-[var(--ink-muted)]"
|
||||||
placeholder="Search key name or pattern"
|
placeholder="Search key name or pattern"
|
||||||
/>
|
/>
|
||||||
|
{searchState.mode === "all" ? null : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSearch("")}
|
||||||
|
className="shrink-0 px-2 py-1 text-xs"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
|
<div className="flex items-center justify-between gap-3 text-xs text-[var(--ink-muted)]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="neutral" className="normal-case tracking-normal">
|
||||||
|
{deferredSearchState.mode === "all" ? "all keys" : deferredSearchState.mode === "pattern" ? "pattern" : "contains"}
|
||||||
|
</Badge>
|
||||||
|
<p>{keyBrowserStatusMessage}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 min-h-0 flex-1 space-y-2 overflow-auto pr-1">
|
<div className="mt-4 min-h-0 flex-1 space-y-2 overflow-auto pr-1">
|
||||||
@@ -1770,9 +1812,7 @@ function App() {
|
|||||||
|
|
||||||
{filteredKeys.length === 0 ? (
|
{filteredKeys.length === 0 ? (
|
||||||
<div className="rounded-[24px] border border-dashed border-[var(--line-strong)] bg-[var(--surface-base)] px-4 py-8 text-center text-sm text-[var(--ink-soft)]">
|
<div className="rounded-[24px] border border-dashed border-[var(--line-strong)] bg-[var(--surface-base)] px-4 py-8 text-center text-sm text-[var(--ink-soft)]">
|
||||||
{isBrowseLoading
|
{keyBrowserEmptyStateMessage}
|
||||||
? `Loading keys from ${activeDb}...`
|
|
||||||
: `No keys match the current pattern in ${activeDb}.`}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -1839,7 +1879,7 @@ function App() {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 flex h-64 items-center justify-center rounded-[22px] border border-dashed border-[var(--line-strong)] bg-[var(--surface-muted)] px-6 text-center text-sm text-[var(--ink-soft)]">
|
<div className="mt-3 flex h-64 items-center justify-center rounded-[22px] border border-dashed border-[var(--line-strong)] bg-[var(--surface-muted)] px-6 text-center text-sm text-[var(--ink-soft)]">
|
||||||
No key matches the current pattern in {activeDb}. Clear or adjust the search to restore an inspector target.
|
{inspectorEmptyStateMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
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",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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}`;
|
|
||||||
}
|
|
||||||
83
apps/desktop/src/lib/key-browser-search.test.ts
Normal file
83
apps/desktop/src/lib/key-browser-search.test.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
buildInspectorEmptyStateMessage,
|
||||||
|
buildKeyBrowserEmptyStateMessage,
|
||||||
|
buildKeyBrowserStatusMessage,
|
||||||
|
resolveKeyBrowserSearchState,
|
||||||
|
} from "./key-browser-search";
|
||||||
|
|
||||||
|
describe("key browser search helpers", () => {
|
||||||
|
it("classifies blank, contains, and wildcard search input", () => {
|
||||||
|
expect(resolveKeyBrowserSearchState(" ")).toEqual({
|
||||||
|
trimmedQuery: "",
|
||||||
|
requestPattern: null,
|
||||||
|
mode: "all",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveKeyBrowserSearchState(" smoke ")).toEqual({
|
||||||
|
trimmedQuery: "smoke",
|
||||||
|
requestPattern: "*smoke*",
|
||||||
|
mode: "contains",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveKeyBrowserSearchState("smoke:*")).toEqual({
|
||||||
|
trimmedQuery: "smoke:*",
|
||||||
|
requestPattern: "smoke:*",
|
||||||
|
mode: "pattern",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds a stable status message for visible counts and load state", () => {
|
||||||
|
expect(
|
||||||
|
buildKeyBrowserStatusMessage({
|
||||||
|
databaseLabel: "db0",
|
||||||
|
visibleCount: 7,
|
||||||
|
hasMore: true,
|
||||||
|
isLoading: false,
|
||||||
|
search: resolveKeyBrowserSearchState("smoke"),
|
||||||
|
}),
|
||||||
|
).toBe('7+ keys visible for contains search "smoke" in db0.');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildKeyBrowserStatusMessage({
|
||||||
|
databaseLabel: "db3",
|
||||||
|
visibleCount: 0,
|
||||||
|
hasMore: false,
|
||||||
|
isLoading: true,
|
||||||
|
search: resolveKeyBrowserSearchState("smoke:*"),
|
||||||
|
}),
|
||||||
|
).toBe('Searching db3 for "smoke:*".');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds browser empty-state copy for blank and filtered results", () => {
|
||||||
|
expect(
|
||||||
|
buildKeyBrowserEmptyStateMessage({
|
||||||
|
databaseLabel: "db0",
|
||||||
|
isLoading: false,
|
||||||
|
search: resolveKeyBrowserSearchState(""),
|
||||||
|
}),
|
||||||
|
).toBe("No keys are visible in db0 yet.");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildKeyBrowserEmptyStateMessage({
|
||||||
|
databaseLabel: "db2",
|
||||||
|
isLoading: false,
|
||||||
|
search: resolveKeyBrowserSearchState("audit:*"),
|
||||||
|
}),
|
||||||
|
).toBe('No keys match "audit:*" in db2.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps inspector empty-state guidance aligned with browser search state", () => {
|
||||||
|
expect(
|
||||||
|
buildInspectorEmptyStateMessage("db1", resolveKeyBrowserSearchState("")),
|
||||||
|
).toBe(
|
||||||
|
"No keys are visible in db1 yet. Browse another database or refresh after data arrives.",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildInspectorEmptyStateMessage("db4", resolveKeyBrowserSearchState("jobs")),
|
||||||
|
).toBe(
|
||||||
|
'No keys match "jobs" in db4. Clear or adjust the search to restore an inspector target.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
100
apps/desktop/src/lib/key-browser-search.ts
Normal file
100
apps/desktop/src/lib/key-browser-search.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
export type KeyBrowserSearchMode = "all" | "contains" | "pattern";
|
||||||
|
|
||||||
|
export type KeyBrowserSearchState = {
|
||||||
|
trimmedQuery: string;
|
||||||
|
requestPattern: string | null;
|
||||||
|
mode: KeyBrowserSearchMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type KeyBrowserFeedbackContext = {
|
||||||
|
databaseLabel: string;
|
||||||
|
visibleCount: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
search: KeyBrowserSearchState;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveKeyBrowserSearchState(value: string): KeyBrowserSearchState {
|
||||||
|
const trimmedQuery = value.trim();
|
||||||
|
if (!trimmedQuery) {
|
||||||
|
return {
|
||||||
|
trimmedQuery: "",
|
||||||
|
requestPattern: null,
|
||||||
|
mode: "all",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[*?\[\]]/.test(trimmedQuery)) {
|
||||||
|
return {
|
||||||
|
trimmedQuery,
|
||||||
|
requestPattern: trimmedQuery,
|
||||||
|
mode: "pattern",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
trimmedQuery,
|
||||||
|
requestPattern: `*${trimmedQuery}*`,
|
||||||
|
mode: "contains",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildKeyBrowserStatusMessage({
|
||||||
|
databaseLabel,
|
||||||
|
visibleCount,
|
||||||
|
hasMore,
|
||||||
|
isLoading,
|
||||||
|
search,
|
||||||
|
}: KeyBrowserFeedbackContext) {
|
||||||
|
if (isLoading) {
|
||||||
|
if (search.mode === "all") {
|
||||||
|
return `Loading keys from ${databaseLabel}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Searching ${databaseLabel} for ${formatSearchLabel(search)}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleLabel = `${visibleCount}${hasMore ? "+" : ""}`;
|
||||||
|
|
||||||
|
if (search.mode === "all") {
|
||||||
|
return `${visibleLabel} keys visible in ${databaseLabel}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchLabel =
|
||||||
|
search.mode === "pattern"
|
||||||
|
? `pattern ${formatSearchLabel(search)}`
|
||||||
|
: `contains search ${formatSearchLabel(search)}`;
|
||||||
|
|
||||||
|
return `${visibleLabel} keys visible for ${searchLabel} in ${databaseLabel}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildKeyBrowserEmptyStateMessage({
|
||||||
|
databaseLabel,
|
||||||
|
isLoading,
|
||||||
|
search,
|
||||||
|
}: Omit<KeyBrowserFeedbackContext, "visibleCount" | "hasMore">) {
|
||||||
|
if (isLoading) {
|
||||||
|
return `Loading keys from ${databaseLabel}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search.mode === "all") {
|
||||||
|
return `No keys are visible in ${databaseLabel} yet.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `No keys match ${formatSearchLabel(search)} in ${databaseLabel}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildInspectorEmptyStateMessage(
|
||||||
|
databaseLabel: string,
|
||||||
|
search: KeyBrowserSearchState,
|
||||||
|
) {
|
||||||
|
if (search.mode === "all") {
|
||||||
|
return `No keys are visible in ${databaseLabel} yet. Browse another database or refresh after data arrives.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `No keys match ${formatSearchLabel(search)} in ${databaseLabel}. Clear or adjust the search to restore an inspector target.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSearchLabel(search: KeyBrowserSearchState) {
|
||||||
|
return `"${search.trimmedQuery}"`;
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# 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.
|
|
||||||
47
plans/2026-03-31-hac-29-browse-search-baseline.md
Normal file
47
plans/2026-03-31-hac-29-browse-search-baseline.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# HAC-29 Browse And Search Baseline Stabilization
|
||||||
|
|
||||||
|
Date: 2026-03-31 UTC
|
||||||
|
Owner: Senior Frontend Engineer
|
||||||
|
Issue: HAC-29
|
||||||
|
Scope: `desktop-v1` browse/search hardening only
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- `apps/desktop` already supports live browse pagination and DB-scoped search, but the visible search surface still leaves a few operator-facing gaps:
|
||||||
|
- no explicit feedback about whether input is treated as substring matching or raw Redis pattern matching
|
||||||
|
- no bounded clear action from the browser header
|
||||||
|
- one generic empty-state sentence was doing too much work across loading, empty DB, and filtered no-match cases
|
||||||
|
- Current phase must stay inside browse/search hardening and must not expand into Add Key, delete execution, non-string editing, or visual refresh work.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- Keep the existing workspace layout and backend contracts unchanged.
|
||||||
|
- Extract search-state interpretation into a shared frontend helper so visible UI copy and backend browse requests use the same source of truth.
|
||||||
|
- Improve only the key-browser and inspector feedback layer:
|
||||||
|
- explicit search mode label (`all keys`, `contains`, `pattern`)
|
||||||
|
- concise result-status message under the search input
|
||||||
|
- clear action for returning to normal browse state
|
||||||
|
- distinct empty-state copy for loading, empty DB, and no-match search results
|
||||||
|
|
||||||
|
## Implemented Slice
|
||||||
|
|
||||||
|
1. Added `apps/desktop/src/lib/key-browser-search.ts` plus focused tests for:
|
||||||
|
- blank input vs contains search vs raw wildcard-pattern classification
|
||||||
|
- backend request-pattern generation
|
||||||
|
- browser status and empty-state copy
|
||||||
|
- inspector empty-state copy aligned to the same search state
|
||||||
|
2. Updated `apps/desktop/src/App.tsx` to:
|
||||||
|
- consume the shared search helper for live browse requests
|
||||||
|
- render a visible search-mode badge and status line under the browser search field
|
||||||
|
- expose a bounded clear action when search input is active
|
||||||
|
- keep browser and inspector empty-state feedback aligned
|
||||||
|
3. Updated `docs/frontend-workspace-baseline.md` so the shared acceptance language now includes the search-mode feedback, clear action, and differentiated empty states.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `pnpm --filter @redis-gui/desktop test` -> pass (9 files / 33 tests)
|
||||||
|
- `pnpm run desktop:build` -> pass
|
||||||
|
|
||||||
|
## Remaining Risk
|
||||||
|
|
||||||
|
- This environment still cannot launch a visible Tauri window, so final GUI-level browse/search smoke evidence remains a QA follow-up even though the frontend code and build path are green.
|
||||||
Reference in New Issue
Block a user