Clean up table editor header (#45452)

## Context

Resolves FE-3126

Just cleaning up the table editor header with a bit of refactors
(pre-req to investigating collapsing filter bar and table editor header
actions into a single row)

## Non-visual changes involved
- Break down components within `GridHeaderActions` into smaller ones
  - `IndexAdvisorPopover`
  - `SecurityDefinerViewPopover`
  - `RealtimeToggle`
- Deprecate use of `useUrlState` in `GridHeaderActions` to use
`useQueryState` instead
- Improve types for `TwoOptionToggle`

## Visual changes involved
- Collapse realtime button toggle into a button icon, with no text (just
tooltip)
- Adjust layout of buttons a little

### Before
<img width="796" height="118" alt="image"
src="https://github.com/user-attachments/assets/436bca94-4d91-471a-a184-487c6f78dc04"
/>

### After
<img width="731" height="132" alt="image"
src="https://github.com/user-attachments/assets/5fd30982-a1fc-4f92-a590-146d1e69d52a"
/>


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

* **New Features**
  * Index Advisor popover with recommendations.
  * Realtime toggle to manage realtime table publication.
  * Security Definer view popover with optional autofix.
  * Insert menu for adding rows/columns and CSV import.

* **Bug Fixes**
  * Adjusted filter bar input sizing for improved readability.

* **Refactor**
* Header layout updated and insert/import actions moved into dedicated
components.

* **Tests**
  * Updated end-to-end selectors for the Insert row menu item.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Joshen Lim
2026-05-01 18:45:21 +08:00
committed by GitHub
parent f8cc6c21bd
commit 7f8ae81d64
20 changed files with 473 additions and 437 deletions
@@ -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'
@@ -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 (
<div>
<div className="flex flex-wrap min-h-10 items-center bg-dash-sidebar dark:bg-surface-100 py-1.5 gap-2">
<div className="flex flex-wrap min-h-10 items-center bg-dash-sidebar dark:bg-surface-100">
{customHeader ? (
<div className="flex-1 px-1.5">{customHeader}</div>
) : snap.selectedRows.size > 0 ? (
@@ -72,165 +64,21 @@ export const Header = ({ customHeader, isRefetching, tableQueriesEnabled = true
<RowHeader tableQueriesEnabled={tableQueriesEnabled} />
</div>
) : (
<div className="w-full flex items-center gap-2 px-1.5 pb-1.5 border-b border-border">
<div className="w-full flex items-center justify-between gap-2 pr-1.5 py-1.5 border-b border-border">
<FilterPopoverNew isRefetching={isRefetching} />
</div>
)}
<div className="flex items-center gap-2 overflow-x-auto px-1.5">
<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} />
{showInsertButton && <InsertButton />}
</div>
</div>
</div>
)
}
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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="table-editor-insert-new-row"
type="primary"
size="tiny"
icon={<ChevronDown strokeWidth={1.5} />}
>
Insert
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
{[
...(onAddRow !== undefined
? [
<DropdownMenuItem key="add-row" className="group gap-x-3" onClick={onAddRow}>
<div className="-mt-2 pr-1.5 shrink-0">
<div className="border border-foreground-lighter w-[15px] h-[4px]" />
<div className="border border-foreground-lighter w-[15px] h-[4px] my-[2px]" />
<div
className={cn([
'border border-foreground-light w-[15px] h-[4px] translate-x-0.5',
'transition duration-200 group-data-highlighted:border-brand group-data-highlighted:translate-x-0',
])}
/>
</div>
<div className="flex-1 min-w-0 pr-4">
<p>Insert row</p>
<p className="text-foreground-light">Insert a new row into {snap.table.name}</p>
</div>
<ShortcutBadge
shortcutId={SHORTCUT_IDS.TABLE_EDITOR_INSERT_ROW}
className="shrink-0"
/>
</DropdownMenuItem>,
]
: []),
...(onAddColumn !== undefined
? [
<DropdownMenuItem key="add-column" className="group gap-x-3" onClick={onAddColumn}>
<div className="flex -mt-2 pr-1.5 shrink-0">
<div className="border border-foreground-lighter w-[4px] h-[15px]" />
<div className="border border-foreground-lighter w-[4px] h-[15px] mx-[2px]" />
<div
className={cn([
'border border-foreground-light w-[4px] h-[15px] -translate-y-0.5',
'transition duration-200 group-data-highlighted:border-brand group-data-highlighted:translate-y-0',
])}
/>
</div>
<div className="flex-1 min-w-0 pr-4">
<p>Insert column</p>
<p className="text-foreground-light">
Insert a new column into {snap.table.name}
</p>
</div>
<ShortcutBadge
shortcutId={SHORTCUT_IDS.TABLE_EDITOR_INSERT_COLUMN}
className="shrink-0"
/>
</DropdownMenuItem>,
]
: []),
...(onImportData !== undefined
? [
<DropdownMenuItem
key="import-data"
className="group gap-x-3"
onClick={() => {
onImportData()
sendEvent({
action: 'import_data_button_clicked',
properties: { tableType: 'Existing Table' },
groups: {
project: projectRef ?? 'Unknown',
organization: org?.slug ?? 'Unknown',
},
})
}}
>
<div className="relative -mt-2 shrink-0">
<FileText size={18} strokeWidth={1.5} className="translate-x-[-2px]" />
<ArrowUp
className={cn(
'transition duration-200 absolute bottom-0 right-0 translate-y-1 opacity-0 bg-brand-400 rounded-full',
'group-data-highlighted:translate-y-0 group-data-highlighted:text-brand group-data-highlighted:opacity-100'
)}
strokeWidth={3}
size={12}
/>
</div>
<div className="flex-1 min-w-0 pr-4">
<p>Import data from CSV</p>
<p className="text-foreground-light">Insert new rows from a CSV</p>
</div>
<ShortcutBadge
shortcutId={SHORTCUT_IDS.TABLE_EDITOR_IMPORT_CSV}
className="shrink-0"
/>
</DropdownMenuItem>,
]
: []),
]}
</DropdownMenuContent>
</DropdownMenu>
)
}
type RowHeaderProps = {
tableQueriesEnabled?: boolean
}
@@ -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}
/>
</div>
@@ -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"
/>
</div>
@@ -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 ? (
<FilterPill
@@ -12,7 +12,7 @@ import {
generateMigrationCliCommand,
generateSeedCliCommand,
} from './SQLEditor.utils'
import TwoOptionToggle from '@/components/ui/TwoOptionToggle'
import { TwoOptionToggle } from '@/components/ui/TwoOptionToggle'
import { DOCS_URL } from '@/lib/constants'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
@@ -4,7 +4,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle } from 'ui'
import { Markdown } from '@/components/interfaces/Markdown'
import CodeEditor from '@/components/ui/CodeEditor/CodeEditor'
import TwoOptionToggle from '@/components/ui/TwoOptionToggle'
import { TwoOptionToggle } from '@/components/ui/TwoOptionToggle'
interface CellDetailPanelProps {
column: string
@@ -41,7 +41,7 @@ export const CellDetailPanel = ({ column, value, visible, onClose }: CellDetailP
options={['MD', 'view']}
activeOption={view}
borderOverride="border-muted"
onClickOption={setView}
onClickOption={(value) => setView(value as 'view' | 'md')}
/>
)}
</SheetTitle>
@@ -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 && (
<Popover_Shadcn_ modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<Button type="default" icon={<Lightbulb strokeWidth={1.5} />}>
Index Advisor
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="w-80 text-sm" align="end">
<h4 className="flex items-center gap-2">
<Lightbulb size={16} /> Index Advisor
</h4>
<div className="grid gap-2 mt-4 text-foreground-light text-xs">
<p>
Index Advisor recommends indexes to improve query performance on this table.
</p>
<p>
Enable Index Advisor to get recommendations based on your actual query patterns.
</p>
<div className="mt-2">
<EnableIndexAdvisorButton />
</div>
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)}
{realtimeEnabled && (
<ButtonTooltip
type="default"
size="tiny"
icon={
<MousePointer2
strokeWidth={1.5}
className={isRealtimeEnabled ? 'text-brand' : 'text-foreground-muted'}
/>
}
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'}
</ButtonTooltip>
)}
{isView && viewHasLints && (
<Popover_Shadcn_ modal={false} open={showWarning} onOpenChange={setShowWarning}>
<PopoverTrigger_Shadcn_ asChild>
<Button type="warning" icon={<Unlock strokeWidth={1.5} />}>
Security Definer view
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="min-w-[395px] text-sm" align="end">
<h3 className="flex items-center gap-2">
<Unlock size={16} /> Secure your View
</h3>
<div className="grid gap-2 mt-4 text-foreground-light text-sm">
<p>
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.
</p>
<p>
Since this view is in the public schema, it is accessible via your project's
APIs.
</p>
<div className="mt-2 flex items-center gap-2">
<Button
type="secondary"
onClick={() => {
setIsAutofixViewSecurityModalOpen(true)
}}
>
Autofix
</Button>
<Button type="default" asChild>
<Link
target="_blank"
href={`/project/${ref}/advisors/security?preset=${matchingViewLint?.level}&id=${matchingViewLint?.cache_key}`}
>
Learn more
</Link>
</Button>
</div>
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
<SecurityDefinerViewPopover
lint={matchingViewLint}
onAutofix={() => {
setIsAutofixViewSecurityModalOpen(true)
}}
/>
)}
{isMaterializedView && materializedViewHasLints && (
<Popover_Shadcn_ modal={false} open={showWarning} onOpenChange={setShowWarning}>
<PopoverTrigger_Shadcn_ asChild>
<Button type="warning" icon={<Unlock strokeWidth={1.5} />}>
Security Definer view
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="min-w-[395px] text-sm" align="end">
<h3 className="flex items-center gap-2">
<Unlock size={16} /> Secure your View
</h3>
<div className="grid gap-2 mt-4 text-foreground-light text-sm">
<p>
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.
</p>
<p>
Since this view is in the public schema, it is accessible via your project's
APIs.
</p>
<div className="mt-2">
<Button type="default" asChild>
<Link
target="_blank"
href={`/project/${ref}/advisors/security?preset=${matchingMaterializedViewLint?.level}&id=${matchingMaterializedViewLint?.cache_key}`}
>
Learn more
</Link>
</Button>
</div>
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
<SecurityDefinerViewPopover lint={matchingMaterializedViewLint} />
)}
{isForeignTable && table.schema === 'public' && (
@@ -489,38 +316,19 @@ 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" />
)}
<RefreshButton tableId={table.id} isRefetching={isRefetching} />
{showHeaderActions && <InsertButton />}
</div>
)}
<ConfirmationModal
visible={showEnableRealtime}
loading={isTogglingRealtime}
title={`${isRealtimeEnabled ? 'Disable' : 'Enable'} realtime for ${table.name}`}
confirmLabel={`${isRealtimeEnabled ? 'Disable' : 'Enable'} realtime`}
confirmLabelLoading={`${isRealtimeEnabled ? 'Disabling' : 'Enabling'} realtime`}
onCancel={() => setShowEnableRealtime(false)}
onConfirm={() => toggleRealtime()}
>
<div className="space-y-2">
<p className="text-sm">
Once realtime has been {isRealtimeEnabled ? 'disabled' : 'enabled'}, the table will{' '}
{isRealtimeEnabled ? 'no longer ' : ''}broadcast any changes to authorized subscribers.
</p>
{!isRealtimeEnabled && (
<p className="text-sm">
You may also select which events to broadcast to subscribers on the{' '}
<Link href={`/project/${ref}/database/publications`} className="text-brand">
database publications
</Link>{' '}
settings.
</p>
)}
</div>
</ConfirmationModal>
<ViewEntityAutofixSecurityModal
table={table}
@@ -0,0 +1,28 @@
import { Lightbulb } from 'lucide-react'
import { Button, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_ } from 'ui'
import { EnableIndexAdvisorButton } from '../QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton'
export const IndexAdvisorPopover = () => {
return (
<Popover_Shadcn_ modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<Button type="default" icon={<Lightbulb strokeWidth={1.5} />}>
Index Advisor
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="w-80 text-sm" align="end">
<h4 className="flex items-center gap-2">
<Lightbulb size={16} /> Index Advisor
</h4>
<div className="grid gap-2 mt-4 text-foreground-light text-xs">
<p>Index Advisor recommends indexes to improve query performance on this table.</p>
<p>Enable Index Advisor to get recommendations based on your actual query patterns.</p>
<div className="mt-2">
<EnableIndexAdvisorButton />
</div>
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
}
@@ -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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="table-editor-insert-new-row"
type="primary"
size="tiny"
icon={<ChevronDown strokeWidth={1.5} />}
>
Insert
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
{[
...(onAddRow !== undefined
? [
<DropdownMenuItem
key="add-row"
className="flex items-center group gap-x-3"
onClick={onAddRow}
>
<div className="shrink-0 w-4">
<div className="border border-foreground-lighter w-[15px] h-[4px]" />
<div className="border border-foreground-lighter w-[15px] h-[4px] my-[2px]" />
<div
className={cn([
'border border-foreground-light w-[15px] h-[4px] translate-x-0.5',
'transition duration-200 group-data-highlighted:border-brand group-data-highlighted:translate-x-0',
])}
/>
</div>
<p className="flex-1 min-w-0 pr-4">Insert row</p>
<ShortcutBadge
shortcutId={SHORTCUT_IDS.TABLE_EDITOR_INSERT_ROW}
className="shrink-0"
/>
</DropdownMenuItem>,
]
: []),
...(onAddColumn !== undefined
? [
<DropdownMenuItem key="add-column" className="group gap-x-3" onClick={onAddColumn}>
<div className="flex shrink-0 w-4">
<div className="border border-foreground-lighter w-[4px] h-[15px]" />
<div className="border border-foreground-lighter w-[4px] h-[15px] mx-[2px]" />
<div
className={cn([
'border border-foreground-light w-[4px] h-[15px] -translate-y-0.5',
'transition duration-200 group-data-highlighted:border-brand group-data-highlighted:translate-y-0',
])}
/>
</div>
<p className="flex-1 min-w-0 pr-4">Insert column</p>
<ShortcutBadge
shortcutId={SHORTCUT_IDS.TABLE_EDITOR_INSERT_COLUMN}
className="shrink-0"
/>
</DropdownMenuItem>,
]
: []),
...(onImportData !== undefined
? [
<DropdownMenuItem
key="import-data"
className="group gap-x-3"
onClick={() => {
onImportData()
sendEvent({
action: 'import_data_button_clicked',
properties: { tableType: 'Existing Table' },
groups: {
project: projectRef ?? 'Unknown',
organization: org?.slug ?? 'Unknown',
},
})
}}
>
<div className="relative shrink-0 w-4">
<FileText size={18} strokeWidth={1.5} className="translate-x-[-2px]" />
<ArrowUp
className={cn(
'transition duration-200 absolute bottom-0 right-0 translate-y-1 opacity-0 bg-brand-400 rounded-full',
'group-data-highlighted:translate-y-0 group-data-highlighted:text-brand group-data-highlighted:opacity-100'
)}
strokeWidth={3}
size={12}
/>
</div>
<p className="flex-1 min-w-0 pr-4">Import data from CSV</p>
<ShortcutBadge
shortcutId={SHORTCUT_IDS.TABLE_EDITOR_IMPORT_CSV}
className="shrink-0"
/>
</DropdownMenuItem>,
]
: []),
]}
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -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 (
<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>
{isRealtimeEnabled ? 'Disable' : 'Enable'} realtime for {table.name}
</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection>
<div className="space-y-2">
<p className="text-sm">
Once realtime has been {isRealtimeEnabled ? 'disabled' : 'enabled'}, the table will{' '}
{isRealtimeEnabled ? 'no longer ' : ''}broadcast any changes to authorized
subscribers.
</p>
{!isRealtimeEnabled && (
<p className="text-sm">
You may also select which events to broadcast to subscribers on the{' '}
<InlineLink href={`/project/${ref}/database/publications`}>
database publications
</InlineLink>{' '}
settings.
</p>
)}
</div>
</DialogSection>
<DialogFooter>
<Button type="default" disabled={isTogglingRealtime} onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="primary" loading={isTogglingRealtime} onClick={toggleRealtime}>
{isRealtimeEnabled ? 'Disable' : 'Enable'} realtime
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -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 (
<Popover_Shadcn_ modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<Button type="warning" icon={<Unlock strokeWidth={1.5} />}>
Security Definer view
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="min-w-[395px] text-sm" align="end">
<h4 className="flex items-center gap-2">
<Unlock size={14} /> Secure your view
</h4>
<div className="grid gap-2 mt-2 text-foreground-light text-sm">
<p>
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.
</p>
<p>Since this view is in the public schema, it is accessible via your project's APIs.</p>
<div className="mt-2 flex items-center gap-2">
{!!onAutofix && (
<Button type="secondary" onClick={onAutofix}>
Autofix
</Button>
)}
<Button type="default" asChild>
<Link
target="_blank"
rel="noopener noreferrer"
href={`/project/${ref}/advisors/security?preset=${lint?.level}&id=${lint?.cache_key}`}
>
Learn more
</Link>
</Button>
</div>
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
}
@@ -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')}
/>
</div>
)}
@@ -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')}
/>
)}
</div>
@@ -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 ?? '' })
@@ -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 {
@@ -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'
)}
></span>
{options.map((option: any, index: number) => (
/>
{options.map((option, index: number) => (
<span
key={`toggle_${index}`}
style={{ width: width + 1 }}
@@ -61,5 +61,3 @@ const TwoOptionToggle = ({
</div>
)
}
export default TwoOptionToggle
@@ -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({
<TwoOptionToggle
options={['SQL', 'chart']}
activeOption={view}
onClickOption={handleViewChange}
onClickOption={(value) => handleViewChange(value as 'sql' | 'chart')}
borderOverride="border-overlay"
/>
</div>
@@ -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)
+14 -14
View File
@@ -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()