diff --git a/apps/studio/components/grid/components/footer/Footer.tsx b/apps/studio/components/grid/components/footer/Footer.tsx index 9a06d3ccd79..5ecd561dbd2 100644 --- a/apps/studio/components/grid/components/footer/Footer.tsx +++ b/apps/studio/components/grid/components/footer/Footer.tsx @@ -3,7 +3,7 @@ import { parseAsString, useQueryState } from 'nuqs' import { Pagination } from './pagination/Pagination' import { GridFooter } from '@/components/ui/GridFooter' -import TwoOptionToggle from '@/components/ui/TwoOptionToggle' +import { TwoOptionToggle } from '@/components/ui/TwoOptionToggle' import { useTableEditorQuery } from '@/data/table-editor/table-editor-query' import { isTableLike, isViewLike } from '@/data/table-editor/table-editor-types' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index 20086e61688..be4b6f661ce 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -1,12 +1,10 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' import { keepPreviousData } from '@tanstack/react-query' import { useParams } from 'common' -import { ArrowUp, ChevronDown, FileText, Trash } from 'lucide-react' +import { ChevronDown, Trash } from 'lucide-react' import { ReactNode, useState } from 'react' import { toast } from 'sonner' import { Button, - cn, copyToClipboard, DropdownMenu, DropdownMenuContent, @@ -31,12 +29,8 @@ import { } from '@/components/layouts/TableEditorLayout/ExportAllRows' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { Shortcut } from '@/components/ui/Shortcut' -import { ShortcutBadge } from '@/components/ui/ShortcutBadge' import { useTableRowsCountQuery } from '@/data/table-rows/table-rows-count-query' import { useTableRowsQuery } from '@/data/table-rows/table-rows-query' -import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' -import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' -import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { RoleImpersonationState } from '@/lib/role-impersonation' import { @@ -44,7 +38,6 @@ import { useSubscribeToImpersonatedRole, } from '@/state/role-impersonation-state' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' -import { useShortcut } from '@/state/shortcuts/useShortcut' import { useTableEditorStateSnapshot } from '@/state/table-editor' import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table' @@ -60,11 +53,10 @@ export const Header = ({ customHeader, isRefetching, tableQueriesEnabled = true useSyncFiltersToUrl() const snap = useTableEditorTableStateSnapshot() - const showInsertButton = snap.selectedRows.size === 0 return (
-
+
{customHeader ? (
{customHeader}
) : snap.selectedRows.size > 0 ? ( @@ -72,165 +64,21 @@ export const Header = ({ customHeader, isRefetching, tableQueriesEnabled = true
) : ( -
+
)} -
+
{!customHeader && snap.selectedRows.size === 0 && ( )} - {showInsertButton && }
) } -const InsertButton = () => { - const { ref: projectRef } = useParams() - const { data: org } = useSelectedOrganizationQuery() - - const snap = useTableEditorTableStateSnapshot() - const tableEditorSnap = useTableEditorStateSnapshot() - const { can: canCreateColumns } = useAsyncCheckPermissions( - PermissionAction.TENANT_SQL_ADMIN_WRITE, - 'columns' - ) - const { mutate: sendEvent } = useSendEventMutation() - - const onAddRow = - snap.editable && (snap.table.columns ?? []).length > 0 ? tableEditorSnap.onAddRow : undefined - const onAddColumn = snap.editable ? tableEditorSnap.onAddColumn : undefined - const onImportData = snap.editable ? tableEditorSnap.onImportData : undefined - - const canAddNew = onAddRow !== undefined || onAddColumn !== undefined - - useShortcut(SHORTCUT_IDS.TABLE_EDITOR_INSERT_ROW, () => onAddRow?.(), { - registerInCommandMenu: true, - enabled: onAddRow !== undefined && canAddNew && canCreateColumns, - }) - useShortcut(SHORTCUT_IDS.TABLE_EDITOR_INSERT_COLUMN, () => onAddColumn?.(), { - registerInCommandMenu: true, - enabled: onAddColumn !== undefined && canAddNew && canCreateColumns, - }) - useShortcut(SHORTCUT_IDS.TABLE_EDITOR_IMPORT_CSV, () => onImportData?.(), { - registerInCommandMenu: true, - enabled: onImportData !== undefined && canAddNew && canCreateColumns, - }) - - if (!canAddNew || !canCreateColumns) return null - - return ( - - - - - - {[ - ...(onAddRow !== undefined - ? [ - -
-
-
-
-
-
-

Insert row

-

Insert a new row into {snap.table.name}

-
- - , - ] - : []), - ...(onAddColumn !== undefined - ? [ - -
-
-
-
-
-
-

Insert column

-

- Insert a new column into {snap.table.name} -

-
- - , - ] - : []), - ...(onImportData !== undefined - ? [ - { - onImportData() - sendEvent({ - action: 'import_data_button_clicked', - properties: { tableType: 'Existing Table' }, - groups: { - project: projectRef ?? 'Unknown', - organization: org?.slug ?? 'Unknown', - }, - }) - }} - > -
- - -
-
-

Import data from CSV

-

Insert new rows from a CSV

-
- -
, - ] - : []), - ]} - - - ) -} - type RowHeaderProps = { tableQueriesEnabled?: boolean } diff --git a/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx b/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx index db262c64b04..604db76167a 100644 --- a/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx +++ b/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx @@ -210,7 +210,7 @@ export const FilterPopoverNew = ({ isRefetching = false }: FilterPopoverProps) = actions={actions} isLoading={isGenerating} variant="pill" - className="bg-transparent border-0 overflow-visible px-1.5" + className="bg-transparent border-0 overflow-visible px-1.5 [&>div>div>div>input]:!text-xs" icon={icon} />
diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx index 7bedec7cfd1..06369681231 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx @@ -25,7 +25,7 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { SpamValidation } from './SpamValidation' import { PreventNavigationOnUnsavedChanges } from '@/components/ui-patterns/Dialogs/PreventNavigationOnUnsavedChanges' import CodeEditor from '@/components/ui/CodeEditor/CodeEditor' -import TwoOptionToggle from '@/components/ui/TwoOptionToggle' +import { TwoOptionToggle } from '@/components/ui/TwoOptionToggle' import { useAuthConfigQuery } from '@/data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation' import { useValidateSpamMutation, ValidateSpamResponse } from '@/data/auth/validate-spam-mutation' @@ -279,7 +279,7 @@ export const TemplateEditor = ({ template }: TemplateEditorProps) => { width={60} options={['preview', 'source']} activeOption={activeView} - onClickOption={(option: 'source' | 'preview') => setActiveView(option)} + onClickOption={(option) => setActiveView(option as 'source' | 'preview')} borderOverride="border-muted" />
diff --git a/apps/studio/components/interfaces/QueryInsights/QueryInsightsTable/QueryInsightsTable.tsx b/apps/studio/components/interfaces/QueryInsights/QueryInsightsTable/QueryInsightsTable.tsx index c3a06a7a301..fbabf2a23a6 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryInsightsTable/QueryInsightsTable.tsx +++ b/apps/studio/components/interfaces/QueryInsights/QueryInsightsTable/QueryInsightsTable.tsx @@ -27,7 +27,7 @@ import type { QueryPlanRow } from '@/components/interfaces/ExplainVisualizer/Exp import { FilterPill } from '@/components/interfaces/QueryPerformance/components/FilterPill' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { FilterPopover } from '@/components/ui/FilterPopover' -import TwoOptionToggle from '@/components/ui/TwoOptionToggle' +import { TwoOptionToggle } from '@/components/ui/TwoOptionToggle' import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' @@ -341,7 +341,7 @@ export const QueryInsightsTable = ({ options={['explorer', 'triage']} activeOption={mode} borderOverride="border" - onClickOption={setMode} + onClickOption={(mode) => setMode(mode as Mode)} /> {appNameFilter.length > 0 ? ( setView(value as 'view' | 'md')} /> )} diff --git a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx index 733de5bfd7b..9006734ec7f 100644 --- a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx @@ -1,6 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' -import { Lightbulb, Lock, MousePointer2, PlusCircle, Unlock } from 'lucide-react' +import { Lock, PlusCircle, Unlock } from 'lucide-react' import Link from 'next/link' import { parseAsBoolean, useQueryState } from 'nuqs' import { useState } from 'react' @@ -18,16 +17,17 @@ import { import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { RoleImpersonationPopover } from '../RoleImpersonationSelector/RoleImpersonationPopover' -import ViewEntityAutofixSecurityModal from './ViewEntityAutofixSecurityModal' +import { IndexAdvisorPopover } from './IndexAdvisorPopover' +import { InsertButton } from './InsertButton' +import { RealtimeToggle } from './RealtimeToggle' +import { SecurityDefinerViewPopover } from './SecurityDefinerViewPopover' +import { ViewEntityAutofixSecurityModal } from './ViewEntityAutofixSecurityModal' import { RefreshButton } from '@/components/grid/components/header/RefreshButton' import { useTableIndexAdvisor } from '@/components/grid/context/TableIndexAdvisorContext' -import { EnableIndexAdvisorButton } from '@/components/interfaces/QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton' 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 { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query' -import { useDatabasePublicationUpdateMutation } from '@/data/database-publications/database-publications-update-mutation' import { useProjectLintsQuery } from '@/data/lint/lint-query' import { Entity, @@ -51,9 +51,8 @@ export interface GridHeaderActionsProps { } export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProps) => { - const { ref } = useParams() - const { data: project } = useSelectedProjectQuery() const track = useTrack() + const { data: project } = useSelectedProjectQuery() const [showWarning, setShowWarning] = useQueryState( 'showWarning', @@ -84,7 +83,6 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp }, }) - const [showEnableRealtime, setShowEnableRealtime] = useState(false) const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false) const [isAutofixViewSecurityModalOpen, setIsAutofixViewSecurityModalOpen] = useState(false) @@ -100,32 +98,6 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp (policy) => policy.schema === table.schema && policy.table === table.name ) - 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 === table?.id) - - const { mutate: updatePublications, isPending: isTogglingRealtime } = - useDatabasePublicationUpdateMutation({ - onSuccess: () => { - setShowEnableRealtime(false) - - track(isRealtimeEnabled ? 'table_realtime_disabled' : 'table_realtime_enabled', { - method: 'ui', - schema_name: table.schema, - table_name: table.name, - }) - }, - onError: (error) => { - toast.error(`Failed to toggle realtime for ${table.name}: ${error.message}`) - }, - }) - const { can: canSqlWriteTables, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables' @@ -163,29 +135,6 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp table.schema ) - const toggleRealtime = async () => { - if (!project || !realtimePublication) return - - const exists = realtimeEnabledTables.some((x) => x.id === table.id) - const tables = !exists - ? [`${table.schema}.${table.name}`].concat( - realtimeEnabledTables.map((t) => `${t.schema}.${t.name}`) - ) - : realtimeEnabledTables.filter((x) => x.id !== table.id).map((x) => `${x.schema}.${x.name}`) - - track('realtime_toggle_table_clicked', { - newState: exists ? 'disabled' : 'enabled', - origin: 'tableGridHeader', - }) - - updatePublications({ - projectRef: project?.ref, - connectionString: project?.connectionString, - id: realtimePublication.id, - tables, - }) - } - const closeConfirmModal = () => { setRlsConfirmModalOpen(false) } @@ -319,139 +268,17 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp ) : null ) : null} - {isTable && isIndexAdvisorAvailable && !isIndexAdvisorEnabled && ( - - - - - -

- Index Advisor -

-
-

- Index Advisor recommends indexes to improve query performance on this table. -

-

- Enable Index Advisor to get recommendations based on your actual query patterns. -

-
- -
-
-
-
- )} - - {realtimeEnabled && ( - - } - onClick={() => setShowEnableRealtime(true)} - className={cn(isRealtimeEnabled && 'w-7 h-7 p-0 text-brand hover:text-brand-hover')} - tooltip={{ - content: { - side: 'bottom', - text: isRealtimeEnabled - ? 'Click to disable realtime for this table' - : 'Click to enable realtime for this table', - }, - }} - > - {!isRealtimeEnabled && 'Enable Realtime'} - - )} - {isView && viewHasLints && ( - - - - - -

- Secure your View -

-
-

- This view is defined with the Security Definer property, giving it permissions - of the view's creator (Postgres), rather than the permissions of the querying - user. -

- -

- Since this view is in the public schema, it is accessible via your project's - APIs. -

- -
- - -
-
-
-
+ { + setIsAutofixViewSecurityModalOpen(true) + }} + /> )} {isMaterializedView && materializedViewHasLints && ( - - - - - -

- Secure your View -

-
-

- This view is defined with the Security Definer property, giving it permissions - of the view's creator (Postgres), rather than the permissions of the querying - user. -

- -

- Since this view is in the public schema, it is accessible via your project's - APIs. -

- -
- -
-
-
-
+ )} {isForeignTable && table.schema === 'public' && ( @@ -489,38 +316,19 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp + {isTable && isIndexAdvisorAvailable && !isIndexAdvisorEnabled && } + + {isTable && realtimeEnabled && } + {doesHaveAutoGeneratedAPIDocs && ( )} + + {showHeaderActions && }
)} - setShowEnableRealtime(false)} - onConfirm={() => toggleRealtime()} - > -
-

