Consolidate grid header actions in table editor into a single row (#45504)

## Consolidate Table Editor grid header actions into a single row


https://github.com/user-attachments/assets/1020c385-8fa9-4ef1-b5e7-03983111508b

## Changes involved
- Index advisor, Realtime, and API docs are now behind a dropdown menu
button (Treated as secondary actions)
- Grid header actions shifted into the same row as filter bar (more
space for data grid)
- Header actions will hide while filter bar is in focus (remove
distractions, more space for filter bar)

## Changes to filter bar
- Filter bar will refocus when deleting a filter
- Clicking on the search icon will focus on the free form input of the
filter bar

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

* **New Features**
* Added a “More” dropdown in grid actions to access Realtime, API docs,
and Index Advisor.
* New dialogs for enabling Index Advisor and toggling Realtime are now
consistently managed.

* **Improvements**
* Improved filter focus handling with auto-refocus when conditions
change and responsive header behavior.
* Adjusted popover alignment, separator visuals,
header/footer/pagination layout and sizing.
* Filter bar now supports programmatic focus; Connect button supports
icon-only mode.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
This commit is contained in:
Joshen Lim
2026-05-06 10:53:49 +08:00
committed by GitHub
parent aee4c8fdd7
commit f7ea722b35
16 changed files with 325 additions and 133 deletions

View File

@@ -30,7 +30,7 @@ export const Footer: React.FC<FooterProps> = ({ enableForeignRowsQuery = true }:
<GridFooter>
{selectedView === 'data' && <Pagination enableForeignRowsQuery={enableForeignRowsQuery} />}
<div className="ml-auto flex items-center gap-x-2">
<div className="flex items-center gap-x-2">
{(isViewSelected || isTableSelected) && (
<TwoOptionToggle
width={75}

View File

@@ -235,7 +235,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
}
return (
<div className="flex items-center gap-x-4">
<div className="flex items-center gap-x-4 min-w-fit">
<div className="flex items-center gap-x-2">
<Button
aria-label="Previous page"

View File

@@ -1,7 +1,8 @@
import { keepPreviousData } from '@tanstack/react-query'
import { useParams } from 'common'
import { useBreakpoint, useParams } from 'common'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronDown, Trash } from 'lucide-react'
import { ReactNode, useState } from 'react'
import { ReactNode, useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
@@ -49,32 +50,78 @@ export type HeaderProps = {
export const Header = ({ customHeader, isRefetching, tableQueriesEnabled = true }: HeaderProps) => {
useInitializeFiltersFromUrl()
useSyncFiltersToUrl()
const isMobile = useBreakpoint('md')
const snap = useTableEditorTableStateSnapshot()
const [isInputFocus, setIsInputFocus] = useState(false)
const filterContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!isInputFocus) return
const handleMouseDown = (e: MouseEvent) => {
const target = e.target as Element
const withinFilter = filterContainerRef.current?.contains(target)
const withinPortal = target?.closest?.('[data-radix-popper-content-wrapper]')
if (!withinFilter && !withinPortal) {
setIsInputFocus(false)
}
}
document.addEventListener('mousedown', handleMouseDown)
return () => document.removeEventListener('mousedown', handleMouseDown)
}, [isInputFocus])
return (
<div>
<div className="flex flex-wrap min-h-10 items-center bg-dash-sidebar dark:bg-surface-100">
{customHeader ? (
<div className="flex-1 px-1.5">{customHeader}</div>
) : snap.selectedRows.size > 0 ? (
<div className="flex-1 px-1.5">
<RowHeader tableQueriesEnabled={tableQueriesEnabled} />
</div>
) : (
<div className="w-full flex items-center justify-between gap-2 pr-1.5 py-1.5 border-b border-border">
<FilterPopoverNew isRefetching={isRefetching} />
</div>
)}
<div className="flex items-center gap-2 overflow-x-auto px-1.5 py-1.5">
{!customHeader && snap.selectedRows.size === 0 && (
<SortPopover tableQueriesEnabled={tableQueriesEnabled} />
<div className="flex flex-wrap md:min-h-10 items-center bg-dash-sidebar dark:bg-surface-100">
{customHeader ? (
<div className="flex-1 px-1.5">{customHeader}</div>
) : snap.selectedRows.size > 0 ? (
<div className="flex-1 px-1.5">
<RowHeader tableQueriesEnabled={tableQueriesEnabled} />
</div>
) : (
<div
ref={filterContainerRef}
className="w-full flex items-center justify-between gap-2 pr-1.5 border-b border-border md:border-none pt-1 md:pt-0"
>
<FilterPopoverNew
isRefetching={isRefetching}
onInputFocus={() => setIsInputFocus(true)}
onInputBlur={() => setIsInputFocus(false)}
/>
{!isMobile && (
<AnimatePresence>
{!isInputFocus && (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{
type: 'spring',
stiffness: 420,
damping: 30,
mass: 0.4,
}}
className="hidden md:flex items-center gap-2 overflow-x-auto"
>
<SortPopover tableQueriesEnabled={tableQueriesEnabled} />
<GridHeaderActions table={snap.originalTable} isRefetching={isRefetching} />
</motion.div>
)}
</AnimatePresence>
)}
</div>
)}
{isMobile && (
<div className="flex items-center gap-2 overflow-x-auto px-1.5 py-1.5">
<SortPopover tableQueriesEnabled={tableQueriesEnabled} />
<GridHeaderActions table={snap.originalTable} isRefetching={isRefetching} />
</div>
</div>
)}
</div>
)
}
@@ -270,6 +317,7 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
<Shortcut
id={SHORTCUT_IDS.TABLE_EDITOR_DELETE_SELECTED_ROWS}
onTrigger={onRowsDelete}
side="bottom"
options={{
registerInCommandMenu: true,
enabled: !(snap.allRowsSelected && isImpersonatingRole),
@@ -366,12 +414,13 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
{snap.selectedRows.size > 0 && totalRows > allRows.length && (
<>
<div className="h-6 ml-0.5">
<Separator orientation="vertical" />
<Separator orientation="vertical" className="bg-border" />
</div>
<Shortcut
id={SHORTCUT_IDS.TABLE_EDITOR_SELECT_ALL_IN_TABLE}
onTrigger={onToggleSelectAllInTable}
options={{ registerInCommandMenu: true }}
side="bottom"
>
<Button type="text" onClick={onToggleSelectAllInTable}>
{snap.allRowsSelected ? 'Deselect all rows in table' : 'Select all rows in table'}

View File

@@ -1,10 +1,11 @@
import { format } from 'date-fns'
import { Loader2 } from 'lucide-react'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import { AiIconAnimation, Button, Calendar } from 'ui'
import {
CustomOptionProps,
FilterBar,
FilterBarHandle,
FilterGroup,
FilterOption,
FilterProperty,
@@ -21,6 +22,8 @@ import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
export interface FilterPopoverProps {
isRefetching?: boolean
onInputFocus?: () => void
onInputBlur?: () => void
}
// Convert Filter[] to FilterGroup
@@ -118,9 +121,14 @@ function serializeFilterProperties(
}))
}
export const FilterPopoverNew = ({ isRefetching = false }: FilterPopoverProps) => {
export const FilterPopoverNew = ({
isRefetching = false,
onInputFocus,
onInputBlur,
}: FilterPopoverProps) => {
const { filters, setFilters } = useTableFilter()
const snap = useTableEditorTableStateSnapshot()
const filterBarRef = useRef<FilterBarHandle>(null)
const [freeformText, setFreeformText] = useState('')
const { mutateAsync: generateFilters, isPending: isGenerating } = useSqlFilterGenerateMutation()
@@ -160,9 +168,14 @@ export const FilterPopoverNew = ({ isRefetching = false }: FilterPopoverProps) =
const handleFilterChange = useCallback(
(newFilterGroup: FilterGroup) => {
const newFilters = filterGroupToFilters(newFilterGroup)
const conditionRemoved = newFilters.length < filters.length
setFilters(newFilters)
if (conditionRemoved) {
setTimeout(() => filterBarRef.current?.focus(), 0)
}
},
[setFilters]
[filters.length, setFilters]
)
const actions = useMemo(
@@ -199,8 +212,9 @@ export const FilterPopoverNew = ({ isRefetching = false }: FilterPopoverProps) =
) : null
return (
<div className="flex-1 min-w-0">
<div className="flex-1 min-w-0" onFocus={() => onInputFocus?.()} onBlur={() => onInputBlur?.()}>
<FilterBar
ref={filterBarRef}
filterProperties={filterProperties}
filters={filterGroup}
onFilterChange={handleFilterChange}

View File

@@ -243,7 +243,7 @@ export const SortPopoverPrimitive = ({
{displayButtonText}
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="p-0 w-96" side="bottom" align="start">
<PopoverContent_Shadcn_ className="p-0 w-96" side="bottom" align="center">
<div className="space-y-2 py-2">
<DndContext
sensors={sensors}

View File

@@ -12,9 +12,14 @@ import { useAppStateSnapshot } from '@/state/app-state'
interface ConnectButtonProps {
buttonType?: ComponentProps<typeof Button>['type']
className?: string
iconOnly?: boolean
}
export const ConnectButton = ({ buttonType = 'default', className }: ConnectButtonProps) => {
export const ConnectButton = ({
buttonType = 'default',
className,
iconOnly = false,
}: ConnectButtonProps) => {
const { data: selectedProject } = useSelectedProjectQuery()
const { setConnectSheetSource } = useAppStateSnapshot()
const isActiveHealthy = selectedProject?.status === PROJECT_STATUS.ACTIVE_HEALTHY
@@ -42,7 +47,7 @@ export const ConnectButton = ({ buttonType = 'default', className }: ConnectButt
},
}}
>
<span>Connect</span>
<span className={cn({ 'sr-only': !iconOnly })}>Connect</span>
</ButtonTooltip>
)
}

View File

@@ -9,7 +9,6 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
} from 'ui'
@@ -21,10 +20,34 @@ import { useTrack } from '@/lib/telemetry/track'
export const EnableIndexAdvisorButton = () => {
const track = useTrack()
const { data: project } = useSelectedProjectQuery()
const [isDialogOpen, setIsDialogOpen] = useState(false)
return (
<>
<Button
type="primary"
onClick={() => {
setIsDialogOpen(true)
track('index_advisor_banner_enable_button_clicked')
}}
>
Enable
</Button>
<EnableIndexAdvisorDialog open={isDialogOpen} setOpen={setIsDialogOpen} />
</>
)
}
export const EnableIndexAdvisorDialog = ({
open,
setOpen,
}: {
open: boolean
setOpen: (value: boolean) => void
}) => {
const track = useTrack()
const { data: project } = useSelectedProjectQuery()
const { data: extensions } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
@@ -60,27 +83,27 @@ export const EnableIndexAdvisorButton = () => {
})
}
toast.success('Successfully enabled Index Advisor!')
setIsDialogOpen(false)
setOpen(false)
} catch (error: any) {
toast.error(`Failed to enable Index Advisor: ${error.message}`)
}
}
return (
<AlertDialog open={isDialogOpen} onOpenChange={() => setIsDialogOpen(!isDialogOpen)}>
<AlertDialogTrigger asChild>
<Button type="primary" onClick={() => track('index_advisor_banner_enable_button_clicked')}>
Enable
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialog open={open} onOpenChange={() => setOpen(!open)}>
<AlertDialogContent size="medium">
<AlertDialogHeader>
<AlertDialogTitle>Enable Index Advisor</AlertDialogTitle>
<AlertDialogDescription>
This will enable the <code className="text-code-inline">index_advisor</code> and{' '}
<code className="text-code-inline">hypopg</code> Postgres extensions so Index Advisor
can analyse queries and suggest performance-improving indexes.
<AlertDialogDescription className="flex flex-col gap-y-2">
<p>
The Index Advisor recommends indexes to improve query performance on your tables based
on your actual query patterns.
</p>
<p>
Enable this will install the <code className="text-code-inline">index_advisor</code>{' '}
and <code className="text-code-inline">hypopg</code> Postgres extensions so Index
Advisor can analyse queries and suggest performance-improving indexes.
</p>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>

View File

@@ -1,5 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Lock, PlusCircle, Unlock } from 'lucide-react'
import { useParams } from 'common'
import { Realtime } from 'icons'
import { BookOpenText, Lightbulb, Lock, MoreVertical, PlusCircle, Unlock } from 'lucide-react'
import Link from 'next/link'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useState } from 'react'
@@ -7,6 +9,11 @@ import { toast } from 'sonner'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
@@ -16,18 +23,18 @@ import {
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { EnableIndexAdvisorDialog } from '../QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton'
import { RoleImpersonationPopover } from '../RoleImpersonationSelector/RoleImpersonationPopover'
import { IndexAdvisorPopover } from './IndexAdvisorPopover'
import { InsertButton } from './InsertButton'
import { RealtimeToggle } from './RealtimeToggle'
import { RealtimeToggleDialog } from './RealtimeToggleDialog'
import { SecurityDefinerViewPopover } from './SecurityDefinerViewPopover'
import { ViewEntityAutofixSecurityModal } from './ViewEntityAutofixSecurityModal'
import { RefreshButton } from '@/components/grid/components/header/RefreshButton'
import { useTableIndexAdvisor } from '@/components/grid/context/TableIndexAdvisorContext'
import { getEntityLintDetails } from '@/components/interfaces/TableGridEditor/TableEntity.utils'
import { APIDocsButton } from '@/components/ui/APIDocsButton'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
import { useIsTableRealtimeEnabled } from '@/data/database-publications/database-publications-query'
import { useProjectLintsQuery } from '@/data/lint/lint-query'
import {
Entity,
@@ -37,22 +44,34 @@ import {
isView as isTableLikeView,
} from '@/data/table-editor/table-editor-types'
import { useTableUpdateMutation } from '@/data/tables/table-update-mutation'
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
import { DOCS_URL } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
import { useAppStateSnapshot } from '@/state/app-state'
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
export interface GridHeaderActionsProps {
table: Entity
isRefetching: boolean
}
export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProps) => {
const track = useTrack()
const { ref } = useParams()
const appSnap = useAppStateSnapshot()
const snap = useTableEditorTableStateSnapshot()
const { data: project } = useSelectedProjectQuery()
const { data: org } = useSelectedOrganizationQuery()
const { mutate: sendEvent } = useSendEventMutation()
const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false)
const [realtimeDialogOpen, setRealtimeDialogOpen] = useState(false)
const [indexAdvisorDialogOpen, setIndexAdvisorDialogOpen] = useState(false)
const [isAutofixViewSecurityModalOpen, setIsAutofixViewSecurityModalOpen] = useState(false)
const [showWarning, setShowWarning] = useQueryState(
'showWarning',
@@ -74,6 +93,8 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
const { realtimeAll: realtimeEnabled } = useIsFeatureEnabled(['realtime:all'])
const { isSchemaLocked } = useIsProtectedSchema({ schema: table.schema })
const isRealtimeEnabled = useIsTableRealtimeEnabled({ id: table.id })
const { mutate: updateTable, isPending: isUpdatingTable } = useTableUpdateMutation({
onError: (error) => {
toast.error(`Failed to toggle RLS: ${error.message}`)
@@ -83,10 +104,6 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
},
})
const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false)
const [isAutofixViewSecurityModalOpen, setIsAutofixViewSecurityModalOpen] = useState(false)
const snap = useTableEditorTableStateSnapshot()
const showHeaderActions = snap.selectedRows.size === 0
const projectRef = project?.ref
@@ -138,6 +155,23 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
const closeConfirmModal = () => {
setRlsConfirmModalOpen(false)
}
const onViewAPIDocs = () => {
appSnap.setActiveDocsSection(['entities', table.name])
appSnap.setShowProjectApiDocs(true)
sendEvent({
action: 'api_docs_opened',
properties: {
source: 'table_editor',
},
groups: {
project: ref ?? 'Unknown',
organization: org?.slug ?? 'Unknown',
},
})
}
const onToggleRLS = async () => {
const payload = {
id: table.id,
@@ -316,13 +350,37 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
<RoleImpersonationPopover header="View data as a role" align="center" />
{isTable && isIndexAdvisorAvailable && !isIndexAdvisorEnabled && <IndexAdvisorPopover />}
{isTable && realtimeEnabled && <RealtimeToggle table={table} />}
{doesHaveAutoGeneratedAPIDocs && (
<APIDocsButton section={['entities', table.name]} source="table_editor" />
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" icon={<MoreVertical />} className="h-7 w-7" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48">
{isTable && realtimeEnabled && (
<DropdownMenuItem className="gap-x-2" onClick={() => setRealtimeDialogOpen(true)}>
<Realtime size={14} className={isRealtimeEnabled ? 'text-brand' : ''} />
<span>{isRealtimeEnabled ? 'Disable' : 'Enable'} Realtime</span>
</DropdownMenuItem>
)}
{doesHaveAutoGeneratedAPIDocs && (
<DropdownMenuItem className="gap-x-2" onClick={() => onViewAPIDocs()}>
<BookOpenText size={14} />
<span>View API docs</span>
</DropdownMenuItem>
)}
{isTable && isIndexAdvisorAvailable && !isIndexAdvisorEnabled && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
className="gap-x-2"
onClick={() => setIndexAdvisorDialogOpen(true)}
>
<Lightbulb size={14} />
<span>Enable Index Advisor</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<RefreshButton tableId={table.id} isRefetching={isRefetching} />
@@ -351,6 +409,14 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
onConfirm={onToggleRLS}
/>
)}
<RealtimeToggleDialog
table={table}
open={realtimeDialogOpen}
setOpen={setRealtimeDialogOpen}
/>
<EnableIndexAdvisorDialog open={indexAdvisorDialogOpen} setOpen={setIndexAdvisorDialogOpen} />
</div>
)
}

View File

@@ -1,10 +1,7 @@
import { useParams } from 'common'
import { Realtime } from 'icons'
import { useState } from 'react'
import { toast } from 'sonner'
import {
Button,
cn,
Dialog,
DialogContent,
DialogFooter,
@@ -12,10 +9,8 @@ import {
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
} from 'ui'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { InlineLink } from '@/components/ui/InlineLink'
import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query'
import { useDatabasePublicationUpdateMutation } from '@/data/database-publications/database-publications-update-mutation'
@@ -23,13 +18,19 @@ import { Entity } from '@/data/table-editor/table-editor-types'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useTrack } from '@/lib/telemetry/track'
export const RealtimeToggle = ({ table }: { table: Entity }) => {
export const RealtimeToggleDialog = ({
table,
open,
setOpen,
}: {
table: Entity
open: boolean
setOpen: (value: boolean) => void
}) => {
const track = useTrack()
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const [open, setOpen] = useState(false)
const { data: publications } = useDatabasePublicationsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
@@ -81,27 +82,6 @@ export const RealtimeToggle = ({ table }: { table: Entity }) => {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<ButtonTooltip
type="default"
size="tiny"
icon={
<Realtime
strokeWidth={1.5}
className={isRealtimeEnabled ? 'text-brand' : 'text-foreground-muted'}
/>
}
className={cn('w-7 h-7 p-0', isRealtimeEnabled && 'text-brand hover:text-brand-hover')}
tooltip={{
content: {
side: 'bottom',
text: isRealtimeEnabled
? 'Disable Realtime for this table'
: 'Enable Realtime for this table',
},
}}
/>
</DialogTrigger>
<DialogContent size="small" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>

View File

@@ -178,7 +178,7 @@ export const TableGridEditor = ({
gridProps={{ height: '100%' }}
customHeader={
(isViewSelected || isTableSelected) && selectedView === 'definition' ? (
<div className="flex items-center space-x-2">
<div className="px-2 flex items-center gap-x-2">
<p>
SQL Definition of <code className="text-sm">{selectedTable.name}</code>{' '}
</p>

View File

@@ -62,7 +62,7 @@ const MobileNavigationBar = ({
{isProjectScope ? (
<>
<ProjectBranchSelector />
<ConnectButton className="[&_span]:hidden h-8 w-8" />
<ConnectButton iconOnly className="w-8 h-8" />
</>
) : IS_PLATFORM && showOrgSelection ? (
<OrgSelector />

View File

@@ -29,7 +29,7 @@ export const ProjectBranchSelectorTrigger = forwardRef<
return (
<SidebarMenuButton
size="lg"
className="group py-1 gap-1.5 w-full flex h-auto text-left data-open:bg-sidebar-accent data-open:text-sidebar-accent-foreground touch-manipulation"
className="group py-1 gap-1.5 w-full flex h-auto text-left data-open:bg-sidebar-accent data-open:text-sidebar-accent-foreground touch-manipulation gap-x-3"
onClick={onClick}
ref={ref}
>

View File

@@ -6,7 +6,7 @@ export const GridFooter = ({ children, className }: PropsWithChildren<{ classNam
<div
aria-label="Table grid footer"
className={cn(
'flex min-h-9 h-9 overflow-hidden overflow-x-auto items-center px-2 w-full border-t',
'flex min-h-10 h-10 overflow-hidden overflow-x-auto items-center justify-between px-2 w-full border-t gap-x-8',
className
)}
>

View File

@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'
import { databasePublicationsKeys } from './keys'
import { get, handleError } from '@/data/fetchers'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export type DatabasePublicationsVariables = {
@@ -53,3 +54,17 @@ export const useDatabasePublicationsQuery = <TData = DatabasePublicationsData>(
enabled: enabled && typeof projectRef !== 'undefined',
...options,
})
export const useIsTableRealtimeEnabled = ({ id }: { id: number }) => {
const { data: project } = useSelectedProjectQuery()
const { data: publications } = useDatabasePublicationsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const realtimePublication = (publications ?? []).find(
(publication) => publication.name === 'supabase_realtime'
)
const realtimeEnabledTables = realtimePublication?.tables ?? []
const isRealtimeEnabled = realtimeEnabledTables.some((t) => t.id === id)
return isRealtimeEnabled
}

View File

@@ -2,18 +2,21 @@
import { motion } from 'framer-motion'
import { Search } from 'lucide-react'
import React from 'react'
import React, { forwardRef } from 'react'
import { cn } from 'ui'
import { FilterBarRoot, useFilterBar, type FilterBarVariant } from './FilterBarContext'
import {
FilterBarHandle,
FilterBarRoot,
useFilterBar,
type FilterBarVariant,
} from './FilterBarContext'
import { FilterGroup } from './FilterGroup'
import { FilterBarAction, FilterGroup as FilterGroupType, FilterProperty } from './types'
export type FilterBarProps = {
filterProperties: FilterProperty[]
onFilterChange: (filters: FilterGroupType) => void
freeformText: string
onFreeformTextChange: (freeformText: string) => void
filters: FilterGroupType
actions?: FilterBarAction[]
isLoading?: boolean
@@ -21,10 +24,20 @@ export type FilterBarProps = {
supportsOperators?: boolean
variant?: FilterBarVariant
icon?: React.ReactNode
onFilterChange: (filters: FilterGroupType) => void
onFreeformTextChange: (freeformText: string) => void
}
function FilterBarContent({ className }: { className?: string }) {
const { filters, error, optionsError, isLoading, variant, icon: loadingIcon } = useFilterBar()
const {
filters,
error,
optionsError,
isLoading,
variant,
icon: loadingIcon,
handleGroupFreeformFocus,
} = useFilterBar()
return (
<div className="w-full space-y-2 relative">
@@ -37,8 +50,10 @@ function FilterBarContent({ className }: { className?: string }) {
<div
className={cn(
'relative flex items-center justify-center shrink-0 px-2 bg-surface-200',
variant === 'pill' ? 'bg-transparent border-r-0' : 'border-r'
variant === 'pill' ? 'bg-transparent border-r-0' : 'border-r',
!isLoading && 'cursor-pointer'
)}
onClick={() => !isLoading && handleGroupFreeformFocus([])}
>
<div
className={cn(
@@ -99,21 +114,25 @@ function FilterBarContent({ className }: { className?: string }) {
* </FilterBar.Root>
* ```
*/
export function FilterBar({
filterProperties,
filters,
onFilterChange,
freeformText,
onFreeformTextChange,
actions,
isLoading,
className,
supportsOperators = false,
variant = 'default',
icon,
}: FilterBarProps) {
export const FilterBar = forwardRef<FilterBarHandle, FilterBarProps>(function FilterBar(
{
filterProperties,
filters,
onFilterChange,
freeformText,
onFreeformTextChange,
actions,
isLoading,
className,
supportsOperators = false,
variant = 'default',
icon,
},
ref
) {
return (
<FilterBarRoot
ref={ref}
filterProperties={filterProperties}
filters={filters}
onFilterChange={onFilterChange}
@@ -128,9 +147,11 @@ export function FilterBar({
<FilterBarContent className={className} />
</FilterBarRoot>
)
}
})
// Composable API exports
FilterBar.Root = FilterBarRoot
FilterBar.Content = FilterBarContent
FilterBar.Group = FilterGroup
Object.assign(FilterBar, {
Root: FilterBarRoot,
Content: FilterBarContent,
Group: FilterGroup,
})

View File

@@ -1,6 +1,14 @@
'use client'
import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react'
import React, {
createContext,
forwardRef,
useCallback,
useContext,
useEffect,
useImperativeHandle,
useRef,
} from 'react'
import { useFilterBarState, useOptionsCache } from './hooks'
import {
@@ -92,19 +100,26 @@ export type FilterBarRootProps = {
export type FilterBarVariant = 'default' | 'pill'
export function FilterBarRoot({
children,
filterProperties,
filters,
onFilterChange,
freeformText,
onFreeformTextChange,
actions,
isLoading: externalLoading,
supportsOperators = false,
variant = 'default',
icon,
}: FilterBarRootProps) {
export type FilterBarHandle = {
focus: () => void
}
export const FilterBarRoot = forwardRef<FilterBarHandle, FilterBarRootProps>(function FilterBarRoot(
{
children,
filterProperties,
filters,
onFilterChange,
freeformText,
onFreeformTextChange,
actions,
isLoading: externalLoading,
supportsOperators = false,
variant = 'default',
icon,
}: FilterBarRootProps,
ref: React.Ref<FilterBarHandle>
) {
const rootRef = useRef<HTMLDivElement>(null)
const {
@@ -357,6 +372,10 @@ export function FilterBarRoot({
rootRef,
}
useImperativeHandle(ref, () => ({
focus: () => handleGroupFreeformFocus([]),
}))
return (
<FilterBarContext.Provider value={contextValue}>
<div ref={rootRef} data-filterbar-root className="h-full min-h-[32px] flex items-stretch">
@@ -364,4 +383,4 @@ export function FilterBarRoot({
</div>
</FilterBarContext.Provider>
)
}
})