Skip to content

DX-1485: Multi tabs and search history #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
65458ea
feat: add multiple tab support
ytkimirti May 20, 2025
ab3be4c
feat: add search history
ytkimirti May 20, 2025
ee65c53
feat: add keyboard support to the search
ytkimirti May 21, 2025
669505b
chore: unused import
ytkimirti May 21, 2025
076d253
chore: remove unusued hook
ytkimirti May 22, 2025
2921747
chore: rename the custom scroll component argument
ytkimirti May 22, 2025
92f561e
chore: update playground for easier debugging
ytkimirti May 26, 2025
f00dcf6
feat: add hideTabs option and fix styles
ytkimirti May 26, 2025
db7492e
feat: add a credentials form to playground
ytkimirti May 29, 2025
792425f
fix: use cloudflare version of redis to avoid direct process access
ytkimirti Jun 3, 2025
f9fbfc0
Merge branch 'master' into DX-1485
ytkimirti Jun 11, 2025
a6e1620
feat: add persistance support
ytkimirti Jun 11, 2025
8f7b999
fix: prevent useEffect from working when tab is not active
ytkimirti Jun 12, 2025
49fd950
fix: automatically rescan until a result shows up
ytkimirti Jun 12, 2025
a8dd13c
fix: only fetch when the tab is active
ytkimirti Jun 13, 2025
6fc3865
fix: skeleton and empty styles
ytkimirti Jun 13, 2025
c037dc3
fix: refactor tabs and improve next tab selection logic when current …
ytkimirti Jun 13, 2025
f9995f5
feat: add useOverflow utility hook
ytkimirti Jun 18, 2025
3faf449
fix: make the tooltip use portals
ytkimirti Jun 18, 2025
4a8fac7
fix: search recomendations overflowing
ytkimirti Jun 18, 2025
44f54dc
feat: show tooltip only when tabs is overflowing
ytkimirti Jun 18, 2025
f7efb5c
fix: adding new key sometimes throwing error
ytkimirti Jun 18, 2025
5bc9a90
fix: not being able to click anywhere after closing delete key modal
ytkimirti Jun 18, 2025
d30c551
feat: add "copy key" to dropdown menu
ytkimirti Jun 18, 2025
2d7875d
feat: make the "delete key" item red
ytkimirti Jun 18, 2025
3ae114f
fix: add zustand migration
ytkimirti Jun 19, 2025
3bb1d4b
fix: review
ytkimirti Jun 24, 2025
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
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-icons": "1.3.0",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-portal": "^1.1.2",
Expand Down
4 changes: 2 additions & 2 deletions src/components/databrowser/components/add-key-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useState } from "react"
import { useDatabrowserStore } from "@/store"
import { DATA_TYPES, type DataType } from "@/types"
import { DialogDescription } from "@radix-ui/react-dialog"
import { PlusIcon } from "@radix-ui/react-icons"
Expand All @@ -25,9 +24,10 @@ import {
import { Spinner } from "@/components/ui/spinner"
import { TypeTag } from "@/components/databrowser/components/type-tag"
import { useAddKey } from "@/components/databrowser/hooks/use-add-key"
import { useTab } from "@/tab-provider"

export function AddKeyModal() {
const { setSelectedKey } = useDatabrowserStore()
const { setSelectedKey } = useTab()
const [open, setOpen] = useState(false)

const { mutateAsync: addKey, isPending } = useAddKey()
Expand Down
32 changes: 32 additions & 0 deletions src/components/databrowser/components/databrowser-instance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"

import { cn } from "@/lib/utils"
import { Toaster } from "@/components/ui/toaster"
import { DataDisplay } from "./display"
import { Sidebar } from "./sidebar"
import { KeysProvider } from "../hooks/use-keys"

export const DatabrowserInstance = ({ hidden }: { hidden?: boolean }) => {
return (
<KeysProvider>
<div className={cn("h-full rounded-md bg-zinc-100", hidden && "hidden")}>
<PanelGroup
autoSaveId="persistence"
direction="horizontal"
className="h-full w-full gap-0.5 text-sm antialiased"
>
<Panel defaultSize={30} minSize={30}>
<Sidebar />
</Panel>
<PanelResizeHandle className="group flex h-full w-1.5 justify-center">
<div className="h-full border-r border-dashed border-zinc-200 transition-colors group-hover:border-zinc-300" />
</PanelResizeHandle>
<Panel minSize={40}>
<DataDisplay />
</Panel>
</PanelGroup>
<Toaster />
</div>
</KeysProvider>
)
}
65 changes: 65 additions & 0 deletions src/components/databrowser/components/databrowser-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { IconPlus, IconX } from "@tabler/icons-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import type { TabId } from "@/store"
import { useDatabrowserStore } from "@/store"
import { TabTypeIcon } from "./tab-type-icon"

const Tab = ({ id }: { id: TabId }) => {
const { selectTab, selectedTab, tabs, removeTab } = useDatabrowserStore()

return (
<div
onClick={() => selectTab(id)}
className={cn(
"flex h-9 translate-y-[1px] cursor-pointer items-center gap-2 rounded-t-lg border border-zinc-200 px-3 text-[13px] transition-colors",
id === selectedTab
? "border-b-white bg-white text-zinc-900"
: "bg-zinc-100 hover:bg-zinc-50"
)}
>
{tabs[id].selectedKey ? (
<>
<TabTypeIcon selectedKey={tabs[id].selectedKey} />
<span className="max-w-32 truncate">{tabs[id].selectedKey}</span>
</>
) : (
"New Tab"
)}
{/* Only show close button if there's more than one tab */}
{Object.keys(tabs).length > 1 && (
<button
onClick={(e) => {
e.stopPropagation()
removeTab(id)
}}
className="p-1 text-zinc-300 transition-colors hover:text-zinc-500"
>
<IconX size={16} />
</button>
)}
</div>
)
}

export const DatabrowserTabs = () => {
const { tabs, addTab } = useDatabrowserStore()

return (
<div className="mb-2 flex items-center gap-1 border-b border-zinc-200">
{Object.keys(tabs).map((id) => (
<Tab id={id as TabId} key={id} />
))}
<Button
variant="secondary"
size="icon-sm"
onClick={addTab}
className="mr-1 flex-shrink-0"
title="Add new tab"
>
<IconPlus className="text-zinc-500" size={16} />
</Button>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useDatabrowserStore } from "@/store"
import { type DataType } from "@/types"
import { IconPlus } from "@tabler/icons-react"

Expand All @@ -7,6 +6,7 @@ import { Button } from "@/components/ui/button"
import { TypeTag } from "../type-tag"
import { HeaderTTLBadge, LengthBadge, SizeBadge } from "./header-badges"
import { KeyActions } from "./key-actions"
import { useTab } from "@/tab-provider"

export const DisplayHeader = ({
dataKey,
Expand All @@ -17,14 +17,14 @@ export const DisplayHeader = ({
dataKey: string
type: DataType
}) => {
const { setSelectedListItem } = useDatabrowserStore()
const { setSelectedListItem } = useTab()

const handleAddItem = () => {
setSelectedListItem({ key: type === "stream" ? "*" : "", isNew: true })
}

return (
<div className="rounded-lg bg-zinc-100 px-3 py-2">
<div className="rounded-lg bg-zinc-100">
<div className="flex min-h-10 items-center justify-between gap-4">
<h2 className="grow truncate text-base">
{dataKey.trim() === "" ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { SelectedItem } from "@/store"
import { useDatabrowserStore } from "@/store"
import type { ListDataType } from "@/types"
import { Controller, FormProvider, useForm, useFormContext } from "react-hook-form"

Expand All @@ -13,6 +12,7 @@ import { useEditListItem } from "../../hooks/use-edit-list-item"
import { headerLabels } from "./display-list"
import { HashFieldTTLBadge } from "./hash/hash-field-ttl-badge"
import { useField } from "./input/use-field"
import { useTab } from "@/tab-provider"

export const ListEditDisplay = ({
dataKey,
Expand All @@ -24,7 +24,7 @@ export const ListEditDisplay = ({
item: SelectedItem
}) => {
return (
<div className="grow rounded-md bg-zinc-100 p-3">
<div className="grow rounded-md bg-zinc-100">
<ListEditForm key={item.key} item={item} type={type} dataKey={dataKey} />
</div>
)
Expand Down Expand Up @@ -62,7 +62,7 @@ const ListEditForm = ({
})

const { mutateAsync: editItem, isPending } = useEditListItem()
const { setSelectedListItem } = useDatabrowserStore()
const { setSelectedListItem } = useTab()

const [keyLabel, valueLabel] = headerLabels[type]

Expand Down
24 changes: 11 additions & 13 deletions src/components/databrowser/components/display/display-list.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useMemo } from "react"
import { useDatabrowserStore } from "@/store"
import type { ListDataType } from "@/types"
import { IconTrash } from "@tabler/icons-react"
import type { InfiniteData, UseInfiniteQueryResult } from "@tanstack/react-query"
Expand All @@ -15,6 +14,7 @@ import { DeleteAlertDialog } from "./delete-alert-dialog"
import { DisplayHeader } from "./display-header"
import { ListEditDisplay } from "./display-list-edit"
import { HashFieldTTLInfo } from "./hash/hash-field-ttl-info"
import { useTab } from "@/tab-provider"

export const headerLabels = {
list: ["Index", "Content"],
Expand All @@ -25,7 +25,7 @@ export const headerLabels = {
} as const

export const ListDisplay = ({ dataKey, type }: { dataKey: string; type: ListDataType }) => {
const { selectedListItem } = useDatabrowserStore()
const { selectedListItem } = useTab()
const query = useFetchListItems({ dataKey, type })

return (
Expand All @@ -38,15 +38,13 @@ export const ListDisplay = ({ dataKey, type }: { dataKey: string; type: ListData

<div className={cn("min-h-0 grow", selectedListItem && "hidden")}>
<InfiniteScroll query={query}>
<div className="pr-3">
<table className="w-full">
<ItemContextMenu dataKey={dataKey} type={type}>
<tbody>
<ListItems dataKey={dataKey} type={type} query={query} />
</tbody>
</ItemContextMenu>
</table>
</div>
<table className="w-full">
<ItemContextMenu dataKey={dataKey} type={type}>
<tbody>
<ListItems dataKey={dataKey} type={type} query={query} />
</tbody>
</ItemContextMenu>
</table>
</InfiniteScroll>
</div>
</div>
Expand All @@ -71,7 +69,7 @@ export const ListItems = ({
type: ListDataType
dataKey: string
}) => {
const { setSelectedListItem } = useDatabrowserStore()
const { setSelectedListItem } = useTab()
const keys = useMemo(() => query.data?.pages.flatMap((page) => page.keys) ?? [], [query.data])
const fields = useMemo(() => keys.map((key) => key.key), [keys])
const { mutate: editItem } = useEditListItem()
Expand All @@ -86,7 +84,7 @@ export const ListItems = ({
onClick={() => {
setSelectedListItem({ key })
}}
className={cn("h-10 border-b border-b-zinc-100 hover:bg-zinc-100 ")}
className={cn("h-10 border-b border-b-zinc-100 transition-colors hover:bg-zinc-100")}
>
<td
className={cn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ export const EditorDisplay = ({ dataKey, type }: { dataKey: string; type: Simple
<div className="flex h-full w-full flex-col gap-2">
<DisplayHeader dataKey={dataKey} type={type} content={data ?? undefined} />

<div
className="flex h-full grow flex-col gap-2
rounded-md bg-zinc-100 p-3"
>
<div className="flex h-full grow flex-col gap-2 rounded-md bg-zinc-100">
{data === undefined ? (
<Spinner isLoadingText={""} isLoading={true} />
) : data === null ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import bytes from "bytes"

import { Skeleton } from "@/components/ui/skeleton"

import { useFetchKeyExpire, useSetTTL } from "../../hooks"
import { useFetchTTL, useSetTTL } from "../../hooks"
import { useFetchKeyLength } from "../../hooks/use-fetch-key-length"
import { useFetchKeySize } from "../../hooks/use-fetch-key-size"
import { TTLBadge } from "./ttl-badge"
Expand Down Expand Up @@ -46,7 +46,7 @@ export const SizeBadge = ({ dataKey }: { dataKey: string }) => {
}

export const HeaderTTLBadge = ({ dataKey }: { dataKey: string }) => {
const { data: expireAt } = useFetchKeyExpire(dataKey)
const { data: expireAt } = useFetchTTL(dataKey)
const { mutate: setTTL, isPending } = useSetTTL()

return (
Expand Down
7 changes: 4 additions & 3 deletions src/components/databrowser/components/display/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
/* eslint-disable unicorn/no-negated-condition */
import { useDatabrowserStore } from "@/store"

import { useKeys, useKeyType } from "../../hooks/use-keys"
import { ListDisplay } from "./display-list"
import { EditorDisplay } from "./display-simple"
import { useTab } from "@/tab-provider"

export const DataDisplay = () => {
const { selectedKey } = useDatabrowserStore()
const { selectedKey } = useTab()

const { query } = useKeys()
const type = useKeyType(selectedKey)

return (
<div className="h-full rounded-xl border bg-white p-1">
<div className="h-full p-4">
{!selectedKey ? (
<div />
) : !type ? (
Expand Down
5 changes: 3 additions & 2 deletions src/components/databrowser/components/sidebar/db-size.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useDatabrowser } from "@/store"
import { useRedis } from "@/redis-context"
import { useQuery } from "@tanstack/react-query"

import { formatNumber } from "@/lib/utils"
Expand All @@ -7,7 +7,8 @@ import { Skeleton } from "@/components/ui/skeleton"
export const FETCH_DB_SIZE_QUERY_KEY = "fetch-db-size"

export const DisplayDbSize = () => {
const { redis } = useDatabrowser()
const { redis } = useRedis()

const { data: keyCount } = useQuery({
queryKey: [FETCH_DB_SIZE_QUERY_KEY],
queryFn: async () => {
Expand Down
6 changes: 3 additions & 3 deletions src/components/databrowser/components/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export function Sidebar() {
const { keys, query } = useKeys()

return (
<div className="flex h-full flex-col gap-2 rounded-xl border bg-white p-1">
<div className="rounded-lg bg-zinc-100 px-3 py-2">
<div className="flex h-full flex-col gap-2 p-4">
<div className="rounded-lg bg-zinc-100">
{/* Meta */}
<div className="flex h-10 items-center justify-between pl-1">
<DisplayDbSize />
Expand Down Expand Up @@ -68,7 +68,7 @@ export function Sidebar() {
<LoadingSkeleton />
) : keys.length > 0 ? (
// Infinite scroll already has a loader at the bottom
<InfiniteScroll query={query}>
<InfiniteScroll query={query} roundedInherit={false}>
<KeysList />
</InfiniteScroll>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import { ScrollArea } from "@/components/ui/scroll-area"
export const InfiniteScroll = ({
query,
children,
...props
}: PropsWithChildren<{
query: UseInfiniteQueryResult
}>) => {
}> &
React.ComponentProps<typeof ScrollArea>) => {
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const { scrollTop, clientHeight, scrollHeight } = e.currentTarget
if (scrollTop + clientHeight > scrollHeight - 100) {
Expand All @@ -23,8 +25,9 @@ export const InfiniteScroll = ({
return (
<ScrollArea
type="always"
className="block h-full w-full transition-all"
className="block h-full w-full overflow-visible rounded-lg border border-zinc-200 bg-white p-1 pr-3 transition-all"
onScroll={handleScroll}
{...props}
>
{children}

Expand Down
Loading