- Once realtime has been {isRealtimeEnabled ? 'disabled' : 'enabled'}, the table will{' '} - {isRealtimeEnabled ? 'no longer ' : ''}broadcast any changes to authorized subscribers. -

- {!isRealtimeEnabled && ( -

- You may also select which events to broadcast to subscribers on the{' '} - - database publications - {' '} - settings. -

- )} -
-
{ + return ( + + + + + +

+ Index Advisor +

+
+

Index Advisor recommends indexes to improve query performance on this table.

+

Enable Index Advisor to get recommendations based on your actual query patterns.

+
+ +
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/TableGridEditor/InsertButton.tsx b/apps/studio/components/interfaces/TableGridEditor/InsertButton.tsx new file mode 100644 index 00000000000..ca1451a062d --- /dev/null +++ b/apps/studio/components/interfaces/TableGridEditor/InsertButton.tsx @@ -0,0 +1,156 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useParams } from 'common' +import { ArrowUp, ChevronDown, FileText } from 'lucide-react' +import { + Button, + cn, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from 'ui' + +import { ShortcutBadge } from '@/components/ui/ShortcutBadge' +import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' +import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' +import { useTableEditorStateSnapshot } from '@/state/table-editor' +import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table' + +export const InsertButton = () => { + const { ref: projectRef } = useParams() + const { data: org } = useSelectedOrganizationQuery() + + const snap = useTableEditorTableStateSnapshot() + const tableEditorSnap = useTableEditorStateSnapshot() + const { can: canCreateColumns } = useAsyncCheckPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'columns' + ) + const { mutate: sendEvent } = useSendEventMutation() + + const onAddRow = + snap.editable && (snap.table.columns ?? []).length > 0 ? tableEditorSnap.onAddRow : undefined + const onAddColumn = snap.editable ? tableEditorSnap.onAddColumn : undefined + const onImportData = snap.editable ? tableEditorSnap.onImportData : undefined + + const canAddNew = onAddRow !== undefined || onAddColumn !== undefined + + useShortcut(SHORTCUT_IDS.TABLE_EDITOR_INSERT_ROW, () => onAddRow?.(), { + registerInCommandMenu: true, + enabled: onAddRow !== undefined && canAddNew && canCreateColumns, + }) + useShortcut(SHORTCUT_IDS.TABLE_EDITOR_INSERT_COLUMN, () => onAddColumn?.(), { + registerInCommandMenu: true, + enabled: onAddColumn !== undefined && canAddNew && canCreateColumns, + }) + useShortcut(SHORTCUT_IDS.TABLE_EDITOR_IMPORT_CSV, () => onImportData?.(), { + registerInCommandMenu: true, + enabled: onImportData !== undefined && canAddNew && canCreateColumns, + }) + + if (!canAddNew || !canCreateColumns) return null + + return ( + + + + + + {[ + ...(onAddRow !== undefined + ? [ + +
+
+
+
+
+

Insert row

+ + , + ] + : []), + ...(onAddColumn !== undefined + ? [ + +
+
+
+
+
+

Insert column

+ + , + ] + : []), + ...(onImportData !== undefined + ? [ + { + onImportData() + sendEvent({ + action: 'import_data_button_clicked', + properties: { tableType: 'Existing Table' }, + groups: { + project: projectRef ?? 'Unknown', + organization: org?.slug ?? 'Unknown', + }, + }) + }} + > +
+ + +
+

Import data from CSV

+ +
, + ] + : []), + ]} + + + ) +} diff --git a/apps/studio/components/interfaces/TableGridEditor/RealtimeToggle.tsx b/apps/studio/components/interfaces/TableGridEditor/RealtimeToggle.tsx new file mode 100644 index 00000000000..9cb77a95149 --- /dev/null +++ b/apps/studio/components/interfaces/TableGridEditor/RealtimeToggle.tsx @@ -0,0 +1,141 @@ +import { useParams } from 'common' +import { Realtime } from 'icons' +import { useState } from 'react' +import { toast } from 'sonner' +import { + Button, + cn, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + 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' +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 }) => { + 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, + }) + const realtimePublication = (publications ?? []).find( + (publication) => publication.name === 'supabase_realtime' + ) + const realtimeEnabledTables = realtimePublication?.tables ?? [] + const isRealtimeEnabled = realtimeEnabledTables.some((t) => t.id === table?.id) + + const { mutate: updatePublications, isPending: isTogglingRealtime } = + useDatabasePublicationUpdateMutation({ + onSuccess: () => { + setOpen(false) + + track(isRealtimeEnabled ? 'table_realtime_disabled' : 'table_realtime_enabled', { + method: 'ui', + schema_name: table.schema, + table_name: table.name, + }) + }, + onError: (error) => { + toast.error(`Failed to toggle realtime for ${table.name}: ${error.message}`) + }, + }) + + const toggleRealtime = async () => { + if (!project || !realtimePublication) return + + const exists = realtimeEnabledTables.some((x) => x.id === table.id) + const tables = !exists + ? [`${table.schema}.${table.name}`].concat( + realtimeEnabledTables.map((t) => `${t.schema}.${t.name}`) + ) + : realtimeEnabledTables.filter((x) => x.id !== table.id).map((x) => `${x.schema}.${x.name}`) + + track('realtime_toggle_table_clicked', { + newState: exists ? 'disabled' : 'enabled', + origin: 'tableGridHeader', + }) + + updatePublications({ + projectRef: project?.ref, + connectionString: project?.connectionString, + id: realtimePublication.id, + tables, + }) + } + + 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', + }, + }} + /> + + + + + {isRealtimeEnabled ? 'Disable' : 'Enable'} realtime for {table.name} + + + + +
+

