Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/components/common/editor-with-types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ const MonacoEditorWithTypes = ({
definitionLinkOpensInPeek: false,
contextmenu: false,
}}
className="[&_.current-line]:!border-none [&_.current-line]:!bg-emerald-50 [&_.monaco-editor-background]:!bg-transparent [&_.monaco-editor]:!bg-transparent [&_[role='presentation']]:!bg-transparent"
className="[&_.current-line]:!border-none [&_.focused_.current-line]:!bg-emerald-50 [&_.monaco-editor-background]:!bg-transparent [&_.monaco-editor]:!bg-transparent [&_[role='presentation']]:!bg-transparent"
/>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Header } from "./header"
import { HeaderError } from "./header-error"
import { QueryBuilder } from "./query-builder"
import { QueryBuilderError } from "./query-builder-error"
import { WizardButton } from "./query-wizard/wizard-button"
import { SearchEmptyState } from "./search-empty-state"
import { Sidebar } from "./sidebar"
import { UIQueryBuilder } from "./ui-query-builder"
Expand Down Expand Up @@ -51,7 +52,8 @@ const QueryBuilderContent = () => {

return (
<div className="relative flex h-full min-h-0 flex-col">
<div className="absolute right-4 top-4 z-[2]">
<div className="absolute right-4 top-4 z-[2] flex items-center gap-2">
<WizardButton />
<Segmented
options={[
{ key: "ui", label: "Query Builder" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const SizeBadge = ({ dataKey }: { dataKey: string }) => {
) : (
bytes(size, {
unitSeparator: " ",
decimalPlaces: 1,
})
)}
</Badge>
Expand All @@ -59,7 +60,7 @@ export const HeaderTTLBadge = ({ dataKey }: { dataKey: string }) => {
}

export const Badge = ({ children, label }: { children: React.ReactNode; label: string }) => (
<div className="flex h-[26px] items-center gap-0.5 whitespace-nowrap rounded-md bg-zinc-200 px-2 text-xs text-zinc-700 dark:bg-zinc-300">
<div className="flex h-[26px] items-center gap-1 whitespace-nowrap rounded-md bg-zinc-200 px-2 text-xs text-zinc-700 dark:bg-zinc-300">
<span className="text-zinc-500 dark:text-zinc-600">{label}</span>
<span className="font-medium">{children}</span>
</div>
Expand Down
90 changes: 20 additions & 70 deletions src/components/databrowser/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,22 @@ import {
IconCircleCheck,
IconCirclePlus,
IconLoader2,
IconPlus,
IconSearch,
IconSparkles,
} from "@tabler/icons-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Segmented } from "@/components/ui/segmented"
import { SimpleTooltip } from "@/components/ui/tooltip"

import type { TabType } from "../.."
import { useDebounce } from "../../hooks/use-debounce"
import { useFetchSearchIndexes } from "../../hooks/use-fetch-search-indexes"
import { AddKeyModal } from "../add-key-modal"
import { QueryWizardPopover } from "../query-wizard/query-wizard-popover"
import { useQueryWizardFn } from "../query-wizard/use-query-wizard"
import { AddIndexKeyButton } from "../search/add-index-key-modal"
import { CreateIndexModal } from "../search/create-index-modal"
import { EditIndexModal } from "../search/edit-index-modal"
import { ExportResultsButton } from "../search/export-results-button"
import { IndexActionsMenu } from "../search/index-actions-menu"
import { RefreshButton } from "./refresh-button"
import { SearchInput } from "./search-input"
import { DataTypeSelector } from "./type-selector"
Expand All @@ -43,14 +40,7 @@ export const Header = ({ tabType, allowSearch }: { tabType: TabType; allowSearch
},
{
key: "values",
label: (
<div className="flex items-center gap-1">
Search
<div className="flex h-[18px] items-center rounded-md bg-emerald-100 px-[5px] text-[11px] text-emerald-700">
NEW
</div>
</div>
),
label: "Search",
},
]}
value={isValuesSearchSelected ? "values" : "keys"}
Expand All @@ -63,7 +53,10 @@ export const Header = ({ tabType, allowSearch }: { tabType: TabType; allowSearch
/>
)}
{isValuesSearchSelected ? (
<IndexSelector />
<>
<IndexSelector />
<IndexActionsMenu />
</>
) : (
<>
<DataTypeSelector allowSearch={allowSearch} />
Expand All @@ -74,9 +67,9 @@ export const Header = ({ tabType, allowSearch }: { tabType: TabType; allowSearch

{/* Actions */}
<div className="flex items-center gap-1.5">
{isValuesSearchSelected && <WizardButton />}
<RefreshButton />
{isValuesSearchSelected ? <AddIndexButton /> : <AddKeyModal />}
{isValuesSearchSelected && <ExportResultsButton />}
{isValuesSearchSelected ? <AddIndexKeyButton /> : <AddKeyModal />}
</div>
</div>
)
Expand Down Expand Up @@ -165,15 +158,19 @@ const IndexSelector = () => {
{indexes?.map((idx) => (
<div
key={idx}
className="flex h-9 items-center rounded-md px-2 transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-200"
className="relative flex h-9 items-center rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-200"
>
{/* Full-row overlay so the whole option (including padding) is clickable */}
<button
type="button"
aria-label={`Select ${idx}`}
onClick={() => {
setValuesSearchIndex(idx)
setOpen(false)
}}
className="flex min-w-0 flex-1 items-center gap-2 text-left text-sm"
>
className="absolute inset-0 rounded-md"
/>
<div className="pointer-events-none flex min-w-0 flex-1 items-center gap-2 px-2 text-left text-sm">
<span
className={cn(
"flex size-5 shrink-0 items-center justify-center",
Expand All @@ -183,14 +180,15 @@ const IndexSelector = () => {
<IconCircleCheck className="size-5" />
</span>
<span className="truncate">{idx}</span>
</button>
</div>
<button
type="button"
onClick={(event) => {
event.stopPropagation()
event.preventDefault()
handleEditIndex(idx)
}}
className="ml-2 text-sm text-zinc-500 underline hover:text-zinc-700"
className="relative z-10 mr-2 text-sm text-zinc-500 underline hover:text-zinc-700"
>
Edit
</button>
Expand Down Expand Up @@ -239,51 +237,3 @@ const CreateIndexButton = () => {
</>
)
}

const AddIndexButton = () => {
const [open, setOpen] = useState(false)

return (
<>
<SimpleTooltip content="Create new Index">
<Button
variant="primary"
onClick={() => setOpen(true)}
className="flex h-8 select-none items-center gap-1 rounded-lg pl-2 pr-3 text-sm font-medium"
>
<IconPlus className="size-5" />
Index
</Button>
</SimpleTooltip>
<CreateIndexModal open={open} onOpenChange={setOpen} />
</>
)
}

const WizardButton = () => {
const queryWizard = useQueryWizardFn()
const [open, setOpen] = useState(false)

if (!queryWizard) return null

return (
<Popover open={open} onOpenChange={setOpen} modal={false}>
<SimpleTooltip content="Query Wizard">
<PopoverTrigger asChild>
<Button size="icon" aria-label="Query Wizard">
<IconSparkles className="size-4 text-zinc-500" />
</Button>
</PopoverTrigger>
</SimpleTooltip>
<PopoverContent
side="bottom"
align="end"
alignOffset={-124}
avoidCollisions={false}
className="w-auto p-0"
>
<QueryWizardPopover onClose={() => setOpen(false)} />
</PopoverContent>
</Popover>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { IconChevronRight } from "@tabler/icons-react"
import { useMutation } from "@tanstack/react-query"

import { scanKeys } from "@/lib/scan-keys"
import { toJsLiteral } from "@/lib/utils"
import { parseJSObjectLiteral, toJsLiteral } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Spinner } from "@/components/ui/spinner"

Expand Down Expand Up @@ -81,7 +81,13 @@ export const QueryWizardPopover = ({ onClose }: { onClose?: () => void }) => {
sampleData: samples,
})

const queryString = toJsLiteral(result.query)
// Format/beautify the generated query with the same formatter used elsewhere.
// Guard against the model returning an already-stringified query object.
const generated =
typeof result.query === "string"
? (parseJSObjectLiteral<unknown>(result.query) ?? result.query)
: result.query
const queryString = toJsLiteral(generated)
setValuesSearchQuery(queryString)

setQueryBuilderMode("code")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useState } from "react"
import { IconSparkles } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { SimpleTooltip } from "@/components/ui/tooltip"

import { QueryWizardPopover } from "./query-wizard-popover"
import { useQueryWizardFn } from "./use-query-wizard"

export const WizardButton = () => {
const queryWizard = useQueryWizardFn()
const [open, setOpen] = useState(false)

if (!queryWizard) return null

return (
<Popover open={open} onOpenChange={setOpen} modal={false}>
<SimpleTooltip content="Query Wizard">
<PopoverTrigger asChild>
<Button
size="icon"
aria-label="Query Wizard"
className="h-7 w-7 border border-purple-300 bg-purple-50 hover:bg-purple-100"
>
<IconSparkles className="size-5 text-purple-500" />
</Button>
</PopoverTrigger>
</SimpleTooltip>
<PopoverContent side="bottom" align="end" sideOffset={6} className="w-auto p-0">
<QueryWizardPopover onClose={() => setOpen(false)} />
</PopoverContent>
</Popover>
)
}
126 changes: 126 additions & 0 deletions src/components/databrowser/components/search/add-index-key-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useState } from "react"
import { useTab } from "@/tab-provider"
import type { DataType } from "@/types"
import { DialogDescription } from "@radix-ui/react-dialog"
import { IconPlus } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Spinner } from "@/components/ui/spinner"
import { SimpleTooltip } from "@/components/ui/tooltip"
import { toast } from "@/components/ui/use-toast"
import { TypeTag } from "@/components/databrowser/components/type-tag"
import { useAddKey } from "@/components/databrowser/hooks/use-add-key"
import { useFetchSearchIndex } from "@/components/databrowser/hooks/use-fetch-search-index"

export function AddIndexKeyButton() {
const { valuesSearch, setSelectedKey } = useTab()
const indexName = valuesSearch.index

const [open, setOpen] = useState(false)
const [suffix, setSuffix] = useState("")
const [error, setError] = useState<string>()

const { data: indexDetails } = useFetchSearchIndex(indexName)
const dataType = (indexDetails?.dataType ?? "string") as DataType
const prefix = indexDetails?.prefixes?.[0] ?? ""

const { mutateAsync: addKey, isPending } = useAddKey()

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(undefined)
const trimmedSuffix = suffix.trim()
if (!trimmedSuffix) {
setError("Please enter a key name")
return
}
const key = `${prefix}${trimmedSuffix}`
try {
await addKey({ key, type: dataType })
setSelectedKey(key)
setOpen(false)
toast({ description: `Key "${key}" created` })
} catch (error_) {
setError(error_ instanceof Error ? error_.message : "Failed to create key")
}
}

return (
<Dialog
open={open}
onOpenChange={(next) => {
if (next) {
setSuffix("")
setError(undefined)
}
setOpen(next)
}}
>
<SimpleTooltip content={indexName ? "Add key to index" : "Select an index first"}>
<Button
variant="primary"
disabled={!indexName}
onClick={() => setOpen(true)}
className="flex h-8 select-none items-center gap-1 rounded-lg pl-2 pr-3 text-sm font-medium"
>
<IconPlus className="size-5" />
Key
</Button>
</SimpleTooltip>

<DialogContent className="max-w-[400px]">
<DialogHeader>
<DialogTitle>Add Key to Index</DialogTitle>
</DialogHeader>
<div className="sr-only">
<DialogDescription>Add a new key to the selected search index</DialogDescription>
</div>

<form className="mt-2 flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex items-center gap-2 text-xs text-zinc-500">
<span>Data type</span>
<TypeTag variant={dataType} type="badge" />
</div>

<div className="flex flex-col gap-1.5">
<Label htmlFor="key-suffix">Key</Label>
<div className="flex">
{prefix && (
<span className="flex select-none items-center whitespace-nowrap rounded-l-md border border-r-0 border-zinc-300 bg-zinc-100 px-2 text-sm text-zinc-500">
{prefix}
</span>
)}
<Input
id="key-suffix"
autoFocus
value={suffix}
onChange={(e) => setSuffix(e.target.value)}
placeholder="123"
className={prefix ? "h-8 rounded-l-none" : "h-8"}
/>
</div>
<p className="text-xs text-zinc-500">
The key is prefixed to match the index and created with its data type.
</p>
</div>

{error && <p className="text-xs text-red-500">{error}</p>}

<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button variant="primary" type="submit" disabled={isPending}>
<Spinner isLoading={isPending} isLoadingText="Creating">
Create
</Spinner>
</Button>
</div>
</form>
</DialogContent>
</Dialog>
)
}
Loading
Loading