mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
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:
@@ -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>
|
||||
|
||||
+2
-2
@@ -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_>
|
||||
)
|
||||
}
|
||||
+2
-2
@@ -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>
|
||||
)}
|
||||
|
||||
+2
-2
@@ -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 ?? '' })
|
||||
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user