mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## 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 -->
163 lines
5.2 KiB
TypeScript
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>
|
|
)
|
|
}
|