Merge pull request #14058 from supabase/chore/remove-pg-meta-schema-store

chore: remove Mobx pg-meta SchemaStore + #14072
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-09-18 16:12:19 +08:00
commit ec0dd7324d
69 files changed
+2316 -1840

No files matched your search

+164 -153
View File
@@ -36,168 +36,179 @@ export const SupabaseGrid = forwardRef<SupabaseGridRef, SupabaseGridProps>((prop
)
})
const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props, ref) => {
const {
editable,
storageRef,
gridProps,
headerActions,
showCustomChildren,
customHeader,
children,
onAddRow,
onAddColumn,
updateTableRow,
onEditForeignKeyColumnValue,
onImportData,
} = props
const dispatch = useDispatch()
const state = useTrackedState()
const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>(
function SupabaseGridLayout(props, ref) {
const {
editable,
storageRef,
gridProps,
headerActions,
showCustomChildren,
customHeader,
children,
onAddRow,
onAddColumn,
updateTableRow,
onEditForeignKeyColumnValue,
onImportData,
} = props
const dispatch = useDispatch()
const state = useTrackedState()
const gridRef = useRef<DataGridHandle>(null)
const [mounted, setMounted] = useState(false)
const gridRef = useRef<DataGridHandle>(null)
const [mounted, setMounted] = useState(false)
const [{ sort, filter }, setParams] = useUrlState({
arrayKeys: ['sort', 'filter'],
})
const sorts = formatSortURLParams(sort as string[])
const filters = formatFilterURLParams(filter as string[])
const [{ sort, filter }, setParams] = useUrlState({
arrayKeys: ['sort', 'filter'],
})
const sorts = formatSortURLParams(sort as string[])
const filters = formatFilterURLParams(filter as string[])
const { project } = useProjectContext()
const { data, isLoading, isRefetching } = useTableRowsQuery(
{
queryKey: [props.table.schema, props.table.name],
projectRef: project?.ref,
connectionString: project?.connectionString,
table: props.table,
sorts,
filters,
page: state.page,
limit: state.rowsPerPage,
},
{
keepPreviousData: true,
onSuccess(data) {
const { project } = useProjectContext()
const { data, error, isSuccess, isError, isLoading, isRefetching } = useTableRowsQuery(
{
queryKey: [props.table.schema, props.table.name],
projectRef: project?.ref,
connectionString: project?.connectionString,
table: props.table,
sorts,
filters,
page: state.page,
limit: state.rowsPerPage,
},
{
keepPreviousData: true,
onSuccess(data) {
dispatch({
type: 'SET_ROWS_COUNT',
payload: data.rows.length,
})
},
}
)
useImperativeHandle(ref, () => ({
rowAdded(row: Dictionary<any>) {
dispatch({
type: 'SET_ROWS_COUNT',
payload: data.rows.length,
type: 'ADD_NEW_ROW',
payload: row,
})
},
}
)
useImperativeHandle(ref, () => ({
rowAdded(row: Dictionary<any>) {
dispatch({
type: 'ADD_NEW_ROW',
payload: row,
})
},
rowEdited(row: Dictionary<any>, idx: number) {
dispatch({
type: 'EDIT_ROW',
payload: { row, idx },
})
},
}))
useEffect(() => {
if (!mounted) setMounted(true)
}, [])
useEffect(() => {
if (mounted) {
dispatch({ type: 'UPDATE_FILTERS', payload: {} })
}
}, [JSON.stringify(filters)])
useEffect(() => {
if (mounted) {
dispatch({ type: 'UPDATE_SORTS', payload: {} })
}
}, [JSON.stringify(sorts)])
useEffect(() => {
if (state.isInitialComplete && storageRef && state.table) {
saveStorageDebounced(state, storageRef, sort as string[], filter as string[])
}
}, [
state.table,
state.isInitialComplete,
state.gridColumns,
JSON.stringify(sorts),
JSON.stringify(filters),
storageRef,
])
useEffect(() => {
dispatch({
type: 'INIT_CALLBACK',
payload: { ...props },
})
}, [])
useEffect(() => {
const initializeData = async () => {
const { savedState } = await initTable(
props,
state,
dispatch,
sort as string[],
filter as string[]
)
if (savedState.sorts || savedState.filters) {
setParams((prevParams) => {
return {
...prevParams,
...(savedState.sorts && { sort: savedState.sorts }),
...(savedState.filters && { filter: savedState.filters }),
}
rowEdited(row: Dictionary<any>, idx: number) {
dispatch({
type: 'EDIT_ROW',
payload: { row, idx },
})
},
}))
useEffect(() => {
if (!mounted) setMounted(true)
}, [])
useEffect(() => {
if (mounted) {
dispatch({ type: 'UPDATE_FILTERS', payload: {} })
}
}
}, [JSON.stringify(filters)])
const refreshTable = JSON.stringify(props.table) !== JSON.stringify(state.table)
useEffect(() => {
if (mounted) {
dispatch({ type: 'UPDATE_SORTS', payload: {} })
}
}, [JSON.stringify(sorts)])
if (!state.table || refreshTable) {
initializeData()
}
}, [state.table, props.table, props.schema])
useEffect(() => {
if (state.isInitialComplete && storageRef && state.table) {
saveStorageDebounced(state, storageRef, sort as string[], filter as string[])
}
}, [
state.table,
state.isInitialComplete,
state.gridColumns,
JSON.stringify(sorts),
JSON.stringify(filters),
storageRef,
])
return (
<div className="sb-grid">
<Header
table={props.table}
sorts={sorts}
filters={filters}
isRefetching={isRefetching}
onAddRow={editable ? onAddRow : undefined}
onAddColumn={editable ? onAddColumn : undefined}
onImportData={editable ? onImportData : undefined}
headerActions={headerActions}
customHeader={customHeader}
/>
{showCustomChildren && children !== undefined ? (
<>{children}</>
) : (
<>
<Grid
ref={gridRef}
{...gridProps}
rows={data?.rows ?? []}
updateRow={updateTableRow}
onAddRow={onAddRow}
onImportData={onImportData}
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
/>
<Footer isLoading={isLoading || isRefetching} />
<Shortcuts gridRef={gridRef} />
</>
)}
useEffect(() => {
dispatch({
type: 'INIT_CALLBACK',
payload: { ...props },
})
}, [])
{mounted &&
createPortal(<RowContextMenu table={props.table} rows={data?.rows ?? []} />, document.body)}
</div>
)
})
useEffect(() => {
const initializeData = async () => {
const { savedState } = await initTable(
props,
state,
dispatch,
sort as string[],
filter as string[]
)
if (savedState.sorts || savedState.filters) {
setParams((prevParams) => {
return {
...prevParams,
...(savedState.sorts && { sort: savedState.sorts }),
...(savedState.filters && { filter: savedState.filters }),
}
})
}
}
const refreshTable = JSON.stringify(props.table) !== JSON.stringify(state.table)
if (!state.table || refreshTable) {
initializeData()
}
}, [state.table, props.table, props.schema])
return (
<div className="sb-grid">
<Header
table={props.table}
sorts={sorts}
filters={filters}
isRefetching={isRefetching}
onAddRow={editable ? onAddRow : undefined}
onAddColumn={editable ? onAddColumn : undefined}
onImportData={editable ? onImportData : undefined}
headerActions={headerActions}
customHeader={customHeader}
/>
{showCustomChildren && children !== undefined ? (
<>{children}</>
) : (
<>
<Grid
ref={gridRef}
{...gridProps}
rows={data?.rows ?? []}
error={error}
isLoading={isLoading}
isSuccess={isSuccess}
isError={isError}
filters={filters}
setParams={setParams}
updateRow={updateTableRow}
onAddRow={onAddRow}
onImportData={onImportData}
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
/>
<Footer isLoading={isLoading || isRefetching} />
<Shortcuts gridRef={gridRef} />
</>
)}
{mounted &&
createPortal(
<RowContextMenu table={props.table} rows={data?.rows ?? []} />,
document.body
)}
</div>
)
}
)
@@ -1,32 +1,44 @@
import { FormatterProps } from '@supabase/react-data-grid'
import Link from 'next/link'
import { PropsWithChildren } from 'react'
import { FormatterProps } from '@supabase/react-data-grid'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useParams } from 'common/hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useTableQuery } from 'data/tables/table-query'
import { Button, IconArrowRight } from 'ui'
import { SupaRow } from '../../types'
import { NullValue } from '../common'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconArrowRight } from 'ui'
import { useStore } from 'hooks'
import { useParams } from 'common/hooks'
import { useTablesQuery } from 'data/tables/tables-query'
export const ForeignKeyFormatter = (props: PropsWithChildren<FormatterProps<SupaRow, unknown>>) => {
const { ref, id } = useParams()
const { meta } = useStore()
const { project } = useProjectContext()
const { ref: projectRef, id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const { row, column } = props
const selectedTable = meta.tables.byId(id as string)
const { data: selectedTable } = useTableQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
id,
})
const relationship = (selectedTable?.relationships ?? []).find(
(r) =>
r.source_schema === selectedTable?.schema &&
r.source_table_name === selectedTable?.name &&
r.source_column_name === column.name
)
const targetTable = meta.tables
.list()
.find(
(table) =>
table.schema === relationship?.target_table_schema &&
table.name === relationship.target_table_name
)
const { data: tables } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: relationship?.target_table_schema,
})
const targetTable = tables?.find(
(table) =>
table.schema === relationship?.target_table_schema &&
table.name === relationship.target_table_name
)
const value = row[column.key]
@@ -39,7 +51,7 @@ export const ForeignKeyFormatter = (props: PropsWithChildren<FormatterProps<Supa
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Link
href={`/project/${ref}/editor/${targetTable?.id}?filter=${relationship?.target_column_name}%3Aeq%3A${value}`}
href={`/project/${projectRef}/editor/${targetTable?.id}?filter=${relationship?.target_column_name}%3Aeq%3A${value}`}
>
<a>
<Button
+75 -20
View File
@@ -7,11 +7,14 @@ import { memo } from 'react-tracked'
import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
import { useDispatch, useTrackedState } from '../../store'
import { GridProps, SupaRow } from '../../types'
import RowRenderer from './RowRenderer'
import { useUrlState } from 'hooks'
import { Button } from 'ui'
import { useDispatch, useTrackedState } from '../../store'
import { Filter, GridProps, SupaRow } from '../../types'
import RowRenderer from './RowRenderer'
const rowKeyGetter = (row: SupaRow) => {
return row?.idx ?? -1
@@ -27,6 +30,12 @@ const updateColumnResizeDebounced = AwesomeDebouncePromise(updateColumnResize, 5
interface IGrid extends GridProps {
rows: any[]
error: any
isLoading: boolean
isSuccess: boolean
isError: boolean
filters: Filter[]
setParams: ReturnType<typeof useUrlState>[1]
updateRow: (previousRow: any, updatedData: any) => void
onAddRow?: () => void
onImportData?: () => void
@@ -48,6 +57,12 @@ export const Grid = memo(
gridClass,
rowClass,
rows,
error,
isLoading,
isSuccess,
isError,
filters,
setParams,
updateRow,
onAddRow,
onImportData,
@@ -121,6 +136,12 @@ export const Grid = memo(
}
}
const removeAllFilters = () => {
setParams((prevParams) => {
return { ...prevParams, filter: [] }
})
}
return (
<div
className={containerClass}
@@ -142,26 +163,60 @@ export const Grid = memo(
rowClass={rowClass}
style={{ height: '100%' }}
noRowsFallback={
<div
style={{ height: `calc(100% - 35px)` }}
className="flex flex-col items-center justify-center"
>
<p className="text-base">This table is empty</p>
{onAddRow !== undefined && onImportData !== undefined && (
<>
{isLoading && (
<div className="p-2">
<GenericSkeletonLoader />
</div>
)}
{isError && (
<div className="p-2">
<AlertError error={error} subject="Failed to retrieve rows from table" />
</div>
)}
{isSuccess && (
<>
<p className="text-sm text-light mt-1">Add or generate rows to get started.</p>
<div className="flex items-center space-x-2 mt-4">
{/* [Joshen] Leaving this as a placeholder */}
{/* <Button type="outline">Generate random data</Button> */}
{onAddRow !== undefined && onImportData !== undefined && (
<Button type="default" onClick={onImportData}>
Import data via CSV
</Button>
)}
</div>
{(filters ?? []).length === 0 ? (
<div
style={{ height: `calc(100% - 35px)` }}
className="flex flex-col items-center justify-center"
>
<p className="text-sm text">This table is empty</p>
{onAddRow !== undefined && onImportData !== undefined && (
<>
<p className="text-sm text-light mt-1">
Add or generate rows to get started.
</p>
<div className="flex items-center space-x-2 mt-4">
{/* [Joshen] Leaving this as a placeholder */}
{/* <Button type="outline">Generate random data</Button> */}
{onAddRow !== undefined && onImportData !== undefined && (
<Button type="default" onClick={onImportData}>
Import data via CSV
</Button>
)}
</div>
</>
)}
</div>
) : (
<div
style={{ height: `calc(100% - 35px)` }}
className="flex flex-col items-center justify-center"
>
<p className="text-sm text-light">
The filters applied has returned no results from this table
</p>
<div className="flex items-center space-x-2 mt-4">
<Button type="default" onClick={() => removeAllFilters()}>
Remove all filters
</Button>
</div>
</div>
)}
</>
)}
</div>
</>
}
/>
</div>
@@ -1,24 +1,38 @@
import { useCallback, useState } from 'react'
import { Button, IconAlertCircle, Modal } from 'ui'
import Link from 'next/link'
import { useStore } from 'hooks'
import { useCallback, useState } from 'react'
import { useParams } from 'common/hooks'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import type { PostgresTable } from '@supabase/postgres-meta'
import { rlsAcknowledgedKey } from 'components/grid/constants'
import RLSDisableModalContent from 'components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/RLSDisableModal'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { useTableQuery } from 'data/tables/table-query'
import useEntityType from 'hooks/misc/useEntityType'
import { Button, IconAlertCircle, Modal } from 'ui'
export default function RLSBannerWarning() {
const { meta } = useStore()
const { ref: projectRef, id: tableID } = useParams()
const { project } = useProjectContext()
const { ref: projectRef, id: _id } = useParams()
const tableID = _id ? Number(_id) : undefined
const rlsKey = rlsAcknowledgedKey(tableID)
const isAcknowledged = localStorage?.getItem(rlsKey) === 'true' ?? false
const [isOpen, setIsOpen] = useState(false)
const tables: PostgresTable[] = meta.tables.list()
const currentTable = tables.find((table) => table.id.toString() === tableID)
const entityType = useEntityType(tableID)
const { data: currentTable } = useTableQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
id: tableID,
},
{
enabled: entityType?.type === ENTITY_TYPE.TABLE,
}
)
const rlsEnabled = currentTable?.rls_enabled
const isPublicTable = currentTable?.schema === 'public'
+2 -1
View File
@@ -18,4 +18,5 @@ export const ERROR_PRIMARY_KEY_NOTFOUND =
export const RLS_ACKNOWLEDGED_KEY = 'supabase-acknowledge-rls-warning'
export const rlsAcknowledgedKey = (tableID?: string) => `${RLS_ACKNOWLEDGED_KEY}-${tableID}`
export const rlsAcknowledgedKey = (tableID?: string | number) =>
`${RLS_ACKNOWLEDGED_KEY}-${String(tableID)}`
@@ -1,8 +1,10 @@
import type { PostgresExtension, PostgresSchema } from '@supabase/postgres-meta'
import type { PostgresExtension } from '@supabase/postgres-meta'
import { useEffect, useState } from 'react'
import { Button, Form, IconDatabase, IconPlus, Input, Listbox, Modal } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useStore } from 'hooks'
interface EnableExtensionModalProps {
@@ -12,11 +14,15 @@ interface EnableExtensionModalProps {
}
const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionModalProps) => {
const { project } = useProjectContext()
const { ui, meta } = useStore()
const [defaultSchema, setDefaultSchema] = useState()
const [fetchingSchemaInfo, setFetchingSchemaInfo] = useState(false)
const schemas = meta.schemas.list()
const { data: schemas, isLoading: isSchemasLoading } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
// [Joshen] Worth checking in with users - whether having this schema selection
// might be confusing, and if we should have a tooltip to explain that schemas
@@ -44,7 +50,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
return () => {
cancel = true
}
}, [visible])
}, [visible, extension.name])
const validate = (values: any) => {
const errors: any = {}
@@ -124,7 +130,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
return (
<div className="space-y-4 py-4">
<Modal.Content>
{fetchingSchemaInfo ? (
{fetchingSchemaInfo || isSchemasLoading ? (
<div className="space-y-2">
<ShimmeringLoader />
<div className="w-3/4">
@@ -156,8 +162,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
Create a new schema "{extension.name}"
</Listbox.Option>
<Modal.Separator />
{/* @ts-ignore */}
{schemas.map((schema: PostgresSchema) => {
{schemas?.map((schema) => {
return (
<Listbox.Option
key={schema.id}
@@ -7,9 +7,11 @@ import { Button, IconPlus, IconTrash, Input, Listbox, Modal, Radio, SidePanel, T
import { Dictionary } from 'components/grid'
import { Function } from 'components/interfaces/Functions/Functions.types'
import { POSTGRES_DATA_TYPES } from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.constants'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import Panel from 'components/ui/Panel'
import SqlEditor from 'components/ui/SqlEditor'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useStore } from 'hooks'
import { isResponseOk } from 'lib/common/fetch'
import { SupaResponse } from 'types'
@@ -294,21 +296,24 @@ interface CreateFunctionProps {
}
const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
const { project } = useProjectContext()
const { ui, meta } = useStore()
const _localState = useLocalObservable(() => new CreateFunctionStore())
_localState.meta = meta as any
const [isClosingPanel, setIsClosingPanel] = useState<boolean>(false)
useEffect(() => {
const fetchSchemas = async () => {
await (_localState!.meta as any).schemas.load()
const schemas = (_localState!.meta as any).schemas.list()
_localState.setSchemas(schemas)
useSchemasQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{
onSuccess(schemas) {
_localState.setSchemas(schemas)
},
}
fetchSchemas()
}, [])
)
useEffect(() => {
_localState.formState.reset(func)
@@ -1,17 +1,20 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PostgresFunction, PostgresSchema } from '@supabase/postgres-meta'
import { PostgresFunction } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop, partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import { Button, IconLock, IconSearch, Input, Listbox } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import Table from 'components/to-be-cleaned/Table'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useCheckPermissions, useStore } from 'hooks'
import FunctionList from './FunctionList'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions, useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import FunctionList from './FunctionList'
interface FunctionsListProps {
createFunction: () => void
@@ -24,6 +27,7 @@ const FunctionsList = ({
editFunction = noop,
deleteFunction = noop,
}: FunctionsListProps) => {
const { project } = useProjectContext()
const { meta } = useStore()
const [selectedSchema, setSelectedSchema] = useState<string>('public')
const [filterString, setFilterString] = useState<string>('')
@@ -33,11 +37,14 @@ const FunctionsList = ({
'functions'
)
const schemas: PostgresSchema[] = meta.schemas.list()
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [protectedSchemas, openSchemas] = partition(schemas ?? [], (schema) =>
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
const schema = schemas.find((schema) => schema.name === selectedSchema)
const schema = schemas?.find((schema) => schema.name === selectedSchema)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
const functions = meta.functions.list()
@@ -1,6 +1,6 @@
import { PostgresTable, PostgresTrigger } from '@supabase/postgres-meta'
import Image from 'next/image'
import { MutableRefObject, useEffect, useRef, useState } from 'react'
import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
import { useParams } from 'common/hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
@@ -12,12 +12,13 @@ import {
EdgeFunctionsResponse,
useEdgeFunctionsQuery,
} from 'data/edge-functions/edge-functions-query'
import { getTable } from 'data/tables/table-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useStore } from 'hooks'
import { tryParseJson, uuidv4 } from 'lib/helpers'
import { isValidHttpUrl, tryParseJson, uuidv4 } from 'lib/helpers'
import { Button, Checkbox, Form, Input, Listbox, Modal, Radio, SidePanel } from 'ui'
import HTTPRequestFields from './HTTPRequestFields'
import { AVAILABLE_WEBHOOK_TYPES, HOOK_EVENTS } from './Hooks.constants'
import { isValidHttpUrl } from 'lib/helpers'
export interface EditHookPanelProps {
visible: boolean
@@ -29,7 +30,7 @@ export type HTTPArgument = { id: string; name: string; value: string }
const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) => {
const { ref } = useParams()
const { meta, ui } = useStore()
const { ui } = useStore()
const submitRef = useRef<any>(null)
const [isEdited, setIsEdited] = useState(false)
const [isClosingPanel, setIsClosingPanel] = useState(false)
@@ -44,6 +45,10 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) =
const [httpParameters, setHttpParameters] = useState<HTTPArgument[]>([])
const { project } = useProjectContext()
const { data } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: functions } = useEdgeFunctionsQuery({ projectRef: ref })
const { mutate: createDatabaseTrigger, isLoading: isCreatingDatabaseTrigger } =
useDatabaseTriggerCreateMutation({
@@ -81,7 +86,10 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) =
})
const isSubmitting = isCreatingDatabaseTrigger || isUpdatingDatabaseTrigger
const tables = meta.tables.list().sort((a, b) => (a.schema > b.schema ? 0 : -1))
const tables = useMemo(
() => [...(data ?? [])].sort((a, b) => (a.schema > b.schema ? 0 : -1)),
[data]
)
const restUrl = project?.restUrl
const restUrlTld = new URL(restUrl as string).hostname.split('.').pop()
@@ -185,7 +193,7 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) =
return setEventsError('Please select at least one event')
}
const selectedTable = meta.tables.byId(values.table_id)
const selectedTable = await getTable(values.table_id)
if (!selectedTable) {
return ui.setNotification({ category: 'error', message: 'Unable to find selected table' })
}
@@ -134,7 +134,7 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
value={selectedSchema}
onChange={setSelectedSchema}
>
{(schemas?.result ?? []).map((schema) => (
{(schemas ?? []).map((schema) => (
<Listbox.Option key={schema.name} value={schema.name} label={schema.name}>
{schema.name}
</Listbox.Option>
@@ -137,7 +137,7 @@ const Indexes = () => {
onChange={setSelectedSchema}
className="w-[260px]"
>
{(schemas?.result ?? []).map((schema) => (
{(schemas ?? []).map((schema) => (
<Listbox.Option
key={schema.name}
value={schema.name}
@@ -1,13 +1,17 @@
import type { PostgresPublication } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import { Button, IconAlertCircle, IconChevronLeft, IconSearch, Input } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import InformationBox from 'components/ui/InformationBox'
import { useCheckPermissions, useStore } from 'hooks'
import Connecting from 'components/ui/Loading/Loading'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { Button, IconAlertCircle, IconChevronLeft, IconSearch, Input } from 'ui'
import PublicationsTableItem from './PublicationsTableItem'
interface PublicationsTablesProps {
@@ -16,7 +20,7 @@ interface PublicationsTablesProps {
}
const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsTablesProps) => {
const { meta } = useStore()
const { project } = useProjectContext()
const [filterString, setFilterString] = useState<string>('')
const canUpdatePublications = useCheckPermissions(
@@ -24,13 +28,27 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT
'publications'
)
const tables =
filterString.length === 0
? meta.tables.list((table: any) => !meta.excludedSchemas.includes(table.schema))
: meta.tables.list(
(table: any) =>
!meta.excludedSchemas.includes(table.schema) && table.name.includes(filterString)
const {
data: tables,
isLoading,
isSuccess,
isError,
error,
} = useTablesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{
select(tables) {
return tables.filter((table) =>
filterString.length === 0
? !EXCLUDED_SCHEMAS.includes(table.schema)
: !EXCLUDED_SCHEMAS.includes(table.schema) && table.name.includes(filterString)
)
},
}
)
// const publication = selectedPublication
// const enabledForAllTables = publication.tables == null
@@ -90,19 +108,28 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT
)}
</div>
</div>
{tables.length === 0 ? (
<NoSearchResults />
) : (
<div>
<Table
head={[
<Table.th key="header-name">Name</Table.th>,
<Table.th key="header-schema">Schema</Table.th>,
<Table.th key="header-desc" className="hidden text-left lg:table-cell">
Description
</Table.th>,
<Table.th key="header-all">
{/* Temporarily disable All tables toggle for publications. See https://github.com/supabase/supabase/pull/7233.
{isLoading && (
<div className="mt-8">
<Connecting />
</div>
)}
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
{isSuccess &&
(tables.length === 0 ? (
<NoSearchResults />
) : (
<div>
<Table
head={[
<Table.th key="header-name">Name</Table.th>,
<Table.th key="header-schema">Schema</Table.th>,
<Table.th key="header-desc" className="hidden text-left lg:table-cell">
Description
</Table.th>,
<Table.th key="header-all">
{/* Temporarily disable All tables toggle for publications. See https://github.com/supabase/supabase/pull/7233.
<div className="flex flex-row space-x-3 items-center justify-end">
<div className="text-xs leading-4 font-medium text-gray-400 text-right ">
All Tables
@@ -116,20 +143,20 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT
onChange={() => toggleReplicationForAllTables(publication, enabledForAllTables)}
/>
</div> */}
</Table.th>,
]}
body={tables.map((table: any, i: number) => (
<PublicationsTableItem
key={table.id}
table={table}
selectedPublication={selectedPublication}
/>
))}
/>
</div>
)}
</Table.th>,
]}
body={tables.map((table: any, i: number) => (
<PublicationsTableItem
key={table.id}
table={table}
selectedPublication={selectedPublication}
/>
))}
/>
</div>
))}
</>
)
}
export default observer(PublicationsTables)
export default PublicationsTables
@@ -1,7 +1,8 @@
import dagre from '@dagrejs/dagre'
import clsx from 'clsx'
import { observer } from 'mobx-react-lite'
import { uniqBy } from 'lodash'
import { Diamond, Fingerprint } from 'lucide-react'
import { observer } from 'mobx-react-lite'
import { useEffect, useMemo } from 'react'
import ReactFlow, {
Background,
@@ -17,11 +18,10 @@ import ReactFlow, {
import { PostgresTable } from '@supabase/postgres-meta'
import { useTheme } from 'common/Providers'
import { useStore } from 'hooks'
import { IconHash, IconKey, IconLoader, IconLock } from 'ui'
import { Diamond, Fingerprint } from 'lucide-react'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useTablesQuery } from 'data/tables/tables-query'
import 'reactflow/dist/style.css'
import { IconHash, IconKey, IconLoader, IconLock } from 'ui'
type TableNodeData = {
name: string
@@ -347,23 +347,32 @@ const TablesGraph = ({ tables }: { tables: PostgresTable[] }) => {
}
const SchemaGraph = ({ schema }: { schema: string }) => {
const { meta } = useStore()
const tables = meta.tables.list((table: { schema: string }) => table.schema === schema)
const { project } = useProjectContext()
const {
data: tables,
isLoading,
isError,
error,
} = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema,
})
if (meta.tables.isLoading) {
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center space-x-2">
<IconLoader className="animate-spin" size={14} />
<p className="text-sm text-scale-1000">Loading schemas...</p>
<p className="text-sm text-scale-1000">Loading table...</p>
</div>
)
}
if (meta.tables.hasError) {
if (isError) {
return (
<div className="px-6 py-4 text-scale-1000">
<p>Error connecting to API</p>
<p>{`${meta.tables.error?.message ?? 'Unknown error'}`}</p>
<p>{`${error?.message ?? 'Unknown error'}`}</p>
</div>
)
}
@@ -4,6 +4,11 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import Table from 'components/to-be-cleaned/Table'
import { useCheckPermissions, useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import {
Button,
IconChevronLeft,
@@ -15,11 +20,6 @@ import {
Input,
} from 'ui'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import Table from 'components/to-be-cleaned/Table'
import { useCheckPermissions, useStore } from 'hooks'
import { ScaffoldHeader } from 'components/layouts/Scaffold'
interface ColumnListProps {
selectedTable: PostgresTable
onSelectBack: () => void
@@ -42,7 +42,7 @@ const ColumnList = ({
? selectedTable.columns
: selectedTable.columns?.filter((column: any) => column.name.includes(filterString))) ?? []
const isLocked = meta.excludedSchemas.includes(selectedTable.schema ?? '')
const isLocked = EXCLUDED_SCHEMAS.includes(selectedTable.schema ?? '')
const canUpdateColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
return (
@@ -1,9 +1,18 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import type { PostgresSchema, PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop, partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import Table from 'components/to-be-cleaned/Table'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions, useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
Button,
IconCheck,
@@ -16,15 +25,9 @@ import {
Input,
Listbox,
} from 'ui'
import Table from 'components/to-be-cleaned/Table'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useCheckPermissions, useStore } from 'hooks'
import { ScaffoldHeader } from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
interface TableListProps {
selectedSchema: string
onSelectSchema: (schema: string) => void
onAddTable: () => void
onEditTable: (table: any) => void
onDeleteTable: (table: any) => void
@@ -32,38 +35,53 @@ interface TableListProps {
}
const TableList = ({
selectedSchema,
onSelectSchema = noop,
onAddTable = noop,
onEditTable = noop,
onDeleteTable = noop,
onOpenTable = noop,
}: TableListProps) => {
const { meta } = useStore()
const { isLoading } = meta.tables
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const [filterString, setFilterString] = useState<string>('')
const canUpdateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const schemas: PostgresSchema[] = meta.schemas.list()
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [protectedSchemas, openSchemas] = partition(schemas ?? [], (schema) =>
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
const allTables: PostgresTable[] = meta.tables.list(
(table: PostgresTable) => table.schema === selectedSchema
const {
data: tables,
isLoading,
isSuccess,
isError,
error,
} = useTablesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: snap.selectedSchemaName,
},
{
select(tables) {
return filterString.length === 0
? tables
: tables.filter((table) => table.name.includes(filterString))
},
}
)
const tables =
filterString.length === 0
? allTables
: // @ts-ignore
allTables.filter((table: PostgresTable) => table.name.includes(filterString))
const publications = meta.publications.list()
const realtimePublication = publications.find(
(publication) => publication.name === 'supabase_realtime'
)
// @ts-ignore
const schema = schemas.find((schema) => schema.name === selectedSchema)
const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
return (
@@ -77,14 +95,13 @@ const TableList = ({
<div className="w-[260px]">
<Listbox
size="small"
value={selectedSchema}
onChange={onSelectSchema}
value={snap.selectedSchemaName}
onChange={snap.setSelectedSchemaName}
icon={isLocked && <IconLock size={14} strokeWidth={2} />}
>
<Listbox.Option disabled key="normal-schemas" value="normal-schemas" label="Schemas">
<p className="text-sm">Schemas</p>
</Listbox.Option>
{/* @ts-ignore */}
{openSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
@@ -125,6 +142,7 @@ const TableList = ({
/>
</div>
</div>
{!isLocked && (
<div>
<Tooltip.Root delayDuration={0}>
@@ -159,11 +177,19 @@ const TableList = ({
)}
</div>
<div className="my-4">
{isLoading ? (
{isLoading && (
<div className="py-4 space-y-2">
<GenericSkeletonLoader />
</div>
)}
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
{isSuccess &&
(tables.length === 0 ? (
<NoSearchResults />
) : (
<div className="w-full">
<div className="my-4 w-full">
<Table
head={[
<Table.th key="name">Name</Table.th>,
@@ -184,17 +210,17 @@ const TableList = ({
body={
<>
{tables.length === 0 && filterString.length === 0 && (
<Table.tr key={selectedSchema}>
<Table.tr key={snap.selectedSchemaName}>
<Table.td colSpan={6}>
<p className="text-sm text-scale-1200">No tables created yet</p>
<p className="text-sm text-light">
There are no tables found in the schema "{selectedSchema}"
There are no tables found in the schema "{snap.selectedSchemaName}"
</p>
</Table.td>
</Table.tr>
)}
{tables.length === 0 && filterString.length > 0 && (
<Table.tr key={selectedSchema}>
<Table.tr key={snap.selectedSchemaName}>
<Table.td colSpan={6}>
<p className="text-sm text-scale-1200">No results found</p>
<p className="text-sm text-light">
@@ -302,21 +328,6 @@ const TableList = ({
</Tooltip.Portal>
)}
</Tooltip.Root>
{/* <Button
type="text"
icon={<IconEdit3 />}
style={{ padding: 5 }}
disabled={isLocked}
onClick={() => onEditTable(x)}
/>
<Button
type="text"
icon={<IconTrash />}
style={{ padding: 5 }}
disabled={isLocked}
onClick={() => onDeleteTable(x)}
/> */}
</div>
</Table.td>
</Table.tr>
@@ -325,8 +336,7 @@ const TableList = ({
}
/>
</div>
)}
</div>
))}
</>
)
}
@@ -1,10 +1,17 @@
import { Dictionary } from 'components/grid'
import { has, isEmpty, mapValues, union, without } from 'lodash'
import { makeAutoObservable } from 'mobx'
import { observer, useLocalObservable } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { createContext, useContext, useEffect, useState } from 'react'
import SVG from 'react-inlinesvg'
import { Dictionary } from 'components/grid'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import FormEmptyBox from 'components/ui/FormBoxEmpty'
import NoTableState from 'components/ui/States/NoTableState'
import { useTablesQuery } from 'data/tables/tables-query'
import { useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
import {
Badge,
Button,
@@ -17,12 +24,6 @@ import {
Modal,
SidePanel,
} from 'ui'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import FormEmptyBox from 'components/ui/FormBoxEmpty'
import NoTableState from 'components/ui/States/NoTableState'
import { useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
import ChooseFunctionForm from './ChooseFunctionForm'
class CreateTriggerFormState {
@@ -256,28 +257,33 @@ interface CreateTriggerProps {
}
const CreateTrigger = ({ trigger, visible, setVisible }: CreateTriggerProps) => {
const { project } = useProjectContext()
const { ui, meta } = useStore()
const [isClosingPanel, setIsClosingPanel] = useState(false)
const _localState = useLocalObservable(() => new CreateTriggerStore())
_localState.meta = meta as any
// for the empty 'no tables' state link
const router = useRouter()
const { ref } = router.query
useTablesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{
onSuccess(tables) {
if (_localState.tables.length <= 0) {
_localState.setTables(tables)
}
},
}
)
useEffect(() => {
const fetchTables = async () => {
await (_localState!.meta as any)!.tables!.load()
const tables = (_localState!.meta as any)!.tables.list()
_localState.setTables(tables)
}
const fetchFunctions = async () => {
await (_localState.meta as any).functions.load()
const triggerFuncs = (_localState!.meta as any)!.functions.listTriggerFunctions()
_localState.setTriggerFunctions(triggerFuncs)
}
fetchTables()
fetchFunctions()
}, [])
@@ -1,17 +1,19 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PostgresSchema } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop, partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import { Button, IconLock, IconSearch, Input, Listbox } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlphaPreview from 'components/to-be-cleaned/AlphaPreview'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions, useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import TriggerList from './TriggerList'
interface TriggersListProps {
@@ -25,15 +27,19 @@ const TriggersList = ({
editTrigger = noop,
deleteTrigger = noop,
}: TriggersListProps) => {
const { project } = useProjectContext()
const { meta } = useStore()
const [selectedSchema, setSelectedSchema] = useState<string>('public')
const [filterString, setFilterString] = useState<string>('')
const schemas: PostgresSchema[] = meta.schemas.list()
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [protectedSchemas, openSchemas] = partition(schemas ?? [], (schema) =>
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
const schema = schemas.find((schema) => schema.name === selectedSchema)
const schema = schemas?.find((schema) => schema.name === selectedSchema)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
const triggers = meta.triggers.list()
@@ -1,12 +1,13 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { isEmpty } from 'lodash'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { Button, Form, IconArrowLeft, IconEdit, IconExternalLink, IconTrash, Input } from 'ui'
import { useParams } from 'common/hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import {
FormActions,
FormPanel,
@@ -15,10 +16,10 @@ import {
FormSectionLabel,
FormsContainer,
} from 'components/ui/Forms'
import { invalidateSchemasQuery } from 'data/database/schemas-query'
import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation'
import { useCheckPermissions, useStore } from 'hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { Button, Form, IconArrowLeft, IconEdit, IconExternalLink, IconTrash, Input } from 'ui'
import InputField from './InputField'
import WrapperTableEditor from './WrapperTableEditor'
import { WRAPPERS } from './Wrappers.constants'
@@ -27,7 +28,8 @@ import { makeValidateRequired } from './Wrappers.utils'
const CreateWrapper = () => {
const formId = 'create-wrapper-form'
const router = useRouter()
const { ui, meta } = useStore()
const queryClient = useQueryClient()
const { ui } = useStore()
const { ref, type } = useParams()
const { project } = useProjectContext()
const canCreateWrapper = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers')
@@ -46,7 +48,7 @@ const CreateWrapper = () => {
setNewTables([])
const hasNewSchema = newTables.some((table) => table.is_new_schema)
if (hasNewSchema) meta.schemas.load()
if (hasNewSchema) invalidateSchemasQuery(queryClient, ref)
router.push(`/project/${ref}/database/wrappers`)
},
@@ -1,48 +1,50 @@
import Link from 'next/link'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { isEmpty } from 'lodash'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { useParams } from 'common/hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import {
FormActions,
FormPanel,
FormSection,
FormSectionContent,
FormSectionLabel,
FormsContainer,
} from 'components/ui/Forms'
import Loading from 'components/ui/Loading'
import { invalidateSchemasQuery } from 'data/database/schemas-query'
import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation'
import { useFDWsQuery } from 'data/fdw/fdws-query'
import { useCheckPermissions, useImmutableValue, useStore } from 'hooks'
import { VaultSecret } from 'types'
import {
Button,
Form,
Input,
IconArrowLeft,
IconExternalLink,
IconEdit,
IconTrash,
IconExternalLink,
IconLoader,
IconTrash,
Input,
} from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { VaultSecret } from 'types'
import { useCheckPermissions, useImmutableValue, useStore } from 'hooks'
import { useParams } from 'common/hooks'
import { useFDWsQuery } from 'data/fdw/fdws-query'
import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation'
import InputField from './InputField'
import { WRAPPERS } from './Wrappers.constants'
import WrapperTableEditor from './WrapperTableEditor'
import { WRAPPERS } from './Wrappers.constants'
import {
convertKVStringArrayToJson,
formatWrapperTables,
makeValidateRequired,
convertKVStringArrayToJson,
} from './Wrappers.utils'
import Loading from 'components/ui/Loading'
import {
FormPanel,
FormActions,
FormSection,
FormSectionLabel,
FormSectionContent,
FormsContainer,
} from 'components/ui/Forms'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
const EditWrapper = () => {
const formId = 'edit-wrapper-form'
const router = useRouter()
const queryClient = useQueryClient()
const { ui, meta, vault } = useStore()
const { ref, id } = useParams()
const { project } = useProjectContext()
@@ -67,7 +69,7 @@ const EditWrapper = () => {
setWrapperTables([])
const hasNewSchema = wrapperTables.some((table) => table.is_new_schema)
if (hasNewSchema) meta.schemas.load()
if (hasNewSchema) invalidateSchemasQuery(queryClient, ref)
router.push(`/project/${ref}/database/wrappers`)
},
@@ -1,6 +1,8 @@
import ActionBar from 'components/interfaces/TableGridEditor/SidePanelEditor/ActionBar'
import { useStore } from 'hooks'
import { useEffect, useState } from 'react'
import ActionBar from 'components/interfaces/TableGridEditor/SidePanelEditor/ActionBar'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useSchemasQuery } from 'data/database/schemas-query'
import { Form, IconDatabase, IconPlus, Input, Listbox, Modal, SidePanel } from 'ui'
import WrapperDynamicColumns from './WrapperDynamicColumns'
import { Table, TableOption } from './Wrappers.types'
@@ -160,8 +162,11 @@ const TableForm = ({
onSubmit: OnSubmitFn
initialData: any
}) => {
const { meta } = useStore()
const schemas = meta.schemas.list()
const { project } = useProjectContext()
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const requiredOptions =
table.options.filter((option) => option.editable && option.required && !option.defaultValue) ??
@@ -208,8 +213,8 @@ const TableForm = ({
Create a new schema
</Listbox.Option>
<Modal.Separator />
{/* @ts-ignore */}
{schemas.map((schema: PostgresSchema) => {
{schemas?.map((schema) => {
return (
<Listbox.Option
key={schema.id}
@@ -1,16 +1,10 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { indexOf } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useContext, useEffect } from 'react'
import { Form, IconAlertCircle, Input, InputNumber } from 'ui'
import { useEffect } from 'react'
import { useParams } from 'common/hooks'
import MultiSelect from 'components/ui/MultiSelect'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-postgrest-config-update-mutation'
import { useCheckPermissions, useStore } from 'hooks'
import { PageContext } from 'pages/project/[ref]/settings/api'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import {
FormActions,
FormPanel,
@@ -18,12 +12,22 @@ import {
FormSectionContent,
FormSectionLabel,
} from 'components/ui/Forms'
import MultiSelect from 'components/ui/MultiSelect'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-postgrest-config-update-mutation'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions, useStore } from 'hooks'
import { Form, IconAlertCircle, Input, InputNumber } from 'ui'
const PostgrestConfig = () => {
const PageState: any = useContext(PageContext)
const { ref: projectRef } = useParams()
const { ui } = useStore()
const { meta } = PageState
const { project } = useProjectContext()
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef })
const { mutate: updatePostgrestConfig, isLoading: isUpdating } =
@@ -53,15 +57,12 @@ const PostgrestConfig = () => {
const permanentSchema = ['public', 'storage']
const hiddenSchema = ['auth', 'pgbouncer', 'hooks', 'extensions']
const schema =
meta.schemas
.list(
(x: any) => {
const find = indexOf(hiddenSchema, x.name)
if (find < 0) return x
},
{ allSchemas: true }
)
.map((x: any) => {
schemas
?.filter((x) => {
const find = indexOf(hiddenSchema, x.name)
if (find < 0) return x
})
.map((x) => {
return {
id: x.id,
value: x.name,
@@ -1,24 +1,33 @@
import { useState } from 'react'
import { SidePanel, IconBookOpen } from 'ui'
import { useStore } from 'hooks'
import { useParams } from 'common/hooks'
import { ResourceContent } from '../Docs'
import LangSelector from '../Docs/LangSelector'
import GeneratingTypes from '../Docs/GeneratingTypes'
import ActionBar from './SidePanelEditor/ActionBar'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query'
import { useTableQuery } from 'data/tables/table-query'
import { snakeToCamel } from 'lib/helpers'
import { IconBookOpen, SidePanel } from 'ui'
import { ResourceContent } from '../Docs'
import GeneratingTypes from '../Docs/GeneratingTypes'
import LangSelector from '../Docs/LangSelector'
import ActionBar from './SidePanelEditor/ActionBar'
interface APIDocumentationPanelProps {
export interface APIDocumentationPanelProps {
visible: boolean
onClose: () => void
}
const APIDocumentationPanel = ({ visible, onClose }: APIDocumentationPanelProps) => {
const { meta } = useStore()
const { ref, page, id } = useParams()
const { project } = useProjectContext()
const { ref, page, id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const { data: table } = useTableQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
id,
})
const {
data: jsonSchema,
error: jsonSchemaError,
@@ -32,7 +41,6 @@ const APIDocumentationPanel = ({ visible, onClose }: APIDocumentationPanelProps)
const [selectedLang, setSelectedLang] = useState<any>('js')
const [showApiKey, setShowApiKey] = useState<any>(DEFAULT_KEY)
const tables = meta.tables.list()
const autoApiService = {
...settings?.autoApiService,
endpoint: `${settings?.autoApiService.protocol ?? 'https'}://${
@@ -113,7 +121,7 @@ const APIDocumentationPanel = ({ visible, onClose }: APIDocumentationPanelProps)
<ResourceContent
autoApiService={autoApiService}
selectedLang={selectedLang}
resourceId={tables.find((table) => table.id === Number(id))?.name}
resourceId={table?.name}
resources={resources}
definitions={jsonSchema.definitions}
paths={jsonSchema.paths}
@@ -0,0 +1,239 @@
import { useQueryClient } from '@tanstack/react-query'
import Link from 'next/link'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { entityTypeKeys } from 'data/entity-types/keys'
import { sqlKeys } from 'data/sql/keys'
import { tableKeys } from 'data/tables/keys'
import { useGetTables } from 'data/tables/tables-query'
import { useStore, useUrlState } from 'hooks'
import { TableLike } from 'hooks/misc/useTable'
import { noop } from 'lib/void'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Alert, Button, Checkbox, IconExternalLink, Modal } from 'ui'
export type DeleteConfirmationDialogsProps = {
projectRef?: string
selectedTable?: TableLike
onAfterDeleteTable?: (tables: TableLike[]) => void
}
const DeleteConfirmationDialogs = ({
projectRef,
selectedTable,
onAfterDeleteTable = noop,
}: DeleteConfirmationDialogsProps) => {
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const [, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] })
const queryClient = useQueryClient()
const { meta, ui } = useStore()
const getTables = useGetTables({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const removeDeletedColumnFromFiltersAndSorts = (columnName: string) => {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const existingSorts = (prevParams?.sort ?? []) as string[]
return {
...prevParams,
filter: existingFilters.filter((filter: string) => {
const [column] = filter.split(':')
if (column !== columnName) return filter
}),
sort: existingSorts.filter((sort: string) => {
const [column] = sort.split(':')
if (column !== columnName) return sort
}),
}
})
}
const isDeleteWithCascade = snap.confirmationDialog?.isDeleteWithCascade ?? false
const onConfirmDeleteColumn = async () => {
if (!(snap.confirmationDialog?.type === 'column')) return
const selectedColumnToDelete = snap.confirmationDialog.column
try {
if (selectedColumnToDelete === undefined) return
const response: any = await meta.columns.del(selectedColumnToDelete.id, isDeleteWithCascade)
if (response.error) throw response.error
removeDeletedColumnFromFiltersAndSorts(selectedColumnToDelete.name)
ui.setNotification({
category: 'success',
message: `Successfully deleted column "${selectedColumnToDelete.name}"`,
})
await Promise.all([
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])),
queryClient.invalidateQueries(
tableKeys.table(project?.ref, selectedColumnToDelete!.table_id)
),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name])
),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [
'table-definition',
selectedTable!.schema,
selectedTable!.name,
])
),
])
if (snap.selectedSchemaName) await meta.views.loadBySchema(snap.selectedSchemaName)
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${selectedColumnToDelete!.name}: ${error.message}`,
})
} finally {
snap.closeConfirmationDialog()
}
}
const onConfirmDeleteTable = async () => {
if (!(snap.confirmationDialog?.type === 'table')) return
const selectedTableToDelete = selectedTable
try {
if (selectedTableToDelete === undefined) return
const response: any = await meta.tables.del(selectedTableToDelete.id, isDeleteWithCascade)
if (response.error) throw response.error
const tables = await getTables(snap.selectedSchemaName)
await queryClient.invalidateQueries(entityTypeKeys.list(projectRef))
onAfterDeleteTable(tables)
ui.setNotification({
category: 'success',
message: `Successfully deleted table "${selectedTableToDelete.name}"`,
})
if (snap.selectedSchemaName) await meta.views.loadBySchema(snap.selectedSchemaName)
} catch (error: any) {
ui.setNotification({
error,
category: 'error',
message: `Failed to delete ${selectedTableToDelete?.name}: ${error.message}`,
})
} finally {
snap.closeConfirmationDialog()
}
}
return (
<>
<ConfirmationModal
danger
size="small"
visible={snap.confirmationDialog?.type === 'column'}
header={`Confirm deletion of column "${
snap.confirmationDialog?.type === 'column' && snap.confirmationDialog.column.name
}"`}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => {
snap.closeConfirmationDialog()
}}
onSelectConfirm={onConfirmDeleteColumn}
>
<Modal.Content>
<div className="py-4 space-y-4">
<p className="text-sm text-scale-1100">
Are you sure you want to delete the selected column? This action cannot be undone.
</p>
<Checkbox
label="Drop column with cascade?"
description="Deletes the column and its dependent objects"
checked={isDeleteWithCascade}
onChange={() => snap.toggleConfirmationIsWithCascade()}
/>
{isDeleteWithCascade && (
<Alert
withIcon
variant="warning"
title="Warning: Dropping with cascade may result in unintended consequences"
>
<p className="mb-4">
All dependent objects will be removed, as will any objects that depend on them,
recursively.
</p>
<Link href="https://www.postgresql.org/docs/current/ddl-depend.html">
<a target="_blank" rel="noreferrer">
<Button size="tiny" type="default" icon={<IconExternalLink />}>
About dependency tracking
</Button>
</a>
</Link>
</Alert>
)}
</div>
</Modal.Content>
</ConfirmationModal>
<ConfirmationModal
danger
size="small"
visible={snap.confirmationDialog?.type === 'table'}
header={
<span className="break-words">{`Confirm deletion of table "${selectedTable?.name}"`}</span>
}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => {
snap.closeConfirmationDialog()
}}
onSelectConfirm={onConfirmDeleteTable}
>
<Modal.Content>
<div className="py-4 space-y-4">
<p className="text-sm text-scale-1100">
Are you sure you want to delete the selected table? This action cannot be undone.
</p>
<Checkbox
label="Drop table with cascade?"
description="Deletes the table and its dependent objects"
checked={isDeleteWithCascade}
onChange={() => snap.toggleConfirmationIsWithCascade(!isDeleteWithCascade)}
/>
{isDeleteWithCascade && (
<Alert
withIcon
variant="warning"
title="Warning: Dropping with cascade may result in unintended consequences"
>
<p className="mb-4">
All dependent objects will be removed, as will any objects that depend on them,
recursively.
</p>
<Link href="https://www.postgresql.org/docs/current/ddl-depend.html">
<a target="_blank" rel="noreferrer">
<Button size="tiny" type="default" icon={<IconExternalLink />}>
About dependency tracking
</Button>
</a>
</Link>
</Alert>
)}
</div>
</Modal.Content>
</ConfirmationModal>
</>
)
}
export default DeleteConfirmationDialogs
@@ -1,19 +1,17 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { observer } from 'mobx-react-lite'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { useCheckPermissions, useLocalStorage, useStore } from 'hooks'
import { useCheckPermissions, useLocalStorage } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
interface EmptyStateProps {
selectedSchema: string
onAddTable: () => void
}
export interface EmptyStateProps {}
const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {
const { meta } = useStore()
const isProtectedSchema = meta.excludedSchemas.includes(selectedSchema)
const EmptyState = ({}: EmptyStateProps) => {
const snap = useTableEditorStateSnapshot()
const isProtectedSchema = EXCLUDED_SCHEMAS.includes(snap.selectedSchemaName)
const canCreateTables =
useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') && !isProtectedSchema
@@ -27,7 +25,7 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedSchema,
schema: snap.selectedSchemaName,
sort,
},
{
@@ -43,7 +41,7 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {
<ProductEmptyState
title="Table Editor"
ctaButtonLabel={canCreateTables ? 'Create a new table' : undefined}
onClickCta={canCreateTables ? onAddTable : undefined}
onClickCta={canCreateTables ? snap.onAddTable : undefined}
>
<p className="text-sm text-scale-1100">There are no tables available in this schema.</p>
</ProductEmptyState>
@@ -52,7 +50,7 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {
<ProductEmptyState
title="Table Editor"
ctaButtonLabel={canCreateTables ? 'Create a new table' : undefined}
onClickCta={canCreateTables ? onAddTable : undefined}
onClickCta={canCreateTables ? snap.onAddTable : undefined}
>
<p className="text-sm text-scale-1100">
Select a table from the navigation panel on the left to view its data
@@ -65,4 +63,4 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {
)
}
export default observer(EmptyState)
export default EmptyState
@@ -4,18 +4,19 @@ import type {
PostgresTable,
PostgresType,
} from '@supabase/postgres-meta'
import { useParams } from 'common'
import { Dictionary } from 'components/grid'
import { isEmpty, noop } from 'lodash'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { Button, Checkbox, IconExternalLink, Input, SidePanel, Toggle } from 'ui'
import { useParams } from 'common'
import { Dictionary } from 'components/grid'
import { EncryptionKeySelector } from 'components/interfaces/Settings/Vault'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
import { useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { Button, Checkbox, IconExternalLink, Input, SidePanel, Toggle } from 'ui'
import { ForeignKeySelector } from '..'
import ActionBar from '../ActionBar'
import { TEXT_TYPES } from '../SidePanelEditor.constants'
@@ -82,9 +83,7 @@ const ColumnEditor = ({
const foreignKeyMeta = data || []
const keys = vault.listKeys()
const enumTypes = meta.types.list(
(type: PostgresType) => !meta.excludedSchemas.includes(type.schema)
)
const enumTypes = meta.types.list((type: PostgresType) => !EXCLUDED_SCHEMAS.includes(type.schema))
const [pgsodiumExtension] = meta.extensions.list(
(ext: PostgresExtension) => ext.name.toLowerCase() === 'pgsodium'
@@ -1,7 +1,13 @@
import type { PostgresColumn, PostgresSchema, PostgresTable } from '@supabase/postgres-meta'
import { Dictionary } from 'components/grid'
import { find, get, isEmpty, sortBy } from 'lodash'
import { useEffect, useState } from 'react'
import { Dictionary } from 'components/grid'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import InformationBox from 'components/ui/InformationBox'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
@@ -13,10 +19,6 @@ import {
Listbox,
SidePanel,
} from 'ui'
import InformationBox from 'components/ui/InformationBox'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import { useStore } from 'hooks'
import ActionBar from '../ActionBar'
import { ColumnField } from '../SidePanelEditor.types'
import { FOREIGN_KEY_CASCADE_OPTIONS } from './ForeignKeySelector.constants'
@@ -46,7 +48,7 @@ const ForeignKeySelector = ({
closePanel,
saveChanges,
}: ForeignKeySelectorProps) => {
const { meta } = useStore()
const { project } = useProjectContext()
const [errors, setErrors] = useState<any>({})
const [selectedForeignKey, setSelectedForeignKey] = useState<ForeignKey>({
schema: 'public',
@@ -56,10 +58,15 @@ const ForeignKeySelector = ({
updateAction: FOREIGN_KEY_CASCADE_ACTION.NO_ACTION,
})
const schemas = meta.schemas.list()
const tables = meta.tables.list(
(table: PostgresTable) => table.schema === selectedForeignKey.schema
)
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: tables } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedForeignKey.schema,
})
const foreignKey = column?.foreignKey
const selectedTable: PostgresTable | undefined = find(tables, {
@@ -70,11 +77,6 @@ const ForeignKeySelector = ({
name: selectedForeignKey?.column,
})
useEffect(() => {
// make sure the public schemas are loaded initially
meta.tables.loadBySchema('public')
}, [])
useEffect(() => {
// Reset the state of the side panel
if (visible) {
@@ -101,7 +103,6 @@ const ForeignKeySelector = ({
}, [visible])
const updateSelectedSchema = (schema: string) => {
meta.tables.loadBySchema(schema)
const updatedForeignKey = {
schema,
table: '',
@@ -200,8 +201,6 @@ const ForeignKeySelector = ({
customFooter={
<ActionBar
backButtonLabel="Cancel"
// if the type of the two columns don't match, disable the save button
disableApply={!matchingColumnTypes}
applyButtonLabel="Save"
closePanel={closePanel}
applyFunction={onSaveChanges}
@@ -227,7 +226,7 @@ const ForeignKeySelector = ({
error={errors.schema}
onChange={(value: string) => updateSelectedSchema(value)}
>
{schemas.map((schema: PostgresSchema) => {
{schemas?.map((schema: PostgresSchema) => {
return (
<Listbox.Option
key={schema.id}
@@ -1,22 +1,22 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { useState } from 'react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { parseSupaTable } from 'components/grid'
import { formatFilterURLParams, formatSortURLParams } from 'components/grid/SupabaseGrid.utils'
import RefreshButton from 'components/grid/components/header/RefreshButton'
import FilterPopover from 'components/grid/components/header/filter'
import SortPopover from 'components/grid/components/header/sort'
import { formatFilterURLParams, formatSortURLParams } from 'components/grid/SupabaseGrid.utils'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query'
import { useStore } from 'hooks'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { useTableQuery } from 'data/tables/table-query'
import { IconLoader, SidePanel } from 'ui'
import ActionBar from '../../ActionBar'
import { useEncryptedColumns } from './ForeignRowSelector.utils'
import SelectorGrid from './SelectorGrid'
import { useEffect, useState } from 'react'
import Pagination from './Pagination'
import SelectorGrid from './SelectorGrid'
export interface ForeignRowSelectorProps {
visible: boolean
@@ -31,23 +31,21 @@ const ForeignRowSelector = ({
onSelect,
closePanel,
}: ForeignRowSelectorProps) => {
const { meta } = useStore()
const { project } = useProjectContext()
const {
target_id: tableId,
target_id: _tableId,
target_schema: schemaName,
target_table: tableName,
target_columns: columnName,
} = foreignKey ?? {}
const tableId = _tableId ? Number(_tableId) : undefined
const table = tableId ? meta.tables.byId(tableId) : undefined
useEffect(() => {
if (!table && tableId) {
meta.tables.loadById(tableId)
}
}, [table, tableId])
const { data: table } = useTableQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
id: tableId,
})
const encryptedColumns = useEncryptedColumns({ schemaName, tableName })
@@ -1,23 +1,22 @@
import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
import { QueryKey, useQueryClient } from '@tanstack/react-query'
import { Dictionary } from 'components/grid'
import { find, isEmpty, isUndefined, noop } from 'lodash'
import { isEmpty, isUndefined, noop } from 'lodash'
import { useState } from 'react'
import { Modal } from 'ui'
import { Dictionary } from 'components/grid'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { entityTypeKeys } from 'data/entity-types/keys'
import { sqlKeys } from 'data/sql/keys'
import { useTableRowCreateMutation } from 'data/table-rows/table-row-create-mutation'
import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation'
import { tableKeys } from 'data/tables/keys'
import { useStore, useUrlState } from 'hooks'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Modal } from 'ui'
import { ColumnEditor, RowEditor, SpreadsheetImport, TableEditor } from '.'
import ForeignRowSelector, {
ForeignRowSelectorProps,
} from './RowEditor/ForeignRowSelector/ForeignRowSelector'
import ForeignRowSelector from './RowEditor/ForeignRowSelector/ForeignRowSelector'
import JsonEdit from './RowEditor/JsonEditor/JsonEditor'
import { JsonEditValue } from './RowEditor/RowEditor.types'
import {
ColumnField,
CreateColumnPayload,
@@ -27,20 +26,7 @@ import {
import { ImportContent } from './TableEditor/TableEditor.types'
export interface SidePanelEditorProps {
selectedSchema: string
selectedTable?: PostgresTable
selectedRowToEdit?: Dictionary<any>
selectedColumnToEdit?: PostgresColumn
selectedTableToEdit?: PostgresTable
selectedValueForJsonEdit?: JsonEditValue
selectedForeignKeyToEdit?: {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}
sidePanelKey?: 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import'
isDuplicating?: boolean
closePanel: () => void
onRowCreated?: (row: Dictionary<any>) => void
onRowUpdated?: (row: Dictionary<any>, idx: number) => void
@@ -51,21 +37,13 @@ export interface SidePanelEditorProps {
}
const SidePanelEditor = ({
selectedSchema,
selectedTable,
selectedRowToEdit,
selectedColumnToEdit,
selectedTableToEdit,
selectedValueForJsonEdit,
selectedForeignKeyToEdit,
sidePanelKey,
isDuplicating = false,
closePanel,
onRowCreated = noop,
onRowUpdated = noop,
onTableCreated = noop,
onColumnSaved = noop,
}: SidePanelEditorProps) => {
const snap = useTableEditorStateSnapshot()
const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] })
const { meta, ui } = useStore()
const queryClient = useQueryClient()
@@ -73,7 +51,6 @@ const SidePanelEditor = ({
const [isEdited, setIsEdited] = useState<boolean>(false)
const [isClosingPanel, setIsClosingPanel] = useState<boolean>(false)
const tables = meta.tables.list()
const enumArrayColumns = (selectedTable?.columns ?? [])
.filter((column) => {
return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
@@ -84,6 +61,8 @@ const SidePanelEditor = ({
const { mutateAsync: createTableRows } = useTableRowCreateMutation()
const { mutateAsync: updateTableRow } = useTableRowUpdateMutation({
async onMutate({ projectRef, table, configuration, payload }) {
snap.closeSidePanel()
const primaryKeyColumns = new Set(Object.keys(configuration.identifiers))
const queryKey = sqlKeys.query(projectRef, [
table.schema,
@@ -192,12 +171,13 @@ const SidePanelEditor = ({
onComplete()
if (!saveRowError) {
setIsEdited(false)
closePanel()
snap.closeSidePanel()
}
}
const onSaveJSON = async (value: string | number) => {
if (selectedTable === undefined || selectedValueForJsonEdit === undefined) return
if (selectedTable === undefined || !(snap.sidePanel?.type === 'json')) return
const selectedValueForJsonEdit = snap.sidePanel.jsonValue
try {
const { row, column } = selectedValueForJsonEdit
@@ -213,7 +193,8 @@ const SidePanelEditor = ({
}
const onSaveForeignRow = async (value: any) => {
if (selectedTable === undefined || selectedForeignKeyToEdit === undefined) return
if (selectedTable === undefined || !(snap.sidePanel?.type === 'foreign-row-selector')) return
const selectedForeignKeyToEdit = snap.sidePanel.foreignKey
try {
const { row, column } = selectedForeignKeyToEdit
@@ -235,6 +216,8 @@ const SidePanelEditor = ({
configuration: { columnId?: string; isEncrypted: boolean; keyId?: string; keyName?: string },
resolve: any
) => {
const selectedColumnToEdit = snap.sidePanel?.type === 'column' && snap.sidePanel.column
const { columnId, ...securityConfig } = configuration
const response = isNewRecord
? await meta.createColumn(
@@ -261,9 +244,10 @@ const SidePanelEditor = ({
) {
reAddRenamedColumnSortAndFilter(selectedColumnToEdit.name, payload.name)
}
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints']))
await Promise.all([
meta.tables.loadById(selectedTable!.id),
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])),
queryClient.invalidateQueries(tableKeys.table(project?.ref, selectedTable!.id)),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name])
),
@@ -278,7 +262,7 @@ const SidePanelEditor = ({
])
onColumnSaved(configuration.isEncrypted)
setIsEdited(false)
closePanel()
snap.closeSidePanel()
}
if (configuration.isEncrypted && selectedTable?.schema) {
@@ -329,8 +313,12 @@ const SidePanelEditor = ({
configuration
try {
if (isDuplicating) {
const duplicateTable = find(tables, { id: tableId }) as PostgresTable
if (
snap.sidePanel?.type === 'table' &&
snap.sidePanel.mode === 'duplicate' &&
selectedTable
) {
const duplicateTable = selectedTable
toastId = ui.setNotification({
category: 'loading',
@@ -344,7 +332,10 @@ const SidePanelEditor = ({
duplicateTable,
})
await queryClient.invalidateQueries(entityTypeKeys.list(project?.ref))
await Promise.all([
queryClient.invalidateQueries(tableKeys.list(project?.ref, table.schema)),
queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)),
])
ui.setNotification({
id: toastId,
@@ -368,7 +359,10 @@ const SidePanelEditor = ({
importContent
)
await queryClient.invalidateQueries(entityTypeKeys.list(project?.ref))
await Promise.all([
queryClient.invalidateQueries(tableKeys.list(project?.ref, table.schema)),
queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)),
])
ui.setNotification({
id: toastId,
@@ -377,15 +371,15 @@ const SidePanelEditor = ({
})
onTableCreated(table)
} else if (selectedTableToEdit) {
} else if (selectedTable) {
toastId = ui.setNotification({
category: 'loading',
message: `Updating table: ${selectedTableToEdit?.name}...`,
message: `Updating table: ${selectedTable?.name}...`,
})
const { table, hasError }: any = await meta.updateTable(
toastId,
selectedTableToEdit,
selectedTable,
payload,
columns,
isRealtimeEnabled
@@ -401,16 +395,17 @@ const SidePanelEditor = ({
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints']))
await Promise.all([
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [selectedTableToEdit.schema, selectedTableToEdit.name])
sqlKeys.query(project?.ref, [selectedTable.schema, selectedTable.name])
),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [
'table-definition',
selectedTableToEdit.schema,
selectedTableToEdit.name,
selectedTable.schema,
selectedTable.name,
])
),
queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)),
queryClient.invalidateQueries(tableKeys.table(project?.ref, table.schema)),
])
ui.setNotification({
@@ -427,7 +422,7 @@ const SidePanelEditor = ({
if (!saveTableError) {
setIsEdited(false)
closePanel()
snap.closeSidePanel()
}
resolve()
@@ -507,14 +502,14 @@ const SidePanelEditor = ({
message: `Successfully imported ${rowCount} rows of data into ${selectedTable.name}`,
})
resolve()
closePanel()
snap.closeSidePanel()
}
const onClosePanel = () => {
if (isEdited) {
setIsClosingPanel(true)
} else {
closePanel()
snap.closeSidePanel()
}
}
@@ -522,9 +517,9 @@ const SidePanelEditor = ({
<>
{!isUndefined(selectedTable) && (
<RowEditor
row={selectedRowToEdit}
row={snap.sidePanel?.type === 'row' ? snap.sidePanel.row : undefined}
selectedTable={selectedTable}
visible={sidePanelKey === 'row'}
visible={snap.sidePanel?.type === 'row'}
closePanel={onClosePanel}
saveChanges={saveRow}
updateEditorDirty={() => setIsEdited(true)}
@@ -532,42 +527,58 @@ const SidePanelEditor = ({
)}
{!isUndefined(selectedTable) && (
<ColumnEditor
column={selectedColumnToEdit}
column={
snap.sidePanel?.type === 'column'
? (snap.sidePanel.column as PostgresColumn)
: undefined
}
selectedTable={selectedTable}
visible={sidePanelKey === 'column'}
visible={snap.sidePanel?.type === 'column'}
closePanel={onClosePanel}
saveChanges={saveColumn}
updateEditorDirty={() => setIsEdited(true)}
/>
)}
<TableEditor
table={selectedTableToEdit}
selectedSchema={selectedSchema}
isDuplicating={isDuplicating}
visible={sidePanelKey === 'table'}
table={
snap.sidePanel?.type === 'table' &&
(snap.sidePanel.mode === 'edit' || snap.sidePanel.mode === 'duplicate')
? selectedTable
: undefined
}
isDuplicating={snap.sidePanel?.type === 'table' && snap.sidePanel.mode === 'duplicate'}
visible={snap.sidePanel?.type === 'table'}
closePanel={onClosePanel}
saveChanges={saveTable}
updateEditorDirty={() => setIsEdited(true)}
/>
<JsonEdit
visible={sidePanelKey === 'json'}
column={selectedValueForJsonEdit?.column ?? ''}
jsonString={selectedValueForJsonEdit?.jsonString ?? ''}
visible={snap.sidePanel?.type === 'json'}
column={(snap.sidePanel?.type === 'json' && snap.sidePanel.jsonValue.column) || ''}
jsonString={(snap.sidePanel?.type === 'json' && snap.sidePanel.jsonValue.jsonString) || ''}
backButtonLabel="Cancel"
applyButtonLabel="Save changes"
closePanel={onClosePanel}
onSaveJSON={onSaveJSON}
/>
<ForeignRowSelector
key={`foreign-row-selector-${selectedForeignKeyToEdit?.foreignKey?.id ?? 'null'}`}
visible={sidePanelKey === 'foreign-row-selector'}
foreignKey={selectedForeignKeyToEdit?.foreignKey}
key={`foreign-row-selector-${
(snap.sidePanel?.type === 'foreign-row-selector' &&
snap.sidePanel.foreignKey.foreignKey.id) ||
'null'
}`}
visible={snap.sidePanel?.type === 'foreign-row-selector'}
foreignKey={
snap.sidePanel?.type === 'foreign-row-selector'
? snap.sidePanel.foreignKey.foreignKey
: undefined
}
closePanel={onClosePanel}
onSelect={onSaveForeignRow}
/>
<SpreadsheetImport
visible={sidePanelKey === 'csv-import'}
selectedTable={selectedTableToEdit}
visible={snap.sidePanel?.type === 'csv-import'}
selectedTable={selectedTable}
saveContent={onImportData}
closePanel={onClosePanel}
updateEditorDirty={setIsEdited}
@@ -580,7 +591,7 @@ const SidePanelEditor = ({
onSelectConfirm={() => {
setIsClosingPanel(false)
setIsEdited(false)
closePanel()
snap.closeSidePanel()
}}
>
<Modal.Content>
@@ -2,7 +2,7 @@ import type { PostgresTable } from '@supabase/postgres-meta'
interface HeaderTitleProps {
schema: string
table: PostgresTable
table?: { name: string }
isDuplicating: boolean
}
@@ -17,13 +17,13 @@ const HeaderTitle = ({ schema, table, isDuplicating }: HeaderTitleProps) => {
if (isDuplicating) {
return (
<>
Duplicate table <code className="text-sm">{table.name}</code>
Duplicate table <code className="text-sm">{table?.name}</code>
</>
)
}
return (
<>
Update table <code className="text-sm">{table.name}</code>
Update table <code className="text-sm">{table?.name}</code>
</>
)
}
@@ -2,12 +2,14 @@ import type { PostgresTable, PostgresType } from '@supabase/postgres-meta'
import { isEmpty, isUndefined, noop } from 'lodash'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { Alert, Badge, Button, Checkbox, IconBookOpen, Input, Modal, SidePanel } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
import { useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Alert, Badge, Button, Checkbox, IconBookOpen, Input, Modal, SidePanel } from 'ui'
import { SpreadsheetImport } from '../'
import ActionBar from '../ActionBar'
import { ColumnField, CreateTablePayload, UpdateTablePayload } from '../SidePanelEditor.types'
@@ -23,9 +25,8 @@ import {
validateFields,
} from './TableEditor.utils'
interface TableEditorProps {
export interface TableEditorProps {
table?: PostgresTable
selectedSchema: string
isDuplicating: boolean
visible: boolean
closePanel: () => void
@@ -47,20 +48,18 @@ interface TableEditorProps {
const TableEditor = ({
table,
selectedSchema,
isDuplicating,
visible = false,
closePanel = noop,
saveChanges = noop,
updateEditorDirty = noop,
}: TableEditorProps) => {
const snap = useTableEditorStateSnapshot()
const { ui, meta } = useStore()
const { project } = useProjectContext()
const isNewRecord = isUndefined(table)
const enumTypes = meta.types.list(
(type: PostgresType) => !meta.excludedSchemas.includes(type.schema)
)
const enumTypes = meta.types.list((type: PostgresType) => !EXCLUDED_SCHEMAS.includes(type.schema))
const publications = meta.publications.list()
const realtimePublication = publications.find(
@@ -135,7 +134,7 @@ const TableEditor = ({
if (isEmpty(errors)) {
const payload: CreateTablePayload | UpdateTablePayload = {
name: tableFields.name,
schema: selectedSchema,
schema: snap.selectedSchemaName,
comment: tableFields.comment,
...(!isNewRecord && { rls_enabled: tableFields.isRLSEnabled }),
}
@@ -170,8 +169,9 @@ const TableEditor = ({
size="large"
key="TableEditor"
visible={visible}
// @ts-ignore
header={<HeaderTitle schema={selectedSchema} table={table} isDuplicating={isDuplicating} />}
header={
<HeaderTitle schema={snap.selectedSchemaName} table={table} isDuplicating={isDuplicating} />
}
className={`transition-all duration-100 ease-in ${isImportingSpreadsheet ? ' mr-32' : ''}`}
onCancel={closePanel}
onConfirm={() => (resolve: () => void) => onSaveChanges(resolve)}
@@ -297,7 +297,7 @@ const TableEditor = ({
<div className="space-y-10 py-6">
{!isDuplicating && (
<ColumnManagement
table={{ name: tableFields.name, schema: selectedSchema }}
table={{ name: tableFields.name, schema: snap.selectedSchemaName }}
columns={tableFields?.columns}
enumTypes={enumTypes}
isNewRecord={isNewRecord}
@@ -1,12 +1,12 @@
import type { PostgresColumn, PostgresRelationship, PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { QueryKey, useQueryClient } from '@tanstack/react-query'
import { useParams } from 'common'
import { find, isUndefined, noop } from 'lodash'
import { find, isUndefined } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import { useParams } from 'common'
import {
Dictionary,
SupaTable,
@@ -16,6 +16,7 @@ import {
} from 'components/grid'
import { ERROR_PRIMARY_KEY_NOTFOUND } from 'components/grid/constants'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import Connecting from 'components/ui/Loading/Loading'
import TwoOptionToggle from 'components/ui/TwoOptionToggle'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import {
@@ -26,83 +27,39 @@ import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { sqlKeys } from 'data/sql/keys'
import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation'
import { useCheckPermissions, useStore, useUrlState } from 'hooks'
import { useCheckPermissions, useLatest, useStore, useUrlState } from 'hooks'
import useEntityType from 'hooks/misc/useEntityType'
import { TableLike } from 'hooks/misc/useTable'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { EMPTY_ARR } from 'lib/void'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { SchemaView } from 'types'
import APIDocumentationPanel from './APIDocumentationPanel'
import GridHeaderActions from './GridHeaderActions'
import NotFoundState from './NotFoundState'
import SidePanelEditor from './SidePanelEditor'
import { ForeignRowSelectorProps } from './SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
import { JsonEditValue } from './SidePanelEditor/RowEditor/RowEditor.types'
import TableDefinition from './TableDefinition'
export interface TableGridEditorProps {
/** Theme for the editor */
theme?: 'dark' | 'light'
selectedSchema?: string
selectedTable: any // PostgresTable | SchemaView
/** Determines what side panel editor to show */
sidePanelKey?: 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import'
/** Toggles if we're duplicating a table */
isDuplicating: boolean
/** Selected entities if we're editing a row, column or table */
selectedRowToEdit?: Dictionary<any>
selectedColumnToEdit?: PostgresColumn
selectedTableToEdit?: PostgresTable
selectedValueForJsonEdit?: JsonEditValue
selectedForeignKeyToEdit?: {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}
onAddRow: () => void
onEditRow: (row: Dictionary<any>) => void
onAddColumn: () => void
onEditColumn: (column: PostgresColumn) => void
onDeleteColumn: (column: PostgresColumn) => void
onExpandJSONEditor: (column: string, row: any) => void
onEditForeignKeyColumnValue: (args: {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}) => void
onClosePanel: () => void
onImportData: () => void
isLoadingSelectedTable?: boolean
selectedTable?: TableLike
}
const TableGridEditor = ({
theme = 'dark',
selectedSchema,
isLoadingSelectedTable = false,
selectedTable,
sidePanelKey,
isDuplicating,
selectedRowToEdit,
selectedColumnToEdit,
selectedTableToEdit,
selectedValueForJsonEdit,
selectedForeignKeyToEdit,
onAddRow = noop,
onEditRow = noop,
onAddColumn = noop,
onEditColumn = noop,
onDeleteColumn = noop,
onExpandJSONEditor = noop,
onEditForeignKeyColumnValue = noop,
onClosePanel = noop,
onImportData = noop,
}: TableGridEditorProps) => {
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const { meta, ui, vault } = useStore()
const router = useRouter()
const { ref: projectRef, id } = useParams()
const gridRef = useRef<SupabaseGridRef>(null)
const { project } = useProjectContext()
const [encryptedColumns, setEncryptedColumns] = useState([])
const [apiPreviewPanelOpen, setApiPreviewPanelOpen] = useState(false)
@@ -201,34 +158,38 @@ const TableGridEditor = ({
}, [selectedTable?.id])
const entityType = useEntityType(selectedTable?.id)
const columnsRef = useLatest(selectedTable?.columns ?? EMPTY_ARR)
// NOTE: DO NOT PUT HOOKS AFTER THIS LINE
if (isLoadingSelectedTable) {
return <Connecting />
}
if (isUndefined(selectedTable)) {
return <NotFoundState id={Number(id)} />
}
const tableId = selectedTable?.id
const isViewSelected =
entityType?.type === ENTITY_TYPE.VIEW || entityType?.type === ENTITY_TYPE.MATERIALIZED_VIEW
const isTableSelected = entityType?.type === ENTITY_TYPE.TABLE
const isForeignTableSelected = entityType?.type === ENTITY_TYPE.FOREIGN_TABLE
const isLocked = meta.excludedSchemas.includes(entityType?.schema ?? '')
const isLocked = EXCLUDED_SCHEMAS.includes(entityType?.schema ?? '')
const canEditViaTableEditor = isTableSelected && !isLocked
// [Joshen] We can tweak below to eventually support composite keys as the data
// returned from foreignKeyMeta should be easy to deal with, rather than pg-meta
const formattedRelationships = (selectedTable?.relationships ?? []).map(
(relationship: PostgresRelationship) => {
const relationshipMeta = foreignKeyMeta.find(
(fk: ForeignKeyConstraint) => fk.id === relationship.id
)
return {
...relationship,
deletion_action: relationshipMeta?.deletion_action ?? FOREIGN_KEY_CASCADE_ACTION.NO_ACTION,
}
const formattedRelationships = (
('relationships' in selectedTable && selectedTable.relationships) ||
[]
).map((relationship: PostgresRelationship) => {
const relationshipMeta = foreignKeyMeta.find(
(fk: ForeignKeyConstraint) => fk.id === relationship.id
)
return {
...relationship,
deletion_action: relationshipMeta?.deletion_action ?? FOREIGN_KEY_CASCADE_ACTION.NO_ACTION,
}
)
})
const gridTable =
!isViewSelected && !isForeignTableSelected
@@ -275,27 +236,31 @@ const TableGridEditor = ({
}
}
const onSelectEditColumn = async (name: string) => {
// For some reason, selectedTable here is stale after adding a table
// temporary workaround is to list grab the selected table again
const tables: PostgresTable[] = meta.tables.list()
// @ts-ignore
const table = tables.find((table) => table.id === Number(tableId))
const column = find(table!.columns, { name }) as PostgresColumn
// columns must be accessed via columnsRef.current as these two functions immediately become
// stale as they are accessed via some react-tracked madness
// [TODO]: refactor out all of react-tracked
const onSelectEditColumn = (name: string) => {
const column = find(columnsRef.current, { name }) as PostgresColumn
if (column) {
onEditColumn(column)
snap.onEditColumn(column)
} else {
console.error(`Unable to find column ${name} in ${table?.name}`)
ui.setNotification({
category: 'error',
message: `Unable to find column ${name} in ${selectedTable?.name}`,
})
}
}
const onSelectDeleteColumn = async (name: string) => {
// For some reason, selectedTable here is stale after adding a table
// temporary workaround is to list grab the selected table again
const tables: PostgresTable[] = meta.tables.list()
const table = tables.find((table) => table.id === Number(tableId))
const column = find(table!.columns, { name }) as PostgresColumn
onDeleteColumn(column)
const onSelectDeleteColumn = (name: string) => {
const column = find(columnsRef.current ?? [], { name }) as PostgresColumn
if (column) {
snap.onDeleteColumn(column)
} else {
ui.setNotification({
category: 'error',
message: `Unable to find column ${name} in ${selectedTable?.name}`,
})
}
}
const onError = (error: any) => {
@@ -308,11 +273,14 @@ const TableGridEditor = ({
const updateTableRow = (previousRow: any, updatedData: any) => {
if (!project) return
const enumArrayColumns = selectedTable.columns
.filter((column: any) => {
return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
})
.map((column: any) => column.name)
const enumArrayColumns =
('columns' in selectedTable &&
selectedTable.columns
?.filter((column) => {
return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
})
.map((column) => column.name)) ||
[]
const identifiers = {} as Dictionary<any>
;(selectedTable as PostgresTable).primary_keys.forEach(
@@ -384,17 +352,19 @@ const TableGridEditor = ({
</>
) : null
}
onAddColumn={onAddColumn}
onAddColumn={snap.onAddColumn}
onEditColumn={onSelectEditColumn}
onDeleteColumn={onSelectDeleteColumn}
onAddRow={onAddRow}
onAddRow={snap.onAddRow}
updateTableRow={updateTableRow}
onEditRow={onEditRow}
onImportData={onImportData}
onEditRow={snap.onEditRow}
onImportData={snap.onImportData}
onError={onError}
onSqlQuery={onSqlQuery}
onExpandJSONEditor={onExpandJSONEditor}
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
onExpandJSONEditor={(column, row) =>
snap.onExpandJSONEditor({ column, row, jsonString: JSON.stringify(row[column]) || '' })
}
onEditForeignKeyColumnValue={snap.onEditForeignKeyColumnValue}
showCustomChildren={(isViewSelected || isTableSelected) && selectedView === 'definition'}
customHeader={
(isViewSelected || isTableSelected) && selectedView === 'definition' ? (
@@ -410,22 +380,13 @@ const TableGridEditor = ({
{(isViewSelected || isTableSelected) && <TableDefinition id={selectedTable?.id} />}
</SupabaseGrid>
{!isUndefined(selectedSchema) && (
{snap.selectedSchemaName !== undefined && (
<SidePanelEditor
selectedSchema={selectedSchema}
isDuplicating={isDuplicating}
selectedTable={selectedTable as PostgresTable}
selectedRowToEdit={selectedRowToEdit}
selectedColumnToEdit={selectedColumnToEdit}
selectedTableToEdit={selectedTableToEdit}
selectedValueForJsonEdit={selectedValueForJsonEdit}
selectedForeignKeyToEdit={selectedForeignKeyToEdit}
sidePanelKey={sidePanelKey}
onRowCreated={onRowCreated}
onRowUpdated={onRowUpdated}
onColumnSaved={onColumnSaved}
onTableCreated={onTableCreated}
closePanel={onClosePanel}
/>
)}
@@ -1,9 +1,8 @@
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect, useState } from 'react'
import { PropsWithChildren, useEffect } from 'react'
import { useParams } from 'common'
import Error from 'components/ui/Error'
import ProductMenu from 'components/ui/ProductMenu'
import { useAuthConfigPrefetch } from 'data/auth/auth-config-query'
import { useStore, withAuth } from 'hooks'
@@ -15,45 +14,26 @@ export interface AuthLayoutProps {
}
const AuthLayout = ({ title, children }: PropsWithChildren<AuthLayoutProps>) => {
const { ui, meta } = useStore()
const { isInitialized, isLoading, error } = meta.tables
const { ref: projectRef = 'default' } = useParams()
const { ui, meta } = useStore()
useAuthConfigPrefetch({ projectRef })
const router = useRouter()
const page = router.pathname.split('/')[4]
const [loaded, setLoaded] = useState<boolean>(isInitialized)
useEffect(() => {
if (ui.selectedProjectRef) {
meta.policies.load()
meta.tables.load()
meta.roles.load()
meta.schemas.load()
}
}, [ui.selectedProjectRef])
useEffect(() => {
if (!isLoading && !loaded) {
setLoaded(true)
}
}, [isLoading])
if (error) {
return (
<ProjectLayout>
<Error error={error} />
</ProjectLayout>
)
}
return (
<ProjectLayout
isLoading={!loaded}
title={title || 'Authentication'}
product="Authentication"
productMenu={<ProductMenu page={page} menu={generateAuthMenu(projectRef)} />}
productMenu={<ProductMenu page={page} menu={generateAuthMenu(projectRef ?? 'default')} />}
>
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
{children}
@@ -1,8 +1,7 @@
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect, useState } from 'react'
import { PropsWithChildren, useEffect } from 'react'
import Error from 'components/ui/Error'
import ProductMenu from 'components/ui/ProductMenu'
import { useSelectedProject, useStore, withAuth } from 'hooks'
import ProjectLayout from '../'
@@ -14,10 +13,6 @@ export interface DatabaseLayoutProps {
const DatabaseLayout = ({ children }: PropsWithChildren<DatabaseLayoutProps>) => {
const { ui, meta, vault } = useStore()
const { isLoading: isSchemasLoading } = meta.schemas
const { isLoading: isVaultLoading } = vault
const { isInitialized, error } = meta.tables
const project = useSelectedProject()
const router = useRouter()
@@ -27,15 +22,8 @@ const DatabaseLayout = ({ children }: PropsWithChildren<DatabaseLayoutProps>) =>
const isVaultEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null
const pgNetExtensionExists = meta.extensions.byId('pg_net') !== undefined
const isLoading = isSchemasLoading || (isVaultEnabled && isVaultLoading)
const [loaded, setLoaded] = useState<boolean>(isInitialized)
useEffect(() => {
if (ui.selectedProjectRef) {
// Eventually should only load the required stores based on the pages
meta.schemas.load()
meta.tables.load()
meta.roles.load()
meta.triggers.load()
meta.extensions.load()
@@ -49,25 +37,8 @@ const DatabaseLayout = ({ children }: PropsWithChildren<DatabaseLayoutProps>) =>
}
}, [ui.selectedProjectRef, isVaultEnabled])
// Optimization required: load logic should be at the page level
// e.g backups page is waiting for meta.tables to load finish when it doesnt even need that data
useEffect(() => {
if (!isLoading && !loaded) {
setLoaded(true)
}
}, [isLoading])
if (error) {
return (
<ProjectLayout>
<Error error={error} />
</ProjectLayout>
)
}
return (
<ProjectLayout
isLoading={!loaded}
product="Database"
productMenu={
<ProductMenu page={page} menu={generateDatabaseMenu(project, { pgNetExtensionExists })} />
@@ -1,10 +1,12 @@
import { createContext, PropsWithChildren, useContext, useMemo } from 'react'
import { useParams } from 'common/hooks'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
import { PROJECT_STATUS } from 'lib/constants'
import { PropsWithChildren, createContext, useContext, useMemo } from 'react'
import { TableEditorStateContextProvider } from 'state/table-editor'
import { Project } from 'types'
type ProjectContextType = {
export interface ProjectContextType {
project: Project | undefined
isLoading: boolean
}
@@ -35,7 +37,13 @@ export const ProjectContextProvider = ({
}
}, [selectedProject, isLoading])
return <ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>
return (
<ProjectContext.Provider value={value}>
<TableEditorStateContextProvider key={`table-editor-state-${projectRef}`}>
{children}
</TableEditorStateContextProvider>
</ProjectContext.Provider>
)
}
export const ProjectContextFromParamsProvider = ({ children }: PropsWithChildren<{}>) => {
@@ -6,28 +6,17 @@ import { Entity } from 'data/entity-types/entity-type-query'
import Link from 'next/link'
import { Dropdown, IconEdit, IconCopy, IconLock, IconTrash, IconChevronDown } from 'ui'
import { BASE_PATH } from 'lib/constants'
import { useTableEditorStateSnapshot } from 'state/table-editor'
export interface EntityListItemProps {
id: number
projectRef: string
item: Entity
isLocked: boolean
onEditTable: (table: Entity) => void
onDeleteTable: (table: Entity) => void
onDuplicateTable: (table: Entity) => void
isLoadingTableMetadata?: boolean
}
const EntityListItem = ({
id,
projectRef,
item: entity,
isLocked,
onEditTable,
onDeleteTable,
onDuplicateTable,
isLoadingTableMetadata,
}: EntityListItemProps) => {
const EntityListItem = ({ id, projectRef, item: entity, isLocked }: EntityListItemProps) => {
const snap = useTableEditorStateSnapshot()
const isActive = Number(id) === entity.id
const formatTooltipText = (entityType: string) => {
return Object.entries(ENTITY_TYPE)
@@ -137,9 +126,8 @@ const EntityListItem = ({
icon={<IconEdit size="tiny" />}
onClick={(e) => {
e.stopPropagation()
onEditTable(entity)
snap.onEditTable()
}}
disabled={isLoadingTableMetadata}
>
Edit Table
</Dropdown.Item>,
@@ -148,9 +136,8 @@ const EntityListItem = ({
icon={<IconCopy size="tiny" />}
onClick={(e) => {
e.stopPropagation()
onDuplicateTable(entity)
snap.onDuplicateTable()
}}
disabled={isLoadingTableMetadata}
>
Duplicate Table
</Dropdown.Item>,
@@ -170,9 +157,8 @@ const EntityListItem = ({
icon={<IconTrash size="tiny" />}
onClick={(e) => {
e.stopPropagation()
onDeleteTable(entity)
snap.onDeleteTable()
}}
disabled={isLoadingTableMetadata}
>
Delete Table
</Dropdown.Item>,
@@ -1,49 +1,15 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { useParams } from 'common/hooks'
import Connecting from 'components/ui/Loading/Loading'
import NoPermission from 'components/ui/NoPermission'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { Entity } from 'data/entity-types/entity-type-query'
import { useCheckPermissions, useSelectedProject, useStore } from 'hooks'
import useEntityType from 'hooks/misc/useEntityType'
import useLatest from 'hooks/misc/useLatest'
import { useIsTableLoaded, useTableEditorStateSnapshot } from 'state/table-editor'
import ProjectLayout from '../'
import useTableRowsPrefetchWrapper from './TableEditorLayout.utils'
import { useCheckPermissions, useStore } from 'hooks'
import { ProjectLayoutWithAuth } from '../'
import TableEditorMenu from './TableEditorMenu'
export interface TableEditorLayoutProps {
selectedSchema?: string
selectedTable?: string
onSelectSchema: (schema: string) => void
onAddTable: () => void
onEditTable: (table: Entity) => void
onDeleteTable: (table: Entity) => void
onDuplicateTable: (table: Entity) => void
}
const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => {
const { vault, meta, ui } = useStore()
const TableEditorLayout = ({
selectedSchema,
selectedTable,
onSelectSchema = noop,
onAddTable = noop,
onEditTable = noop,
onDeleteTable = noop,
onDuplicateTable = noop,
children,
}: PropsWithChildren<TableEditorLayoutProps>) => {
const { ui, vault, meta } = useStore()
const selectedProject = useSelectedProject()
const router = useRouter()
const { ref, id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const snap = useTableEditorStateSnapshot()
const canReadTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables')
const vaultExtension = meta.extensions.byId('supabase_vault')
@@ -51,7 +17,6 @@ const TableEditorLayout = ({
useEffect(() => {
if (ui.selectedProjectRef) {
meta.schemas.load()
meta.types.load()
meta.policies.load()
meta.publications.load()
@@ -59,87 +24,24 @@ const TableEditorLayout = ({
}
}, [ui.selectedProjectRef])
const isLoaded = useIsTableLoaded(ref, id)
const entity = useEntityType(id, function onNotFound(id) {
if (ref) snap.addLoadedId(ref, id)
})
const prefetch = useLatest(useTableRowsPrefetchWrapper())
useEffect(() => {
let mounted = true
function loadTable() {
if (entity?.type) {
switch (entity.type) {
case ENTITY_TYPE.MATERIALIZED_VIEW:
return meta.materializedViews.loadById(entity.id)
case ENTITY_TYPE.VIEW:
return meta.views.loadById(entity.id)
case ENTITY_TYPE.FOREIGN_TABLE:
return meta.foreignTables.loadById(entity.id)
default:
return meta.tables.loadById(entity.id)
}
}
}
loadTable()
?.then(async (entity: any) => {
await prefetch.current(entity)
return entity
})
.then((entity: any) => {
if (mounted && ref) {
snap.addLoadedId(ref, entity.id)
}
})
.catch(() => {
if (mounted && entity?.id && ref) {
snap.addLoadedId(ref, entity.id)
}
})
return () => {
mounted = false
}
}, [entity?.id])
useEffect(() => {
if (isVaultEnabled) {
vault.load()
}
}, [selectedProject?.ref, isVaultEnabled])
}, [ui.selectedProjectRef, isVaultEnabled])
if (!canReadTables) {
return (
<ProjectLayout>
<ProjectLayoutWithAuth>
<NoPermission isFullPage resourceText="view tables from this project" />
</ProjectLayout>
</ProjectLayoutWithAuth>
)
}
return (
<ProjectLayout
product="Table editor"
selectedTable={selectedTable}
productMenu={
<TableEditorMenu
selectedSchema={selectedSchema}
onSelectSchema={onSelectSchema}
onAddTable={onAddTable}
onEditTable={onEditTable}
onDeleteTable={onDeleteTable}
onDuplicateTable={onDuplicateTable}
/>
}
>
{router.isReady && id !== undefined ? isLoaded ? children : <Connecting /> : children}
</ProjectLayout>
<ProjectLayoutWithAuth product="Table editor" productMenu={<TableEditorMenu />}>
{children}
</ProjectLayoutWithAuth>
)
}
@@ -1,6 +1,15 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { useMemo, useState } from 'react'
import { noop, partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useParams } from 'common/hooks'
import InfiniteList from 'components/ui/InfiniteList'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { useCheckPermissions, useLocalStorage } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
Alert,
Button,
@@ -16,37 +25,12 @@ import {
Listbox,
Menu,
} from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import type { PostgresSchema } from '@supabase/postgres-meta'
import { useParams } from 'common/hooks'
import { useCheckPermissions, useStore, useLocalStorage } from 'hooks'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { Entity } from 'data/entity-types/entity-type-query'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
import InfiniteList from 'components/ui/InfiniteList'
import EntityListItem from './EntityListItem'
export interface TableEditorMenuProps {
selectedSchema?: string
onSelectSchema: (schema: string) => void
onAddTable: () => void
onEditTable: (table: Entity) => void
onDeleteTable: (table: Entity) => void
onDuplicateTable: (table: Entity) => void
}
const TableEditorMenu = ({
selectedSchema,
onSelectSchema = noop,
onAddTable = noop,
onEditTable = noop,
onDeleteTable = noop,
onDuplicateTable = noop,
}: TableEditorMenuProps) => {
const { meta } = useStore()
const TableEditorMenu = () => {
const { id } = useParams()
const snap = useTableEditorStateSnapshot()
const [searchText, setSearchText] = useState<string>('')
const [sort, setSort] = useLocalStorage<'alphabetical' | 'grouped-alphabetical'>(
@@ -68,7 +52,7 @@ const TableEditorMenu = ({
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedSchema,
schema: snap.selectedSchemaName,
search: searchText || undefined,
sort,
},
@@ -83,18 +67,27 @@ const TableEditorMenu = ({
[data?.pages]
)
const schemas: PostgresSchema[] = meta.schemas.list()
const schema = schemas.find((schema) => schema.name === selectedSchema)
const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const {
data: schemas,
isLoading: isSchemasLoading,
isSuccess: isSchemasSuccess,
isError: isSchemasError,
error: schemasError,
refetch: refetchSchemas,
} = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const isLoadingTableMetadata = id ? !meta.tables.byId(id) : true
const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName)
const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const refreshTables = async () => {
await refetch()
}
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
@@ -105,25 +98,29 @@ const TableEditorMenu = ({
>
{/* Schema selection dropdown */}
<div className="px-3 mx-4">
{!meta.schemas.isInitialized ? (
{isSchemasLoading && (
<div className="flex h-[26px] items-center space-x-3 rounded border border-gray-500 px-3">
<IconLoader className="animate-spin" size={12} />
<span className="text-xs text-scale-900">Loading schemas...</span>
</div>
) : meta.schemas.hasError ? (
)}
{isSchemasError && (
<Alert variant="warning" title="Failed to load schemas" className="!px-3 !py-3">
<p className="mb-2">Error: {meta.schemas?.error?.message}</p>
<Button type="default" size="tiny" onClick={() => meta.schemas.load()}>
<p className="mb-2">Error: {schemasError.message}</p>
<Button type="default" size="tiny" onClick={() => refetchSchemas()}>
Reload schemas
</Button>
</Alert>
) : (
)}
{isSchemasSuccess && (
<Listbox
size="tiny"
value={selectedSchema}
value={snap.selectedSchemaName}
onChange={(name: string) => {
setSearchText('')
onSelectSchema(name)
snap.setSelectedSchemaName(name)
}}
>
<Listbox.Option
@@ -188,7 +185,7 @@ const TableEditorMenu = ({
}
type="default"
style={{ justifyContent: 'start' }}
onClick={onAddTable}
onClick={snap.onAddTable}
>
<span>New table</span>
</Button>
@@ -352,10 +349,6 @@ const TableEditorMenu = ({
itemProps={{
projectRef: project?.ref,
id: Number(id),
onEditTable,
onDeleteTable,
onDuplicateTable,
isLoadingTableMetadata,
}}
getItemSize={() => 28}
hasNextPage={hasNextPage}
@@ -369,4 +362,4 @@ const TableEditorMenu = ({
)
}
export default observer(TableEditorMenu)
export default TableEditorMenu
+1
View File
@@ -1,4 +1,5 @@
export const databaseKeys = {
schemaList: (projectRef: string | undefined) => ['projects', projectRef, 'schemas'] as const,
backups: (projectRef: string | undefined) => [projectRef, 'database', 'backups'] as const,
poolingConfiguration: (projectRef: string | undefined) =>
[projectRef, 'database', 'pooling-configuration'] as const,
+46 -46
View File
@@ -1,61 +1,61 @@
import { useCallback } from 'react'
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query'
import { components } from 'data/api'
import { get } from 'data/fetchers'
import { ResponseError } from 'types'
export type Schema = {
name: string
}
export const getSchemasQuery = () => {
const sql = /* SQL */ `
SELECT nspname as name
FROM pg_namespace
WHERE
nspname not in ('information_schema', 'pg_catalog', 'pg_toast')
AND nspname not like 'pg_temp_%'
AND nspname not like 'pg_toast_temp_%'
AND has_schema_privilege(oid, 'CREATE, USAGE')
ORDER BY nspname;
`.trim()
return sql
}
import { databaseKeys } from './keys'
export type SchemasVariables = {
projectRef?: string
connectionString?: string
}
export type SchemasData = { result: Schema[] }
export type SchemasError = unknown
export type Schema = components['schemas']['PostgresSchema']
export const useSchemasQuery = <TData extends SchemasData = SchemasData>(
export async function getSchemas(
{ projectRef, connectionString }: SchemasVariables,
options: UseQueryOptions<ExecuteSqlData, SchemasError, TData> = {}
) => {
return useExecuteSqlQuery(
{
projectRef,
connectionString,
sql: getSchemasQuery(),
queryKey: ['schemas'],
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { data, error } = await get('/platform/pg-meta/{ref}/schemas', {
params: {
header: {
'x-connection-encrypted': connectionString!,
},
path: {
ref: projectRef,
},
},
options
)
headers: Object.fromEntries(headers),
signal,
})
if (error) {
throw error
}
return data
}
export const useSchemasPrefetch = () => {
const prefetch = useExecuteSqlPrefetch()
export type SchemasData = Awaited<ReturnType<typeof getSchemas>>
export type SchemasError = ResponseError
return useCallback(
({ projectRef, connectionString }: SchemasVariables) =>
prefetch({
projectRef,
connectionString,
sql: getSchemasQuery(),
queryKey: ['schemas'],
}),
[prefetch]
export const useSchemasQuery = <TData = SchemasData>(
{ projectRef, connectionString }: SchemasVariables,
{ enabled = true, ...options }: UseQueryOptions<SchemasData, SchemasError, TData> = {}
) =>
useQuery<SchemasData, SchemasError, TData>(
databaseKeys.schemaList(projectRef),
({ signal }) => getSchemas({ projectRef, connectionString }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
export function invalidateSchemasQuery(client: QueryClient, projectRef: string | undefined) {
return client.invalidateQueries(databaseKeys.schemaList(projectRef))
}
@@ -0,0 +1,57 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { foreignTableKeys } from './keys'
export type ForeignTableVariables = {
id?: number
projectRef?: string
connectionString?: string
}
export type ForeignTable = PostgresTable
export type ForeignTableResponse = ForeignTable | { error?: any }
export async function getForeignTable(
{ id, projectRef, connectionString }: ForeignTableVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
if (!id) {
throw new Error('id is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/foreign-tables?id=${id}`, {
headers: Object.fromEntries(headers),
signal,
})) as ForeignTableResponse
if ('error' in response) {
throw response.error
}
return response as PostgresTable
}
export type ForeignTableData = Awaited<ReturnType<typeof getForeignTable>>
export type ForeignTableError = unknown
export const useForeignTableQuery = <TData = ForeignTableData>(
{ projectRef, id }: ForeignTableVariables,
{ enabled = true, ...options }: UseQueryOptions<ForeignTableData, ForeignTableError, TData> = {}
) =>
useQuery<ForeignTableData, ForeignTableError, TData>(
foreignTableKeys.foreignTable(projectRef, id),
({ signal }) => getForeignTable({ projectRef, id }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined',
...options,
}
)
@@ -0,0 +1,49 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { ForeignTable } from './foreign-table-query'
import { foreignTableKeys } from './keys'
export type ForeignTablesVariables = {
projectRef?: string
connectionString?: string
}
export type ForeignTablesResponse = ForeignTable[] | { error?: any }
export async function getForeignTables(
{ projectRef, connectionString }: ForeignTablesVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/foreign-tables`, {
headers: Object.fromEntries(headers),
signal,
})) as ForeignTablesResponse
if (!Array.isArray(response) && response.error) {
throw response.error
}
return response as PostgresTable[]
}
export type ForeignTablesData = Awaited<ReturnType<typeof getForeignTables>>
export type ForeignTablesError = unknown
export const useForeignTablesQuery = <TData = ForeignTablesData>(
{ projectRef, connectionString }: ForeignTablesVariables,
{ enabled = true, ...options }: UseQueryOptions<ForeignTablesData, ForeignTablesError, TData> = {}
) =>
useQuery<ForeignTablesData, ForeignTablesError, TData>(
foreignTableKeys.list(projectRef),
({ signal }) => getForeignTables({ projectRef, connectionString }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
+5
View File
@@ -0,0 +1,5 @@
export const foreignTableKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'foreignTables'] as const,
foreignTable: (projectRef: string | undefined, id: number | undefined) =>
['projects', projectRef, 'foreignTables', id] as const,
}
+5
View File
@@ -0,0 +1,5 @@
export const materializedViewKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'materializedViews'] as const,
materializedView: (projectRef: string | undefined, id: number | undefined) =>
['projects', projectRef, 'materializedViews', id] as const,
}
@@ -0,0 +1,60 @@
import { PostgresMaterializedView } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { materializedViewKeys } from './keys'
export type MaterializedViewVariables = {
id?: number
projectRef?: string
connectionString?: string
}
export type MaterializedView = PostgresMaterializedView
export type MaterializedViewResponse = MaterializedView | { error?: any }
export async function getMaterializedView(
{ id, projectRef, connectionString }: MaterializedViewVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
if (!id) {
throw new Error('id is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/materialized-views?id=${id}`, {
headers: Object.fromEntries(headers),
signal,
})) as MaterializedViewResponse
if ('error' in response) {
throw response.error
}
return response as PostgresMaterializedView
}
export type MaterializedViewData = Awaited<ReturnType<typeof getMaterializedView>>
export type MaterializedViewError = unknown
export const useMaterializedViewQuery = <TData = MaterializedViewData>(
{ projectRef, connectionString, id }: MaterializedViewVariables,
{
enabled = true,
...options
}: UseQueryOptions<MaterializedViewData, MaterializedViewError, TData> = {}
) =>
useQuery<MaterializedViewData, MaterializedViewError, TData>(
materializedViewKeys.materializedView(projectRef, id),
({ signal }) => getMaterializedView({ projectRef, connectionString, id }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined',
...options,
}
)
@@ -0,0 +1,52 @@
import { PostgresMaterializedView } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { materializedViewKeys } from './keys'
import { MaterializedView } from './materialized-view-query'
export type MaterializedViewsVariables = {
projectRef?: string
connectionString?: string
}
export type MaterializedViewsResponse = MaterializedView[] | { error?: any }
export async function getMaterializedViews(
{ projectRef, connectionString }: MaterializedViewsVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/materialized-views`, {
headers: Object.fromEntries(headers),
signal,
})) as MaterializedViewsResponse
if (!Array.isArray(response) && response.error) {
throw response.error
}
return response as PostgresMaterializedView[]
}
export type MaterializedViewsData = Awaited<ReturnType<typeof getMaterializedViews>>
export type MaterializedViewsError = unknown
export const useMaterializedViewsQuery = <TData = MaterializedViewsData>(
{ projectRef, connectionString }: MaterializedViewsVariables,
{
enabled = true,
...options
}: UseQueryOptions<MaterializedViewsData, MaterializedViewsError, TData> = {}
) =>
useQuery<MaterializedViewsData, MaterializedViewsError, TData>(
materializedViewKeys.list(projectRef),
({ signal }) => getMaterializedViews({ projectRef, connectionString }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
@@ -35,3 +35,16 @@ export const useProjectDetailQuery = <TData = ProjectDetailData>(
export function invalidateProjectDetailsQuery(client: QueryClient, ref: string) {
return client.invalidateQueries(projectKeys.detail(ref))
}
// get the cached value or fallback to fetching it
export async function getCachedProjectDetail(
client: QueryClient,
ref: string | undefined
): Promise<ProjectDetailData | undefined> {
if (!ref) return undefined
const cached = client.getQueryData<ProjectDetailData>(projectKeys.detail(ref))
if (cached) return cached
return await client.fetchQuery<ProjectDetailData, ProjectDetailError>(projectKeys.detail(ref))
}
+6
View File
@@ -0,0 +1,6 @@
export const tableKeys = {
list: (projectRef: string | undefined, schema: string | undefined) =>
['projects', projectRef, 'tables', { schema }] as const,
table: (projectRef: string | undefined, id: number | undefined) =>
['projects', projectRef, 'tables', id] as const,
}
+79
View File
@@ -0,0 +1,79 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { tableKeys } from './keys'
import { useCallback } from 'react'
export type TableVariables = {
id?: number
projectRef?: string
connectionString?: string
}
export type Table = PostgresTable
export type TableResponse = Table | { error?: any }
export async function getTable(
{ id, projectRef, connectionString }: TableVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
if (!id) {
throw new Error('id is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/tables?id=${id}`, {
headers: Object.fromEntries(headers),
signal,
})) as TableResponse
if ('error' in response) {
throw response.error
}
return response as PostgresTable
}
export type TableData = Awaited<ReturnType<typeof getTable>>
export type TableError = unknown
export const useTableQuery = <TData = TableData>(
{ projectRef, connectionString, id }: TableVariables,
{ enabled = true, ...options }: UseQueryOptions<TableData, TableError, TData> = {}
) =>
useQuery<TableData, TableError, TData>(
tableKeys.table(projectRef, id),
({ signal }) => getTable({ projectRef, connectionString, id }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined',
...options,
}
)
/**
* useGetTable
* Tries to get a table from the react-query cache, or loads it from the server if it's not cached.
*/
export function useGetTable({
projectRef,
connectionString,
}: Pick<TableVariables, 'projectRef' | 'connectionString'>) {
const queryClient = useQueryClient()
return useCallback(
(id: NonNullable<TableVariables['id']>) => {
return queryClient.fetchQuery({
queryKey: tableKeys.table(projectRef, id),
queryFn: ({ signal }) => getTable({ id, projectRef, connectionString }, signal),
})
},
[connectionString, projectRef, queryClient]
)
}
+80
View File
@@ -0,0 +1,80 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { useCallback } from 'react'
import { ResponseError } from 'types'
import { tableKeys } from './keys'
import { Table } from './table-query'
export type TablesVariables = {
projectRef?: string
connectionString?: string
schema?: string
}
export type TablesResponse = Table[] | { error?: any }
export async function getTables(
{ projectRef, connectionString, schema }: TablesVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
let queryParams = new URLSearchParams()
if (schema) queryParams.set('included_schemas', schema)
const searchStr = queryParams.toString()
const response = (await get(
`${API_URL}/pg-meta/${projectRef}/tables${searchStr ? `?${searchStr}` : ''}`,
{
headers: Object.fromEntries(headers),
signal,
}
)) as TablesResponse
if (!Array.isArray(response) && response.error) {
throw response.error
}
return response as PostgresTable[]
}
export type TablesData = Awaited<ReturnType<typeof getTables>>
export type TablesError = ResponseError
export const useTablesQuery = <TData = TablesData>(
{ projectRef, connectionString, schema }: TablesVariables,
{ enabled = true, ...options }: UseQueryOptions<TablesData, TablesError, TData> = {}
) =>
useQuery<TablesData, TablesError, TData>(
tableKeys.list(projectRef, schema),
({ signal }) => getTables({ projectRef, connectionString, schema }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
/**
* useGetTables
* Tries to get tables from the react-query cache, or loads it from the server if it's not cached.
*/
export function useGetTables({
projectRef,
connectionString,
}: Pick<TablesVariables, 'projectRef' | 'connectionString'>) {
const queryClient = useQueryClient()
return useCallback(
(schema?: TablesVariables['schema']) => {
return queryClient.fetchQuery({
queryKey: tableKeys.list(projectRef, schema),
queryFn: ({ signal }) => getTables({ projectRef, connectionString, schema }, signal),
})
},
[connectionString, projectRef, queryClient]
)
}
+5
View File
@@ -0,0 +1,5 @@
export const viewKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'views'] as const,
view: (projectRef: string | undefined, id: number | undefined) =>
['projects', projectRef, 'views', id] as const,
}
+57
View File
@@ -0,0 +1,57 @@
import { PostgresView } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { viewKeys } from './keys'
export type ViewVariables = {
id?: number
projectRef?: string
connectionString?: string
}
export type View = PostgresView
export type ViewResponse = View | { error?: any }
export async function getView(
{ id, projectRef, connectionString }: ViewVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
if (!id) {
throw new Error('id is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/views?id=${id}`, {
headers: Object.fromEntries(headers),
signal,
})) as ViewResponse
if ('error' in response) {
throw response.error
}
return response as PostgresView
}
export type ViewData = Awaited<ReturnType<typeof getView>>
export type ViewError = unknown
export const useViewQuery = <TData = ViewData>(
{ projectRef, connectionString, id }: ViewVariables,
{ enabled = true, ...options }: UseQueryOptions<ViewData, ViewError, TData> = {}
) =>
useQuery<ViewData, ViewError, TData>(
viewKeys.view(projectRef, id),
({ signal }) => getView({ projectRef, connectionString, id }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined',
...options,
}
)
+49
View File
@@ -0,0 +1,49 @@
import { PostgresView } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { viewKeys } from './keys'
import { View } from './view-query'
export type ViewsVariables = {
projectRef?: string
connectionString?: string
}
export type ViewsResponse = View[] | { error?: any }
export async function getViews(
{ projectRef, connectionString }: ViewsVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/views`, {
headers: Object.fromEntries(headers),
signal,
})) as ViewsResponse
if (!Array.isArray(response) && response.error) {
throw response.error
}
return response as PostgresView[]
}
export type ViewsData = Awaited<ReturnType<typeof getViews>>
export type ViewsError = unknown
export const useViewsQuery = <TData = ViewsData>(
{ projectRef, connectionString }: ViewsVariables,
{ enabled = true, ...options }: UseQueryOptions<ViewsData, ViewsError, TData> = {}
) =>
useQuery<ViewsData, ViewsError, TData>(
viewKeys.list(projectRef),
({ signal }) => getViews({ projectRef, connectionString }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
+81
View File
@@ -0,0 +1,81 @@
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { ForeignTable, useForeignTableQuery } from 'data/foreign-tables/foreign-table-query'
import {
MaterializedView,
useMaterializedViewQuery,
} from 'data/materialized-views/materialized-view-query'
import { Table, useTableQuery } from 'data/tables/table-query'
import { View, useViewQuery } from 'data/views/view-query'
import useEntityType from './useEntityType'
export type TableLike = Table | View | MaterializedView | ForeignTable
/**
* A hook that loads all table-like objects. e.g. tables, views, materialized views, etc...
*/
function useTable(id?: number) {
const { project } = useProjectContext()
const entity = useEntityType(id)
const tableResult = useTableQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
id: entity?.id,
},
{
enabled: entity?.type === ENTITY_TYPE.TABLE,
}
)
const viewResult = useViewQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
id: entity?.id,
},
{
enabled: entity?.type === ENTITY_TYPE.VIEW,
}
)
const materializedViewResult = useMaterializedViewQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
id: entity?.id,
},
{
enabled: entity?.type === ENTITY_TYPE.MATERIALIZED_VIEW,
}
)
const foreignTableResult = useForeignTableQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
id: entity?.id,
},
{
enabled: entity?.type === ENTITY_TYPE.FOREIGN_TABLE,
}
)
switch (entity?.type) {
case ENTITY_TYPE.TABLE:
return tableResult
case ENTITY_TYPE.VIEW:
return viewResult
case ENTITY_TYPE.MATERIALIZED_VIEW:
return materializedViewResult
case ENTITY_TYPE.FOREIGN_TABLE:
return foreignTableResult
default:
// While entity is loading, we can fallback
// to the loading state of the table query.
return tableResult
}
}
export default useTable
+20
View File
@@ -0,0 +1,20 @@
/**
* A list of system schemas that users should not interact with
*/
export const EXCLUDED_SCHEMAS = [
'auth',
'extensions',
'information_schema',
'net',
'pgsodium',
'pgsodium_masks',
'pgbouncer',
'pgtle',
'realtime',
'storage',
'supabase_functions',
'supabase_migrations',
'vault',
'graphql',
'graphql_public',
]
@@ -34,7 +34,6 @@ const GraphiQLPage: NextPageWithLayout = () => {
useEffect(() => {
if (ui.selectedProjectRef) {
// Schemas may be needed when enabling the GraphQL extension
meta.schemas.load()
meta.extensions.load()
}
}, [ui.selectedProjectRef])
+96 -56
View File
@@ -1,16 +1,23 @@
import { useState, useEffect } from 'react'
import { partition } from 'lodash'
import { Button, Listbox, IconSearch, Input, IconExternalLink, IconLock } from 'ui'
import { observer } from 'mobx-react-lite'
import { PostgresTable, PostgresPolicy } from '@supabase/postgres-meta'
import { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useEffect, useState } from 'react'
import { NextPageWithLayout } from 'types'
import { useCheckPermissions, useStore } from 'hooks'
import { useParams } from 'common/hooks'
import { AuthLayout } from 'components/layouts'
import { Policies } from 'components/interfaces/Auth/Policies'
import { AuthLayout } from 'components/layouts'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
import NoPermission from 'components/ui/NoPermission'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions, useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { NextPageWithLayout } from 'types'
import { Button, IconExternalLink, IconLock, IconSearch, Input, Listbox } from 'ui'
/**
* Filter tables by table name and policy name
@@ -48,27 +55,47 @@ const onFilterTables = (
}
const AuthPoliciesPage: NextPageWithLayout = () => {
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const { meta } = useStore()
const { search } = useParams()
const [selectedSchema, setSelectedSchema] = useState<string>('public')
const [searchString, setSearchString] = useState<string>('')
useEffect(() => {
if (search) setSearchString(search)
}, [search])
const schemas = meta.schemas.list()
const {
data: schemas,
isLoading: isLoadingSchemas,
isSuccess: isSuccessSchemas,
isError: isErrorSchemas,
error: errorSchemas,
} = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
// @ts-ignore
const schema = schemas.find((schema) => schema.name === selectedSchema)
const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
const policies = meta.policies.list()
const tables = meta.tables.list((table: { schema: string }) => table.schema === selectedSchema)
const filteredTables = onFilterTables(tables, policies, searchString)
const {
data: tables,
isLoading,
isSuccess,
isError,
error,
} = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: snap.selectedSchemaName,
})
const filteredTables = onFilterTables(tables ?? [], policies, searchString)
const canReadPolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'policies')
@@ -82,53 +109,62 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="w-[230px]">
<Listbox
size="small"
value={selectedSchema}
onChange={(schema: string) => {
setSelectedSchema(schema)
setSearchString('')
}}
icon={isLocked && <IconLock size={14} strokeWidth={2} />}
>
<Listbox.Option
disabled
key="normal-schemas"
value="normal-schemas"
label="Schemas"
{isLoadingSchemas && (
<div className="h-[34px] w-full bg-scale-1000 rounded shimmering-loader" />
)}
{isErrorSchemas && (
<AlertError error={errorSchemas} subject="Failed to retrieve schemas" />
)}
{isSuccessSchemas && (
<Listbox
size="small"
value={snap.selectedSchemaName}
onChange={(schema: string) => {
snap.setSelectedSchemaName(schema)
setSearchString('')
}}
icon={isLocked && <IconLock size={14} strokeWidth={2} />}
>
<p className="text-sm">Schemas</p>
</Listbox.Option>
{/* @ts-ignore */}
{openSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
disabled
key="normal-schemas"
value="normal-schemas"
label="Schemas"
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
<p className="text-sm">Schemas</p>
</Listbox.Option>
))}
<Listbox.Option
disabled
key="protected-schemas"
value="protected-schemas"
label="Protected schemas"
>
<p className="text-sm">Protected schemas</p>
</Listbox.Option>
{protectedSchemas.map((schema) => (
{openSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
disabled
key="protected-schemas"
value="protected-schemas"
label="Protected schemas"
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
<p className="text-sm">Protected schemas</p>
</Listbox.Option>
))}
</Listbox>
{protectedSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
</Listbox>
)}
</div>
<Input
size="small"
@@ -150,7 +186,11 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
</a>
</div>
</div>
<Policies tables={filteredTables} hasTables={tables.length > 0} isLocked={isLocked} />
{isLoading && <GenericSkeletonLoader />}
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
{isSuccess && (
<Policies tables={filteredTables} hasTables={tables.length > 0} isLocked={isLocked} />
)}
</div>
)
}
@@ -5,15 +5,13 @@ import { useEffect, useState } from 'react'
import { CreateFunction, DeleteFunction } from 'components/interfaces/Database'
import FunctionsList from 'components/interfaces/Database/Functions/FunctionsList/FunctionsList'
import { DatabaseLayout } from 'components/layouts'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import NoPermission from 'components/ui/NoPermission'
import { useCheckPermissions, useStore } from 'hooks'
import { NextPageWithLayout } from 'types'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
const FunctionsPage: NextPageWithLayout = () => {
const { meta } = useStore()
const { project } = useProjectContext()
const { ui, meta } = useStore()
const [selectedFunction, setSelectedFunction] = useState<any>()
const [showCreateFunctionForm, setShowCreateFunctionForm] = useState<boolean>(false)
const [showDeleteFunctionForm, setShowDeleteFunctionForm] = useState<boolean>(false)
@@ -21,10 +19,10 @@ const FunctionsPage: NextPageWithLayout = () => {
const canReadFunctions = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'functions')
useEffect(() => {
if (project?.ref) {
if (ui.selectedProjectRef) {
fetchFunctions()
}
}, [project?.ref])
}, [ui.selectedProjectRef])
const fetchFunctions = async () => {
meta.functions.load()
+20 -11
View File
@@ -1,39 +1,48 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import clsx from 'clsx'
import { useParams } from 'common'
import { observer } from 'mobx-react-lite'
import { useEffect, useState } from 'react'
import { IconLoader } from 'ui'
import { useParams } from 'common'
import DeleteHookModal from 'components/interfaces/Database/Hooks/DeleteHookModal'
import EditHookPanel from 'components/interfaces/Database/Hooks/EditHookPanel'
import HooksList from 'components/interfaces/Database/Hooks/HooksList/HooksList'
import { DatabaseLayout } from 'components/layouts'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import NoPermission from 'components/ui/NoPermission'
import { useHooksEnableMutation } from 'data/database/hooks-enable-mutation'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions, useStore } from 'hooks'
import { NextPageWithLayout } from 'types'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import { IconLoader } from 'ui'
const HooksPage: NextPageWithLayout = () => {
const { project } = useProjectContext()
const { meta, ui } = useStore()
const { ref } = useParams()
const schemas = meta.schemas.list()
const { isLoading: isLoadingSchemas } = meta.schemas
const { ref: projectRef } = useParams()
const {
data: schemas,
isLoading: isLoadingSchemas,
refetch,
} = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [selectedHook, setSelectedHook] = useState<any>()
const [showCreateHookForm, setShowCreateHookForm] = useState<boolean>(false)
const [showDeleteHookForm, setShowDeleteHookForm] = useState<boolean>(false)
const isHooksEnabled = schemas.some((schema: any) => schema.name === 'supabase_functions')
const isHooksEnabled = schemas?.some((schema) => schema.name === 'supabase_functions')
const canReadWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers')
const canCreateWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers')
const { mutate: enableHooks, isLoading: isEnablingHooks } = useHooksEnableMutation({
onSuccess: () => {
meta.schemas.load()
refetch()
ui.setNotification({
category: 'success',
message: `Successfully enabled webhooks`,
@@ -46,8 +55,8 @@ const HooksPage: NextPageWithLayout = () => {
}, [ui.selectedProjectRef])
const enableHooksForProject = async () => {
if (!ref) return console.error('Project ref is required')
enableHooks({ ref })
if (!projectRef) return console.error('Project ref is required')
enableHooks({ ref: projectRef })
}
const createHook = () => {
+63 -44
View File
@@ -1,66 +1,85 @@
import { observer } from 'mobx-react-lite'
import { partition } from 'lodash'
import { useState } from 'react'
import SchemaGraph from 'components/interfaces/Database/Schemas/SchemaGraph'
import { DatabaseLayout } from 'components/layouts'
import { useStore } from 'hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
import { useSchemasQuery } from 'data/database/schemas-query'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { NextPageWithLayout } from 'types'
import { IconLock, Listbox } from 'ui'
const SchemasPage: NextPageWithLayout = () => {
const { meta } = useStore()
const schemas = meta.schemas.list()
const { project } = useProjectContext()
const {
data: schemas,
isSuccess,
isLoading,
isError,
error,
} = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [selectedSchema, setSelectedSchema] = useState<string>('public')
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
const schema = schemas.find((schema) => schema.name === selectedSchema)
const schema = schemas?.find((schema) => schema.name === selectedSchema)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
return (
<>
<div className="flex w-full h-full flex-col">
<div className="p-4 border-b border-scale-500">
<Listbox
className="w-[260px]"
size="small"
value={selectedSchema}
onChange={setSelectedSchema}
icon={isLocked && <IconLock size={14} strokeWidth={2} />}
>
<Listbox.Option disabled key="normal-schemas" value="normal-schemas" label="Schemas">
<p className="text-sm">Schemas</p>
</Listbox.Option>
{openSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
<Listbox.Option
disabled
key="protected-schemas"
value="protected-schemas"
label="Protected schemas"
{isLoading && (
<div className="h-[34px] w-[260px] bg-scale-1000 rounded shimmering-loader" />
)}
{isError && <AlertError error={error} subject="Failed to retrieve schemas" />}
{isSuccess && (
<Listbox
className="w-[260px]"
size="small"
value={selectedSchema}
onChange={setSelectedSchema}
icon={isLocked && <IconLock size={14} strokeWidth={2} />}
>
<p className="text-sm">Protected schemas</p>
</Listbox.Option>
{protectedSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
<Listbox.Option disabled key="normal-schemas" value="normal-schemas" label="Schemas">
<p className="text-sm">Schemas</p>
</Listbox.Option>
))}
</Listbox>
{openSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
<Listbox.Option
disabled
key="protected-schemas"
value="protected-schemas"
label="Protected schemas"
>
<p className="text-sm">Protected schemas</p>
</Listbox.Option>
{protectedSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
</Listbox>
)}
</div>
<SchemaGraph schema={selectedSchema}></SchemaGraph>
@@ -71,4 +90,4 @@ const SchemasPage: NextPageWithLayout = () => {
SchemasPage.getLayout = (page) => <DatabaseLayout title="Database">{page}</DatabaseLayout>
export default observer(SchemasPage)
export default SchemasPage
+26 -177
View File
@@ -1,166 +1,56 @@
import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
import { useQueryClient } from '@tanstack/react-query'
import { isUndefined } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useEffect, useState } from 'react'
import { Modal } from 'ui'
import { useParams } from 'common'
import { ColumnList, TableList } from 'components/interfaces/Database'
import { SidePanelEditor } from 'components/interfaces/TableGridEditor'
import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs'
import { DatabaseLayout } from 'components/layouts'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { sqlKeys } from 'data/sql/keys'
import { Table } from 'data/tables/table-query'
import { useStore } from 'hooks'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { NextPageWithLayout } from 'types'
const DatabaseTables: NextPageWithLayout = () => {
const { meta, ui } = useStore()
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const { ui, meta } = useStore()
const queryClient = useQueryClient()
const [isDeleting, setIsDeleting] = useState<boolean>(false)
const [selectedSchema, setSelectedSchema] = useState('public')
const [selectedTable, setSelectedTable] = useState<any>()
const [sidePanelKey, setSidePanelKey] = useState<'column' | 'table'>()
const [selectedColumnToEdit, setSelectedColumnToEdit] = useState<PostgresColumn>()
const [selectedTableToEdit, setSelectedTableToEdit] = useState<PostgresTable>()
const [selectedColumnToDelete, setSelectedColumnToDelete] = useState<PostgresColumn>()
const [selectedTableToDelete, setSelectedTableToDelete] = useState<PostgresTable>()
const { ref: projectRef } = useParams()
useEffect(() => {
if (project?.ref) {
if (ui.selectedProjectRef) {
meta.types.load()
}
}, [project?.ref])
}, [ui.selectedProjectRef])
const onAddTable = () => {
setSidePanelKey('table')
setSelectedTableToEdit(undefined)
}
const onEditTable = (table: PostgresTable) => {
setSidePanelKey('table')
setSelectedTableToEdit(table)
}
const onDeleteTable = (table: PostgresTable) => {
setIsDeleting(true)
setSelectedTableToDelete(table)
}
const onAddColumn = () => {
setSidePanelKey('column')
setSelectedColumnToEdit(undefined)
}
const onEditColumn = (column: PostgresColumn) => {
setSidePanelKey('column')
setSelectedColumnToEdit(column)
}
const onDeleteColumn = (column: PostgresColumn) => {
setIsDeleting(true)
setSelectedColumnToDelete(column)
}
const onColumnUpdated = async () => {
if (selectedTable === undefined) return
const updatedTable = await meta.tables.loadById(selectedTable.id)
setSelectedTable(updatedTable)
}
const onClosePanel = () => setSidePanelKey(undefined)
const onConfirmDeleteTable = async () => {
try {
if (isUndefined(selectedTableToDelete)) return
const response: any = await meta.tables.del(selectedTableToDelete.id)
if (response.error) {
throw response.error
} else {
ui.setNotification({
category: 'success',
message: `Successfully removed ${selectedTableToDelete.name}.`,
})
}
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${selectedTableToDelete?.name}: ${error.message}`,
})
} finally {
setIsDeleting(false)
setSelectedTableToDelete(undefined)
}
}
const onConfirmDeleteColumn = async () => {
try {
if (isUndefined(selectedColumnToDelete)) return
const response: any = await meta.columns.del(selectedColumnToDelete.id)
if (response.error) {
throw response.error
} else {
onColumnUpdated()
ui.setNotification({
category: 'success',
message: `Successfully removed ${selectedColumnToDelete.name}.`,
})
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints']))
await Promise.all([
meta.tables.loadById(selectedColumnToDelete!.table_id),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name])
),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [
'table-definition',
selectedTable!.schema,
selectedTable!.name,
])
),
])
}
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${selectedColumnToDelete?.name}: ${error.message}`,
})
} finally {
setIsDeleting(false)
setSelectedColumnToDelete(undefined)
}
}
const [selectedTable, setSelectedTable] = useState<Table | undefined>(undefined)
const [selectedTableToEdit, setSelectedTableToEdit] = useState<Table | undefined>(undefined)
return (
<>
<ScaffoldContainer>
<ScaffoldSection>
<div className="col-span-12">
{isUndefined(selectedTable) ? (
{selectedTable === undefined ? (
<TableList
selectedSchema={selectedSchema}
onSelectSchema={setSelectedSchema}
onAddTable={onAddTable}
onEditTable={onEditTable}
onDeleteTable={onDeleteTable}
onAddTable={snap.onAddTable}
onEditTable={(table) => {
setSelectedTableToEdit(table)
snap.onEditTable()
}}
onDeleteTable={(table) => {
setSelectedTableToEdit(table)
snap.onDeleteTable()
}}
onOpenTable={setSelectedTable}
/>
) : (
<ColumnList
selectedTable={selectedTable}
onAddColumn={onAddColumn}
onEditColumn={onEditColumn}
onDeleteColumn={onDeleteColumn}
onAddColumn={snap.onAddColumn}
onEditColumn={snap.onEditColumn}
onDeleteColumn={snap.onDeleteColumn}
onSelectBack={() => setSelectedTable(undefined)}
/>
)}
@@ -168,49 +58,8 @@ const DatabaseTables: NextPageWithLayout = () => {
</ScaffoldSection>
</ScaffoldContainer>
<ConfirmationModal
danger
visible={isDeleting && !isUndefined(selectedTableToDelete)}
header={
<span className="break-words">{`Confirm deletion of table "${selectedTableToDelete?.name}"`}</span>
}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => setIsDeleting(false)}
onSelectConfirm={onConfirmDeleteTable}
>
<Modal.Content>
<p className="py-4 text-sm text-scale-1100">
Are you sure you want to delete the selected table? This action cannot be undone.
</p>
</Modal.Content>
</ConfirmationModal>
<ConfirmationModal
danger
visible={isDeleting && !isUndefined(selectedColumnToDelete)}
header={`Confirm deletion of column "${selectedColumnToDelete?.name}"`}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => setIsDeleting(false)}
onSelectConfirm={onConfirmDeleteColumn}
>
<Modal.Content>
<p className="py-4 text-sm text-scale-1100">
Are you sure you want to delete the selected column? This action cannot be undone.
</p>
</Modal.Content>
</ConfirmationModal>
<SidePanelEditor
sidePanelKey={sidePanelKey}
selectedSchema={selectedSchema}
selectedTable={selectedTable}
onColumnSaved={onColumnUpdated}
closePanel={onClosePanel}
selectedColumnToEdit={selectedColumnToEdit}
selectedTableToEdit={selectedTableToEdit}
/>
<DeleteConfirmationDialogs projectRef={projectRef} selectedTable={selectedTableToEdit} />
<SidePanelEditor selectedTable={selectedTableToEdit} />
</>
)
}
+28 -382
View File
@@ -1,402 +1,48 @@
import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
import { useQueryClient } from '@tanstack/react-query'
import { useParams, useTheme } from 'common'
import { isNaN, isUndefined } from 'lodash'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { Alert, Button, Checkbox, IconExternalLink, Modal } from 'ui'
import { Dictionary } from 'components/grid'
import { useTheme } from 'common'
import { useParams } from 'common/hooks'
import { TableGridEditor } from 'components/interfaces'
import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
import { JsonEditValue } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types'
import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs'
import { TableEditorLayout } from 'components/layouts'
import {
ProjectContextFromParamsProvider,
useProjectContext,
} from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { Entity } from 'data/entity-types/entity-type-query'
import { entityTypeKeys } from 'data/entity-types/keys'
import { sqlKeys } from 'data/sql/keys'
import { useStore, useUrlState, withAuth } from 'hooks'
import { NextPageWithLayout, SchemaView } from 'types'
import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext'
import useTable from 'hooks/misc/useTable'
import { useRouter } from 'next/router'
import { NextPageWithLayout } from 'types'
const TableEditorPage: NextPageWithLayout = () => {
const router = useRouter()
const { isDarkMode } = useTheme()
const { id, ref: projectRef } = useParams()
const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] })
const { id: _id, ref: projectRef } = useParams()
const id = _id ? Number(_id) : undefined
const { project } = useProjectContext()
const queryClient = useQueryClient()
const { meta, ui } = useStore()
const [selectedSchema, setSelectedSchema] = useState<string>()
const [isDeleting, setIsDeleting] = useState<boolean>(false)
const [isDuplicating, setIsDuplicating] = useState<boolean>(false)
const [isDeleteWithCascade, setIsDeleteWithCascade] = useState(false)
const [selectedColumnToDelete, setSelectedColumnToDelete] = useState<PostgresColumn>()
const [selectedTableToDelete, setSelectedTableToDelete] = useState<PostgresTable>()
const [sidePanelKey, setSidePanelKey] = useState<
'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import'
>()
const [selectedRowToEdit, setSelectedRowToEdit] = useState<Dictionary<any>>()
const [selectedColumnToEdit, setSelectedColumnToEdit] = useState<PostgresColumn>()
const [selectedTableToEdit, setSelectedTableToEdit] = useState<PostgresTable>()
const [selectedValueForJsonEdit, setSelectedValueForJsonEdit] = useState<JsonEditValue>()
const [selectedForeignKeyToEdit, setSelectedForeignKeyToEdit] = useState<{
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}>()
const tables: PostgresTable[] = meta.tables.list()
const views: SchemaView[] = meta.views.list()
const materializedViews = meta.materializedViews.list()
const foreignTables: Partial<PostgresTable>[] = meta.foreignTables.list()
const selectedTable = !isNaN(Number(id))
? // @ts-ignore
tables
// @ts-ignore
.concat(views)
// @ts-ignore
.concat(materializedViews)
// @ts-ignore
.concat(foreignTables)
.find((table) => table.id === Number(id))
: undefined
useEffect(() => {
if (selectedTable && 'schema' in selectedTable) {
setSelectedSchema(selectedTable.schema)
}
}, [selectedTable?.name])
const onAddRow = () => {
setSidePanelKey('row')
setSelectedRowToEdit(undefined)
}
const onEditRow = (row: Dictionary<any>) => {
setSidePanelKey('row')
setSelectedRowToEdit(row)
}
const onAddColumn = () => {
setSidePanelKey('column')
setSelectedColumnToEdit(undefined)
}
const onEditColumn = (column: PostgresColumn) => {
setSidePanelKey('column')
setSelectedColumnToEdit(column)
}
const onDeleteColumn = (column: PostgresColumn) => {
setIsDeleting(true)
setSelectedColumnToDelete(column)
setIsDeleteWithCascade(false)
}
const onAddTable = () => {
setSidePanelKey('table')
setIsDuplicating(false)
setSelectedTableToEdit(undefined)
}
const onEditTable = (entity: Entity) => {
setSidePanelKey('table')
setIsDuplicating(false)
const table = meta.tables.byId(entity.id)
setSelectedTableToEdit(table)
}
const onDeleteTable = (entity: Entity) => {
setIsDeleting(true)
const table = meta.tables.byId(entity.id)
setSelectedTableToDelete(table)
setIsDeleteWithCascade(false)
}
const onDuplicateTable = (entity: Entity) => {
setSidePanelKey('table')
setIsDuplicating(true)
const table = meta.tables.byId(entity.id)
setSelectedTableToEdit(table)
}
const onExpandJSONEditor = (column: string, row: any) => {
setSidePanelKey('json')
setSelectedValueForJsonEdit({ column, row, jsonString: JSON.stringify(row[column]) || '' })
}
const onImportData = () => {
if (id) {
setSidePanelKey('csv-import')
const table = meta.tables.byId(id)
setSelectedTableToEdit(table)
} else {
console.error('Table ID not found')
}
}
const onEditForeignKeyColumnValue = ({
foreignKey,
row,
column,
}: {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}) => {
setSidePanelKey('foreign-row-selector')
setSelectedForeignKeyToEdit({
foreignKey,
row,
column,
})
}
const onClosePanel = () => {
setSidePanelKey(undefined)
}
const removeDeletedColumnFromFiltersAndSorts = (columnName: string) => {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const existingSorts = (prevParams?.sort ?? []) as string[]
return {
...prevParams,
filter: existingFilters.filter((filter: string) => {
const [column] = filter.split(':')
if (column !== columnName) return filter
}),
sort: existingSorts.filter((sort: string) => {
const [column] = sort.split(':')
if (column !== columnName) return sort
}),
}
})
}
const onConfirmDeleteColumn = async () => {
try {
if (selectedColumnToDelete === undefined) return
const response: any = await meta.columns.del(selectedColumnToDelete.id, isDeleteWithCascade)
if (response.error) throw response.error
removeDeletedColumnFromFiltersAndSorts(selectedColumnToDelete.name)
ui.setNotification({
category: 'success',
message: `Successfully deleted column "${selectedColumnToDelete.name}"`,
})
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints']))
await Promise.all([
meta.tables.loadById(selectedColumnToDelete!.table_id),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name])
),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [
'table-definition',
selectedTable!.schema,
selectedTable!.name,
])
),
])
if (selectedSchema) await meta.views.loadBySchema(selectedSchema)
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${selectedColumnToDelete!.name}: ${error.message}`,
})
} finally {
setIsDeleting(false)
setSelectedColumnToDelete(undefined)
}
}
const onConfirmDeleteTable = async () => {
try {
if (selectedTableToDelete === undefined) return
const response: any = await meta.tables.del(selectedTableToDelete.id, isDeleteWithCascade)
if (response.error) throw response.error
const tables = meta.tables.list((table: PostgresTable) => table.schema === selectedSchema)
await queryClient.invalidateQueries(entityTypeKeys.list(projectRef))
// For simplicity for now, we just open the first table within the same schema
if (tables.length > 0) {
router.push(`/project/${projectRef}/editor/${tables[0].id}`)
} else {
router.push(`/project/${projectRef}/editor/`)
}
ui.setNotification({
category: 'success',
message: `Successfully deleted table "${selectedTableToDelete.name}"`,
})
if (selectedSchema) await meta.views.loadBySchema(selectedSchema)
} catch (error: any) {
ui.setNotification({
error,
category: 'error',
message: `Failed to delete ${selectedTableToDelete?.name}: ${error.message}`,
})
} finally {
setIsDeleting(false)
setSelectedTableToDelete(undefined)
}
}
const { data: selectedTable, isLoading } = useTable(id)
return (
<TableEditorLayout
selectedSchema={selectedSchema}
selectedTable={selectedTable?.name}
onSelectSchema={setSelectedSchema}
onAddTable={onAddTable}
onEditTable={onEditTable}
onDeleteTable={onDeleteTable}
onDuplicateTable={onDuplicateTable}
>
<>
<TableGridEditor
selectedSchema={selectedSchema}
isLoadingSelectedTable={isLoading}
selectedTable={selectedTable}
sidePanelKey={sidePanelKey}
isDuplicating={isDuplicating}
selectedRowToEdit={selectedRowToEdit}
selectedColumnToEdit={selectedColumnToEdit}
selectedTableToEdit={selectedTableToEdit}
selectedValueForJsonEdit={selectedValueForJsonEdit}
selectedForeignKeyToEdit={selectedForeignKeyToEdit}
onAddRow={onAddRow}
onEditRow={onEditRow}
onAddColumn={onAddColumn}
onEditColumn={onEditColumn}
onDeleteColumn={onDeleteColumn}
onExpandJSONEditor={onExpandJSONEditor}
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
onClosePanel={onClosePanel}
onImportData={onImportData}
theme={isDarkMode ? 'dark' : 'light'}
/>
<ConfirmationModal
danger
size="small"
visible={isDeleting && !isUndefined(selectedColumnToDelete)}
header={`Confirm deletion of column "${selectedColumnToDelete?.name}"`}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => {
setIsDeleting(false)
setSelectedColumnToDelete(undefined)
<DeleteConfirmationDialogs
projectRef={projectRef}
selectedTable={selectedTable}
onAfterDeleteTable={(tables) => {
// For simplicity for now, we just open the first table within the same schema
if (tables.length > 0) {
router.push(`/project/${projectRef}/editor/${tables[0].id}`)
} else {
router.push(`/project/${projectRef}/editor/`)
}
}}
onSelectConfirm={onConfirmDeleteColumn}
>
<Modal.Content>
<div className="py-4 space-y-4">
<p className="text-sm text-scale-1100">
Are you sure you want to delete the selected column? This action cannot be undone.
</p>
<Checkbox
label="Drop column with cascade?"
description="Deletes the column and its dependent objects"
checked={isDeleteWithCascade}
onChange={() => setIsDeleteWithCascade(!isDeleteWithCascade)}
/>
{isDeleteWithCascade && (
<Alert
withIcon
variant="warning"
title="Warning: Dropping with cascade may result in unintended consequences"
>
<p className="mb-4">
All dependent objects will be removed, as will any objects that depend on them,
recursively.
</p>
<Link href="https://www.postgresql.org/docs/current/ddl-depend.html">
<a target="_blank" rel="noreferrer">
<Button size="tiny" type="default" icon={<IconExternalLink />}>
About dependency tracking
</Button>
</a>
</Link>
</Alert>
)}
</div>
</Modal.Content>
</ConfirmationModal>
<ConfirmationModal
danger
size="small"
visible={isDeleting && !isUndefined(selectedTableToDelete)}
header={
<span className="break-words">{`Confirm deletion of table "${selectedTableToDelete?.name}"`}</span>
}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => {
setIsDeleting(false)
setSelectedTableToDelete(undefined)
}}
onSelectConfirm={onConfirmDeleteTable}
>
<Modal.Content>
<div className="py-4 space-y-4">
<p className="text-sm text-scale-1100">
Are you sure you want to delete the selected table? This action cannot be undone.
</p>
<Checkbox
label="Drop table with cascade?"
description="Deletes the table and its dependent objects"
checked={isDeleteWithCascade}
onChange={() => setIsDeleteWithCascade(!isDeleteWithCascade)}
/>
{isDeleteWithCascade && (
<Alert
withIcon
variant="warning"
title="Warning: Dropping with cascade may result in unintended consequences"
>
<p className="mb-4">
All dependent objects will be removed, as will any objects that depend on them,
recursively.
</p>
<Link href="https://www.postgresql.org/docs/current/ddl-depend.html">
<a target="_blank" rel="noreferrer">
<Button size="tiny" type="default" icon={<IconExternalLink />}>
About dependency tracking
</Button>
</a>
</Link>
</Alert>
)}
</div>
</Modal.Content>
</ConfirmationModal>
</TableEditorLayout>
/>
</>
)
}
TableEditorPage.getLayout = (page) => (
<ProjectContextFromParamsProvider>{page}</ProjectContextFromParamsProvider>
<ProjectContextFromParamsProvider>
<TableEditorLayout>{page}</TableEditorLayout>
</ProjectContextFromParamsProvider>
)
export default withAuth(observer(TableEditorPage))
export default TableEditorPage
+16 -103
View File
@@ -1,119 +1,32 @@
import type { PostgresTable } from '@supabase/postgres-meta'
import { isUndefined } from 'lodash'
import { observer } from 'mobx-react-lite'
import router from 'next/router'
import { useState } from 'react'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { EmptyState, SidePanelEditor } from 'components/interfaces/TableGridEditor'
import { TableEditorLayout } from 'components/layouts'
import {
ProjectContextFromParamsProvider,
useProjectContext,
} from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { Entity } from 'data/entity-types/entity-type-query'
import { useStore, withAuth } from 'hooks'
import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext'
import { Table } from 'data/tables/table-query'
import { NextPageWithLayout } from 'types'
const TableEditorPage: NextPageWithLayout = () => {
const { meta, ui } = useStore()
const { project } = useProjectContext()
const projectRef = project?.ref
const { ref: projectRef } = useParams()
const router = useRouter()
const [sidePanelKey, setSidePanelKey] = useState<'row' | 'column' | 'table'>()
const [isDeleting, setIsDeleting] = useState<boolean>(false)
const [isDuplicating, setIsDuplicating] = useState<boolean>(false)
const [selectedSchema, setSelectedSchema] = useState<string>('public')
const [selectedTableToEdit, setSelectedTableToEdit] = useState<PostgresTable>()
const [selectedTableToDelete, setSelectedTableToDelete] = useState<PostgresTable>()
const onAddTable = () => {
setSidePanelKey('table')
setIsDuplicating(false)
setSelectedTableToEdit(undefined)
}
const onEditTable = (entity: Entity) => {
setSidePanelKey('table')
setIsDuplicating(false)
const table = meta.tables.byId(entity.id)
setSelectedTableToEdit(table)
}
const onDeleteTable = (entity: Entity) => {
setIsDeleting(true)
const table = meta.tables.byId(entity.id)
setSelectedTableToDelete(table)
}
const onDuplicateTable = (entity: Entity) => {
setSidePanelKey('table')
setIsDuplicating(true)
const table = meta.tables.byId(entity.id)
setSelectedTableToEdit(table)
}
const onClosePanel = () => {
setSidePanelKey(undefined)
}
const onConfirmDeleteTable = async () => {
try {
await meta.tables.del(selectedTableToDelete!.id)
setIsDeleting(false)
ui.setNotification({
category: 'success',
message: `Successfully deleted ${selectedTableToDelete!.name}`,
})
await meta.views.loadBySchema(selectedSchema)
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${selectedTableToDelete!.name}`,
})
}
const onTableCreated = (table: Table) => {
router.push(`/project/${projectRef}/editor/${table.id}`)
}
return (
<TableEditorLayout
selectedSchema={selectedSchema}
onSelectSchema={setSelectedSchema}
onAddTable={onAddTable}
onEditTable={onEditTable}
onDeleteTable={onDeleteTable}
onDuplicateTable={onDuplicateTable}
>
<EmptyState selectedSchema={selectedSchema} onAddTable={onAddTable} />
{/* On this page it'll only handle tables */}
<ConfirmationModal
danger
visible={isDeleting && !isUndefined(selectedTableToDelete)}
header={
<span className="break-words">{`Confirm deletion of table "${selectedTableToDelete?.name}"`}</span>
}
description={`Are you sure you want to delete the selected table? This action cannot be undone`}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => setIsDeleting(false)}
onSelectConfirm={onConfirmDeleteTable}
/>
<SidePanelEditor
selectedSchema={selectedSchema}
isDuplicating={isDuplicating}
selectedTableToEdit={selectedTableToEdit}
sidePanelKey={sidePanelKey}
closePanel={onClosePanel}
onTableCreated={(table: any) => router.push(`/project/${projectRef}/editor/${table.id}`)}
/>
</TableEditorLayout>
<>
<EmptyState />
<SidePanelEditor onTableCreated={onTableCreated} />
</>
)
}
TableEditorPage.getLayout = (page) => (
<ProjectContextFromParamsProvider>{page}</ProjectContextFromParamsProvider>
<ProjectContextFromParamsProvider>
<TableEditorLayout>{page}</TableEditorLayout>
</ProjectContextFromParamsProvider>
)
export default withAuth(observer(TableEditorPage))
export default TableEditorPage
+1 -7
View File
@@ -1,5 +1,5 @@
import { observer, useLocalObservable } from 'mobx-react-lite'
import { createContext, useEffect } from 'react'
import { createContext } from 'react'
import { useParams } from 'common/hooks'
import ServiceList from 'components/interfaces/Settings/API/ServiceList'
@@ -31,12 +31,6 @@ const ApiSettings: NextPageWithLayout = () => {
PageContext.project = project
if (meta) PageState.meta = meta
useEffect(() => {
if (ui.selectedProjectRef) {
meta.schemas.load()
}
}, [ui.selectedProjectRef])
return (
<PageContext.Provider value={PageState}>
<div className="flex flex-col gap-8 px-5 py-6 mx-auto 1xl:px-28 lg:px-16 xl:px-24 2xl:px-32">
+1 -1
View File
@@ -67,7 +67,7 @@ const SqlEditor: NextPageWithLayout = () => {
pgInfoRef.current = {}
}
pgInfoRef.current.tableColumns = tableColumns?.result
pgInfoRef.current.schemas = schemas?.result
pgInfoRef.current.schemas = schemas
pgInfoRef.current.keywords = keywords?.result
pgInfoRef.current.functions = functions?.result
}
+28
View File
@@ -0,0 +1,28 @@
import { proxy, snapshot, useSnapshot } from 'valtio'
import { proxySet } from 'valtio/utils'
export const tableEditorGlobalState = proxy({
projectRef: undefined as undefined | string,
loadedIds: proxySet<number>([]),
addLoadedId: (ref: string, id: number) => {
if (ref !== tableEditorGlobalState.projectRef) {
tableEditorGlobalState.loadedIds.clear()
tableEditorGlobalState.projectRef = ref
}
tableEditorGlobalState.loadedIds.add(id)
},
})
export const getTableEditorGlobalStateSnapshot = () => snapshot(tableEditorGlobalState)
export const useTableEditorGlobalStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
useSnapshot(tableEditorGlobalState, options)
export const useIsTableLoaded = (ref?: string, id?: number) => {
const snap = useTableEditorGlobalStateSnapshot()
if (!ref || !id) return false
if (ref !== snap.projectRef) return false
return snap.loadedIds.has(id)
}
-28
View File
@@ -1,28 +0,0 @@
import { proxy, snapshot, useSnapshot } from 'valtio'
import { proxySet } from 'valtio/utils'
export const tableEditorState = proxy({
projectRef: undefined as undefined | string,
loadedIds: proxySet<number>([]),
addLoadedId: (ref: string, id: number) => {
if (ref !== tableEditorState.projectRef) {
tableEditorState.loadedIds.clear()
tableEditorState.projectRef = ref
}
tableEditorState.loadedIds.add(id)
},
})
export const getTableEditorStateSnapshot = () => snapshot(tableEditorState)
export const useTableEditorStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
useSnapshot(tableEditorState, options)
export const useIsTableLoaded = (ref?: string, id?: number) => {
const snap = useTableEditorStateSnapshot()
if (!ref || !id) return false
if (ref !== snap.projectRef) return false
return snap.loadedIds.has(id)
}
+171
View File
@@ -0,0 +1,171 @@
import { PostgresColumn } from '@supabase/postgres-meta'
import { Dictionary } from 'components/grid'
import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
import { JsonEditValue } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types'
import { PropsWithChildren, createContext, useContext, useRef } from 'react'
import { proxy, useSnapshot } from 'valtio'
type ForeignKey = {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: Dictionary<any>
column: PostgresColumn
}
export type SidePanel =
| { type: 'row'; row?: Dictionary<any> }
| { type: 'column'; column?: PostgresColumn }
| { type: 'table'; mode: 'new' | 'edit' | 'duplicate' }
| { type: 'json'; jsonValue: JsonEditValue }
| {
type: 'foreign-row-selector'
foreignKey: ForeignKey
}
| { type: 'csv-import' }
export type ConfirmationDialog =
| { type: 'table'; isDeleteWithCascade: boolean }
| { type: 'column'; column: PostgresColumn; isDeleteWithCascade: boolean }
export type UIState =
| {
open: 'none'
}
| {
open: 'side-panel'
sidePanel: SidePanel
}
| {
open: 'confirmation-dialog'
confirmationDialog: ConfirmationDialog
}
export const createTableEditorState = () => {
const state = proxy({
selectedSchemaName: 'public',
setSelectedSchemaName: (schemaName: string) => {
state.selectedSchemaName = schemaName
},
ui: { open: 'none' } as UIState,
get sidePanel() {
return state.ui.open === 'side-panel' ? state.ui.sidePanel : undefined
},
get confirmationDialog() {
return state.ui.open === 'confirmation-dialog' ? state.ui.confirmationDialog : undefined
},
closeSidePanel: () => {
state.ui = { open: 'none' }
},
closeConfirmationDialog: () => {
state.ui = { open: 'none' }
},
/* Tables */
onAddTable: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'table', mode: 'new' },
}
},
onEditTable: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'table', mode: 'edit' },
}
},
onDuplicateTable: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'table', mode: 'duplicate' },
}
},
onDeleteTable: () => {
state.ui = {
open: 'confirmation-dialog',
confirmationDialog: { type: 'table', isDeleteWithCascade: false },
}
},
/* Columns */
onAddColumn: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'column' },
}
},
onEditColumn: (column: PostgresColumn) => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'column', column },
}
},
onDeleteColumn: (column: PostgresColumn) => {
state.ui = {
open: 'confirmation-dialog',
confirmationDialog: { type: 'column', column, isDeleteWithCascade: false },
}
},
/* Rows */
onAddRow: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'row' },
}
},
onEditRow: (row: Dictionary<any>) => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'row', row },
}
},
/* Misc */
onExpandJSONEditor: (jsonValue: JsonEditValue) => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'json', jsonValue },
}
},
onEditForeignKeyColumnValue: (foreignKey: ForeignKey) => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'foreign-row-selector', foreignKey },
}
},
onImportData: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'csv-import' },
}
},
/* Utils */
toggleConfirmationIsWithCascade: (overrideIsDeleteWithCascade?: boolean) => {
if (state.ui.open === 'confirmation-dialog') {
state.ui.confirmationDialog.isDeleteWithCascade =
overrideIsDeleteWithCascade ?? !state.ui.confirmationDialog.isDeleteWithCascade
}
},
})
return state
}
export type TableEditorState = ReturnType<typeof createTableEditorState>
export const TableEditorStateContext = createContext<TableEditorState>(createTableEditorState())
export const TableEditorStateContextProvider = ({ children }: PropsWithChildren<{}>) => {
const state = useRef(createTableEditorState()).current
return (
<TableEditorStateContext.Provider value={state}>{children}</TableEditorStateContext.Provider>
)
}
export const useTableEditorStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) => {
const state = useContext(TableEditorStateContext)
return useSnapshot(state, options)
}
+65 -61
View File
@@ -1,62 +1,62 @@
import Papa from 'papaparse'
import { makeObservable, observable } from 'mobx'
import { find, isUndefined, isEqual, isEmpty, chunk } from 'lodash'
import { Query } from 'components/grid/query/Query'
import { chunk, find, isEmpty, isEqual, isUndefined } from 'lodash'
import { makeObservable } from 'mobx'
import Papa from 'papaparse'
import type {
PostgresColumn,
PostgresTable,
PostgresRelationship,
PostgresPrimaryKey,
PostgresSchema,
PostgresRelationship,
PostgresTable,
} from '@supabase/postgres-meta'
import { IS_PLATFORM, API_URL } from 'lib/constants'
import { post } from 'lib/common/fetch'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { timeout, tryParseJson } from 'lib/helpers'
import { ResponseError } from 'types'
import { IRootStore } from '../RootStore'
import ColumnStore from './ColumnStore'
import SchemaStore from './SchemaStore'
import TableStore, { ITableStore } from './TableStore'
import OpenApiStore, { IOpenApiStore } from './OpenApiStore'
import { IPostgresMetaInterface } from '../common/PostgresMetaInterface'
import ColumnStore from './ColumnStore'
import OpenApiStore, { IOpenApiStore } from './OpenApiStore'
import TableStore, { ITableStore } from './TableStore'
import {
generateCreateColumnPayload,
generateUpdateColumnPayload,
} from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils'
import {
ColumnField,
CreateColumnPayload,
ExtendedPostgresRelationship,
UpdateColumnPayload,
} from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.types'
import {
generateCreateColumnPayload,
generateUpdateColumnPayload,
} from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils'
import { ImportContent } from 'components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types'
import RolesStore, { IRolesStore } from './RolesStore'
import PoliciesStore from './PoliciesStore'
import TriggersStore from './TriggersStore'
import PublicationStore from './PublicationStore'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import { getCachedProjectDetail } from 'data/projects/project-detail-query'
import { getQueryClient } from 'data/query-client'
import { tableKeys } from 'data/tables/keys'
import { getTable } from 'data/tables/table-query'
import { getTables } from 'data/tables/tables-query'
import ExtensionsStore from './ExtensionsStore'
import ForeignTableStore, { IForeignTableStore } from './ForeignTableStore'
import FunctionsStore from './FunctionsStore'
import HooksStore from './HooksStore'
import ExtensionsStore from './ExtensionsStore'
import TypesStore from './TypesStore'
import ForeignTableStore, { IForeignTableStore } from './ForeignTableStore'
import ViewStore, { IViewStore } from './ViewStore'
import MaterializedViewStore, { IMaterializedViewStore } from './MaterializedViewStore'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import PoliciesStore from './PoliciesStore'
import PublicationStore from './PublicationStore'
import RolesStore, { IRolesStore } from './RolesStore'
import TriggersStore from './TriggersStore'
import TypesStore from './TypesStore'
import ViewStore, { IViewStore } from './ViewStore'
const BATCH_SIZE = 1000
const CHUNK_SIZE = 1024 * 1024 * 0.1 // 0.1MB
export interface IMetaStore {
excludedSchemas: string[]
openApi: IOpenApiStore
tables: ITableStore
columns: IPostgresMetaInterface<PostgresColumn>
schemas: IPostgresMetaInterface<PostgresSchema>
views: IViewStore
materializedViews: IMaterializedViewStore
foreignTables: IForeignTableStore
@@ -150,7 +150,6 @@ export default class MetaStore implements IMetaStore {
openApi: OpenApiStore
tables: TableStore
columns: ColumnStore
schemas: SchemaStore
views: ViewStore
materializedViews: MaterializedViewStore
foreignTables: ForeignTableStore
@@ -169,25 +168,6 @@ export default class MetaStore implements IMetaStore {
baseUrl: string
headers: { [prop: string]: any }
// [Joshen] I'm going to treat this as a list of system schemas
excludedSchemas = [
'auth',
'extensions',
'information_schema',
'net',
'pgsodium',
'pgsodium_masks',
'pgbouncer',
'pgtle',
'realtime',
'storage',
'supabase_functions',
'supabase_migrations',
'vault',
'graphql',
'graphql_public',
]
constructor(rootStore: IRootStore, options: { projectRef: string; connectionString?: string }) {
const { projectRef, connectionString } = options
this.rootStore = rootStore
@@ -206,7 +186,6 @@ export default class MetaStore implements IMetaStore {
)
this.tables = new TableStore(this.rootStore, `${this.baseUrl}/tables`, this.headers)
this.columns = new ColumnStore(this.rootStore, `${this.baseUrl}/columns`, this.headers)
this.schemas = new SchemaStore(this.rootStore, `${this.baseUrl}/schemas`, this.headers)
this.views = new ViewStore(this.rootStore, `${this.baseUrl}/views`, this.headers)
this.materializedViews = new MaterializedViewStore(
this.rootStore,
@@ -239,9 +218,7 @@ export default class MetaStore implements IMetaStore {
)
this.types = new TypesStore(this.rootStore, `${this.baseUrl}/types`, this.headers)
makeObservable(this, {
excludedSchemas: observable,
})
makeObservable(this, {})
}
/**
@@ -349,7 +326,16 @@ export default class MetaStore implements IMetaStore {
async updateTableRealtime(table: PostgresTable, enable: boolean) {
let publicationUpdateError
const publications = this.publications.list()
const publicTables = this.tables.list((table: PostgresTable) => table.schema === 'public')
const queryClient = getQueryClient()
const project = await getCachedProjectDetail(queryClient, this.rootStore.ui.selectedProjectRef)
const projectRef = project?.ref
const connectionString = project?.connectionString
const publicTables = await queryClient.fetchQuery({
queryKey: tableKeys.list(projectRef, 'public'),
queryFn: ({ signal }) =>
getTables({ projectRef, connectionString, schema: 'public' }, signal),
})
let realtimePublication = publications.find((pub) => pub.name === 'supabase_realtime')
if (realtimePublication === undefined) {
@@ -596,8 +582,15 @@ export default class MetaStore implements IMetaStore {
})
}
await this.tables.load()
const tables = this.tables.list()
const queryClient = getQueryClient()
const project = await getCachedProjectDetail(queryClient, this.rootStore.ui.selectedProjectRef)
const projectRef = project?.ref
const connectionString = project?.connectionString
const tables = await queryClient.fetchQuery({
queryKey: tableKeys.list(projectRef, 'public'),
queryFn: ({ signal }) => getTables({ projectRef, connectionString }, signal),
})
const duplicatedTable = find(tables, { schema: sourceTableSchema, name: duplicatedTableName })
if (isRLSEnabled) {
@@ -621,8 +614,8 @@ export default class MetaStore implements IMetaStore {
importContent?: ImportContent
) {
// Create the table first
const table: any = await this.tables.create(payload)
if (table.error) throw table.error
const table = await this.tables.create(payload)
if ('error' in table) throw table.error
// If we face any errors during this process after the actual table creation
// We'll delete the table as a way to clean up and not leave behind bits that
@@ -745,7 +738,7 @@ export default class MetaStore implements IMetaStore {
}
// Finally, return the created table
return await this.tables.loadById(table.id)
return table
} catch (error: any) {
this.tables.del(table.id)
throw error
@@ -865,7 +858,21 @@ export default class MetaStore implements IMetaStore {
// Update table's realtime configuration
await this.updateTableRealtime(table, isRealtimeEnabled)
return { table: await this.tables.loadById(table.id), hasError }
const queryClient = getQueryClient()
const project = await getCachedProjectDetail(queryClient, this.rootStore.ui.selectedProjectRef)
const projectRef = project?.ref
const connectionString = project?.connectionString
queryClient.invalidateQueries(tableKeys.table(projectRef, table.id))
return {
table: await getTable({
projectRef,
connectionString,
id: table.id,
}),
hasError,
}
}
async insertRowsViaSpreadsheet(
@@ -1000,9 +1007,6 @@ export default class MetaStore implements IMetaStore {
this.columns.setUrl(`${this.baseUrl}/columns`)
this.columns.setHeaders(this.headers)
this.schemas.setUrl(`${this.baseUrl}/schemas`)
this.schemas.setHeaders(this.headers)
this.views.setUrl(`${this.baseUrl}/views`)
this.views.setHeaders(this.headers)
-24
View File
@@ -1,24 +0,0 @@
import type { PostgresSchema } from '@supabase/postgres-meta'
import PostgresMetaInterface from '../common/PostgresMetaInterface'
import { IRootStore } from '../RootStore'
export default class SchemaStore extends PostgresMetaInterface<PostgresSchema> {
constructor(
rootStore: IRootStore,
dataUrl: string,
headers?: {
[prop: string]: any
},
options?: { identifier: string }
) {
super(rootStore, dataUrl, headers, options)
}
// Dashboard to hide schemas with pg_ prefix
list(filter: any) {
const schemasFilter = (schema: PostgresSchema) =>
!schema.name.startsWith('pg_') && (typeof filter === 'function' ? filter(schema) : true)
return super.list(schemasFilter)
}
}