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 8 commits into
base: master
Choose a base branch
from
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
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("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-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>
)
}
69 changes: 69 additions & 0 deletions src/components/databrowser/components/databrowser-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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 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 whitespace-nowrap">{tabs[id].selectedKey}</span>
</>
) : (
<span className="whitespace-nowrap">New Tab</span>
)}
{/* 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="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">
{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>
</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} disableRoundedInherit className="min-h-0">
<KeysList />
</InfiniteScroll>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { IconLoader2 } from "@tabler/icons-react"
import type { UseInfiniteQueryResult } from "@tanstack/react-query"

import { ScrollArea } from "@/components/ui/scroll-area"
import { cn } from "@/lib/utils"

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 +26,12 @@ export const InfiniteScroll = ({
return (
<ScrollArea
type="always"
className="block h-full w-full transition-all"
onScroll={handleScroll}
{...props}
className={cn(
"block h-full w-full overflow-visible rounded-lg border border-zinc-200 bg-white p-1 pr-3 transition-all",
props.className
)}
>
{children}

Expand Down
Loading