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

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
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 index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
</head>
<body>
<div id="root"></div>
<script src="/src/playground.tsx" type="module"></script>
<script src="/src/playground/index.tsx" type="module"></script>
</body>
</html>
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
"@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-label": "^2.1.7",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-portal": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.0.3",
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>
Copy link
Preview

Copilot AI Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new useTab hook is used in many child components but there's no TabProvider wrapping the tree. Wrap the content (e.g., around KeysProvider) with <TabProvider> so that useTab has the required context.

Copilot uses AI. Check for mistakes.

<div className={cn("min-h-0 grow 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-3 justify-center">
<div className="h-full border-r border-dashed border-zinc-200 transition-colors group-hover:border-zinc-500" />
</PanelResizeHandle>
<Panel minSize={40}>
<DataDisplay />
</Panel>
</PanelGroup>
<Toaster />
</div>
</KeysProvider>
)
}
33 changes: 33 additions & 0 deletions src/components/databrowser/components/databrowser-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { IconPlus } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import type { TabId } from "@/store"
import { useDatabrowserStore } from "@/store"
import { TabIdProvider } from "@/tab-provider"
import { Tab } from "./tab"

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

return (
<div className="relative mb-2 shrink-0">
<div className="absolute bottom-0 left-0 right-0 -z-10 h-[1px] w-full bg-zinc-200" />

<div className="scrollbar-hide flex translate-y-[1px] items-center gap-1 overflow-x-scroll pb-[1px] [&::-webkit-scrollbar]:hidden">
{tabs.map(([id]) => (
<TabIdProvider key={id} value={id as TabId}>
<Tab id={id} />
</TabIdProvider>
))}
<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>
</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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Editor, useMonaco } from "@monaco-editor/react"

import { cn } from "@/lib/utils"
import { CopyButton } from "@/components/databrowser/copy-button"
import { useTab } from "@/tab-provider"

export const CustomEditor = ({
language,
Expand All @@ -19,17 +20,18 @@ export const CustomEditor = ({
showCopyButton?: boolean
readOnly?: boolean
}) => {
const { active } = useTab()
const monaco = useMonaco()
const editorRef = useRef()

useEffect(() => {
if (!monaco || !editorRef.current) {
if (!active || !monaco || !editorRef.current) {
return
}

// @ts-expect-error not typing the editor type
monaco?.editor.setModelLanguage(editorRef.current.getModel(), language)
}, [monaco, language])
}, [monaco, language, active])

return (
<div
Expand Down
16 changes: 14 additions & 2 deletions src/components/databrowser/components/display/key-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function KeyActions({ dataKey, content }: { dataKey: string; content?: st
const { mutateAsync: deleteKey } = useDeleteKey()

return (
<DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button size="icon-sm">
<IconDotsVertical className="size-4 text-zinc-500" />
Expand All @@ -36,11 +36,23 @@ export function KeyActions({ dataKey, content }: { dataKey: string; content?: st
Copy content
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(dataKey)
}}
>
Copy key
</DropdownMenuItem>
<DeleteAlertDialog
deletionType="key"
onDeleteConfirm={async () => await deleteKey(dataKey)}
>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>Delete key</DropdownMenuItem>
<DropdownMenuItem
className="text-red-500 focus:bg-red-500 focus:text-white"
onSelect={(e) => e.preventDefault()}
>
Delete key
</DropdownMenuItem>
</DeleteAlertDialog>
</DropdownMenuContent>
</DropdownMenu>
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
2 changes: 1 addition & 1 deletion src/components/databrowser/components/sidebar/empty.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const Empty = () => {
return (
<div className="flex h-full w-full items-center justify-center rounded-md border border-dashed px-4 py-6 text-center">
<div className="flex h-full w-full items-center justify-center rounded-md border bg-white px-4 py-6 text-center">
<div className="space-y-5">
<p className="text-md font-medium">Data on a break</p>
<p className="text-balance text-center">"Quick, lure it back with some CLI magic!"</p>
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} disableRoundedInherit className="min-h-0">
<KeysList />
</InfiniteScroll>
) : (
Expand Down
Loading