mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
feat: Make the protected schemas dynamic, namespace schemas are now protected (#37290)
* Add hooks for async protected schemas. * Migrate the ProtectedSchemaWarning to support the new implementation. * sq * Migrate all uses of protected schemas to the new approach. * Delete extra file. * Refactor the import foreign schema dialog to forbid protected and exposed schemas. * Add the type to the protected schema. * Revert ImportForeignSchemaDialog, it'll be addressed in another PR. * Update apps/studio/hooks/useProtectedSchemas.ts Co-authored-by: Joshen Lim <joshenlimek@gmail.com> * Fix a bad commit. * Minor fixes. * Fix the FDW delete mutation to handle names with numbers. * Simplify the logic to skip a fetch. * Minor fixes. * Make the useIcebergFdwSchemasQuery work for all iceberg FDWs. * Fix the tab schemas to always show in the Table Editor. * Apply suggestion from @joshenlim Co-authored-by: Joshen Lim <joshenlimek@gmail.com> * Fix a minor typo. * Refactor ProtectedSchemaWarning to use Admonition, and standardise input field for target schema iceberg --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
0a985a5643
commit
b3c6992e56
28 files changed
+342
-285
No files matched your search
@@ -10,7 +10,7 @@ import {
|
||||
PolicyTableRow,
|
||||
PolicyTableRowProps,
|
||||
} from 'components/interfaces/Auth/Policies/PolicyTableRow'
|
||||
import ProtectedSchemaWarning from 'components/interfaces/Database/ProtectedSchemaWarning'
|
||||
import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
|
||||
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
useEnumeratedTypesQuery,
|
||||
} from 'data/enumerated-types/enumerated-types-query'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
Input,
|
||||
} from 'ui'
|
||||
import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
|
||||
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
|
||||
import CreateEnumeratedTypeSidePanel from './CreateEnumeratedTypeSidePanel'
|
||||
import DeleteEnumeratedTypeModal from './DeleteEnumeratedTypeModal'
|
||||
import EditEnumeratedTypeSidePanel from './EditEnumeratedTypeSidePanel'
|
||||
@@ -52,11 +52,7 @@ const EnumeratedTypes = () => {
|
||||
)
|
||||
: enumeratedTypes.filter((x) => x.schema === selectedSchema)
|
||||
|
||||
const protectedSchemas = (schemas ?? []).filter((schema) =>
|
||||
PROTECTED_SCHEMAS.includes(schema?.name ?? '')
|
||||
)
|
||||
const schema = schemas?.find((schema) => schema.name === selectedSchema)
|
||||
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
|
||||
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -81,7 +77,7 @@ const EnumeratedTypes = () => {
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<DocsButton href="https://www.postgresql.org/docs/current/datatype-enum.html" />
|
||||
{!isLocked && (
|
||||
{!isSchemaLocked && (
|
||||
<Button
|
||||
className="ml-auto flex-1"
|
||||
type="primary"
|
||||
@@ -93,7 +89,9 @@ const EnumeratedTypes = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="enumerated types" />}
|
||||
{isSchemaLocked && (
|
||||
<ProtectedSchemaWarning schema={selectedSchema} entity="enumerated types" />
|
||||
)}
|
||||
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
|
||||
@@ -140,7 +138,7 @@ const EnumeratedTypes = () => {
|
||||
<Table.td>{type.name}</Table.td>
|
||||
<Table.td>{type.enums.join(', ')}</Table.td>
|
||||
<Table.td>
|
||||
{!isLocked && (
|
||||
{!isSchemaLocked && (
|
||||
<div className="flex justify-end items-center space-x-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-ex
|
||||
import { useDatabaseFunctionCreateMutation } from 'data/database-functions/database-functions-create-mutation'
|
||||
import { DatabaseFunction } from 'data/database-functions/database-functions-query'
|
||||
import { useDatabaseFunctionUpdateMutation } from 'data/database-functions/database-functions-update-mutation'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
|
||||
import type { FormSchema } from 'types'
|
||||
import {
|
||||
Button,
|
||||
@@ -149,6 +149,8 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
|
||||
}
|
||||
}, [visible, func])
|
||||
|
||||
const { data: protectedSchemas } = useProtectedSchemas()
|
||||
|
||||
return (
|
||||
<Sheet open={visible} onOpenChange={() => isClosingSidePanel()}>
|
||||
<SheetContent
|
||||
@@ -205,7 +207,7 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
|
||||
<SchemaSelector
|
||||
portal={false}
|
||||
selectedSchemaName={field.value}
|
||||
excludedSchemas={PROTECTED_SCHEMAS}
|
||||
excludedSchemas={protectedSchemas?.map((s) => s.name)}
|
||||
size="small"
|
||||
onSelectSchema={(name) => field.onChange(name)}
|
||||
/>
|
||||
|
||||
+8
-11
@@ -1,6 +1,6 @@
|
||||
import type { PostgresFunction } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { noop, partition } from 'lodash'
|
||||
import { noop } from 'lodash'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
@@ -16,10 +16,10 @@ import { useDatabaseFunctionsQuery } from 'data/database-functions/database-func
|
||||
import { useSchemasQuery } from 'data/database/schemas-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
|
||||
import { AiIconAnimation, Input } from 'ui'
|
||||
import ProtectedSchemaWarning from '../../ProtectedSchemaWarning'
|
||||
import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning'
|
||||
import FunctionList from './FunctionList'
|
||||
|
||||
interface FunctionsListProps {
|
||||
@@ -60,11 +60,8 @@ const FunctionsList = ({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const [protectedSchemas] = partition(schemas ?? [], (schema) =>
|
||||
PROTECTED_SCHEMAS.includes(schema?.name ?? '')
|
||||
)
|
||||
const foundSchema = schemas?.find((schema) => schema.name === selectedSchema)
|
||||
const isLocked = protectedSchemas.some((s) => s.id === foundSchema?.id)
|
||||
|
||||
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
|
||||
|
||||
const {
|
||||
data: functions,
|
||||
@@ -126,7 +123,7 @@ const FunctionsList = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
{!isLocked && (
|
||||
{!isSchemaLocked && (
|
||||
<>
|
||||
<ButtonTooltip
|
||||
disabled={!canCreateFunctions}
|
||||
@@ -169,7 +166,7 @@ const FunctionsList = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="functions" />}
|
||||
{isSchemaLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="functions" />}
|
||||
|
||||
<Table
|
||||
className="table-fixed overflow-x-auto"
|
||||
@@ -192,7 +189,7 @@ const FunctionsList = ({
|
||||
<FunctionList
|
||||
schema={selectedSchema}
|
||||
filterString={filterString}
|
||||
isLocked={isLocked}
|
||||
isLocked={isSchemaLocked}
|
||||
editFunction={editFunction}
|
||||
deleteFunction={deleteFunction}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { partition, sortBy } from 'lodash'
|
||||
import { sortBy } from 'lodash'
|
||||
import { AlertCircle, Search, Trash } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
@@ -14,10 +14,10 @@ import { useDatabaseIndexDeleteMutation } from 'data/database-indexes/index-dele
|
||||
import { DatabaseIndex, useIndexesQuery } from 'data/database-indexes/indexes-query'
|
||||
import { useSchemasQuery } from 'data/database/schemas-query'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import { Button, Input, SidePanel } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
|
||||
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
|
||||
import CreateIndexSidePanel from './CreateIndexSidePanel'
|
||||
|
||||
const Indexes = () => {
|
||||
@@ -58,11 +58,7 @@ const Indexes = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const [protectedSchemas] = partition(schemas ?? [], (schema) =>
|
||||
PROTECTED_SCHEMAS.includes(schema?.name ?? '')
|
||||
)
|
||||
const schema = schemas?.find((schema) => schema.name === selectedSchema)
|
||||
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
|
||||
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
|
||||
|
||||
const sortedIndexes = sortBy(allIndexes ?? [], (index) => index.name.toLocaleLowerCase())
|
||||
const indexes =
|
||||
@@ -122,7 +118,7 @@ const Indexes = () => {
|
||||
icon={<Search size={14} />}
|
||||
/>
|
||||
|
||||
{!isLocked && (
|
||||
{!isSchemaLocked && (
|
||||
<Button
|
||||
className="ml-auto flex-grow lg:flex-grow-0"
|
||||
type="primary"
|
||||
@@ -134,7 +130,7 @@ const Indexes = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="indexes" />}
|
||||
{isSchemaLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="indexes" />}
|
||||
|
||||
{isLoadingIndexes && <GenericSkeletonLoader />}
|
||||
|
||||
@@ -190,7 +186,7 @@ const Indexes = () => {
|
||||
<Button type="default" onClick={() => setSelectedIndex(index)}>
|
||||
View definition
|
||||
</Button>
|
||||
{!isLocked && (
|
||||
{!isSchemaLocked && (
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
import { useState } from 'react'
|
||||
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, Modal } from 'ui'
|
||||
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogSection,
|
||||
DialogSectionSeparator,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
cn,
|
||||
} from 'ui'
|
||||
|
||||
export const ProtectedSchemaModal = ({
|
||||
visible,
|
||||
onClose,
|
||||
}: {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}) => {
|
||||
import { INTERNAL_SCHEMAS, useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
|
||||
export const ProtectedSchemaDialog = ({ onClose }: { onClose: () => void }) => {
|
||||
return (
|
||||
<Modal
|
||||
size="medium"
|
||||
visible={visible}
|
||||
header="Schemas managed by Supabase"
|
||||
customFooter={
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button type="default" onClick={() => onClose()}>
|
||||
Understood
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
onCancel={() => onClose()}
|
||||
>
|
||||
<Modal.Content className="space-y-2">
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Schemas managed by Supabase</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogSectionSeparator />
|
||||
<DialogSection className="space-y-2 prose">
|
||||
<p className="text-sm">
|
||||
The following schemas are managed by Supabase and are currently protected from write
|
||||
access through the dashboard.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{PROTECTED_SCHEMAS.map((schema) => (
|
||||
{INTERNAL_SCHEMAS.map((schema) => (
|
||||
<code key={schema} className="text-xs">
|
||||
{schema}
|
||||
</code>
|
||||
@@ -45,32 +43,67 @@ export const ProtectedSchemaModal = ({
|
||||
You can, however, still interact with those schemas through the SQL Editor although we
|
||||
advise you only do so if you know what you are doing.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const ProtectedSchemaWarning = ({ schema, entity }: { schema: string; entity: string }) => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert_Shadcn_>
|
||||
<AlertCircle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>Currently viewing {entity} from a protected schema</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
<p className="mb-2">
|
||||
The <code className="text-xs">{schema}</code> schema is managed by Supabase and is
|
||||
read-only through the dashboard.
|
||||
</p>
|
||||
<Button type="default" size="tiny" onClick={() => setShowModal(true)}>
|
||||
Learn more
|
||||
</DialogSection>
|
||||
<DialogFooter>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button type="default" onClick={onClose}>
|
||||
Understood
|
||||
</Button>
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
<ProtectedSchemaModal visible={showModal} onClose={() => setShowModal(false)} />
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProtectedSchemaWarning
|
||||
export const ProtectedSchemaWarning = ({
|
||||
size = 'md',
|
||||
schema,
|
||||
entity,
|
||||
}: {
|
||||
size?: 'sm' | 'md'
|
||||
schema: string
|
||||
entity: string
|
||||
}) => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const { isSchemaLocked, reason } = useIsProtectedSchema({ schema })
|
||||
|
||||
if (!isSchemaLocked) return null
|
||||
|
||||
return (
|
||||
<Admonition
|
||||
showIcon={false}
|
||||
type="note"
|
||||
title={
|
||||
size === 'sm' ? `Viewing protected schema` : `Viewing ${entity} from a protected schema`
|
||||
}
|
||||
className={cn(
|
||||
'[&>div>p]:prose [&>div>p]:max-w-full [&>div>p]:!leading-normal',
|
||||
size === 'sm' ? '[&>div>p]:text-xs' : '[&>div>p]:text-sm'
|
||||
)}
|
||||
>
|
||||
{reason === 'fdw' ? (
|
||||
<p>
|
||||
The <code className="text-xs">{schema}</code> schema is used by Supabase to connect to
|
||||
analytics buckets and is read-only through the dashboard.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
The <code className="text-xs">{schema}</code> schema is managed by Supabase and is
|
||||
read-only through the dashboard.
|
||||
</p>
|
||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="default" size="tiny" onClick={() => setShowModal(true)}>
|
||||
Learn more
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<ProtectedSchemaDialog onClose={() => setShowModal(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</Admonition>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PostgresPublication } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
|
||||
@@ -10,10 +10,10 @@ import InformationBox from 'components/ui/InformationBox'
|
||||
import { Loading } from 'components/ui/Loading'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
|
||||
import { AlertCircle, ChevronLeft, Search } from 'lucide-react'
|
||||
import { Button, Input } from 'ui'
|
||||
import PublicationsTableItem from './PublicationsTableItem'
|
||||
import { ChevronLeft, Search, AlertCircle } from 'lucide-react'
|
||||
|
||||
interface PublicationsTablesProps {
|
||||
selectedPublication: PostgresPublication
|
||||
@@ -29,27 +29,26 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT
|
||||
'publications'
|
||||
)
|
||||
|
||||
const { data: protectedSchemas } = useProtectedSchemas()
|
||||
|
||||
const {
|
||||
data: tables,
|
||||
data: tablesData,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
isError,
|
||||
error,
|
||||
} = useTablesQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
},
|
||||
{
|
||||
select(tables) {
|
||||
return tables.filter((table) =>
|
||||
filterString.length === 0
|
||||
? !PROTECTED_SCHEMAS.includes(table.schema)
|
||||
: !PROTECTED_SCHEMAS.includes(table.schema) && table.name.includes(filterString)
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
} = useTablesQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const tables = useMemo(() => {
|
||||
return (tablesData || []).filter((table) =>
|
||||
filterString.length === 0
|
||||
? !protectedSchemas.find((s) => s.name === table.schema)
|
||||
: !protectedSchemas.find((s) => s.name === table.schema) &&
|
||||
table.name.includes(filterString)
|
||||
)
|
||||
}, [tablesData, protectedSchemas, filterString])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
|
||||
import { isTableLike } from 'data/table-editor/table-editor-types'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
|
||||
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
|
||||
|
||||
interface ColumnListProps {
|
||||
onAddColumn: () => void
|
||||
@@ -63,7 +63,7 @@ const ColumnList = ({
|
||||
? selectedTable?.columns ?? []
|
||||
: selectedTable?.columns?.filter((column) => column.name.includes(filterString))) ?? []
|
||||
|
||||
const isLocked = PROTECTED_SCHEMAS.includes(selectedTable?.schema ?? '')
|
||||
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
|
||||
const canUpdateColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
|
||||
|
||||
return (
|
||||
@@ -81,7 +81,7 @@ const ColumnList = ({
|
||||
icon={<Search size={12} />}
|
||||
/>
|
||||
</div>
|
||||
{!isLocked && isTableEntity && (
|
||||
{!isSchemaLocked && isTableEntity && (
|
||||
<ButtonTooltip
|
||||
icon={<Plus />}
|
||||
disabled={!canUpdateColumns}
|
||||
@@ -100,7 +100,9 @@ const ColumnList = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLocked && <ProtectedSchemaWarning schema={selectedTable?.schema ?? ''} entity="columns" />}
|
||||
{isSchemaLocked && (
|
||||
<ProtectedSchemaWarning schema={selectedTable?.schema ?? ''} entity="columns" />
|
||||
)}
|
||||
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
|
||||
@@ -156,7 +158,7 @@ const ColumnList = ({
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td className="text-right">
|
||||
{!isLocked && isTableEntity && (
|
||||
{!isSchemaLocked && isTableEntity && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" className="px-1" icon={<MoreVertical />} />
|
||||
@@ -183,7 +185,7 @@ const ColumnList = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<DropdownMenuItem
|
||||
disabled={!canUpdateColumns || isLocked}
|
||||
disabled={!canUpdateColumns || isSchemaLocked}
|
||||
onClick={() => onDeleteColumn(x)}
|
||||
className="space-x-2"
|
||||
>
|
||||
|
||||
@@ -35,7 +35,7 @@ import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useViewsQuery } from 'data/views/views-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import {
|
||||
Button,
|
||||
Checkbox_Shadcn_,
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
TooltipTrigger,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
|
||||
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
|
||||
import { formatAllEntities } from './Tables.utils'
|
||||
|
||||
interface TableListProps {
|
||||
@@ -184,7 +184,7 @@ const TableList = ({
|
||||
(x) => visibleTypes.includes(x.type)
|
||||
)
|
||||
|
||||
const isLocked = PROTECTED_SCHEMAS.includes(selectedSchema)
|
||||
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
|
||||
|
||||
const error = tablesError || viewsError || materializedViewsError || foreignTablesError
|
||||
const isError = isErrorTables || isErrorViews || isErrorMaterializedViews || isErrorForeignTables
|
||||
@@ -269,7 +269,7 @@ const TableList = ({
|
||||
icon={<Search size={12} />}
|
||||
/>
|
||||
|
||||
{!isLocked && (
|
||||
{!isSchemaLocked && (
|
||||
<ButtonTooltip
|
||||
className="w-auto ml-auto"
|
||||
icon={<Plus />}
|
||||
@@ -290,7 +290,7 @@ const TableList = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="tables" />}
|
||||
{isSchemaLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="tables" />}
|
||||
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
|
||||
@@ -462,7 +462,7 @@ const TableList = ({
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{!isLocked && (
|
||||
{!isSchemaLocked && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" className="px-1" icon={<MoreVertical />} />
|
||||
@@ -519,10 +519,10 @@ const TableList = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
disabled={!canUpdateTables || isLocked}
|
||||
disabled={!canUpdateTables || isSchemaLocked}
|
||||
className="!pointer-events-auto gap-x-2"
|
||||
onClick={() => {
|
||||
if (canUpdateTables && !isLocked) {
|
||||
if (canUpdateTables && !isSchemaLocked) {
|
||||
onDeleteTable({
|
||||
...x,
|
||||
schema: selectedSchema,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useDatabaseTriggerCreateMutation } from 'data/database-triggers/databas
|
||||
import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/database-trigger-update-mutation'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
|
||||
import {
|
||||
Button,
|
||||
Checkbox_Shadcn_,
|
||||
@@ -107,14 +107,16 @@ export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetPro
|
||||
}
|
||||
)
|
||||
|
||||
const { data = [], isSuccess } = useTablesQuery({
|
||||
const { data = [], isSuccess: isSuccessTables } = useTablesQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const { data: protectedSchemas, isSuccess: isSuccessProtectedSchemas } = useProtectedSchemas()
|
||||
const isSuccess = isSuccessTables && isSuccessProtectedSchemas
|
||||
|
||||
const tables = data
|
||||
.sort((a, b) => a.schema.localeCompare(b.schema))
|
||||
.filter((a) => !PROTECTED_SCHEMAS.includes(a.schema))
|
||||
.filter((a) => !protectedSchemas.find((s) => s.name === a.schema))
|
||||
const isEditing = !!selectedTrigger
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PostgresTrigger } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { noop, partition } from 'lodash'
|
||||
import { noop } from 'lodash'
|
||||
import { Plus, Search } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
@@ -13,14 +13,13 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import SchemaSelector from 'components/ui/SchemaSelector'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-query'
|
||||
import { useSchemasQuery } from 'data/database/schemas-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useIsProtectedSchema, useProtectedSchemas } from 'hooks/useProtectedSchemas'
|
||||
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
|
||||
import { AiIconAnimation, Input } from 'ui'
|
||||
import ProtectedSchemaWarning from '../../ProtectedSchemaWarning'
|
||||
import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning'
|
||||
import TriggerList from './TriggerList'
|
||||
|
||||
interface TriggersListProps {
|
||||
@@ -39,21 +38,15 @@ const TriggersList = ({
|
||||
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
|
||||
const [filterString, setFilterString] = useState<string>('')
|
||||
|
||||
const { data: schemas } = useSchemasQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const [protectedSchemas] = partition(schemas ?? [], (schema) =>
|
||||
PROTECTED_SCHEMAS.includes(schema?.name ?? '')
|
||||
)
|
||||
const schema = schemas?.find((schema) => schema.name === selectedSchema)
|
||||
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
|
||||
const { data: protectedSchemas } = useProtectedSchemas()
|
||||
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
|
||||
|
||||
const { data = [], isSuccess } = useTablesQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const hasTables = data.filter((a) => !PROTECTED_SCHEMAS.includes(a.schema)).length > 0
|
||||
const hasTables =
|
||||
data.filter((a) => !protectedSchemas.find((s) => s.name === a.schema)).length > 0
|
||||
|
||||
const {
|
||||
data: triggers,
|
||||
@@ -115,7 +108,7 @@ const TriggersList = ({
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{!isLocked && (
|
||||
{!isSchemaLocked && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<ButtonTooltip
|
||||
disabled={!hasTables || !canCreateTriggers}
|
||||
@@ -182,7 +175,7 @@ const TriggersList = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="triggers" />}
|
||||
{isSchemaLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="triggers" />}
|
||||
|
||||
<div className="w-full overflow-hidden overflow-x-auto">
|
||||
<Table
|
||||
@@ -203,7 +196,7 @@ const TriggersList = ({
|
||||
<TriggerList
|
||||
schema={selectedSchema}
|
||||
filterString={filterString}
|
||||
isLocked={isLocked}
|
||||
isLocked={isSchemaLocked}
|
||||
editTrigger={editTrigger}
|
||||
deleteTrigger={deleteTrigger}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -337,7 +337,7 @@ export const CreateIcebergWrapperSheet = ({
|
||||
/>
|
||||
<p className="text-foreground-lighter text-sm">
|
||||
A new schema will be created. For security purposes, the wrapper tables
|
||||
from the foreign schema cannot be created within an existing schema
|
||||
from the foreign schema cannot be created within an existing schema.
|
||||
</p>
|
||||
</div>
|
||||
</FormSectionContent>
|
||||
|
||||
@@ -440,7 +440,7 @@ export const CreateWrapperSheet = ({
|
||||
/>
|
||||
<p className="text-foreground-lighter text-sm">
|
||||
A new schema will be created. For security purposes, the wrapper tables
|
||||
from the foreign schema cannot be created within an existing schema
|
||||
from the foreign schema cannot be created within an existing schema.
|
||||
</p>
|
||||
</div>
|
||||
</FormSectionContent>
|
||||
|
||||
@@ -107,7 +107,7 @@ export const ImportForeignSchemaDialog = ({
|
||||
const serverOptions = await getDecryptedParameters({
|
||||
ref: project?.ref,
|
||||
connectionString: project?.connectionString ?? undefined,
|
||||
serverName,
|
||||
wrapper,
|
||||
})
|
||||
|
||||
const formValues: Record<string, string> = {
|
||||
@@ -158,11 +158,7 @@ export const ImportForeignSchemaDialog = ({
|
||||
hideFooter
|
||||
visible={visible}
|
||||
size="medium"
|
||||
header={
|
||||
<span>
|
||||
Connect namespace <span className="text-brand">{namespace}</span>
|
||||
</span>
|
||||
}
|
||||
header={<span>Connect namespace "{namespace}"</span>}
|
||||
onCancel={() => onClose()}
|
||||
>
|
||||
<Form_Shadcn_ {...form}>
|
||||
@@ -173,9 +169,9 @@ export const ImportForeignSchemaDialog = ({
|
||||
name="targetSchema"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="Target Schema"
|
||||
description="Enter a schema name under which the Iceberg data will be accessible. The schema will be created."
|
||||
layout="vertical"
|
||||
label="Specify a new schema for where the Iceberg data will be accessible"
|
||||
description="A new schema will be created. For security purposes, the wrapper tables from the foreign schema cannot be created within an existing schema."
|
||||
>
|
||||
<Input_Shadcn_ {...field} placeholder="Enter schema name" />
|
||||
</FormItemLayout>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getFDWs } from 'data/fdw/fdws-query'
|
||||
import { type FDW } from 'data/fdw/fdws-query'
|
||||
import { getDecryptedValues } from 'data/vault/vault-secret-decrypted-value-query'
|
||||
import { INTEGRATIONS } from '../Integrations/Landing/Integrations.constants'
|
||||
import { WrapperMeta } from '../Integrations/Wrappers/Wrappers.types'
|
||||
@@ -7,18 +7,16 @@ import { convertKVStringArrayToJson } from '../Integrations/Wrappers/Wrappers.ut
|
||||
export const getDecryptedParameters = async ({
|
||||
ref,
|
||||
connectionString,
|
||||
serverName,
|
||||
wrapper,
|
||||
}: {
|
||||
ref?: string
|
||||
connectionString?: string
|
||||
serverName: string
|
||||
wrapper: FDW
|
||||
}) => {
|
||||
const integration = INTEGRATIONS.find((i) => i.id === 'iceberg_wrapper' && i.type === 'wrapper')
|
||||
const wrapperMeta = (integration?.type === 'wrapper' && integration.meta) as WrapperMeta
|
||||
const wrapperServerOptions = wrapperMeta.server.options
|
||||
|
||||
const FDWs = await getFDWs({ projectRef: ref, connectionString: connectionString })
|
||||
const wrapper = FDWs.find((fdw) => fdw.server_name === serverName)
|
||||
const serverOptions = convertKVStringArrayToJson(wrapper?.server_options ?? [])
|
||||
|
||||
const paramsToBeDecrypted = Object.fromEntries(
|
||||
|
||||
@@ -25,7 +25,7 @@ import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
|
||||
import {
|
||||
Button,
|
||||
@@ -60,7 +60,7 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
|
||||
const isMaterializedView = isTableLikeMaterializedView(table)
|
||||
|
||||
const realtimeEnabled = useIsFeatureEnabled('realtime:all')
|
||||
const isLocked = PROTECTED_SCHEMAS.includes(table.schema)
|
||||
const { isSchemaLocked } = useIsProtectedSchema({ schema: table.schema })
|
||||
|
||||
const { mutate: updateTable } = useTableUpdateMutation({
|
||||
onError: (error) => {
|
||||
@@ -200,10 +200,10 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isTable && !isLocked ? (
|
||||
{isTable && !isSchemaLocked ? (
|
||||
table.rls_enabled ? (
|
||||
<>
|
||||
{policies.length < 1 && !isLocked ? (
|
||||
{policies.length < 1 && !isSchemaLocked ? (
|
||||
<ButtonTooltip
|
||||
asChild
|
||||
type="default"
|
||||
@@ -227,10 +227,10 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
|
||||
) : (
|
||||
<Button
|
||||
asChild
|
||||
type={policies.length < 1 && !isLocked ? 'warning' : 'default'}
|
||||
type={policies.length < 1 && !isSchemaLocked ? 'warning' : 'default'}
|
||||
className="group"
|
||||
icon={
|
||||
isLocked || policies.length > 0 ? (
|
||||
isSchemaLocked || policies.length > 0 ? (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded-full bg-border-stronger h-[16px]',
|
||||
@@ -281,7 +281,7 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
|
||||
With RLS enabled, anonymous users will not be able to read/write data in the
|
||||
table.
|
||||
</p>
|
||||
{!isLocked && (
|
||||
{!isSchemaLocked && (
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
type="default"
|
||||
|
||||
+3
-2
@@ -17,7 +17,7 @@ import {
|
||||
useForeignKeyConstraintsQuery,
|
||||
} from 'data/database/foreign-key-constraints-query'
|
||||
import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-query'
|
||||
import { PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS } from 'lib/constants/schemas'
|
||||
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
|
||||
import type { Dictionary } from 'types'
|
||||
import { Button, Checkbox, Input, SidePanel, Toggle } from 'ui'
|
||||
import ActionBar from '../ActionBar'
|
||||
@@ -83,8 +83,9 @@ const ColumnEditor = ({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const { data: protectedSchemas } = useProtectedSchemas({ excludeSchemas: ['extensions'] })
|
||||
const enumTypes = (types ?? []).filter(
|
||||
(type) => !PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS.includes(type.schema)
|
||||
(type) => !protectedSchemas.find((s) => s.name === type.schema)
|
||||
)
|
||||
|
||||
const { data: constraints } = useTableConstraintsQuery({
|
||||
|
||||
+5
-4
@@ -16,10 +16,12 @@ import {
|
||||
useForeignKeyConstraintsQuery,
|
||||
} from 'data/database/foreign-key-constraints-query'
|
||||
import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-query'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useUrlState } from 'hooks/ui/useUrlState'
|
||||
import { PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS } from 'lib/constants/schemas'
|
||||
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import { Badge, Checkbox, Input, SidePanel } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
@@ -41,8 +43,6 @@ import {
|
||||
generateTableFieldFromPostgresTable,
|
||||
validateFields,
|
||||
} from './TableEditor.utils'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
|
||||
export interface TableEditorProps {
|
||||
table?: PostgresTable
|
||||
@@ -100,8 +100,9 @@ const TableEditor = ({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const { data: protectedSchemas } = useProtectedSchemas({ excludeSchemas: ['extensions'] })
|
||||
const enumTypes = (types ?? []).filter(
|
||||
(type) => !PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS.includes(type.schema)
|
||||
(type) => !protectedSchemas.find((s) => s.name === type.schema)
|
||||
)
|
||||
|
||||
const { data: publications } = useDatabasePublicationsQuery({
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from 'data/table-editor/table-editor-types'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useUrlState } from 'hooks/ui/useUrlState'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { TableEditorTableStateContextProvider } from 'state/table-editor-table'
|
||||
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
|
||||
@@ -72,6 +72,8 @@ export const TableGridEditor = ({
|
||||
}
|
||||
}, [onClearDashboardHistory, router, selectedTable, tabs])
|
||||
|
||||
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
|
||||
|
||||
// NOTE: DO NOT PUT HOOKS AFTER THIS LINE
|
||||
if (isLoadingSelectedTable || !projectRef) {
|
||||
return (
|
||||
@@ -138,8 +140,8 @@ export const TableGridEditor = ({
|
||||
|
||||
const isViewSelected = isView(selectedTable) || isMaterializedView(selectedTable)
|
||||
const isTableSelected = isTableLike(selectedTable)
|
||||
const isLocked = PROTECTED_SCHEMAS.includes(selectedTable?.schema ?? '')
|
||||
const canEditViaTableEditor = isTableSelected && !isLocked
|
||||
|
||||
const canEditViaTableEditor = isTableSelected && !isSchemaLocked
|
||||
const editable = !isReadOnly && canEditViaTableEditor
|
||||
|
||||
const gridKey = `${selectedTable.schema}_${selectedTable.name}`
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { type PostgresColumn } from '@supabase/postgres-meta'
|
||||
import { AlertTriangle, Code, Loader2, Table2 } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils'
|
||||
import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering'
|
||||
import { type SqlSnippet, useSqlSnippetsQuery } from 'data/content/sql-snippets-query'
|
||||
import { useSqlSnippetsQuery, type SqlSnippet } from 'data/content/sql-snippets-query'
|
||||
import { usePrefetchTables, useTablesQuery, type TablesData } from 'data/tables/tables-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import {
|
||||
cn,
|
||||
@@ -34,9 +37,6 @@ import {
|
||||
useSetCommandMenuSize,
|
||||
useSetPage,
|
||||
} from 'ui-patterns/CommandMenu'
|
||||
import { usePrefetchTables, useTablesQuery, type TablesData } from 'data/tables/tables-query'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export function useSqlEditorGotoCommands(options?: CommandOptions) {
|
||||
let { ref } = useParams()
|
||||
@@ -295,19 +295,20 @@ export function useQueryTableCommands(options?: CommandOptions) {
|
||||
function TableSelector() {
|
||||
const router = useRouter()
|
||||
const project = useSelectedProject()
|
||||
const { data: protectedSchemas } = useProtectedSchemas()
|
||||
const {
|
||||
data: tables,
|
||||
data: tablesData,
|
||||
isLoading,
|
||||
isError,
|
||||
isSuccess,
|
||||
} = useTablesQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
includeColumns: true,
|
||||
},
|
||||
{ select: excludeSupabaseControlledSchemas }
|
||||
)
|
||||
} = useTablesQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
includeColumns: true,
|
||||
})
|
||||
const tables = useMemo(() => {
|
||||
return tablesData?.filter((table) => !protectedSchemas.find((s) => s.name === table.schema))
|
||||
}, [tablesData, protectedSchemas])
|
||||
|
||||
return (
|
||||
<CommandWrapper>
|
||||
@@ -360,10 +361,6 @@ from ${formatTableIdentifier(table)}
|
||||
`.trim()
|
||||
}
|
||||
|
||||
function excludeSupabaseControlledSchemas(tables: TablesData) {
|
||||
return tables.filter((table) => !PROTECTED_SCHEMAS.includes(table.schema))
|
||||
}
|
||||
|
||||
// Not a perfectly spec-compliant regex , since Postgres also allows non-Latin
|
||||
// letters and letters with diacritical marks, but quoting them defensively
|
||||
// is easier than writing the regex. ¯\_(ツ)_/¯
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { partition } from 'lodash'
|
||||
import { Filter, Plus } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useBreakpoint } from 'common/hooks/useBreakpoint'
|
||||
import { ExportDialog } from 'components/grid/components/header/ExportDialog'
|
||||
import { ProtectedSchemaModal } from 'components/interfaces/Database/ProtectedSchemaWarning'
|
||||
import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning'
|
||||
import EditorMenuListSkeleton from 'components/layouts/TableEditorLayout/EditorMenuListSkeleton'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import InfiniteList from 'components/ui/InfiniteList'
|
||||
import SchemaSelector from 'components/ui/SchemaSelector'
|
||||
import { useSchemasQuery } from 'data/database/schemas-query'
|
||||
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
|
||||
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
|
||||
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
Checkbox_Shadcn_,
|
||||
Label_Shadcn_,
|
||||
@@ -44,14 +39,13 @@ import { useTableEditorTabsCleanUp } from '../Tabs/Tabs.utils'
|
||||
import EntityListItem from './EntityListItem'
|
||||
import { TableMenuEmptyState } from './TableMenuEmptyState'
|
||||
|
||||
const TableEditorMenu = () => {
|
||||
export const TableEditorMenu = () => {
|
||||
const { id: _id } = useParams()
|
||||
const id = _id ? Number(_id) : undefined
|
||||
const snap = useTableEditorStateSnapshot()
|
||||
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
|
||||
const isMobile = useBreakpoint()
|
||||
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [searchText, setSearchText] = useState<string>('')
|
||||
const [tableToExport, setTableToExport] = useState<{ name: string; schema: string }>()
|
||||
const [visibleTypes, setVisibleTypes] = useState<string[]>(Object.values(ENTITY_TYPE))
|
||||
@@ -89,19 +83,9 @@ const TableEditorMenu = () => {
|
||||
[data?.pages]
|
||||
)
|
||||
|
||||
const { data: schemas } = useSchemasQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
const schema = schemas?.find((schema) => schema.name === selectedSchema)
|
||||
const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
|
||||
|
||||
const [protectedSchemas] = partition(
|
||||
(schemas ?? []).sort((a, b) => a.name.localeCompare(b.name)),
|
||||
(schema) => PROTECTED_SCHEMAS.includes(schema?.name ?? '')
|
||||
)
|
||||
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
|
||||
const { isSchemaLocked, reason } = useIsProtectedSchema({ schema: selectedSchema })
|
||||
|
||||
const { data: selectedTable } = useTableEditorQuery({
|
||||
projectRef: project?.ref,
|
||||
@@ -138,7 +122,7 @@ const TableEditorMenu = () => {
|
||||
/>
|
||||
|
||||
<div className="grid gap-3 mx-4">
|
||||
{!isLocked ? (
|
||||
{!isSchemaLocked ? (
|
||||
<ButtonTooltip
|
||||
block
|
||||
title="Create a new table"
|
||||
@@ -161,19 +145,7 @@ const TableEditorMenu = () => {
|
||||
New table
|
||||
</ButtonTooltip>
|
||||
) : (
|
||||
<Alert_Shadcn_>
|
||||
<AlertTitle_Shadcn_ className="text-sm">
|
||||
Viewing protected schema
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_ className="text-xs">
|
||||
<p className="mb-2">
|
||||
This schema is managed by Supabase and is read-only through the table editor
|
||||
</p>
|
||||
<Button type="default" size="tiny" onClick={() => setShowModal(true)}>
|
||||
Learn more
|
||||
</Button>
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
<ProtectedSchemaWarning size="sm" schema={selectedSchema} entity="table" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,7 +254,7 @@ const TableEditorMenu = () => {
|
||||
itemProps={{
|
||||
projectRef: project?.ref!,
|
||||
id: Number(id),
|
||||
isLocked,
|
||||
isSchemaLocked,
|
||||
onExportCLI: () => {
|
||||
const entity = entityTypes?.find((x) => x.id === id)
|
||||
if (!entity) return
|
||||
@@ -308,7 +280,6 @@ const TableEditorMenu = () => {
|
||||
if (!open) setTableToExport(undefined)
|
||||
}}
|
||||
/>
|
||||
<ProtectedSchemaModal visible={showModal} onClose={() => setShowModal(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { X } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { useTabsStateSnapshot, type Tab } from 'state/tabs'
|
||||
import { cn, TabsTrigger_Shadcn_ } from 'ui'
|
||||
|
||||
@@ -28,8 +28,7 @@ export const SortableTab = ({
|
||||
openTabs: Tab[]
|
||||
onClose: (id: string) => void
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const currentSchema = (router.query.schema as string) || 'public'
|
||||
const { selectedSchema: currentSchema } = useQuerySchemaState()
|
||||
const tabs = useTabsStateSnapshot()
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: tab.id,
|
||||
@@ -45,16 +44,8 @@ export const SortableTab = ({
|
||||
const shouldShowSchema = useMemo(() => {
|
||||
// For both table and schema tabs, show schema if:
|
||||
// Any tab has a different schema than the current schema parameter
|
||||
if (tab.type === 'r') {
|
||||
const anyTabHasDifferentSchema = openTabs
|
||||
.filter((t) => t.type === 'r')
|
||||
.some((t) => t.metadata?.schema !== currentSchema)
|
||||
|
||||
return anyTabHasDifferentSchema
|
||||
}
|
||||
|
||||
return false
|
||||
}, [openTabs, currentSchema, tab.type])
|
||||
return openTabs.some((t) => t.metadata?.schema !== currentSchema)
|
||||
}, [openTabs, currentSchema])
|
||||
|
||||
// Create a motion version of TabsTrigger while preserving all functionality
|
||||
// const MotionTabsTrigger = motion(TabsTrigger_Shadcn_)
|
||||
|
||||
@@ -73,7 +73,7 @@ export const getDeleteFDWSql = ({
|
||||
const deleteEncryptedSecretsSql = deleteEncryptedSecretsSqlArray.join('\n')
|
||||
|
||||
const sql = /* SQL */ `
|
||||
drop foreign data wrapper if exists ${wrapper.name} cascade;
|
||||
drop foreign data wrapper if exists "${wrapper.name}" cascade;
|
||||
|
||||
${deleteEncryptedSecretsSql}
|
||||
`
|
||||
|
||||
@@ -12,7 +12,12 @@ const useIsomorphicUseQueryState = (defaultSchema: string) => {
|
||||
return [defaultSchema, () => {}] as const
|
||||
} else {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useQueryState('schema', parseAsString.withDefault(defaultSchema))
|
||||
return useQueryState(
|
||||
'schema',
|
||||
parseAsString.withDefault(defaultSchema).withOptions({
|
||||
clearOnDefault: false,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { uniq } from 'lodash'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { WRAPPER_HANDLERS } from 'components/interfaces/Integrations/Wrappers/Wrappers.constants'
|
||||
import {
|
||||
convertKVStringArrayToJson,
|
||||
wrapperMetaComparator,
|
||||
} from 'components/interfaces/Integrations/Wrappers/Wrappers.utils'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { QUEUES_SCHEMA } from 'data/database-queues/database-queues-toggle-postgrest-mutation'
|
||||
import { useFDWsQuery } from 'data/fdw/fdws-query'
|
||||
|
||||
/**
|
||||
* A list of system schemas that users should not interact with
|
||||
*/
|
||||
export const INTERNAL_SCHEMAS = [
|
||||
'auth',
|
||||
'cron',
|
||||
'extensions',
|
||||
'information_schema',
|
||||
'net',
|
||||
'pgsodium',
|
||||
'pgsodium_masks',
|
||||
'pgbouncer',
|
||||
'pgtle',
|
||||
'realtime',
|
||||
'storage',
|
||||
'supabase_functions',
|
||||
'supabase_migrations',
|
||||
'vault',
|
||||
'graphql',
|
||||
'graphql_public',
|
||||
QUEUES_SCHEMA,
|
||||
]
|
||||
|
||||
/**
|
||||
* Get the list of schemas used by IcebergFDWs
|
||||
*/
|
||||
const useIcebergFdwSchemasQuery = () => {
|
||||
const { project } = useProjectContext()
|
||||
const result = useFDWsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
const schemas = useMemo(() => {
|
||||
const icebergFDWs = result.data?.filter((wrapper) =>
|
||||
wrapperMetaComparator(
|
||||
{ handlerName: WRAPPER_HANDLERS.ICEBERG, server: { options: [] } },
|
||||
wrapper
|
||||
)
|
||||
)
|
||||
|
||||
const fdwSchemas = icebergFDWs
|
||||
?.map((fdw) => convertKVStringArrayToJson(fdw.server_options))
|
||||
.map((options) => options['supabase_target_schema'])
|
||||
.flatMap((s) => s?.split(','))
|
||||
.filter(Boolean)
|
||||
|
||||
return uniq(fdwSchemas)
|
||||
}, [result.data])
|
||||
|
||||
return { ...result, data: schemas }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of schemas that are protected by Supabase (internal schemas or schemas used by Iceberg FDWs).
|
||||
*/
|
||||
export const useProtectedSchemas = ({
|
||||
excludeSchemas = [],
|
||||
}: { excludeSchemas?: string[] } = {}) => {
|
||||
// Stabilize the excludeSchemas array to prevent unnecessary re-computations
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const stableExcludeSchemas = useMemo(() => excludeSchemas, [JSON.stringify(excludeSchemas)])
|
||||
|
||||
const result = useIcebergFdwSchemasQuery()
|
||||
|
||||
const schemas = useMemo<{ name: string; type: 'fdw' | 'internal' }[]>(() => {
|
||||
const internalSchemas = INTERNAL_SCHEMAS.map((s) => ({ name: s, type: 'internal' as const }))
|
||||
const icebergFdwSchemas = result.data?.map((s) => ({ name: s, type: 'fdw' as const }))
|
||||
|
||||
const schemas = uniq([...internalSchemas, ...icebergFdwSchemas])
|
||||
return schemas.filter((schema) => !stableExcludeSchemas.includes(schema.name))
|
||||
}, [result.data, stableExcludeSchemas])
|
||||
|
||||
return { ...result, data: schemas }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a given schema is protected by Supabase (internal schema or schema used by Iceberg FDWs).
|
||||
*/
|
||||
export const useIsProtectedSchema = ({
|
||||
schema,
|
||||
excludedSchemas = [],
|
||||
}: {
|
||||
schema: string
|
||||
excludedSchemas?: string[]
|
||||
}):
|
||||
| { isSchemaLocked: false; reason: undefined }
|
||||
| { isSchemaLocked: true; reason: 'fdw' | 'internal' } => {
|
||||
const { data: schemas } = useProtectedSchemas({ excludeSchemas: excludedSchemas })
|
||||
|
||||
const foundSchema = schemas.find((s) => s.name === schema)
|
||||
|
||||
if (foundSchema) {
|
||||
return {
|
||||
isSchemaLocked: true,
|
||||
reason: foundSchema.type,
|
||||
}
|
||||
}
|
||||
return { isSchemaLocked: false, reason: undefined }
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { QUEUES_SCHEMA } from 'data/database-queues/database-queues-toggle-postgrest-mutation'
|
||||
|
||||
/**
|
||||
* A list of system schemas that users should not interact with
|
||||
*/
|
||||
export const PROTECTED_SCHEMAS = [
|
||||
'auth',
|
||||
'cron',
|
||||
'extensions',
|
||||
'information_schema',
|
||||
'net',
|
||||
'pgsodium',
|
||||
'pgsodium_masks',
|
||||
'pgbouncer',
|
||||
'pgtle',
|
||||
'realtime',
|
||||
'storage',
|
||||
'supabase_functions',
|
||||
'supabase_migrations',
|
||||
'vault',
|
||||
'graphql',
|
||||
'graphql_public',
|
||||
QUEUES_SCHEMA,
|
||||
]
|
||||
|
||||
export const PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS = PROTECTED_SCHEMAS.filter(
|
||||
(x) => x !== 'extensions'
|
||||
)
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { partition } from 'lodash'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
@@ -18,11 +17,10 @@ import NoPermission from 'components/ui/NoPermission'
|
||||
import SchemaSelector from 'components/ui/SchemaSelector'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
|
||||
import { useSchemasQuery } from 'data/database/schemas-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
|
||||
import { useUrlState } from 'hooks/ui/useUrlState'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { Input } from 'ui'
|
||||
@@ -76,16 +74,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false)
|
||||
const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState<PostgresPolicy>()
|
||||
|
||||
const { data: schemas } = useSchemasQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const [protectedSchemas] = partition(
|
||||
schemas,
|
||||
(schema) => schema?.name !== 'realtime' && PROTECTED_SCHEMAS.includes(schema?.name ?? '')
|
||||
)
|
||||
const selectedSchema = schemas?.find((s) => s.name === schema)
|
||||
const isLocked = protectedSchemas.some((s) => s.id === selectedSchema?.id)
|
||||
const { isSchemaLocked } = useIsProtectedSchema({ schema: schema, excludedSchemas: ['realtime'] })
|
||||
|
||||
const { data: policies } = useDatabasePoliciesQuery({
|
||||
projectRef: project?.ref,
|
||||
@@ -151,7 +140,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
schema={schema}
|
||||
tables={filteredTables}
|
||||
hasTables={tables.length > 0}
|
||||
isLocked={isLocked}
|
||||
isLocked={isSchemaLocked}
|
||||
onSelectCreatePolicy={(table: string) => {
|
||||
if (isInlineEditorEnabled) {
|
||||
setEditorPanel({
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from 'components/interfaces/Database/Privileges/Privileges.utils'
|
||||
import PrivilegesHead from 'components/interfaces/Database/Privileges/PrivilegesHead'
|
||||
import PrivilegesTable from 'components/interfaces/Database/Privileges/PrivilegesTable'
|
||||
import ProtectedSchemaWarning from 'components/interfaces/Database/ProtectedSchemaWarning'
|
||||
import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning'
|
||||
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
@@ -30,7 +30,7 @@ import { useTablePrivilegesQuery } from 'data/privileges/table-privileges-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
|
||||
|
||||
@@ -132,7 +132,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
|
||||
const table = tableList?.find(
|
||||
(table) => table.schema === selectedSchema && table.name === selectedTable
|
||||
)
|
||||
const isLocked = PROTECTED_SCHEMAS.includes(selectedSchema)
|
||||
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
|
||||
|
||||
const {
|
||||
tableCheckedStates,
|
||||
@@ -286,7 +286,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
|
||||
)}
|
||||
|
||||
<PrivilegesHead
|
||||
disabled={isLocked}
|
||||
disabled={isSchemaLocked}
|
||||
selectedSchema={selectedSchema}
|
||||
selectedRole={selectedRole}
|
||||
selectedTable={table}
|
||||
@@ -300,7 +300,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
|
||||
hasChanges={hasChanges}
|
||||
isApplyingChanges={isApplyingChanges}
|
||||
/>
|
||||
{isLocked && (
|
||||
{isSchemaLocked && (
|
||||
<ProtectedSchemaWarning schema={selectedSchema} entity="column privileges" />
|
||||
)}
|
||||
{isLoading ? (
|
||||
@@ -310,7 +310,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
|
||||
) : table && tablePrivilege ? (
|
||||
<div>
|
||||
<PrivilegesTable
|
||||
disabled={isLocked}
|
||||
disabled={isSchemaLocked}
|
||||
columnPrivileges={columnPrivileges}
|
||||
tableCheckedStates={tableCheckedStates}
|
||||
columnCheckedStates={columnCheckedStates}
|
||||
|
||||
Reference in new issue
Block a user