Files
Joshen Lim 2c53ca4a79 Support listing and reading custom reports from Assistant (#48530)
## Context

This is pre-requisite work for adding support to managing custom reports
from the Assistant. Planning to break this into a number of PRs, briefly
- Adding read support for custom reports
- Adding write support for custom reports
- Adding run support for custom reports
  - Should be able to infer data from the results then

This PR starts with adding support for listing and reading custom
reports from the Assistant

## Other changes involved
- Updates setting up of the home page report to have better title and
description
- Swaps the variant of the ToggleGroup in the SQL block for custom
reports as the default variant blends into the background color of the
PopoverContent

## To test
- [ ] Assistant should be able to list custom reports + read its
contents
<img width="428" height="755" alt="image"
src="https://github.com/user-attachments/assets/6a15b660-c0ee-4a06-984c-87eff3943eec"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added AI-assisted tools to list reports and retrieve report details,
including chart counts, layouts, configurations, and SQL-backed chart
information.
  - Added clearer empty-state messaging when no snippets are available.

- **Improvements**
- New homepage reports now use the name “Homepage Report” and include a
descriptive project-home summary.
  - Updated query controls with refreshed visual styling.
  - Improved content requests to support additional request context.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-03 13:59:50 +07:00

163 lines
5.2 KiB
TypeScript

import { keepPreviousData } from '@tanstack/react-query'
import { useDebounce, useIntersectionObserver } from '@uidotdev/usehooks'
import { Plus } from 'lucide-react'
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
ScrollArea,
} from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { useContentInfiniteQuery } from '@/data/content/content-infinite-query'
import type { Content } from '@/data/content/content-query'
import { SNIPPET_PAGE_LIMIT } from '@/data/content/sql-folders-query'
import { editorPanelState } from '@/state/editor-panel-state'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
type SnippetDropdownProps = {
projectRef?: string
trigger: ReactNode
side?: 'top' | 'bottom' | 'left' | 'right'
align?: 'start' | 'center' | 'end'
className?: string
autoFocus?: boolean
onSelect: (snippet: { id: string; name: string; visibility: Content['visibility'] }) => void
}
type SqlContentItem = Extract<Content, { type: 'sql' }>
export const SnippetDropdown = ({
projectRef,
trigger,
side = 'bottom',
align = 'end',
className,
autoFocus = false,
onSelect,
}: SnippetDropdownProps) => {
const { openSidebar } = useSidebarManagerSnapshot()
const scrollRootRef = useRef<HTMLDivElement | null>(null)
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const debouncedSearch = useDebounce(search, 500)
const {
data,
isPending: isLoading,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = useContentInfiniteQuery(
{
projectRef,
type: 'sql',
limit: SNIPPET_PAGE_LIMIT,
name: search.length === 0 ? search : debouncedSearch,
},
{ placeholderData: keepPreviousData }
)
const snippets = useMemo(() => {
const items = data?.pages.flatMap((page) => page.content) ?? []
return items as SqlContentItem[]
}, [data?.pages])
const [sentinelRef, entry] = useIntersectionObserver({
root: scrollRootRef.current,
threshold: 0,
rootMargin: '0px',
})
useEffect(() => {
if (entry?.isIntersecting && hasNextPage && !isFetchingNextPage && !isLoading) {
fetchNextPage()
}
}, [entry?.isIntersecting, hasNextPage, isFetchingNextPage, isLoading, fetchNextPage])
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent
side={side}
align={align}
className={['w-80 p-0', className].filter(Boolean).join(' ')}
>
<Command shouldFilter={false}>
<CommandInput
showResetIcon
autoFocus={autoFocus}
placeholder="Search snippets..."
value={search}
onValueChange={setSearch}
handleReset={() => setSearch('')}
/>
<CommandList ref={scrollRootRef}>
{isLoading ? (
<p className="text-xs text-center text-foreground-lighter py-3">Loading...</p>
) : search.length > 0 && snippets.length === 0 ? (
<p className="text-xs text-center text-foreground-lighter py-3">No snippets found</p>
) : search.length === 0 && snippets.length === 0 ? (
<p className="text-xs text-center text-foreground-lighter py-3">
No snippets available
</p>
) : (
<CommandGroup>
<ScrollArea className={snippets.length > 7 ? 'h-[210px]' : ''}>
{snippets.map((snippet) => (
<CommandItem
key={snippet.id}
value={snippet.id}
onSelect={() =>
onSelect({
id: snippet.id,
name: snippet.name,
visibility: snippet.visibility,
})
}
>
{snippet.name}
</CommandItem>
))}
<div ref={sentinelRef} className="h-1 -mt-1" />
{hasNextPage && (
<div className="px-2 py-1">
<ShimmeringLoader className="py-2" />
</div>
)}
</ScrollArea>
</CommandGroup>
)}
<div className="h-px bg-border-overlay -mx-1" />
<CommandGroup>
<CommandItem
className="cursor-pointer w-full"
onSelect={() => {
setOpen(false)
editorPanelState.openAsNew()
openSidebar(SIDEBAR_KEYS.EDITOR_PANEL)
}}
>
<div className="w-full flex items-center gap-2">
<Plus size={14} strokeWidth={1.5} />
<p>Create snippet</p>
</div>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuContent>
</DropdownMenu>
)
}