| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020 |
- import * as Sentry from '@sentry/nextjs'
- import type { PGTable } from '@supabase/pg-meta'
- import { useQueryClient } from '@tanstack/react-query'
- import { useParams } from 'common'
- import { isEmpty, isUndefined, noop } from 'lodash'
- import { useState } from 'react'
- import { toast } from 'sonner'
- import { SonnerProgress } from 'ui'
- import { ColumnEditor } from './ColumnEditor/ColumnEditor'
- import type { ForeignKey } from './ForeignKeySelector/ForeignKeySelector.types'
- import { OperationQueueSidePanel } from './OperationQueueSidePanel/OperationQueueSidePanel'
- import { ForeignRowSelector } from './RowEditor/ForeignRowSelector/ForeignRowSelector'
- import { JsonEditor } from './RowEditor/JsonEditor'
- import { RowEditor } from './RowEditor/RowEditor'
- import { convertByteaToHex } from './RowEditor/RowEditor.utils'
- import { TextEditor } from './RowEditor/TextEditor'
- import { SchemaEditor } from './SchemaEditor'
- import type { ColumnField, CreateColumnPayload, UpdateColumnPayload } from './SidePanelEditor.types'
- import {
- createColumn,
- createTable,
- duplicateTable,
- getRowFromSidePanel,
- insertRowsViaSpreadsheet,
- insertTableRows,
- updateColumn,
- updateTable,
- } from './SidePanelEditor.utils'
- import { SpreadsheetImport } from './SpreadsheetImport/SpreadsheetImport'
- import {
- useTableApiAccessHandlerWithHistory,
- type TableApiAccessParams,
- } from './TableEditor/ApiAccessToggle'
- import { TableEditor } from './TableEditor/TableEditor'
- import type { ImportContent } from './TableEditor/TableEditor.types'
- import { useTableRowOperations } from '@/components/grid/hooks/useTableRowOperations'
- import { useIsQueueOperationsEnabled } from '@/components/interfaces/Account/Preferences/useDashboardSettings'
- import {
- acceptGeneratedPolicy,
- type GeneratedPolicy,
- } from '@/components/interfaces/Auth/Policies/Policies.utils'
- import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
- import { databasePoliciesKeys } from '@/data/database-policies/keys'
- import { useDatabasePublicationCreateMutation } from '@/data/database-publications/database-publications-create-mutation'
- import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query'
- import { useDatabasePublicationUpdateMutation } from '@/data/database-publications/database-publications-update-mutation'
- import type { Constraint } from '@/data/database/constraints-query'
- import type { ForeignKeyConstraint } from '@/data/database/foreign-key-constraints-query'
- import { databaseKeys } from '@/data/database/keys'
- import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants'
- import { entityTypeKeys } from '@/data/entity-types/keys'
- import { lintKeys } from '@/data/lint/keys'
- import { privilegeKeys } from '@/data/privileges/keys'
- import { useTableApiAccessPrivilegesMutation } from '@/data/privileges/table-api-access-mutation'
- import { tableEditorKeys } from '@/data/table-editor/keys'
- import { isTableLike, type Entity } from '@/data/table-editor/table-editor-types'
- import { tableRowKeys } from '@/data/table-rows/keys'
- import { tableKeys } from '@/data/tables/keys'
- import { RetrieveTableResult } from '@/data/tables/table-retrieve-query'
- import { getTables } from '@/data/tables/tables-query'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
- import { useUrlState } from '@/hooks/ui/useUrlState'
- import { useVisibleKey } from '@/hooks/ui/useVisibleKey'
- import { type ApiPrivilegesByRole } from '@/lib/data-api-types'
- import { isObjectContainingKeys } from '@/lib/helpers'
- import type { SafePostgresTable } from '@/lib/postgres-types'
- import { useTrack } from '@/lib/telemetry/track'
- import type { DeepReadonly, Prettify } from '@/lib/type-helpers'
- import { useTableEditorStateSnapshot, type TableEditorState } from '@/state/table-editor'
- import { createTabId, useTabsStateSnapshot } from '@/state/tabs'
- import type { Dictionary } from '@/types'
- export type SaveTableParams =
- | SaveTableParamsNew
- | SaveTableParamsDuplicate
- | SaveTableParamsExisting
- type SaveTableParamsBase = {
- configuration: SaveTableConfiguration
- columns: ColumnField[]
- foreignKeyRelations: ForeignKey[]
- resolve: () => void
- generatedPolicies?: GeneratedPolicy[]
- }
- type SaveTableParamsNew = SaveTableParamsBase & {
- action: 'create'
- payload: SaveTablePayloadNew
- }
- type SaveTableParamsDuplicate = SaveTableParamsBase & {
- action: 'duplicate'
- payload: SaveTablePayloadDuplicate
- }
- type SaveTableParamsExisting = SaveTableParamsBase & {
- action: 'update'
- payload: SaveTablePayloadExisting
- }
- type SaveTablePayloadBase = {
- /**
- * Comment to set on the table
- *
- * `null` removes existing comment
- * `undefined` leaves comment unchanged
- */
- comment?: string | null
- }
- type SaveTablePayloadNew = SaveTablePayloadBase & {
- name: string
- schema: string
- }
- type SaveTablePayloadDuplicate = SaveTablePayloadBase & {
- name: string
- }
- type SaveTablePayloadExisting = SaveTablePayloadBase & {
- name?: string
- rls_enabled?: boolean
- }
- type SaveTableConfiguration = Prettify<{
- tableId?: number
- importContent?: ImportContent
- isRLSEnabled: boolean
- isRealtimeEnabled: boolean
- isDuplicateRows: boolean
- existingForeignKeyRelations: ForeignKeyConstraint[]
- primaryKey?: Constraint
- }>
- const DUMMY_TABLE_API_ACCESS_PARAMS: TableApiAccessParams = {
- type: 'new',
- }
- const createTableApiAccessHandlerParams = ({
- snap,
- selectedTable,
- }: {
- snap: DeepReadonly<TableEditorState>
- selectedTable?: PGTable
- }): TableApiAccessParams | undefined => {
- const tableSidePanel = snap.sidePanel?.type === 'table' ? snap.sidePanel : undefined
- if (!tableSidePanel) return undefined
- if (tableSidePanel.mode === 'new') {
- return {
- type: 'new',
- }
- }
- if (!selectedTable) return undefined
- if (tableSidePanel.mode === 'duplicate') {
- return {
- type: 'duplicate',
- templateSchemaName: selectedTable.schema,
- templateTableName: selectedTable.name,
- }
- }
- return {
- type: 'edit',
- schemaName: selectedTable.schema,
- tableName: selectedTable.name,
- }
- }
- export interface SidePanelEditorProps {
- editable?: boolean
- selectedTable?: SafePostgresTable
- includeColumns?: boolean // This is mainly used for invalidating useTablesQuery
- // Because the panel is shared between grid editor and database pages
- // Both require different responses upon success of these events
- onTableCreated?: (table: RetrieveTableResult) => void
- }
- export const SidePanelEditor = ({
- editable = true,
- selectedTable,
- includeColumns = false,
- onTableCreated = noop,
- }: SidePanelEditorProps) => {
- const { ref } = useParams()
- const snap = useTableEditorStateSnapshot()
- const tabsSnap = useTabsStateSnapshot()
- const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] })
- const track = useTrack()
- const queryClient = useQueryClient()
- const { data: project } = useSelectedProjectQuery()
- const { data: org } = useSelectedOrganizationQuery()
- const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
- const { updateRow, addRow, isEditPending } = useTableRowOperations()
- const [isEdited, setIsEdited] = useState<boolean>(false)
- const csvImportKey = useVisibleKey(snap.sidePanel?.type === 'csv-import')
- const { data: publications } = useDatabasePublicationsQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- const tableApiAccessParams = createTableApiAccessHandlerParams({
- snap,
- selectedTable,
- })
- const apiAccessToggleHandler = useTableApiAccessHandlerWithHistory(
- // Dummy params used to appease TypeScript, actually gated by enabled flag
- tableApiAccessParams ?? DUMMY_TABLE_API_ACCESS_PARAMS,
- {
- enabled: tableApiAccessParams !== undefined,
- }
- )
- const { confirmOnClose, modalProps } = useConfirmOnClose({
- checkIsDirty: () => isEdited,
- onClose: () => {
- setIsEdited(false)
- snap.closeSidePanel()
- },
- })
- const enumArrayColumns = (selectedTable?.columns ?? [])
- .filter((column) => {
- return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
- })
- .map((column) => column.name)
- const { mutateAsync: createPublication } = useDatabasePublicationCreateMutation()
- const { mutateAsync: updatePublication } = useDatabasePublicationUpdateMutation({
- onError: () => {},
- })
- const { mutateAsync: updateApiPrivileges } = useTableApiAccessPrivilegesMutation({
- onError: () => {}, // Errors handled inline
- })
- const isDuplicating = snap.sidePanel?.type === 'table' && snap.sidePanel.mode === 'duplicate'
- const saveRow = async (
- payload: any,
- isNewRecord: boolean,
- configuration: { identifiers: any; rowIdx: number; createMore?: boolean },
- onComplete: (err?: any) => void
- ) => {
- if (!project || selectedTable === undefined) {
- return console.error('no project or table selected')
- }
- let saveRowError: Error | undefined
- if (isNewRecord) {
- try {
- await addRow({
- tableId: selectedTable.id,
- table: selectedTable as unknown as Entity,
- rowData: payload,
- enumArrayColumns,
- })
- } catch (error: any) {
- saveRowError = error
- }
- } else {
- const hasChanges = !isEmpty(payload)
- if (hasChanges) {
- if (selectedTable.primary_keys.length > 0) {
- const row = getRowFromSidePanel(snap.sidePanel)
- if (!row) {
- saveRowError = new Error('No row found')
- toast.error('No row found')
- onComplete(saveRowError)
- return
- }
- try {
- await updateRow({
- tableId: selectedTable.id,
- table: selectedTable as unknown as Entity,
- row,
- rowIdentifiers: configuration.identifiers,
- payload,
- enumArrayColumns,
- onSuccess: () => toast.success('Successfully updated row'),
- })
- } catch (error: any) {
- saveRowError = error
- }
- } else {
- saveRowError = new Error('No primary key')
- toast.error(
- "We can't make changes to this table because there is no primary key. Please create a primary key and try again."
- )
- }
- }
- }
- onComplete(saveRowError)
- if (!saveRowError) {
- setIsEdited(false)
- if (!configuration.createMore) snap.closeSidePanel()
- }
- }
- const onSaveColumnValue = async (value: string | number | null, resolve: () => void) => {
- if (selectedTable === undefined) return
- let payload
- let configuration
- const isNewRecord = false
- const identifiers = {} as Dictionary<any>
- if (snap.sidePanel?.type === 'json') {
- const selectedValueForJsonEdit = snap.sidePanel.jsonValue
- const { row, column } = selectedValueForJsonEdit
- payload = { [column]: value === null ? null : JSON.parse(value as any) }
- selectedTable.primary_keys.forEach((column) => (identifiers[column.name] = row![column.name]))
- configuration = { identifiers, rowIdx: row.idx }
- } else if (snap.sidePanel?.type === 'cell') {
- const column = snap.sidePanel.value?.column
- const row = snap.sidePanel.value?.row
- if (!column || !row) return
- payload = { [column]: value === null ? null : value }
- selectedTable.primary_keys.forEach((column) => (identifiers[column.name] = row![column.name]))
- configuration = { identifiers, rowIdx: row.idx }
- }
- if (payload !== undefined && configuration !== undefined) {
- try {
- await saveRow(payload, isNewRecord, configuration, () => {})
- } catch (error) {
- // [Joshen] No error handler required as error is handled within saveRow
- } finally {
- resolve()
- }
- }
- }
- const onSaveForeignRow = async (value?: { [key: string]: any }) => {
- if (selectedTable === undefined || !(snap.sidePanel?.type === 'foreign-row-selector')) return
- const selectedForeignKeyToEdit = snap.sidePanel.foreignKey
- try {
- const { row } = selectedForeignKeyToEdit
- const identifiers = {} as Dictionary<any>
- selectedTable.primary_keys.forEach((column) => {
- const col = selectedTable.columns?.find((x) => x.name === column.name)
- identifiers[column.name] =
- col?.format === 'bytea' ? convertByteaToHex(row![column.name]) : row![column.name]
- })
- const isNewRecord = false
- const configuration = { identifiers, rowIdx: row.idx }
- await saveRow(value, isNewRecord, configuration, (error) => {
- if (error) {
- toast.error(`Failed to save row: ${error?.message ?? 'Unknown error'}`)
- }
- })
- } catch (error: any) {
- toast.error(`Failed to save row: ${error?.message ?? 'Unknown error'}`)
- Sentry.captureException(error, { tags: { workflow: 'save-foreign-row' } })
- }
- }
- const saveColumn = async (
- payload: CreateColumnPayload | UpdateColumnPayload,
- isNewRecord: boolean,
- configuration: {
- columnId?: string
- primaryKey?: Constraint
- foreignKeyRelations: ForeignKey[]
- existingForeignKeyRelations: ForeignKeyConstraint[]
- createMore?: boolean
- },
- resolve: any
- ) => {
- const selectedColumnToEdit =
- snap.sidePanel?.type === 'column' ? snap.sidePanel.column : undefined
- const { primaryKey, foreignKeyRelations, existingForeignKeyRelations } = configuration
- if (!project || selectedTable === undefined) {
- return console.error('no project or table selected')
- }
- let response
- if (isNewRecord) {
- response = await createColumn({
- projectRef: project.ref,
- connectionString: project.connectionString,
- payload: payload as CreateColumnPayload,
- selectedTable,
- primaryKey,
- foreignKeyRelations,
- })
- } else {
- if (!selectedColumnToEdit) {
- return console.error('no column selected to update')
- }
- response = await updateColumn({
- projectRef: project.ref,
- connectionString: project.connectionString,
- originalColumn: selectedColumnToEdit,
- payload: payload as UpdateColumnPayload,
- selectedTable,
- primaryKey,
- foreignKeyRelations,
- existingForeignKeyRelations,
- })
- }
- if (response?.error) {
- toast.error(response.error.message)
- } else {
- if (
- !isNewRecord &&
- payload.name &&
- selectedColumnToEdit &&
- selectedColumnToEdit.name !== payload.name
- ) {
- reAddRenamedColumnSortAndFilter(selectedColumnToEdit.name, payload.name)
- }
- await Promise.all([
- queryClient.invalidateQueries({
- queryKey: tableEditorKeys.tableEditor(project?.ref, selectedTable?.id),
- }),
- queryClient.invalidateQueries({
- queryKey: databaseKeys.foreignKeyConstraints(project?.ref, selectedTable?.schema),
- }),
- queryClient.invalidateQueries({
- queryKey: databaseKeys.tableDefinition(project?.ref, selectedTable?.id),
- }),
- queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(project?.ref) }),
- queryClient.invalidateQueries({
- queryKey: tableKeys.list(project?.ref, selectedTable?.schema, includeColumns),
- }),
- ])
- // We need to invalidate tableRowsAndCount after tableEditor
- // to ensure the query sent is correct
- await queryClient.invalidateQueries({
- queryKey: tableRowKeys.tableRowsAndCount(project?.ref, selectedTable?.id),
- })
- setIsEdited(false)
- if (!configuration.createMore) snap.closeSidePanel()
- }
- resolve(response?.error)
- }
- /**
- * Adds the renamed column's filter and/or sort rules.
- */
- const reAddRenamedColumnSortAndFilter = (oldColumnName: string, newColumnName: string) => {
- setParams((prevParams) => {
- const existingFilters = (prevParams?.filter ?? []) as string[]
- const existingSorts = (prevParams?.sort ?? []) as string[]
- return {
- ...prevParams,
- filter: existingFilters.map((filter: string) => {
- const [column] = filter.split(':')
- return column === oldColumnName ? filter.replace(column, newColumnName) : filter
- }),
- sort: existingSorts.map((sort: string) => {
- const [column] = sort.split(':')
- return column === oldColumnName ? sort.replace(column, newColumnName) : sort
- }),
- }
- })
- }
- const updateTableRealtime = async (table: RetrieveTableResult, enabled: boolean) => {
- if (!project) return console.error('Project is required')
- const realtimePublication = publications?.find((pub) => pub.name === 'briven_realtime')
- try {
- if (realtimePublication === undefined) {
- const realtimeTables = enabled ? [`${table.schema}.${table.name}`] : []
- await createPublication({
- projectRef: project.ref,
- connectionString: project.connectionString,
- name: 'briven_realtime',
- publish_insert: true,
- publish_update: true,
- publish_delete: true,
- tables: realtimeTables,
- })
- track(enabled ? 'table_realtime_enabled' : 'table_realtime_disabled', {
- method: 'ui',
- schema_name: table.schema,
- table_name: table.name,
- })
- return
- }
- if (realtimePublication.tables === null) {
- // UI doesn't have support for toggling realtime for ALL tables
- // Switch it to individual tables via an array of strings
- // Refer to PublicationStore for more information about this
- const publicTables = await queryClient.fetchQuery({
- queryKey: tableKeys.list(project.ref, 'public', includeColumns),
- queryFn: ({ signal }) =>
- getTables(
- {
- projectRef: project.ref,
- connectionString: project.connectionString,
- schema: 'public',
- },
- signal
- ),
- })
- // TODO: support tables in non-public schemas
- const realtimeTables = enabled
- ? publicTables.map((t) => `${t.schema}.${t.name}`)
- : publicTables.filter((t) => t.id !== table.id).map((t) => `${t.schema}.${t.name}`)
- await updatePublication({
- id: realtimePublication.id,
- projectRef: project.ref,
- connectionString: project.connectionString,
- tables: realtimeTables,
- })
- track(enabled ? 'table_realtime_enabled' : 'table_realtime_disabled', {
- method: 'ui',
- schema_name: table.schema,
- table_name: table.name,
- })
- return
- }
- const isAlreadyEnabled = realtimePublication.tables.some((x) => x.id == table.id)
- const realtimeTables =
- isAlreadyEnabled && !enabled
- ? // Toggle realtime off
- realtimePublication.tables
- .filter((t) => t.id !== table.id)
- .map((t) => `${t.schema}.${t.name}`)
- : !isAlreadyEnabled && enabled
- ? // Toggle realtime on
- realtimePublication.tables
- .map((t) => `${t.schema}.${t.name}`)
- .concat([`${table.schema}.${table.name}`])
- : null
- if (realtimeTables === null) return
- await updatePublication({
- id: realtimePublication.id,
- projectRef: project.ref,
- connectionString: project.connectionString,
- tables: realtimeTables,
- })
- track(enabled ? 'table_realtime_enabled' : 'table_realtime_disabled', {
- method: 'ui',
- schema_name: table.schema,
- table_name: table.name,
- })
- } catch (error: any) {
- toast.error(`Failed to update realtime for ${table.name}: ${error.message}`)
- }
- }
- const updateTableApiAccess = async (
- table: RetrieveTableResult,
- privileges: DeepReadonly<ApiPrivilegesByRole>
- ) => {
- if (!project) return console.error('Project is required')
- try {
- await updateApiPrivileges({
- projectRef: project.ref,
- connectionString: project.connectionString ?? undefined,
- relationId: table.id,
- privileges,
- })
- } catch (error) {
- const message = error instanceof Error ? error.message : undefined
- const toastDetail = message ? `: ${message}` : ''
- toast.error(`Failed to update API access privileges for ${table.name}${toastDetail}`)
- }
- }
- const saveTable = async ({
- action,
- payload,
- configuration,
- columns,
- foreignKeyRelations,
- generatedPolicies = [],
- resolve,
- }: SaveTableParams) => {
- let toastId
- let saveTableError = false
- if (!apiAccessToggleHandler.isSuccess) {
- if (apiAccessToggleHandler.isPending) {
- toast.info(
- 'Cannot save table yet because Data API settings are still loading. Please try again in a moment.'
- )
- } else {
- toast.error(
- 'Cannot save table because there was an error loading Data API settings. Please refresh the page and try again.'
- )
- }
- return
- }
- const {
- importContent,
- isRLSEnabled,
- isRealtimeEnabled,
- isDuplicateRows,
- existingForeignKeyRelations,
- primaryKey,
- } = configuration
- try {
- if (action === 'create') {
- await Sentry.startSpan(
- {
- name: 'Create Table',
- op: 'db.table.create',
- },
- async (createTableSpan) => {
- toastId = toast.loading(`Creating new table: ${payload.name}...`)
- // Get existing table count from cache — try entity types first (always loaded
- // by the Table Editor sidebar), then fall back to tables query cache.
- // Entity types uses useInfiniteQuery, so the cache shape is { pages: [...] }.
- // Each page has data.count (total count from SQL count(*) over()).
- const entityTypesEntries = queryClient.getQueriesData<{
- pages?: Array<{ data?: { count?: number } }>
- }>({
- queryKey: ['projects', project?.ref, 'entity-types'],
- })
- const existingTableCount =
- entityTypesEntries
- .map(([, data]) => data?.pages?.[0]?.data?.count)
- .find((count) => typeof count === 'number') ??
- queryClient.getQueryData<unknown[]>(
- tableKeys.list(project?.ref, payload.schema, true)
- )?.length ??
- queryClient.getQueryData<unknown[]>(
- tableKeys.list(project?.ref, payload.schema, false)
- )?.length
- createTableSpan.setAttributes({
- 'table.name': payload.name,
- 'table.schema': payload.schema ?? 'public',
- 'table.columns_count': columns.length,
- 'table.has_rls': isRLSEnabled ? 1 : 0,
- 'table.has_foreign_keys': foreignKeyRelations.length > 0 ? 1 : 0,
- 'table.has_import': importContent !== undefined ? 1 : 0,
- 'table.generated_policies_count': generatedPolicies.length,
- 'project.region': project?.region ?? 'local',
- ...(project?.cloud_provider && {
- 'project.cloud_provider': project.cloud_provider,
- }),
- ...(existingTableCount != null && {
- 'project.existing_table_count': String(existingTableCount),
- }),
- })
- try {
- // The Save click is the explicit user gesture that promotes generated policy
- // SQL (programmatic or AI) to executable. Programmatic fragments are already
- // SafeSqlFragment; AI fragments are UntrustedSqlFragment — both are accepted
- // here before being passed into createTable.
- const acceptedPolicies = generatedPolicies.map(acceptGeneratedPolicy)
- const { table, failedPolicies } = await createTable({
- projectRef: project?.ref!,
- connectionString: project?.connectionString,
- toastId,
- payload,
- columns,
- foreignKeyRelations,
- isRLSEnabled,
- importContent,
- organizationSlug: org?.slug,
- generatedPolicies: acceptedPolicies,
- onCreatePoliciesSuccess: () => track('rls_generated_policies_created'),
- })
- createTableSpan.setAttribute('table.created', 1)
- createTableSpan.setAttribute('table.failed_policies', failedPolicies.length)
- await Sentry.startSpan(
- { name: 'create_table.post_creation', op: 'db.table.post_creation' },
- async () => {
- if (isRealtimeEnabled) await updateTableRealtime(table, true)
- const privilegesToSet = apiAccessToggleHandler.data?.schemaExposed
- ? apiAccessToggleHandler.data.privileges
- : undefined
- if (privilegesToSet) {
- await updateTableApiAccess(table, privilegesToSet)
- }
- }
- )
- // Invalidate queries for table creation
- await Sentry.startSpan(
- { name: 'create_table.cache_invalidation', op: 'cache.invalidate' },
- async () => {
- await Promise.all([
- queryClient.invalidateQueries({
- queryKey: tableKeys.list(project?.ref, table.schema, includeColumns),
- }),
- queryClient.invalidateQueries({
- queryKey: entityTypeKeys.list(project?.ref),
- }),
- queryClient.invalidateQueries({
- queryKey: databasePoliciesKeys.list(project?.ref),
- }),
- queryClient.invalidateQueries({
- queryKey: privilegeKeys.tablePrivilegesList(project?.ref),
- }),
- queryClient.invalidateQueries({ queryKey: lintKeys.lint(project?.ref) }),
- ])
- }
- )
- // Show success toast after everything is complete
- if (failedPolicies.length > 0) {
- toast.success(
- `Table ${table.name} is created successfully, but we ran into issues creating ${failedPolicies.length} policie${failedPolicies.length > 1 ? 's' : ''}`,
- {
- id: toastId,
- description: (
- <ul className="list-disc pl-6">
- {failedPolicies.map((x) => (
- <li key={x.name}>{x.name}</li>
- ))}
- </ul>
- ),
- }
- )
- } else {
- toast.success(`Table ${table.name} is good to go!`, { id: toastId })
- }
- onTableCreated(table)
- } catch (error) {
- createTableSpan.setAttribute('table.error', 1)
- Sentry.captureException(error, {
- tags: { workflow: 'create-table' },
- })
- saveTableError = true
- throw error
- }
- }
- )
- } else if (action === 'duplicate' && !!selectedTable) {
- const tableToDuplicate = selectedTable
- toastId = toast.loading(`Duplicating table: ${tableToDuplicate.name}...`)
- const table = await duplicateTable(project?.ref!, project?.connectionString, payload, {
- isRLSEnabled,
- isDuplicateRows,
- duplicateTable: tableToDuplicate,
- foreignKeyRelations,
- })
- if (isRealtimeEnabled) await updateTableRealtime(table, isRealtimeEnabled)
- const privilegesToSet = apiAccessToggleHandler.data?.schemaExposed
- ? apiAccessToggleHandler.data.privileges
- : undefined
- if (privilegesToSet) {
- await updateTableApiAccess(table, privilegesToSet)
- }
- await Promise.all([
- queryClient.invalidateQueries({
- queryKey: tableKeys.list(project?.ref, table.schema, includeColumns),
- }),
- queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(project?.ref) }),
- queryClient.invalidateQueries({
- queryKey: privilegeKeys.tablePrivilegesList(project?.ref),
- }),
- queryClient.invalidateQueries({ queryKey: lintKeys.lint(project?.ref) }),
- ])
- toast.success(
- `Table ${tableToDuplicate.name} has been successfully duplicated into ${table.name}!`,
- { id: toastId }
- )
- onTableCreated(table)
- } else if (action === 'update' && selectedTable) {
- toastId = toast.loading(`Updating table: ${selectedTable.name}...`)
- const { table, hasError } = await updateTable({
- projectRef: project?.ref!,
- connectionString: project?.connectionString,
- toastId,
- table: selectedTable,
- payload,
- columns,
- foreignKeyRelations,
- existingForeignKeyRelations,
- primaryKey,
- organizationSlug: org?.slug,
- })
- if (table === undefined) {
- return toast.error('Failed to update table')
- }
- if (isTableLike(table)) {
- await updateTableRealtime(table, isRealtimeEnabled)
- const privilegesToSet = apiAccessToggleHandler.data?.schemaExposed
- ? apiAccessToggleHandler.data.privileges
- : undefined
- if (privilegesToSet) {
- await updateTableApiAccess(table, privilegesToSet)
- }
- }
- if (hasError) {
- toast.warning(
- `Table ${table.name} has been updated but there were some errors. Please check these errors separately.`
- )
- } else {
- if (ref && payload.name) {
- // [Joshen] Only table entities can be updated via the dashboard
- const tabId = createTabId(ENTITY_TYPE.TABLE, { id: selectedTable.id })
- tabsSnap.updateTab(tabId, { label: payload.name })
- }
- toast.success(`Successfully updated ${table.name}!`, { id: toastId })
- }
- }
- } catch (error: any) {
- saveTableError = true
- toast.error(error.message, { id: toastId })
- }
- if (!saveTableError) {
- setIsEdited(false)
- snap.closeSidePanel()
- }
- resolve()
- }
- const onImportData = async (importContent: ImportContent) => {
- if (!project || selectedTable === undefined) {
- return console.error('no project or table selected')
- }
- const { file, rowCount, selectedHeaders, emptyStringAsNullHeaders, resolve } = importContent
- const toastId = toast.loading(
- `Adding ${rowCount.toLocaleString()} rows to ${selectedTable.name}`
- )
- if (file && rowCount > 0) {
- const res = await insertRowsViaSpreadsheet({
- projectRef: project.ref!,
- connectionString: project.connectionString,
- file,
- table: selectedTable,
- selectedHeaders,
- emptyStringAsNullHeaders,
- onProgressUpdate: (progress: number) => {
- toast.loading(
- <SonnerProgress
- progress={progress}
- message={`Adding ${rowCount.toLocaleString()} rows to ${selectedTable.name}`}
- />,
- { id: toastId }
- )
- },
- })
- if (res.error) {
- const message = isObjectContainingKeys(res.error, ['message'])
- ? res.error.message
- : 'An unknown error occurred during import'
- toast.error(`Failed to import data: ${message}`, { id: toastId })
- return resolve()
- }
- } else {
- const res = await insertTableRows({
- projectRef: project.ref!,
- connectionString: project.connectionString,
- table: selectedTable,
- rows: importContent.rows,
- selectedHeaders,
- emptyStringAsNullHeaders,
- onProgressUpdate: (progress: number) => {
- toast.loading(
- <SonnerProgress
- progress={progress}
- message={`Adding ${importContent.rows.length.toLocaleString()} rows to ${
- selectedTable.name
- }`}
- />,
- { id: toastId }
- )
- },
- })
- if (res.error) {
- const message = isObjectContainingKeys(res.error, ['message'])
- ? res.error.message
- : 'An unknown error occurred during import'
- toast.error(`Failed to import data: ${message}`, { id: toastId })
- return resolve()
- }
- }
- await queryClient.invalidateQueries({
- queryKey: tableRowKeys.tableRowsAndCount(project?.ref, selectedTable?.id),
- })
- toast.success(`Successfully imported ${rowCount} rows of data into ${selectedTable.name}`, {
- id: toastId,
- })
- resolve()
- snap.closeSidePanel()
- }
- const onClosePanel = confirmOnClose
- return (
- <>
- {!isUndefined(selectedTable) && (
- <RowEditor
- row={snap.sidePanel?.type === 'row' ? snap.sidePanel.row : undefined}
- selectedTable={selectedTable}
- visible={snap.sidePanel?.type === 'row'}
- editable={editable}
- closePanel={onClosePanel}
- saveChanges={saveRow}
- updateEditorDirty={() => setIsEdited(true)}
- />
- )}
- {!isUndefined(selectedTable) && (
- <ColumnEditor
- column={snap.sidePanel?.type === 'column' ? snap.sidePanel.column : undefined}
- selectedTable={selectedTable}
- visible={snap.sidePanel?.type === 'column'}
- closePanel={onClosePanel}
- saveChanges={saveColumn}
- updateEditorDirty={() => setIsEdited(true)}
- />
- )}
- <TableEditor
- table={
- snap.sidePanel?.type === 'table' &&
- (snap.sidePanel.mode === 'edit' || snap.sidePanel.mode === 'duplicate')
- ? selectedTable
- : undefined
- }
- isDuplicating={isDuplicating}
- templateData={
- snap.sidePanel?.type === 'table' && snap.sidePanel.templateData
- ? {
- ...snap.sidePanel.templateData,
- columns: snap.sidePanel.templateData.columns
- ? [...snap.sidePanel.templateData.columns]
- : undefined,
- }
- : undefined
- }
- visible={snap.sidePanel?.type === 'table'}
- closePanel={onClosePanel}
- saveChanges={saveTable}
- updateEditorDirty={() => setIsEdited(true)}
- apiAccessToggleHandler={apiAccessToggleHandler}
- />
- <SchemaEditor
- visible={snap.sidePanel?.type === 'schema'}
- onSuccess={onClosePanel}
- closePanel={onClosePanel}
- />
- <JsonEditor
- visible={snap.sidePanel?.type === 'json'}
- row={(snap.sidePanel?.type === 'json' && snap.sidePanel.jsonValue.row) || {}}
- column={(snap.sidePanel?.type === 'json' && snap.sidePanel.jsonValue.column) || ''}
- backButtonLabel="Cancel"
- applyButtonLabel={isQueueOperationsEnabled ? 'Queue changes' : 'Save changes'}
- readOnly={!editable}
- closePanel={onClosePanel}
- onSaveJSON={onSaveColumnValue}
- />
- <TextEditor
- visible={snap.sidePanel?.type === 'cell'}
- column={(snap.sidePanel?.type === 'cell' && snap.sidePanel.value?.column) || ''}
- row={(snap.sidePanel?.type === 'cell' && snap.sidePanel.value?.row) || {}}
- closePanel={onClosePanel}
- onSaveField={onSaveColumnValue}
- />
- <ForeignRowSelector
- visible={snap.sidePanel?.type === 'foreign-row-selector'}
- // @ts-ignore
- foreignKey={
- snap.sidePanel?.type === 'foreign-row-selector'
- ? snap.sidePanel.foreignKey.foreignKey
- : undefined
- }
- isSaving={isEditPending}
- closePanel={onClosePanel}
- onSelect={onSaveForeignRow}
- />
- <SpreadsheetImport
- key={csvImportKey}
- visible={snap.sidePanel?.type === 'csv-import'}
- selectedTable={selectedTable}
- saveContent={onImportData}
- closePanel={onClosePanel}
- updateEditorDirty={setIsEdited}
- />
- <OperationQueueSidePanel />
- <DiscardChangesConfirmationDialog {...modalProps} />
- </>
- )
- }
|