+ Once realtime has been {isRealtimeEnabled ? 'disabled' : 'enabled'}, the table will{' '} + {isRealtimeEnabled ? 'no longer ' : ''}broadcast any changes to authorized + subscribers. +

+ {!isRealtimeEnabled && ( +

+ You may also select which events to broadcast to subscribers on the{' '} + + database publications + {' '} + settings. +

+ )} +
+
+ + + + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/TableGridEditor/SecurityDefinerViewPopover.tsx b/apps/studio/components/interfaces/TableGridEditor/SecurityDefinerViewPopover.tsx new file mode 100644 index 00000000000..d512c231dce --- /dev/null +++ b/apps/studio/components/interfaces/TableGridEditor/SecurityDefinerViewPopover.tsx @@ -0,0 +1,56 @@ +import { useParams } from 'common' +import { Unlock } from 'lucide-react' +import Link from 'next/link' +import { Button, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_ } from 'ui' + +import { type Lint } from '@/data/lint/lint-query' + +export const SecurityDefinerViewPopover = ({ + lint, + onAutofix, +}: { + lint: Lint | null + onAutofix?: () => void +}) => { + const { ref } = useParams() + + return ( + + + + + +

+ Secure your view +

+
+

+ This view is defined with the Security Definer property, giving it permissions of the + view's creator (Postgres), rather than the permissions of the querying user. +

+ +

Since this view is in the public schema, it is accessible via your project's APIs.

+ +
+ {!!onAutofix && ( + + )} + +
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/index.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/index.tsx index 68320878f56..cc906632951 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/index.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/index.tsx @@ -10,7 +10,7 @@ import { isValueTruncated } from '../RowEditor.utils' import { DrilldownViewer } from './DrilldownViewer/DrilldownViewer' import { JsonCodeEditor } from './JsonCodeEditor' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' -import TwoOptionToggle from '@/components/ui/TwoOptionToggle' +import { TwoOptionToggle } from '@/components/ui/TwoOptionToggle' import { useTableEditorQuery } from '@/data/table-editor/table-editor-query' import { isTableLike } from '@/data/table-editor/table-editor-types' import { useGetCellValueMutation } from '@/data/table-rows/get-cell-value-mutation' @@ -149,7 +149,7 @@ export const JsonEditor = ({ options={['view', 'edit']} activeOption={view} borderOverride="border-muted" - onClickOption={setView} + onClickOption={(value) => setView(value as 'view' | 'edit')} />
)} diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx index c82999c3a05..7ec00878a57 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx @@ -10,7 +10,7 @@ import { Button, cn, SidePanel } from 'ui' import { ActionBar } from '../ActionBar' import { isValueTruncated } from './RowEditor.utils' import { Markdown } from '@/components/interfaces/Markdown' -import TwoOptionToggle from '@/components/ui/TwoOptionToggle' +import { TwoOptionToggle } from '@/components/ui/TwoOptionToggle' import { useTableEditorQuery } from '@/data/table-editor/table-editor-query' import { isTableLike } from '@/data/table-editor/table-editor-types' import { useGetCellValueMutation } from '@/data/table-rows/get-cell-value-mutation' @@ -111,7 +111,7 @@ export const TextEditor = ({ options={['view', 'edit']} activeOption={view} borderOverride="border-muted" - onClickOption={setView} + onClickOption={(value) => setView(value as 'view' | 'edit')} /> )}
diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index 6f78afc56ce..e427f59bcbb 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -2,6 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import Link from 'next/link' import { useRouter } from 'next/router' +import { parseAsString, useQueryState } from 'nuqs' import { useCallback } from 'react' import { Button } from 'ui' import { Admonition, GenericSkeletonLoader } from 'ui-patterns' @@ -22,7 +23,6 @@ import { import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory' import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' -import { useUrlState } from '@/hooks/ui/useUrlState' import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas' import { TableEditorTableStateContextProvider } from '@/state/table-editor-table' import { createTabId, useTabsStateSnapshot } from '@/state/tabs' @@ -48,7 +48,8 @@ export const TableGridEditor = ({ table: selectedTable, }) - const [{ view: selectedView = 'data' }] = useUrlState() + const [selectedView] = useQueryState('view', parseAsString.withDefault('data')) + const { can: canEditTables } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables' @@ -67,7 +68,7 @@ export const TableGridEditor = ({ `/project/${projectRef}/editor/${table.id}${!!selectedSchema ? `?schema=${selectedSchema}` : ''}` ) }, - [projectRef, router] + [projectRef, router, selectedSchema] ) const onTableDeleted = useCallback(async () => { @@ -82,7 +83,7 @@ export const TableGridEditor = ({ onClearDashboardHistory: () => setLastVisitedTable(undefined), }) } - }, [router, selectedTable, tabs]) + }, [router, selectedTable, setLastVisitedTable, tabs]) const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' }) diff --git a/apps/studio/components/interfaces/TableGridEditor/ViewEntityAutofixSecurityModal.tsx b/apps/studio/components/interfaces/TableGridEditor/ViewEntityAutofixSecurityModal.tsx index 408f8e1545d..39ce5efe34c 100644 --- a/apps/studio/components/interfaces/TableGridEditor/ViewEntityAutofixSecurityModal.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/ViewEntityAutofixSecurityModal.tsx @@ -1,7 +1,7 @@ import { useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { ScrollArea } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' @@ -17,11 +17,11 @@ interface ViewEntityAutofixSecurityModalProps { setIsAutofixViewSecurityModalOpen: (isAutofixViewSecurityModalOpen: boolean) => void } -export default function ViewEntityAutofixSecurityModal({ +export const ViewEntityAutofixSecurityModal = ({ table, isAutofixViewSecurityModalOpen, setIsAutofixViewSecurityModalOpen, -}: ViewEntityAutofixSecurityModalProps) { +}: ViewEntityAutofixSecurityModalProps) => { const { data: project } = useSelectedProjectQuery() const queryClient = useQueryClient() const { diff --git a/apps/studio/components/ui/TwoOptionToggle.tsx b/apps/studio/components/ui/TwoOptionToggle.tsx index 247f5ea9d9e..4358e9a24b9 100644 --- a/apps/studio/components/ui/TwoOptionToggle.tsx +++ b/apps/studio/components/ui/TwoOptionToggle.tsx @@ -1,14 +1,14 @@ import { cn } from 'ui' interface TwoOptionToggleProps { - options: any + options: string[] width?: number - activeOption: any - onClickOption: any + activeOption: string + onClickOption: (value: string) => void borderOverride: string } -const TwoOptionToggle = ({ +export const TwoOptionToggle = ({ options, width = 50, activeOption, @@ -35,8 +35,8 @@ const TwoOptionToggle = ({ 'z-0 inline-block rounded-sm h-full bg-overlay-hover shadow-sm transform', 'transition-all ease-in-out border border-strong' )} - > - {options.map((option: any, index: number) => ( + /> + {options.map((option, index: number) => ( ) } - -export default TwoOptionToggle diff --git a/apps/www/components/SurveyResults/SurveyChart.tsx b/apps/www/components/SurveyResults/SurveyChart.tsx index 5089446bcee..02e50a42144 100644 --- a/apps/www/components/SurveyResults/SurveyChart.tsx +++ b/apps/www/components/SurveyResults/SurveyChart.tsx @@ -1,7 +1,10 @@ 'use client' -import { useEffect, useState, useRef, useCallback } from 'react' + import { createClient } from '@supabase/supabase-js' +import CodeBlock from '~/components/CodeBlock/CodeBlock' import { motion } from 'framer-motion' +import { ChevronsUpDown } from 'lucide-react' +import { useCallback, useEffect, useRef, useState } from 'react' import { Button, DropdownMenu, @@ -9,9 +12,8 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from 'ui' -import { ChevronsUpDown } from 'lucide-react' -import TwoOptionToggle from '../../../studio/components/ui/TwoOptionToggle' -import CodeBlock from '~/components/CodeBlock/CodeBlock' + +import { TwoOptionToggle } from '../../../studio/components/ui/TwoOptionToggle' // Separate Supabase client for survey project const externalSupabase = createClient( @@ -340,7 +342,7 @@ export function SurveyChart({ handleViewChange(value as 'sql' | 'chart')} borderOverride="border-overlay" />
diff --git a/e2e/studio/features/queue-table-operations.spec.ts b/e2e/studio/features/queue-table-operations.spec.ts index bbc357bd4f6..c191d781310 100644 --- a/e2e/studio/features/queue-table-operations.spec.ts +++ b/e2e/studio/features/queue-table-operations.spec.ts @@ -182,7 +182,7 @@ test.describe('Queue Table Operations', () => { await page.waitForURL(/\/editor\/\d+\?schema=public$/) await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('new row value') await page.getByTestId('action-bar-save-row').click() @@ -224,12 +224,12 @@ test.describe('Queue Table Operations', () => { await page.waitForURL(/\/editor\/\d+\?schema=public$/) await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('row one') await page.getByTestId('action-bar-save-row').click() await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('row two') await page.getByTestId('action-bar-save-row').click() @@ -269,12 +269,12 @@ test.describe('Queue Table Operations', () => { await page.waitForURL(/\/editor\/\d+\?schema=public$/) await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('keep this row') await page.getByTestId('action-bar-save-row').click() await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('remove this row') await page.getByTestId('action-bar-save-row').click() @@ -316,7 +316,7 @@ test.describe('Queue Table Operations', () => { await page.waitForURL(/\/editor\/\d+\?schema=public$/) await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('shortcut test') await page.getByTestId('action-bar-save-row').click() @@ -359,7 +359,7 @@ test.describe('Queue Table Operations', () => { await expect(page.getByRole('gridcell', { name: 'existing row' })).toBeVisible() await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('undo this row') await page.getByTestId('action-bar-save-row').click() @@ -395,12 +395,12 @@ test.describe('Queue Table Operations', () => { await page.waitForURL(/\/editor\/\d+\?schema=public$/) await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('first row') await page.getByTestId('action-bar-save-row').click() await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('second row') await page.getByTestId('action-bar-save-row').click() @@ -608,7 +608,7 @@ test.describe('Queue Table Operations', () => { await page.getByRole('menuitem', { name: 'Delete row' }).click() await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('new row') await page.getByTestId('action-bar-save-row').click() @@ -636,9 +636,7 @@ test.describe('Queue Table Operations', () => { await using _ = await withSetupCleanup( async () => { - await createTable(tableName, columnName, [ - { name: 'existing row' }, - ]) + await createTable(tableName, columnName, [{ name: 'existing row' }]) }, async () => { await dropTable(tableName) @@ -655,7 +653,7 @@ test.describe('Queue Table Operations', () => { // Add a new row await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('new row') await page.getByTestId('action-bar-save-row').click() @@ -710,7 +708,7 @@ test.describe('Queue Table Operations', () => { await page.waitForURL(/\/editor\/\d+\?schema=public$/) await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('pending in table 1') await page.getByTestId('action-bar-save-row').click() @@ -719,7 +717,7 @@ test.describe('Queue Table Operations', () => { await page.getByRole('button', { name: `View ${tableName2}`, exact: true }).click() await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('pending in table 2') await page.getByTestId('action-bar-save-row').click() @@ -764,7 +762,7 @@ test.describe('Queue Table Operations', () => { await page.waitForURL(/\/editor\/\d+\?schema=public$/) await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('only in table 1') await page.getByTestId('action-bar-save-row').click() @@ -776,7 +774,7 @@ test.describe('Queue Table Operations', () => { // Add a row in table 2 await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('only in table 2') await page.getByTestId('action-bar-save-row').click() @@ -846,10 +844,10 @@ test.describe('Queue Table Operations', () => { last_name text )` ) - await query( - `INSERT INTO ${tableName} (first_name, last_name) VALUES ($1, $2)`, - ['Alice', 'Smith'] - ) + await query(`INSERT INTO ${tableName} (first_name, last_name) VALUES ($1, $2)`, [ + 'Alice', + 'Smith', + ]) }, async () => { await dropTable(tableName) diff --git a/e2e/studio/features/table-editor.spec.ts b/e2e/studio/features/table-editor.spec.ts index 1fc0bc481f3..66038d9219c 100644 --- a/e2e/studio/features/table-editor.spec.ts +++ b/e2e/studio/features/table-editor.spec.ts @@ -243,7 +243,7 @@ testRunner('table editor', () => { // insert row with enum value await page.getByTestId('table-editor-insert-new-row').click() - await page.getByText('Insert a new row into').click() + await page.getByText('Insert row').click() await page.getByRole('combobox').click() await page.getByRole('option', { name: 'value1' }).click() await page.getByTestId('action-bar-save-row').click() @@ -252,7 +252,7 @@ testRunner('table editor', () => { // insert row with another enum value await page.getByTestId('table-editor-insert-new-row').click() - await page.getByText('Insert a new row into').click() + await page.getByText('Insert row').click() await page.getByRole('combobox').click() await page.getByRole('option', { name: 'value2' }).click() await page.getByTestId('action-bar-save-row').click() @@ -290,7 +290,7 @@ testRunner('table editor', () => { // create 3 rows for (const value of ['789', '456', '123']) { await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId('pw_column-input').fill(value) const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST', @@ -514,7 +514,7 @@ testRunner('table editor', () => { for (const value of ['789', '456', '123']) { await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${colName}-input`).fill(value) const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST', @@ -769,7 +769,7 @@ testRunner('table editor', () => { // Insert first row with value 'first_row_value' await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${colName}-input`).fill('first_row_value') const insertFirstPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST', @@ -779,7 +779,7 @@ testRunner('table editor', () => { // Insert second row with value 'second_row_value' await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${colName}-input`).fill('second_row_value') const insertSecondPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST', @@ -862,7 +862,7 @@ testRunner('table editor', () => { // Insert a row with TRUE value via side panel await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByRole('combobox').click() await page.getByRole('option', { name: 'TRUE' }).click() const insertTruePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { @@ -878,7 +878,7 @@ testRunner('table editor', () => { // Insert a row with FALSE value via side panel await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByRole('combobox').click() await page.getByRole('option', { name: 'FALSE' }).click() const insertFalsePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { @@ -983,7 +983,7 @@ testRunner('table editor', () => { // Insert a row with TRUE value await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByRole('combobox').click() await page.getByRole('option', { name: 'TRUE' }).click() const insertTruePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { @@ -999,7 +999,7 @@ testRunner('table editor', () => { // Insert a row with FALSE value await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByRole('combobox').click() await page.getByRole('option', { name: 'FALSE' }).click() const insertFalsePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { @@ -1354,7 +1354,7 @@ testRunner('table editor', () => { .toBe('229|229|t') await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId('name-input').fill('Dave') const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST', @@ -1433,7 +1433,7 @@ testRunner('table editor', () => { .toBe('229|229|t') await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId('name-input').fill('Dave') const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST', @@ -1472,7 +1472,7 @@ testRunner('table editor', () => { // Open side panel to insert a new row await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() await page.getByTestId(`${columnName}-input`).fill('immediate insert') // Wait for the POST mutation to complete when saving @@ -1769,7 +1769,7 @@ testRunner('table editor', () => { // Open side panel to insert a new row await page.getByTestId('table-editor-insert-new-row').click() - await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByRole('menuitem', { name: 'Insert row' }).click() // Open the field actions dropdown and set the text column to NULL await page.getByTestId(`${columnName}-field-actions`).click()