Skip to content

Commit ab2e0e1

Browse files
ytkimirtiCahidArdaalitariksahin
authored
Redis search support (#42)
* bump: redis * feat: add search type to keys * feat: add the new header * chore: format * fix: fetching * feat: add typesafe editor * feat: very nice editor * fix: ui fixes * feat: implement new designs of the keys list and tabs bar * fix: change stroke width to 1.5 * feat: implement many new styles * chore: format * chore: install lint-staged and husky * fix: freeze redis version to the rc * feat: add query builder ui * fix: small drag & boolean fixes * test: rename ttl forever -> no * feat: bump redis * refactor: code cleanup and fixes * fix: lint errors * fix: query condition bug when type changes * feat: implement search * refactor: organize * fix: improve schema-parser * fix: schema parser fixes * fix: editor types loading issue * fix: handle $must $should in query builder * chore: improve playground * feat: bump redis to rc.4 * fix: better error messages for schema * fix: remove .fast() option from number fields * feat: make the edit schema open a popup instead of link * fix: show errors in ui-query-builder when query does not match the types * feat: better styling for the monaco editors * test: add bun tests * fix: schema editor height not being dynamic * fix: how $mustNot is handled in the queryBuilder * refactor: bugs with $mustNot * feat: add max height and scroll shadows to UI query builder * feat: add confirmation and progress to the save index modal * refactor: how the types are generated for the query builder editor * feat: design review and fix bugs * feat: darkmode fixes and add hideSearchTab prop * fix: hide Search type if hideSearchTab force pushed to remove the old commit which included changes to lock file, which broke linter * feat: allow for tabs seperation with an optional prop * feat: change the +Key button with +Index when it's search mode * fix: sending search command even when search tab is not selected * feat: add new option "allowSearch" that would show the type filter in keys search and default to false * fix: dark mode colors * fix: update search getting started page * feat: small fixes and add docs link to query editor * chore: format * fix: minor style fixes * feat: bump redis and radix * feat: add claude.md * fix: many fixes for redis search * fix: many fixes for redis search - 2 * fix: minor styling fixes * Dx 2405 search wizard (#46) * WIP * feat: add redis search wizard * fix: redesign wizard popover * feat: add redis search wizard * fix: revert the last commit * fix: lint issues * refactor: move callback into a context and organize * refactor: minor fixes and organization --------- Co-authored-by: ytkimirti <yusuftaha9@gmail.com> --------- Co-authored-by: CahidArda <cahidardaooz@hotmail.com> Co-authored-by: Ali Tarık Şahin <112548301+alitariksahin@users.noreply.github.com>
1 parent c68ee6e commit ab2e0e1

File tree

114 files changed

+8983
-575
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

114 files changed

+8983
-575
lines changed

.github/workflows/playwright.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Playwright Tests
1+
name: E2E Test
22
on:
33
push:
44
branches: [ main, master ]

.github/workflows/tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Tests
1+
name: Lint & Test
22
on:
33
push:
44
branches:

CLAUDE.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
`@upstash/react-redis-browser` is a React component library that provides a UI for browsing and editing data in Upstash Redis instances. It exports `RedisBrowser` and `RedisBrowserStorage` as its public API. Supports string, list, hash, set, sorted set, JSON, stream, and search index data types.
8+
9+
## Architecture
10+
11+
### Provider Stack (nested in this order)
12+
13+
`DarkModeProvider``RedisProvider``QueryClientProvider``DatabrowserProvider``TabIdProvider`
14+
15+
- **RedisProvider** (`src/redis-context.tsx`): Supplies pipelined and non-pipelined Redis client instances
16+
- **DatabrowserProvider** (`src/store.tsx`): Zustand store for UI state (tabs, search, selections, filters)
17+
- **TabIdProvider** (`src/tab-provider.tsx`): Current tab context; hooks `useTabId()` and `useTab()`
18+
19+
### State Management
20+
21+
- **Zustand** with persistence middleware manages tabs, key search, type filters, selections, and search history
22+
- **React Query** handles server state (key lists, values, metadata)
23+
- **Persistence:** Optional `RedisBrowserStorage` interface for localStorage/custom storage. Schema is at version 7 with automated migrations from earlier versions.
24+
25+
### Key Source Locations
26+
27+
- `src/index.ts` — Public exports
28+
- `src/components/databrowser/index.tsx` — Root `RedisBrowser` component
29+
- `src/components/databrowser/components/sidebar/` — Key list, search, filters
30+
- `src/components/databrowser/components/search/` — Search index UI (create, query, schema editor)
31+
- `src/components/databrowser/components/display/` — Data rendering per type
32+
- `src/components/ui/` — shadcn/ui primitives (Radix-based)
33+
- `src/lib/clients.ts` — Redis client initialization and QueryClient setup
34+
- `src/playground/` — Local dev playground app
35+
36+
### CSS Namespacing
37+
38+
All generated CSS classes are prefixed with `.ups-db` via postcss-prefix-selector. Tailwind uses CSS variables for theming with class-based dark mode.
39+
40+
## Code Conventions
41+
42+
- **Semicolons:** Off (Prettier enforced)
43+
- **Quotes:** Double quotes
44+
- **Print width:** 100 characters
45+
- **Import order:** Enforced by `@ianvs/prettier-plugin-sort-imports` — React first, then third-party, then types/config/lib/hooks, then components, then relative imports
46+
- **Unused variables:** Prefix with `_` to suppress lint errors
47+
- **No `console.log`:** Only `console.warn` and `console.error` allowed
48+
- **No null:** Unicorn rule `no-null` is enabled
49+
- **Type imports:** Use `import type` consistently (`consistent-type-imports` rule)
50+
- **Pre-commit hooks:** Husky + lint-staged runs Prettier and ESLint --fix automatically

bun.lockb

22 KB
Binary file not shown.

components.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"tsx": true,
66
"tailwind": {
77
"config": "tailwind.config.js",
8-
"css": "app/globals.css",
8+
"css": "src/globals.css",
99
"baseColor": "neutral",
1010
"cssVariables": false
1111
},

package.json

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
"build": "tsup",
2424
"dev": "vite",
2525
"lint": "tsc && eslint",
26-
"fmt": "prettier --write ./src"
26+
"fmt": "prettier --write ./src",
27+
"test": "bun test src",
28+
"test:e2e": "playwright test",
29+
"prepare": "husky"
2730
},
2831
"lint-staged": {
2932
"**/*.{js,ts,tsx}": [
@@ -36,24 +39,26 @@
3639
"@dnd-kit/modifiers": "^9.0.0",
3740
"@dnd-kit/sortable": "^10.0.0",
3841
"@dnd-kit/utilities": "^3.2.2",
39-
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
42+
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
4043
"@monaco-editor/react": "^4.6.0",
41-
"@radix-ui/react-alert-dialog": "^1.0.5",
42-
"@radix-ui/react-context-menu": "^2.2.2",
43-
"@radix-ui/react-dialog": "^1.1.14",
44-
"@radix-ui/react-dropdown-menu": "^2.1.15",
45-
"@radix-ui/react-label": "^2.1.7",
46-
"@radix-ui/react-popover": "^1.0.7",
47-
"@radix-ui/react-portal": "^1.1.2",
48-
"@radix-ui/react-scroll-area": "^1.0.3",
49-
"@radix-ui/react-select": "^2.0.0",
50-
"@radix-ui/react-slot": "^1.0.2",
51-
"@radix-ui/react-toast": "^1.1.5",
52-
"@radix-ui/react-tooltip": "^1.0.7",
44+
"@radix-ui/react-context-menu": "^2.2.16",
45+
"@radix-ui/react-dialog": "^1.1.15",
46+
"@radix-ui/react-dropdown-menu": "^2.1.16",
47+
"@radix-ui/react-label": "^2.1.8",
48+
"@radix-ui/react-popover": "^1.1.15",
49+
"@radix-ui/react-portal": "^1.1.10",
50+
"@radix-ui/react-progress": "^1.1.8",
51+
"@radix-ui/react-scroll-area": "^1.2.10",
52+
"@radix-ui/react-select": "^2.2.6",
53+
"@radix-ui/react-separator": "^1.1.8",
54+
"@radix-ui/react-slot": "^1.2.4",
55+
"@radix-ui/react-switch": "^1.2.6",
56+
"@radix-ui/react-toast": "^1.2.15",
57+
"@radix-ui/react-tooltip": "^1.2.8",
5358
"@tabler/icons-react": "^3.19.0",
5459
"@tanstack/react-query": "^5.32.0",
5560
"@types/bytes": "^3.1.4",
56-
"@upstash/redis": "^1.35.8",
61+
"@upstash/redis": "1.37.0-rc.9",
5762
"bytes": "^3.1.2",
5863
"cmdk": "^1.1.1",
5964
"react-hook-form": "^7.53.0",
@@ -62,6 +67,7 @@
6267
},
6368
"devDependencies": {
6469
"@playwright/test": "^1.56.1",
70+
"@types/bun": "^1.3.7",
6571
"@types/node": "^22.8.4",
6672
"@types/react": "^18.3.12",
6773
"@types/react-dom": "^18.3.1",
@@ -74,6 +80,8 @@
7480
"dotenv": "^16.5.0",
7581
"eslint": "9.10.0",
7682
"eslint-plugin-unicorn": "55.0.0",
83+
"husky": "^9.1.7",
84+
"lint-staged": "^16.2.7",
7785
"postcss": "^8.4.31",
7886
"postcss-prefix-selector": "^2.1.0",
7987
"prettier": "^3.0.3",

playground.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/* eslint-disable no-console */
2+
import { Redis, s } from "@upstash/redis"
3+
4+
const redis = Redis.fromEnv()
5+
6+
// Random data pools
7+
const firstNames = [
8+
"Yusuf",
9+
"Fatima",
10+
"Josh",
11+
"Arda",
12+
"Ali",
13+
"Sertug",
14+
"Emma",
15+
"Liam",
16+
"Olivia",
17+
"Noah",
18+
"Ava",
19+
"Ethan",
20+
"Sophia",
21+
"Mason",
22+
"Isabella",
23+
"William",
24+
"Mia",
25+
"James",
26+
"Charlotte",
27+
"Benjamin",
28+
"Amelia",
29+
"Lucas",
30+
"Harper",
31+
"Henry",
32+
"Evelyn",
33+
"Alexander",
34+
"Abigail",
35+
"Michael",
36+
"Emily",
37+
"Daniel",
38+
"Elizabeth",
39+
"Matthew",
40+
"Sofia",
41+
"Jackson",
42+
"Avery",
43+
"Sebastian",
44+
"Ella",
45+
"David",
46+
"Scarlett",
47+
"Joseph",
48+
"Grace",
49+
"Samuel",
50+
"Chloe",
51+
"Owen",
52+
"Victoria",
53+
"John",
54+
"Riley",
55+
"Luke",
56+
"Aria",
57+
"Gabriel",
58+
]
59+
60+
const lastNames = [
61+
"Smith",
62+
"Johnson",
63+
"Williams",
64+
"Brown",
65+
"Jones",
66+
"Garcia",
67+
"Miller",
68+
"Davis",
69+
"Rodriguez",
70+
"Martinez",
71+
"Hernandez",
72+
"Lopez",
73+
"Gonzalez",
74+
"Wilson",
75+
"Anderson",
76+
"Thomas",
77+
"Taylor",
78+
"Moore",
79+
"Jackson",
80+
"Martin",
81+
"Lee",
82+
"Perez",
83+
"Thompson",
84+
"White",
85+
"Harris",
86+
"Sanchez",
87+
"Clark",
88+
"Ramirez",
89+
"Lewis",
90+
"Robinson",
91+
]
92+
93+
const emailDomains = [
94+
"gmail.com",
95+
"yahoo.com",
96+
"outlook.com",
97+
"hotmail.com",
98+
"proton.me",
99+
"icloud.com",
100+
"mail.com",
101+
]
102+
103+
const areaCodes = ["212", "310", "415", "305", "312", "206", "404", "617", "702", "503"]
104+
105+
// Helper functions
106+
function randomElement<T>(arr: T[]): T {
107+
return arr[Math.floor(Math.random() * arr.length)]
108+
}
109+
110+
function randomInt(min: number, max: number): number {
111+
return Math.floor(Math.random() * (max - min + 1)) + min
112+
}
113+
114+
function randomPhone(): string {
115+
return `${randomElement(areaCodes)}${randomInt(100, 999)}${randomInt(1000, 9999)}`
116+
}
117+
118+
function generateUser(id: number) {
119+
const firstName = randomElement(firstNames)
120+
const lastName = randomElement(lastNames)
121+
const name = `${firstName} ${lastName}`
122+
const age = randomInt(5, 65)
123+
const isStudent = age < 25 ? Math.random() > 0.3 : Math.random() > 0.8
124+
const isEmployed = age >= 18 ? Math.random() > 0.3 : false
125+
const gender = Math.random() > 0.5 ? "M" : "F"
126+
const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}${id}@${randomElement(emailDomains)}`
127+
const phone = randomPhone()
128+
129+
return {
130+
name,
131+
age,
132+
isStudent,
133+
isEmployed,
134+
gender,
135+
contact: {
136+
email,
137+
phone,
138+
},
139+
}
140+
}
141+
142+
const schema = s.object({
143+
name: s.string(),
144+
age: s.number(),
145+
isStudent: s.boolean(),
146+
isEmployed: s.boolean(),
147+
gender: s.string(),
148+
contact: s.object({
149+
email: s.string(),
150+
phone: s.string(),
151+
}),
152+
})
153+
154+
const INDEX_COUNT = 10
155+
console.log(`Creating ${INDEX_COUNT} indexes...`)
156+
157+
for (let i = 1; i <= INDEX_COUNT; i++) {
158+
const indexName = `user-index-${i}`
159+
const index = redis.search.index({
160+
name: indexName,
161+
schema: schema,
162+
})
163+
164+
try {
165+
if (await index.describe()) {
166+
console.log(`Index ${indexName} exists, dropping`)
167+
await index.drop()
168+
}
169+
} catch {
170+
// Index doesn't exist, that's fine
171+
}
172+
173+
await redis.search.createIndex({
174+
dataType: "string",
175+
prefix: "user:",
176+
name: indexName,
177+
schema: schema,
178+
})
179+
180+
console.log(`Created index ${i}/${INDEX_COUNT}: ${indexName}`)
181+
}
182+
183+
console.log("All indexes created")
184+
185+
// Create 100 user entries
186+
const USER_COUNT = 100
187+
console.log(`\nCreating ${USER_COUNT} user entries...`)
188+
189+
const userData: Record<string, string> = {}
190+
for (let i = 1; i <= USER_COUNT; i++) {
191+
const user = generateUser(i)
192+
const key = `user:${user.name.toLowerCase().replace(" ", "-")}-${i}`
193+
userData[key] = JSON.stringify(user)
194+
}
195+
196+
await redis.mset(userData)
197+
198+
console.log(`Created ${USER_COUNT} user entries`)
199+
200+
// Test query on first index
201+
const testIndex = redis.search.index({
202+
name: "user-index-1",
203+
schema: schema,
204+
})
205+
206+
console.log("\nWaiting for indexing...")
207+
await new Promise((resolve) => setTimeout(resolve, 2000))
208+
209+
const queryResult = await testIndex.query({
210+
filter: {
211+
isStudent: true,
212+
},
213+
})
214+
215+
console.log(`Test query result (students): found ${queryResult.length} results`)
216+
217+
console.log("\nDone!")
File renamed without changes.

0 commit comments

Comments
 (0)