diff --git a/apps/studio/components/grid/components/footer/Footer.tsx b/apps/studio/components/grid/components/footer/Footer.tsx index 5ecd561dbd..77a81ff1e9 100644 --- a/apps/studio/components/grid/components/footer/Footer.tsx +++ b/apps/studio/components/grid/components/footer/Footer.tsx @@ -30,7 +30,7 @@ export const Footer: React.FC = ({ enableForeignRowsQuery = true }: {selectedView === 'data' && } -
+
{(isViewSelected || isTableSelected) && ( +
+ + + ) +} + +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 ( - setIsDialogOpen(!isDialogOpen)}> - - - - - + setOpen(!open)}> + Enable Index Advisor - - This will enable the index_advisor and{' '} - hypopg Postgres extensions so Index Advisor - can analyse queries and suggest performance-improving indexes. + +

+ The Index Advisor recommends indexes to improve query performance on your tables based + on your actual query patterns. +

+

+ Enable this will install the index_advisor{' '} + and hypopg Postgres extensions so Index + Advisor can analyse queries and suggest performance-improving indexes. +

diff --git a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx index 9006734ec7..fde211127e 100644 --- a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx @@ -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 - {isTable && isIndexAdvisorAvailable && !isIndexAdvisorEnabled && } - - {isTable && realtimeEnabled && } - - {doesHaveAutoGeneratedAPIDocs && ( - - )} + + +
) } diff --git a/apps/studio/components/interfaces/TableGridEditor/RealtimeToggle.tsx b/apps/studio/components/interfaces/TableGridEditor/RealtimeToggleDialog.tsx similarity index 80% rename from apps/studio/components/interfaces/TableGridEditor/RealtimeToggle.tsx rename to apps/studio/components/interfaces/TableGridEditor/RealtimeToggleDialog.tsx index 9cb77a9514..8d52df417d 100644 --- a/apps/studio/components/interfaces/TableGridEditor/RealtimeToggle.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/RealtimeToggleDialog.tsx @@ -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 ( - - - } - 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', - }, - }} - /> - diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index e427f59bcb..c116e66693 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -178,7 +178,7 @@ export const TableGridEditor = ({ gridProps={{ height: '100%' }} customHeader={ (isViewSelected || isTableSelected) && selectedView === 'definition' ? ( -
+

SQL Definition of {selectedTable.name}{' '}

diff --git a/apps/studio/components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx b/apps/studio/components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx index 17f1204307..18c4d1e2ea 100644 --- a/apps/studio/components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx +++ b/apps/studio/components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx @@ -62,7 +62,7 @@ const MobileNavigationBar = ({ {isProjectScope ? ( <> - + ) : IS_PLATFORM && showOrgSelection ? ( diff --git a/apps/studio/components/layouts/Navigation/NavigationBar/ProjectBranchSelectorTrigger.tsx b/apps/studio/components/layouts/Navigation/NavigationBar/ProjectBranchSelectorTrigger.tsx index add89f8045..d2c4ad5c49 100644 --- a/apps/studio/components/layouts/Navigation/NavigationBar/ProjectBranchSelectorTrigger.tsx +++ b/apps/studio/components/layouts/Navigation/NavigationBar/ProjectBranchSelectorTrigger.tsx @@ -29,7 +29,7 @@ export const ProjectBranchSelectorTrigger = forwardRef< return ( diff --git a/apps/studio/components/ui/GridFooter.tsx b/apps/studio/components/ui/GridFooter.tsx index 4e297d9b4c..e0d24d4605 100644 --- a/apps/studio/components/ui/GridFooter.tsx +++ b/apps/studio/components/ui/GridFooter.tsx @@ -6,7 +6,7 @@ export const GridFooter = ({ children, className }: PropsWithChildren<{ classNam
diff --git a/apps/studio/data/database-publications/database-publications-query.ts b/apps/studio/data/database-publications/database-publications-query.ts index 974ffd607b..f6cd1d8c11 100644 --- a/apps/studio/data/database-publications/database-publications-query.ts +++ b/apps/studio/data/database-publications/database-publications-query.ts @@ -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 = ( 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 +} diff --git a/packages/ui-patterns/src/FilterBar/FilterBar.tsx b/packages/ui-patterns/src/FilterBar/FilterBar.tsx index a109ed6c27..a77dbe4d04 100644 --- a/packages/ui-patterns/src/FilterBar/FilterBar.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterBar.tsx @@ -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 (
@@ -37,8 +50,10 @@ function FilterBarContent({ className }: { className?: string }) {
!isLoading && handleGroupFreeformFocus([])} >
* ``` */ -export function FilterBar({ - filterProperties, - filters, - onFilterChange, - freeformText, - onFreeformTextChange, - actions, - isLoading, - className, - supportsOperators = false, - variant = 'default', - icon, -}: FilterBarProps) { +export const FilterBar = forwardRef(function FilterBar( + { + filterProperties, + filters, + onFilterChange, + freeformText, + onFreeformTextChange, + actions, + isLoading, + className, + supportsOperators = false, + variant = 'default', + icon, + }, + ref +) { return ( ) -} +}) // Composable API exports -FilterBar.Root = FilterBarRoot -FilterBar.Content = FilterBarContent -FilterBar.Group = FilterGroup +Object.assign(FilterBar, { + Root: FilterBarRoot, + Content: FilterBarContent, + Group: FilterGroup, +}) diff --git a/packages/ui-patterns/src/FilterBar/FilterBarContext.tsx b/packages/ui-patterns/src/FilterBar/FilterBarContext.tsx index f9212a9132..d9c091bf5c 100644 --- a/packages/ui-patterns/src/FilterBar/FilterBarContext.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterBarContext.tsx @@ -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(function FilterBarRoot( + { + children, + filterProperties, + filters, + onFilterChange, + freeformText, + onFreeformTextChange, + actions, + isLoading: externalLoading, + supportsOperators = false, + variant = 'default', + icon, + }: FilterBarRootProps, + ref: React.Ref +) { const rootRef = useRef(null) const { @@ -357,6 +372,10 @@ export function FilterBarRoot({ rootRef, } + useImperativeHandle(ref, () => ({ + focus: () => handleGroupFreeformFocus([]), + })) + return (
@@ -364,4 +383,4 @@ export function FilterBarRoot({
) -} +})