Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/frontend/src/components/key-browser/KeyBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ChartPie
} from "lucide-react"
import { truncateText } from "@common/src/truncate-text"
import { toScanPattern } from "@common/src/scan-pattern"
import { AppHeader } from "../ui/app-header"
import { ChartModal } from "../ui/chart-modal"
import DonutChart from "../ui/donut-chart"
Expand Down Expand Up @@ -75,7 +76,7 @@ export function KeyBrowser() {
if (id) {
dispatch(getKeysRequested({
connectionId: id,
pattern: searchPattern || "*",
pattern: toScanPattern(searchPattern),
}))
}
}
Expand Down Expand Up @@ -208,7 +209,7 @@ export function KeyBrowser() {
disabled={loading}
onChange={(e) => setSearchPattern(e.target.value)}
onClear={handleClearSearch}
placeholder="Search keys (use * to search patterns like user:* and press Enter)"
placeholder="Search keys (supports glob: * ? [ ])"
value={searchPattern}
/>
</form>
Expand Down
41 changes: 41 additions & 0 deletions common/src/__tests__/scan-pattern.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, it } from "node:test"
import assert from "node:assert"
import { toScanPattern } from "../scan-pattern"

describe("toScanPattern", () => {
it("wraps plain text with wildcards for substring matching", () => {
assert.strictEqual(toScanPattern("hash"), "*hash*")
})

it("wraps multi-word input with wildcards", () => {
assert.strictEqual(toScanPattern("user:session"), "*user:session*")
})

it("returns * for empty input", () => {
assert.strictEqual(toScanPattern(""), "*")
})

it("passes through input containing *", () => {
assert.strictEqual(toScanPattern("user:*"), "user:*")
})

it("passes through input containing ?", () => {
assert.strictEqual(toScanPattern("session?"), "session?")
})

it("passes through input containing [", () => {
assert.strictEqual(toScanPattern("key[0-9]"), "key[0-9]")
})

it("passes through input containing ]", () => {
assert.strictEqual(toScanPattern("key[abc]"), "key[abc]")
})

it("passes through input with multiple glob characters", () => {
assert.strictEqual(toScanPattern("user:*:session?"), "user:*:session?")
})

it("wraps input that looks like a glob but has no glob chars", () => {
assert.strictEqual(toScanPattern("user:123:data"), "*user:123:data*")
})
})
1 change: 1 addition & 0 deletions common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from "./format-metric-value"
export * from "./json-utils"
export * from "./key-tree-builder"
export * from "./key-validators"
export * from "./scan-pattern"
export * from "./memory-usage-calculation"
export * from "./reply-id"
export * from "./time-utils"
Expand Down
11 changes: 11 additions & 0 deletions common/src/scan-pattern.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Convert user search input into a SCAN MATCH pattern.
* If the input contains no glob characters, wrap with wildcards for substring matching.
* If it contains glob characters, use as-is (power user mode).
* Empty input returns "*" (match all).
*/
export const toScanPattern = (input: string): string => {
if (!input) return "*"
const hasGlob = /[*?[\]]/.test(input)
return hasGlob ? input : `*${input}*`
}
Loading