mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 14:05:05 +08:00
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:
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,14 +50,31 @@ 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">
|
||||
<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 ? (
|
||||
@@ -64,17 +82,46 @@ export const Header = ({ customHeader, isRefetching, tableQueriesEnabled = true
|
||||
<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
|
||||
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">
|
||||
{!customHeader && snap.selectedRows.size === 0 && (
|
||||
<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'}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,7 +114,8 @@ function FilterBarContent({ className }: { className?: string }) {
|
||||
* </FilterBar.Root>
|
||||
* ```
|
||||
*/
|
||||
export function FilterBar({
|
||||
export const FilterBar = forwardRef<FilterBarHandle, FilterBarProps>(function FilterBar(
|
||||
{
|
||||
filterProperties,
|
||||
filters,
|
||||
onFilterChange,
|
||||
@@ -111,9 +127,12 @@ export function FilterBar({
|
||||
supportsOperators = false,
|
||||
variant = 'default',
|
||||
icon,
|
||||
}: FilterBarProps) {
|
||||
},
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -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,7 +100,12 @@ export type FilterBarRootProps = {
|
||||
|
||||
export type FilterBarVariant = 'default' | 'pill'
|
||||
|
||||
export function FilterBarRoot({
|
||||
export type FilterBarHandle = {
|
||||
focus: () => void
|
||||
}
|
||||
|
||||
export const FilterBarRoot = forwardRef<FilterBarHandle, FilterBarRootProps>(function FilterBarRoot(
|
||||
{
|
||||
children,
|
||||
filterProperties,
|
||||
filters,
|
||||
@@ -104,7 +117,9 @@ export function FilterBarRoot({
|
||||
supportsOperators = false,
|
||||
variant = 'default',
|
||||
icon,
|
||||
}: FilterBarRootProps) {
|
||||
}: 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>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user