From 04575b65b14588e9727b4a73aa47ee9023f5be22 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 2 May 2023 00:13:39 +1000 Subject: [PATCH 01/17] chore: remove pg meta schema store --- .../Extensions/EnableExtensionModal.tsx | 19 ++-- .../Functions/FunctionsList/FunctionList.tsx | 10 +-- .../Functions/FunctionsList/FunctionsList.tsx | 20 ++--- .../Publications/PublicationsTables.tsx | 13 +-- .../interfaces/Database/Tables/ColumnList.tsx | 20 +++-- .../interfaces/Database/Tables/TableList.tsx | 47 +++++----- .../Database/Wrappers/WrapperTableEditor.tsx | 16 ++-- .../Settings/API/PostgrestConfig.tsx | 32 +++---- .../interfaces/TableGridEditor/EmptyState.tsx | 19 ++-- .../ColumnEditor/ColumnEditor.tsx | 6 +- .../ForeignKeySelector/ForeignKeySelector.tsx | 27 ++++-- .../SidePanelEditor/SidePanelEditor.tsx | 3 - .../TableEditor/TableEditor.tsx | 44 ++++----- .../TableGridEditor/TableGridEditor.tsx | 21 +++-- .../layouts/AuthLayout/AuthLayout.tsx | 8 +- .../layouts/DatabaseLayout/DatabaseLayout.tsx | 17 +--- .../layouts/ProjectLayout/ProjectContext.tsx | 9 +- .../TableEditorLayout/TableEditorLayout.tsx | 11 +-- .../TableEditorLayout/TableEditorMenu.tsx | 46 +++++----- studio/data/database/keys.ts | 5 ++ studio/data/database/schemas-query.ts | 90 +++++++++---------- studio/lib/constants/schemas.ts | 20 +++++ studio/pages/project/[ref]/api/graphiql.tsx | 2 - studio/pages/project/[ref]/auth/policies.tsx | 25 ++++-- .../project/[ref]/database/hooks/index.tsx | 28 ++++-- .../pages/project/[ref]/database/tables.tsx | 4 - studio/pages/project/[ref]/editor/[id].tsx | 24 ++--- studio/pages/project/[ref]/editor/index.tsx | 10 +-- studio/pages/project/[ref]/settings/api.tsx | 4 - studio/pages/project/[ref]/sql/[id].tsx | 2 +- studio/state/table-editor-global.ts | 28 ++++++ studio/state/table-editor.ts | 28 ------ studio/state/table-editor.tsx | 30 +++++++ studio/stores/pgmeta/MetaStore.ts | 30 +------ studio/stores/pgmeta/SchemaStore.ts | 24 ----- 35 files changed, 378 insertions(+), 364 deletions(-) create mode 100644 studio/data/database/keys.ts create mode 100644 studio/lib/constants/schemas.ts create mode 100644 studio/state/table-editor-global.ts delete mode 100644 studio/state/table-editor.ts create mode 100644 studio/state/table-editor.tsx delete mode 100644 studio/stores/pgmeta/SchemaStore.ts diff --git a/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx b/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx index 9f569536579..f83d463e198 100644 --- a/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx +++ b/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx @@ -4,19 +4,25 @@ import type { PostgresExtension, PostgresSchema } from '@supabase/postgres-meta' import { useStore } from 'hooks' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useSchemasQuery } from 'data/database/schemas-query' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -interface Props { +export interface EnableExtensionModalProps { visible: boolean extension: PostgresExtension onCancel: () => void } -const EnableExtensionModal: FC = ({ visible, extension, onCancel }) => { +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: FC = ({ visible, extension, onCancel }) => { return () => { cancel = true } - }, [visible]) + }, [visible, extension.name]) const validate = (values: any) => { const errors: any = {} @@ -124,7 +130,7 @@ const EnableExtensionModal: FC = ({ visible, extension, onCancel }) => { return (
- {fetchingSchemaInfo ? ( + {fetchingSchemaInfo || isSchemasLoading ? (
@@ -156,8 +162,7 @@ const EnableExtensionModal: FC = ({ visible, extension, onCancel }) => { Create a new schema "{extension.name}" - {/* @ts-ignore */} - {schemas.map((schema: PostgresSchema) => { + {schemas?.map((schema: PostgresSchema) => { return ( void deleteFunction: (fn: any) => void } -const FunctionList: FC = ({ +const FunctionList = ({ schema, filterString, editFunction = () => {}, deleteFunction = () => {}, -}) => { +}: FunctionListProps) => { const router = useRouter() const { ui, meta } = useStore() - const functions = meta.functions.list((fn: any) => !meta.excludedSchemas.includes(fn.schema)) + const functions = meta.functions.list((fn: any) => !EXCLUDED_SCHEMAS.includes(fn.schema)) const filteredFunctions = functions.filter((x: any) => includes(x.name.toLowerCase(), filterString.toLowerCase()) ) diff --git a/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx index b5571b31790..7f5f13c4255 100644 --- a/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx +++ b/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx @@ -1,5 +1,5 @@ -import { FC, useState } from 'react' -import { uniqBy, map as lodashMap, includes } from 'lodash' +import { useState } from 'react' +import { uniqBy, map as lodashMap, includes, noop } from 'lodash' import { Button, IconSearch, IconLoader, Input } from 'ui' import { observer } from 'mobx-react-lite' import * as Tooltip from '@radix-ui/react-tooltip' @@ -7,26 +7,26 @@ import { PostgresFunction } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { checkPermissions, useStore } from 'hooks' -import AlphaPreview from 'components/to-be-cleaned/AlphaPreview' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import SchemaTable from './SchemaTable' -interface Props { +export interface FunctionsListProps { createFunction: () => void editFunction: (fn: PostgresFunction) => void deleteFunction: (fn: PostgresFunction) => void } -const FunctionsList: FC = ({ - createFunction = () => {}, - editFunction = () => {}, - deleteFunction = () => {}, -}) => { +const FunctionsList = ({ + createFunction = noop, + editFunction = noop, + deleteFunction = noop, +}: FunctionsListProps) => { const { meta } = useStore() const [filterString, setFilterString] = useState('') const functions = meta.functions.list( - (fn: PostgresFunction) => !meta.excludedSchemas.includes(fn.schema) + (fn: PostgresFunction) => !EXCLUDED_SCHEMAS.includes(fn.schema) ) const filteredFunctions = functions.filter((x: PostgresFunction) => includes(x.name?.toLowerCase(), filterString.toLowerCase()) diff --git a/studio/components/interfaces/Database/Publications/PublicationsTables.tsx b/studio/components/interfaces/Database/Publications/PublicationsTables.tsx index 73d2f67bbd5..ad2b4afd075 100644 --- a/studio/components/interfaces/Database/Publications/PublicationsTables.tsx +++ b/studio/components/interfaces/Database/Publications/PublicationsTables.tsx @@ -1,21 +1,22 @@ -import { FC, useState } from 'react' +import { useState } from 'react' import { observer } from 'mobx-react-lite' import { Button, Input, IconChevronLeft, IconSearch, IconAlertCircle } from 'ui' import type { PostgresPublication } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { checkPermissions, useStore } from 'hooks' -import PublicationsTableItem from './PublicationsTableItem' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' import Table from 'components/to-be-cleaned/Table' import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' import InformationBox from 'components/ui/InformationBox' +import PublicationsTableItem from './PublicationsTableItem' -interface Props { +export interface PublicationsTablesProps { selectedPublication: PostgresPublication onSelectBack: () => void } -const PublicationsTables: FC = ({ selectedPublication, onSelectBack }) => { +const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsTablesProps) => { const { meta } = useStore() const [filterString, setFilterString] = useState('') @@ -26,10 +27,10 @@ const PublicationsTables: FC = ({ selectedPublication, onSelectBack }) => const tables = filterString.length === 0 - ? meta.tables.list((table: any) => !meta.excludedSchemas.includes(table.schema)) + ? meta.tables.list((table: any) => !EXCLUDED_SCHEMAS.includes(table.schema)) : meta.tables.list( (table: any) => - !meta.excludedSchemas.includes(table.schema) && table.name.includes(filterString) + !EXCLUDED_SCHEMAS.includes(table.schema) && table.name.includes(filterString) ) // const publication = selectedPublication diff --git a/studio/components/interfaces/Database/Tables/ColumnList.tsx b/studio/components/interfaces/Database/Tables/ColumnList.tsx index 447e4a82750..e6fb58e8143 100644 --- a/studio/components/interfaces/Database/Tables/ColumnList.tsx +++ b/studio/components/interfaces/Database/Tables/ColumnList.tsx @@ -1,15 +1,17 @@ -import { FC, useState } from 'react' +import { useState } from 'react' import { observer } from 'mobx-react-lite' import * as Tooltip from '@radix-ui/react-tooltip' +import { noop } from 'lodash' import { Input, Button, IconSearch, IconPlus, IconChevronLeft, IconEdit3, IconTrash } from 'ui' import type { PostgresTable } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useStore, checkPermissions } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' import Table from 'components/to-be-cleaned/Table' import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' -interface Props { +export interface ColumnListProps { selectedTable: PostgresTable onSelectBack: () => void onAddColumn: () => void @@ -17,13 +19,13 @@ interface Props { onDeleteColumn: (column: any) => void } -const ColumnList: FC = ({ +const ColumnList = ({ selectedTable, - onSelectBack = () => {}, - onAddColumn = () => {}, - onEditColumn = () => {}, - onDeleteColumn = () => {}, -}) => { + onSelectBack = noop, + onAddColumn = noop, + onEditColumn = noop, + onDeleteColumn = noop, +}: ColumnListProps) => { const { meta } = useStore() const [filterString, setFilterString] = useState('') const columns = @@ -31,7 +33,7 @@ const ColumnList: FC = ({ ? 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 = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') return ( diff --git a/studio/components/interfaces/Database/Tables/TableList.tsx b/studio/components/interfaces/Database/Tables/TableList.tsx index bfb97d7e2ac..5a3ed7209f7 100644 --- a/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/studio/components/interfaces/Database/Tables/TableList.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react' +import { useState } from 'react' import { observer } from 'mobx-react-lite' import { Button, @@ -12,45 +12,50 @@ import { IconLock, IconCheck, } from 'ui' -import { partition } from 'lodash' +import { noop, partition } from 'lodash' import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' +import type { PostgresTable, PostgresSchema } from '@supabase/postgres-meta' import { useStore, checkPermissions } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { useTableEditorStateSnapshot } from 'state/table-editor' import Table from 'components/to-be-cleaned/Table' import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' -import type { PostgresTable, PostgresSchema } from '@supabase/postgres-meta' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useSchemasQuery } from 'data/database/schemas-query' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -interface Props { - selectedSchema: string - onSelectSchema: (schema: string) => void +export interface TableListProps { onAddTable: () => void onEditTable: (table: any) => void onDeleteTable: (table: any) => void onOpenTable: (table: any) => void } -const TableList: FC = ({ - selectedSchema, - onSelectSchema = () => {}, - onAddTable = () => {}, - onEditTable = () => {}, - onDeleteTable = () => {}, - onOpenTable = () => {}, -}) => { +const TableList = ({ + onAddTable = noop, + onEditTable = noop, + onDeleteTable = noop, + onOpenTable = noop, +}: TableListProps) => { + const { project } = useProjectContext() + const snap = useTableEditorStateSnapshot() const { meta } = useStore() const { isLoading } = meta.tables const [filterString, setFilterString] = useState('') const canUpdateTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') - const schemas: PostgresSchema[] = meta.schemas.list() + const { data: schemas } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) const [protectedSchemas, openSchemas] = partition(schemas, (schema) => - meta.excludedSchemas.includes(schema?.name ?? '') + EXCLUDED_SCHEMAS.includes(schema?.name ?? '') ) const allTables: PostgresTable[] = meta.tables.list( - (table: PostgresTable) => table.schema === selectedSchema + (table: PostgresTable) => table.schema === snap.selectedSchemaName ) const tables = filterString.length === 0 @@ -62,8 +67,8 @@ const TableList: FC = ({ 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 ( @@ -73,8 +78,8 @@ const TableList: FC = ({
} > diff --git a/studio/components/interfaces/Database/Wrappers/WrapperTableEditor.tsx b/studio/components/interfaces/Database/Wrappers/WrapperTableEditor.tsx index 7aa5d2fefad..fecbd3ab716 100644 --- a/studio/components/interfaces/Database/Wrappers/WrapperTableEditor.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrapperTableEditor.tsx @@ -1,9 +1,10 @@ import { useEffect, useState } from 'react' import { Form, IconDatabase, Input, Listbox, SidePanel, Modal, IconPlus } from 'ui' -import { useStore } from 'hooks' +import { useSchemasQuery } from 'data/database/schemas-query' +import ActionBar from 'components/interfaces/TableGridEditor/SidePanelEditor/ActionBar' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { Table, TableOption } from './Wrappers.types' import { makeValidateRequired } from './Wrappers.utils' -import ActionBar from 'components/interfaces/TableGridEditor/SidePanelEditor/ActionBar' export type WrapperTableEditorProps = { visible: boolean @@ -126,8 +127,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) ?? @@ -173,8 +177,8 @@ const TableForm = ({ Create a new schema - {/* @ts-ignore */} - {schemas.map((schema: PostgresSchema) => { + + {schemas?.map((schema) => { return ( = ({}) => { - const PageState: any = useContext(PageContext) +const PostgrestConfig = () => { + const { project } = useProjectContext() const { ui } = useStore() - const { meta } = PageState const { ref: projectRef } = useParams() const formId = 'project-postgres-config' const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef }) + const { data: schemas } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const initialValues = { db_schema: '', max_rows: '', @@ -68,15 +71,12 @@ const PostgrestConfig: FC = ({}) => { 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, diff --git a/studio/components/interfaces/TableGridEditor/EmptyState.tsx b/studio/components/interfaces/TableGridEditor/EmptyState.tsx index 3667bf1e4ef..8aeb7128028 100644 --- a/studio/components/interfaces/TableGridEditor/EmptyState.tsx +++ b/studio/components/interfaces/TableGridEditor/EmptyState.tsx @@ -1,19 +1,18 @@ -import { FC } from 'react' -import { observer } from 'mobx-react-lite' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { checkPermissions, useLocalStorage, useStore } from 'hooks' +import { checkPermissions, useLocalStorage } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { useTableEditorStateSnapshot } from 'state/table-editor' import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -interface Props { - selectedSchema: string +export interface EmptyStateProps { onAddTable: () => void } -const EmptyState: FC = ({ selectedSchema, onAddTable }) => { - const { meta } = useStore() - const isProtectedSchema = meta.excludedSchemas.includes(selectedSchema) +const EmptyState = ({ onAddTable }: EmptyStateProps) => { + const snap = useTableEditorStateSnapshot() + const isProtectedSchema = EXCLUDED_SCHEMAS.includes(snap.selectedSchemaName) const canCreateTables = !isProtectedSchema && checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') @@ -27,7 +26,7 @@ const EmptyState: FC = ({ selectedSchema, onAddTable }) => { { projectRef: project?.ref, connectionString: project?.connectionString, - schema: selectedSchema, + schema: snap.selectedSchemaName, sort, }, { @@ -65,4 +64,4 @@ const EmptyState: FC = ({ selectedSchema, onAddTable }) => { ) } -export default observer(EmptyState) +export default EmptyState diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx index a82e5d41a4d..0710c1c6415 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx @@ -12,6 +12,7 @@ import type { import { useFlag, useStore } from 'hooks' import { useParams } from 'common/hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' import ActionBar from '../ActionBar' import HeaderTitle from './HeaderTitle' import ColumnType from './ColumnType' @@ -35,7 +36,6 @@ import { } from '../SidePanelEditor.types' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms' import { EncryptionKeySelector } from 'components/interfaces/Settings/Vault' - import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query' @@ -84,9 +84,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' diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx index d40c56df3ee..aaa15bec5fe 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx @@ -1,19 +1,21 @@ -import React, { FC, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { get, find, isEmpty, sortBy } from 'lodash' import { Dictionary } from 'components/grid' -import { SidePanel, Input, Listbox, IconHelpCircle, IconDatabase, Toggle } from 'ui' +import { SidePanel, Input, Listbox, IconHelpCircle, IconDatabase } from 'ui' import type { PostgresTable, PostgresColumn, PostgresSchema } from '@supabase/postgres-meta' import { useStore } from 'hooks' +import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants' +import { useSchemasQuery } from 'data/database/schemas-query' +import InformationBox from 'components/ui/InformationBox' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import ActionBar from '../ActionBar' import { ForeignKey } from './ForeignKeySelector.types' import { ColumnField } from '../SidePanelEditor.types' -import InformationBox from 'components/ui/InformationBox' -import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants' import { FOREIGN_KEY_DELETION_OPTIONS } from './ForeignKeySelector.constants' import { generateDeletionActionDescription } from './ForeignKeySelector.utils' -interface Props { +export interface ForeignKeySelectorProps { column: ColumnField metadata?: any visible: boolean @@ -23,7 +25,13 @@ interface Props { ) => void } -const ForeignKeySelector: FC = ({ column, visible = false, closePanel, saveChanges }) => { +const ForeignKeySelector = ({ + column, + visible = false, + closePanel, + saveChanges, +}: ForeignKeySelectorProps) => { + const { project } = useProjectContext() const { meta } = useStore() const [errors, setErrors] = useState({}) const [selectedForeignKey, setSelectedForeignKey] = useState({ @@ -33,7 +41,10 @@ const ForeignKeySelector: FC = ({ column, visible = false, closePanel, sa deletionAction: FOREIGN_KEY_DELETION_ACTION.NO_ACTION, }) - const schemas = meta.schemas.list() + const { data: schemas } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) const tables = meta.tables.list( (table: PostgresTable) => table.schema === selectedForeignKey.schema ) @@ -190,7 +201,7 @@ const ForeignKeySelector: FC = ({ column, visible = false, closePanel, sa error={errors.schema} onChange={(value: string) => updateSelectedSchema(value)} > - {schemas.map((schema: PostgresSchema) => { + {schemas?.map((schema: PostgresSchema) => { return ( selectedColumnToEdit?: PostgresColumn @@ -51,7 +50,6 @@ export interface SidePanelEditorProps { } const SidePanelEditor = ({ - selectedSchema, selectedTable, selectedRowToEdit, selectedColumnToEdit, @@ -551,7 +549,6 @@ const SidePanelEditor = ({ )} void @@ -45,22 +46,20 @@ interface Props { updateEditorDirty: () => void } -const TableEditor: FC = ({ +const TableEditor = ({ table, - selectedSchema, isDuplicating, visible = false, - closePanel = () => {}, - saveChanges = () => {}, - updateEditorDirty = () => {}, -}) => { + 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: FC = ({ 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,13 @@ const TableEditor: FC = ({ size="large" key="TableEditor" visible={visible} - // @ts-ignore - header={} + header={ + + } className={`transition-all duration-100 ease-in ${isImportingSpreadsheet ? ' mr-32' : ''}`} onCancel={closePanel} onConfirm={() => (resolve: () => void) => onSaveChanges(resolve)} @@ -297,7 +301,7 @@ const TableEditor: FC = ({
{!isDuplicating && ( { + const { project } = useProjectContext() + const snap = useTableEditorStateSnapshot() const { meta, ui, vault } = useStore() const router = useRouter() const { ref: projectRef, id } = useParams() const gridRef = useRef(null) - const { project } = useProjectContext() const isVaultEnabled = useFlag('vaultExtension') const [encryptedColumns, setEncryptedColumns] = useState([]) const [apiPreviewPanelOpen, setApiPreviewPanelOpen] = useState(false) @@ -213,7 +213,7 @@ const TableGridEditor = ({ 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 canUpdateTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') const canEditViaTableEditor = isTableSelected && !isLocked @@ -411,9 +411,8 @@ const TableGridEditor = ({ {(isViewSelected || isTableSelected) && } - {!isUndefined(selectedSchema) && ( + {!isUndefined(snap.selectedSchemaName) && ( = ({ title, children }) => { +const AuthLayout = ({ title, children }: PropsWithChildren) => { const { ui, meta } = useStore() const { isInitialized, isLoading, error } = meta.tables const projectRef = ui.selectedProject?.ref ?? 'default' @@ -28,7 +27,6 @@ const AuthLayout: FC = ({ title, children }) => { meta.policies.load() meta.tables.load() meta.roles.load() - meta.schemas.load() } }, [ui.selectedProject?.ref]) diff --git a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index deb9fb8ab91..b38073847c2 100644 --- a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -16,8 +16,7 @@ interface Props { const DatabaseLayout: FC = ({ title, children }) => { const { meta, ui, vault, backups } = useStore() - const { isLoading } = meta.schemas - const { isInitialized, error } = meta.tables + const { error } = meta.tables const project = ui.selectedProject const router = useRouter() @@ -27,12 +26,8 @@ const DatabaseLayout: FC = ({ title, children }) => { const isVaultEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null const foreignDataWrappersEnabled = useFlag('foreignDataWrappers') - const [loaded, setLoaded] = useState(isInitialized) - useEffect(() => { if (ui.selectedProject?.ref) { - // Eventually should only load the required stores based on the pages - meta.schemas.load() meta.tables.load() meta.roles.load() @@ -52,14 +47,6 @@ const DatabaseLayout: FC = ({ title, children }) => { } }, [ui.selectedProject?.ref, 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 ( @@ -70,7 +57,7 @@ const DatabaseLayout: FC = ({ title, children }) => { return ( diff --git a/studio/components/layouts/ProjectLayout/ProjectContext.tsx b/studio/components/layouts/ProjectLayout/ProjectContext.tsx index 8fc28f52926..d8af63e6159 100644 --- a/studio/components/layouts/ProjectLayout/ProjectContext.tsx +++ b/studio/components/layouts/ProjectLayout/ProjectContext.tsx @@ -1,6 +1,7 @@ import { useParams } from 'common/hooks' import { useStore } from 'hooks' import { PROJECT_STATUS } from 'lib/constants' +import { TableEditorStateContextProvider } from 'state/table-editor' import { observer } from 'mobx-react-lite' import { createContext, PropsWithChildren, useContext, useMemo } from 'react' import { Project } from 'types' @@ -43,7 +44,13 @@ export const ProjectContextProvider = observer( } }, [ui.selectedProject?.ref, projectRef]) - return {children} + return ( + + + {children} + + + ) } ) diff --git a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx index af9d3c9d067..8ab79999ccf 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -8,7 +8,7 @@ import { checkPermissions, useStore } from 'hooks' import { useParams } from 'common/hooks' import { Entity } from 'data/entity-types/entity-type-query' import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' -import { useIsTableLoaded, useTableEditorStateSnapshot } from 'state/table-editor' +import { useIsTableLoaded, useTableEditorGlobalStateSnapshot } from 'state/table-editor-global' import ProjectLayout from '../' import TableEditorMenu from './TableEditorMenu' import NoPermission from 'components/ui/NoPermission' @@ -18,8 +18,6 @@ import useLatest from 'hooks/misc/useLatest' import useTableRowsPrefetchWrapper from './TableEditorLayout.utils' export interface TableEditorLayoutProps { - selectedSchema?: string - onSelectSchema: (schema: string) => void onAddTable: () => void onEditTable: (table: Entity) => void onDeleteTable: (table: Entity) => void @@ -27,8 +25,6 @@ export interface TableEditorLayoutProps { } const TableEditorLayout = ({ - selectedSchema, - onSelectSchema = noop, onAddTable = noop, onEditTable = noop, onDeleteTable = noop, @@ -40,7 +36,7 @@ const TableEditorLayout = ({ const { ref, id: _id } = useParams() const id = _id ? Number(_id) : undefined - const snap = useTableEditorStateSnapshot() + const snap = useTableEditorGlobalStateSnapshot() const canReadTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables') const vaultExtension = meta.extensions.byId('supabase_vault') @@ -48,7 +44,6 @@ const TableEditorLayout = ({ useEffect(() => { if (ui.selectedProject?.ref) { - meta.schemas.load() meta.types.load() meta.policies.load() meta.publications.load() @@ -125,8 +120,6 @@ const TableEditorLayout = ({ product="Table editor" productMenu={ void onAddTable: () => void onEditTable: (table: Entity) => void onDeleteTable: (table: Entity) => void @@ -37,15 +36,13 @@ export interface TableEditorMenuProps { } const TableEditorMenu = ({ - selectedSchema, - onSelectSchema = noop, onAddTable = noop, onEditTable = noop, onDeleteTable = noop, onDuplicateTable = noop, }: TableEditorMenuProps) => { - const { meta } = useStore() const { id } = useParams() + const snap = useTableEditorStateSnapshot() const [searchText, setSearchText] = useState('') const [sort, setSort] = useLocalStorage<'alphabetical' | 'grouped-alphabetical'>( @@ -67,7 +64,7 @@ const TableEditorMenu = ({ { projectRef: project?.ref, connectionString: project?.connectionString, - schema: selectedSchema, + schema: snap.selectedSchemaName, search: searchText || undefined, sort, }, @@ -82,19 +79,24 @@ const TableEditorMenu = ({ [data?.pages] ) - const schemas: PostgresSchema[] = meta.schemas.list() + const { + data: schemas, + isLoading: isSchemasLoading, + isSuccess: isSchemasSuccess, + } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) - const schema = schemas.find((schema) => schema.name === selectedSchema) + const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName) const canCreateTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') - const isLoadingTableMetadata = id ? !meta.tables.byId(id) : true - 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,18 +107,20 @@ const TableEditorMenu = ({ > {/* Schema selection dropdown */}
- {!meta.schemas.isInitialized ? ( + {isSchemasLoading && (
Loading schemas...
- ) : ( + )} + + {isSchemasSuccess && ( { setSearchText('') - onSelectSchema(name) + snap.setSelectedSchemaName(name) }} > 28} hasNextPage={hasNextPage} @@ -362,4 +366,4 @@ const TableEditorMenu = ({ ) } -export default observer(TableEditorMenu) +export default TableEditorMenu diff --git a/studio/data/database/keys.ts b/studio/data/database/keys.ts new file mode 100644 index 00000000000..b1ae0abf417 --- /dev/null +++ b/studio/data/database/keys.ts @@ -0,0 +1,5 @@ +export const databaseKeys = { + schemaList: (projectRef: string | undefined) => ['projects', projectRef, 'schemas'] as const, + schema: (projectRef: string | undefined, id: string | undefined) => + ['projects', projectRef, 'schemas', id] as const, +} diff --git a/studio/data/database/schemas-query.ts b/studio/data/database/schemas-query.ts index 964b1228f54..0476e211b3f 100644 --- a/studio/data/database/schemas-query.ts +++ b/studio/data/database/schemas-query.ts @@ -1,60 +1,50 @@ -import { useCallback } from 'react' -import { UseQueryOptions } from '@tanstack/react-query' -import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query' - -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 { PostgresSchema } from '@supabase/postgres-meta' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { databaseKeys } from './keys' export type SchemasVariables = { projectRef?: string connectionString?: string } -export type SchemasData = { result: Schema[] } +export type Schema = PostgresSchema + +export type SchemasResponse = Schema[] | { error?: any } + +export async function getSchemas( + { projectRef, connectionString }: SchemasVariables, + 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}/schemas`, { + headers: Object.fromEntries(headers), + signal, + })) as SchemasResponse + + if (!Array.isArray(response) && response.error) { + throw response.error + } + + return response as PostgresSchema[] +} + +export type SchemasData = Awaited> export type SchemasError = unknown -export const useSchemasQuery = ( +export const useSchemasQuery = ( { projectRef, connectionString }: SchemasVariables, - options: UseQueryOptions = {} -) => { - return useExecuteSqlQuery( - { - projectRef, - connectionString, - sql: getSchemasQuery(), - queryKey: ['schemas'], - }, - options + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + databaseKeys.schemaList(projectRef), + ({ signal }) => getSchemas({ projectRef, connectionString }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } ) -} - -export const useSchemasPrefetch = () => { - const prefetch = useExecuteSqlPrefetch() - - return useCallback( - ({ projectRef, connectionString }: SchemasVariables) => - prefetch({ - projectRef, - connectionString, - sql: getSchemasQuery(), - queryKey: ['schemas'], - }), - [prefetch] - ) -} diff --git a/studio/lib/constants/schemas.ts b/studio/lib/constants/schemas.ts new file mode 100644 index 00000000000..5531f5b32cb --- /dev/null +++ b/studio/lib/constants/schemas.ts @@ -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', + 'realtime', + 'storage', + 'supabase_functions', + 'vault', + 'graphql', + 'graphql_public', + 'pgtle', + 'supabase_migrations', +] diff --git a/studio/pages/project/[ref]/api/graphiql.tsx b/studio/pages/project/[ref]/api/graphiql.tsx index 95492f7a2d1..8c8b02807bb 100644 --- a/studio/pages/project/[ref]/api/graphiql.tsx +++ b/studio/pages/project/[ref]/api/graphiql.tsx @@ -33,8 +33,6 @@ const GraphiQLPage: NextPageWithLayout = () => { useEffect(() => { if (ui.selectedProject?.ref) { - // Schemas may be needed when enabling the GraphQL extension - meta.schemas.load() meta.extensions.load() } }, [ui.selectedProject?.ref]) diff --git a/studio/pages/project/[ref]/auth/policies.tsx b/studio/pages/project/[ref]/auth/policies.tsx index 82bf69f90b2..7ae77e49e06 100644 --- a/studio/pages/project/[ref]/auth/policies.tsx +++ b/studio/pages/project/[ref]/auth/policies.tsx @@ -11,6 +11,10 @@ import { useParams } from 'common/hooks' import { AuthLayout } from 'components/layouts' import { Policies } from 'components/interfaces/Auth/Policies' import NoPermission from 'components/ui/NoPermission' +import { useSchemasQuery } from 'data/database/schemas-query' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useTableEditorStateSnapshot } from 'state/table-editor' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' /** * Filter tables by table name and policy name @@ -48,26 +52,31 @@ const onFilterTables = ( } const AuthPoliciesPage: NextPageWithLayout = () => { + const { project } = useProjectContext() + const snap = useTableEditorStateSnapshot() const { meta } = useStore() const { search } = useParams() - const [selectedSchema, setSelectedSchema] = useState('public') const [searchString, setSearchString] = useState('') useEffect(() => { if (search) setSearchString(search) }, [search]) - const schemas = meta.schemas.list() + const { data: schemas } = 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 tables = meta.tables.list( + (table: { schema: string }) => table.schema === snap.selectedSchemaName + ) const filteredTables = onFilterTables(tables, policies, searchString) const canReadPolicies = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'policies') @@ -84,9 +93,9 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
{ - setSelectedSchema(schema) + snap.setSelectedSchemaName(schema) setSearchString('') }} icon={isLocked && } diff --git a/studio/pages/project/[ref]/database/hooks/index.tsx b/studio/pages/project/[ref]/database/hooks/index.tsx index 4981cae2e65..1aa5b8371f1 100644 --- a/studio/pages/project/[ref]/database/hooks/index.tsx +++ b/studio/pages/project/[ref]/database/hooks/index.tsx @@ -1,12 +1,12 @@ import clsx from 'clsx' import { IconLoader } from 'ui' import { useEffect, useState } from 'react' -import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' import { PermissionAction } from '@supabase/shared-types/out/constants' import { NextPageWithLayout } from 'types' import { checkPermissions, useStore } from 'hooks' +import { useParams } from 'common' import { API_URL } from 'lib/constants' import { post } from 'lib/common/fetch' import { DatabaseLayout } from 'components/layouts' @@ -15,20 +15,30 @@ import HooksList from 'components/interfaces/Database/Hooks/HooksList/HooksList' import DeleteHookModal from 'components/interfaces/Database/Hooks/DeleteHookModal' import EditHookPanel from 'components/interfaces/Database/Hooks/EditHookPanel' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' +import { useSchemasQuery } from 'data/database/schemas-query' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' const HooksPage: NextPageWithLayout = () => { + const { project } = useProjectContext() const { meta, ui } = useStore() - const router = useRouter() - const { ref } = router.query - const schemas = meta.schemas.list() - const { isLoading: isLoadingSchemas } = meta.schemas + const { ref: projectRef } = useParams() + + const { + data: schemas, + isLoading: isSchemasLoading, + refetch, + } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) const [selectedHook, setSelectedHook] = useState() const [showCreateHookForm, setShowCreateHookForm] = useState(false) const [showDeleteHookForm, setShowDeleteHookForm] = useState(false) - const isHooksEnabled = schemas.some((schema: any) => schema.name === 'supabase_functions') + const isHooksEnabled = + schemas?.some((schema: any) => schema.name === 'supabase_functions') ?? false const canReadWebhooks = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers') const canCreateWebhooks = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers') @@ -37,14 +47,14 @@ const HooksPage: NextPageWithLayout = () => { }, [ui.selectedProject?.ref]) const enableHooksForProject = async () => { - const res = await post(`${API_URL}/database/${ref}/hook-enable`, {}) + const res = await post(`${API_URL}/database/${projectRef}/hook-enable`, {}) if (res.error) { ui.setNotification({ category: 'error', message: `Failed to enable webhooks: ${res.error.message}`, }) } else { - meta.schemas.load() + refetch() ui.setNotification({ category: 'success', message: `Successfully enabled webhooks`, @@ -71,7 +81,7 @@ const HooksPage: NextPageWithLayout = () => { return } - if (isLoadingSchemas) { + if (isSchemasLoading) { return (
diff --git a/studio/pages/project/[ref]/database/tables.tsx b/studio/pages/project/[ref]/database/tables.tsx index 74bfaa45637..60d7a31acd1 100644 --- a/studio/pages/project/[ref]/database/tables.tsx +++ b/studio/pages/project/[ref]/database/tables.tsx @@ -23,7 +23,6 @@ const DatabaseTables: NextPageWithLayout = () => { const queryClient = useQueryClient() const [isDeleting, setIsDeleting] = useState(false) - const [selectedSchema, setSelectedSchema] = useState('public') const [selectedTable, setSelectedTable] = useState() const [sidePanelKey, setSidePanelKey] = useState<'column' | 'table'>() @@ -147,8 +146,6 @@ const DatabaseTables: NextPageWithLayout = () => {
{isUndefined(selectedTable) ? ( { { const router = useRouter() @@ -32,11 +33,11 @@ const TableEditorPage: NextPageWithLayout = () => { const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] }) const { project } = useProjectContext() + const snap = useTableEditorStateSnapshot() const queryClient = useQueryClient() const { meta, ui } = useStore() - const [selectedSchema, setSelectedSchema] = useState() const [isDeleting, setIsDeleting] = useState(false) const [isDuplicating, setIsDuplicating] = useState(false) @@ -75,11 +76,11 @@ const TableEditorPage: NextPageWithLayout = () => { .find((table) => table.id === Number(id)) : undefined - useEffect(() => { - if (selectedTable && 'schema' in selectedTable) { - setSelectedSchema(selectedTable.schema) - } - }, [selectedTable?.name]) + // useEffect(() => { + // if (selectedTable && 'schema' in selectedTable) { + // setSelectedSchema(selectedTable.schema) + // } + // }, [selectedTable?.name]) const onAddRow = () => { setSidePanelKey('row') @@ -221,7 +222,7 @@ const TableEditorPage: NextPageWithLayout = () => { ), ]) - if (selectedSchema) await meta.views.loadBySchema(selectedSchema) + if (snap.selectedSchemaName) await meta.views.loadBySchema(snap.selectedSchemaName) } catch (error: any) { ui.setNotification({ category: 'error', @@ -240,7 +241,9 @@ const TableEditorPage: NextPageWithLayout = () => { 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) + const tables = meta.tables.list( + (table: PostgresTable) => table.schema === snap.selectedSchemaName + ) await queryClient.invalidateQueries(entityTypeKeys.list(projectRef)) @@ -254,7 +257,7 @@ const TableEditorPage: NextPageWithLayout = () => { category: 'success', message: `Successfully deleted table "${selectedTableToDelete.name}"`, }) - if (selectedSchema) await meta.views.loadBySchema(selectedSchema) + if (snap.selectedSchemaName) await meta.views.loadBySchema(snap.selectedSchemaName) } catch (error: any) { ui.setNotification({ error, @@ -269,15 +272,12 @@ const TableEditorPage: NextPageWithLayout = () => { return ( { + const snap = useTableEditorStateSnapshot() const { meta, ui } = useStore() const projectRef = ui.selectedProject?.ref const [sidePanelKey, setSidePanelKey] = useState<'row' | 'column' | 'table'>() const [isDeleting, setIsDeleting] = useState(false) const [isDuplicating, setIsDuplicating] = useState(false) - const [selectedSchema, setSelectedSchema] = useState('public') const [selectedTableToEdit, setSelectedTableToEdit] = useState() const [selectedTableToDelete, setSelectedTableToDelete] = useState() @@ -63,7 +64,7 @@ const TableEditorPage: NextPageWithLayout = () => { category: 'success', message: `Successfully deleted ${selectedTableToDelete!.name}`, }) - await meta.views.loadBySchema(selectedSchema) + await meta.views.loadBySchema(snap.selectedSchemaName) } catch (error: any) { ui.setNotification({ category: 'error', @@ -74,14 +75,12 @@ const TableEditorPage: NextPageWithLayout = () => { return ( - + {/* On this page it'll only handle tables */} { onSelectConfirm={onConfirmDeleteTable} /> { PageContext.project = project if (meta) PageState.meta = meta - useEffect(() => { - if (project?.ref) meta.schemas.load() - }, [project?.ref]) - return (
diff --git a/studio/pages/project/[ref]/sql/[id].tsx b/studio/pages/project/[ref]/sql/[id].tsx index 538f48a21ca..2f052c32ae9 100644 --- a/studio/pages/project/[ref]/sql/[id].tsx +++ b/studio/pages/project/[ref]/sql/[id].tsx @@ -65,7 +65,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 } diff --git a/studio/state/table-editor-global.ts b/studio/state/table-editor-global.ts new file mode 100644 index 00000000000..f8d1ae7e04d --- /dev/null +++ b/studio/state/table-editor-global.ts @@ -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([]), + 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[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) +} diff --git a/studio/state/table-editor.ts b/studio/state/table-editor.ts deleted file mode 100644 index 99fe3309946..00000000000 --- a/studio/state/table-editor.ts +++ /dev/null @@ -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([]), - 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[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) -} diff --git a/studio/state/table-editor.tsx b/studio/state/table-editor.tsx new file mode 100644 index 00000000000..94135e0d035 --- /dev/null +++ b/studio/state/table-editor.tsx @@ -0,0 +1,30 @@ +import { PropsWithChildren, createContext, useContext, useRef } from 'react' +import { proxy, useSnapshot } from 'valtio' + +export const createTableEditorState = () => { + const state = proxy({ + selectedSchemaName: 'public', + setSelectedSchemaName: (schemaName: string) => { + state.selectedSchemaName = schemaName + }, + }) + + return state +} + +export type TableEditorState = ReturnType + +export const TableEditorStateContext = createContext(createTableEditorState()) + +export const TableEditorStateContextProvider = ({ children }: PropsWithChildren<{}>) => { + const state = useRef(createTableEditorState()).current + + return ( + {children} + ) +} + +export const useTableEditorStateSnapshot = (options?: Parameters[1]) => { + const state = useContext(TableEditorStateContext) + return useSnapshot(state, options) +} diff --git a/studio/stores/pgmeta/MetaStore.ts b/studio/stores/pgmeta/MetaStore.ts index f8e9dcdd1f1..b3afae54e0d 100644 --- a/studio/stores/pgmeta/MetaStore.ts +++ b/studio/stores/pgmeta/MetaStore.ts @@ -18,7 +18,6 @@ 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' @@ -51,12 +50,9 @@ const BATCH_SIZE = 1000 const CHUNK_SIZE = 1024 * 1024 * 0.1 // 0.1MB export interface IMetaStore { - excludedSchemas: string[] - openApi: IOpenApiStore tables: ITableStore columns: IPostgresMetaInterface - schemas: IPostgresMetaInterface views: IViewStore materializedViews: IMaterializedViewStore foreignTables: IForeignTableStore @@ -150,7 +146,6 @@ export default class MetaStore implements IMetaStore { openApi: OpenApiStore tables: TableStore columns: ColumnStore - schemas: SchemaStore views: ViewStore materializedViews: MaterializedViewStore foreignTables: ForeignTableStore @@ -169,23 +164,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', - 'realtime', - 'storage', - 'supabase_functions', - 'vault', - 'graphql', - 'graphql_public', - ] - constructor(rootStore: IRootStore, options: { projectRef: string; connectionString?: string }) { const { projectRef, connectionString } = options this.rootStore = rootStore @@ -204,7 +182,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, @@ -237,9 +214,7 @@ export default class MetaStore implements IMetaStore { ) this.types = new TypesStore(this.rootStore, `${this.baseUrl}/types`, this.headers) - makeObservable(this, { - excludedSchemas: observable, - }) + makeObservable(this) } /** @@ -989,9 +964,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) diff --git a/studio/stores/pgmeta/SchemaStore.ts b/studio/stores/pgmeta/SchemaStore.ts deleted file mode 100644 index 2d9a0cc38bf..00000000000 --- a/studio/stores/pgmeta/SchemaStore.ts +++ /dev/null @@ -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 { - 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) - } -} From 3c6a28d9c89c0d665b61676cbacb0114f1a336be Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 2 May 2023 00:19:13 +1000 Subject: [PATCH 02/17] restore isLoadingTableMetadata in TableEditorMenu --- .../layouts/DatabaseLayout/DatabaseLayout.tsx | 1 - .../layouts/TableEditorLayout/TableEditorMenu.tsx | 10 +++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index b38073847c2..5c7ae50b811 100644 --- a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -57,7 +57,6 @@ const DatabaseLayout: FC = ({ title, children }) => { return ( diff --git a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index 037272466ac..a4a3ee92cf6 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from 'react' import { noop, partition } from 'lodash' +import { observer } from 'mobx-react-lite' import { Button, Dropdown, @@ -19,7 +20,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common/hooks' import { useTableEditorStateSnapshot } from 'state/table-editor' -import { checkPermissions, useLocalStorage } from 'hooks' +import { checkPermissions, useLocalStorage, useStore } from 'hooks' import InfiniteList from 'components/ui/InfiniteList' import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query' import { Entity } from 'data/entity-types/entity-type-query' @@ -41,6 +42,7 @@ const TableEditorMenu = ({ onDeleteTable = noop, onDuplicateTable = noop, }: TableEditorMenuProps) => { + const { meta } = useStore() const { id } = useParams() const snap = useTableEditorStateSnapshot() @@ -91,6 +93,8 @@ const TableEditorMenu = ({ const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName) const canCreateTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const isLoadingTableMetadata = id ? !meta.tables.byId(id) : true + const refreshTables = async () => { await refetch() } @@ -352,7 +356,7 @@ const TableEditorMenu = ({ onEditTable, onDeleteTable, onDuplicateTable, - isLoadingTableMetadata: false, // TODO(alaister) + isLoadingTableMetadata, }} getItemSize={() => 28} hasNextPage={hasNextPage} @@ -366,4 +370,4 @@ const TableEditorMenu = ({ ) } -export default TableEditorMenu +export default observer(TableEditorMenu) From 3b93964cd13c328cd5b7294b3557ec3c88f90082 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 2 May 2023 00:35:11 +1000 Subject: [PATCH 03/17] fix tests --- studio/components/interfaces/Database/Tables/TableList.tsx | 3 +-- studio/stores/pgmeta/MetaStore.ts | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/studio/components/interfaces/Database/Tables/TableList.tsx b/studio/components/interfaces/Database/Tables/TableList.tsx index 5a3ed7209f7..a63291cbdd7 100644 --- a/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/studio/components/interfaces/Database/Tables/TableList.tsx @@ -50,7 +50,7 @@ const TableList = ({ projectRef: project?.ref, connectionString: project?.connectionString, }) - const [protectedSchemas, openSchemas] = partition(schemas, (schema) => + const [protectedSchemas, openSchemas] = partition(schemas ?? [], (schema) => EXCLUDED_SCHEMAS.includes(schema?.name ?? '') ) @@ -85,7 +85,6 @@ const TableList = ({

Schemas

- {/* @ts-ignore */} {openSchemas.map((schema) => ( Date: Tue, 2 May 2023 18:41:02 +1000 Subject: [PATCH 04/17] adds react-query queries for table-like objects --- .../TableGridEditor/TableGridEditor.tsx | 7 ++ .../TableEditorLayout/TableEditorLayout.tsx | 64 +--------------- .../foreign-tables/foreign-table-query.ts | 57 ++++++++++++++ .../foreign-tables/foreign-tables-query.ts | 49 ++++++++++++ studio/data/foreign-tables/keys.ts | 5 ++ studio/data/materialized-views/keys.ts | 5 ++ .../materialized-view-query.ts | 60 +++++++++++++++ .../materialized-views-query.ts | 52 +++++++++++++ studio/data/tables/keys.ts | 5 ++ studio/data/tables/table-query.ts | 57 ++++++++++++++ studio/data/tables/tables-query.ts | 49 ++++++++++++ studio/data/views/keys.ts | 5 ++ studio/data/views/view-query.ts | 57 ++++++++++++++ studio/data/views/views-query.ts | 49 ++++++++++++ studio/hooks/misc/useTable.ts | 76 +++++++++++++++++++ studio/pages/project/[ref]/editor/[id].tsx | 32 ++------ 16 files changed, 541 insertions(+), 88 deletions(-) create mode 100644 studio/data/foreign-tables/foreign-table-query.ts create mode 100644 studio/data/foreign-tables/foreign-tables-query.ts create mode 100644 studio/data/foreign-tables/keys.ts create mode 100644 studio/data/materialized-views/keys.ts create mode 100644 studio/data/materialized-views/materialized-view-query.ts create mode 100644 studio/data/materialized-views/materialized-views-query.ts create mode 100644 studio/data/tables/keys.ts create mode 100644 studio/data/tables/table-query.ts create mode 100644 studio/data/tables/tables-query.ts create mode 100644 studio/data/views/keys.ts create mode 100644 studio/data/views/view-query.ts create mode 100644 studio/data/views/views-query.ts create mode 100644 studio/hooks/misc/useTable.ts diff --git a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index 74e4e6c19e1..d87be9046c1 100644 --- a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -38,11 +38,13 @@ import { JsonEditValue } from './SidePanelEditor/RowEditor/RowEditor.types' import { ForeignRowSelectorProps } from './SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector' import TableDefinition from './TableDefinition' import APIDocumentationPanel from './APIDocumentationPanel' +import Connecting from 'components/ui/Loading/Loading' export interface TableGridEditorProps { /** Theme for the editor */ theme?: 'dark' | 'light' + isLoadingSelectedTable?: boolean selectedTable: any // PostgresTable | SchemaView /** Determines what side panel editor to show */ @@ -77,6 +79,7 @@ export interface TableGridEditorProps { const TableGridEditor = ({ theme = 'dark', + isLoadingSelectedTable = false, selectedTable, sidePanelKey, isDuplicating, @@ -202,6 +205,10 @@ const TableGridEditor = ({ const entityType = useEntityType(selectedTable?.id) + if (isLoadingSelectedTable) { + return + } + // NOTE: DO NOT PUT HOOKS AFTER THIS LINE if (isUndefined(selectedTable)) { return diff --git a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx index 8ab79999ccf..d27a43a4c85 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -1,21 +1,13 @@ import { PropsWithChildren, useEffect } from 'react' -import { useRouter } from 'next/router' import { noop } from 'lodash' import { observer } from 'mobx-react-lite' import { PermissionAction } from '@supabase/shared-types/out/constants' import { checkPermissions, useStore } from 'hooks' -import { useParams } from 'common/hooks' import { Entity } from 'data/entity-types/entity-type-query' -import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' -import { useIsTableLoaded, useTableEditorGlobalStateSnapshot } from 'state/table-editor-global' import ProjectLayout from '../' import TableEditorMenu from './TableEditorMenu' import NoPermission from 'components/ui/NoPermission' -import useEntityType from 'hooks/misc/useEntityType' -import Connecting from 'components/ui/Loading/Loading' -import useLatest from 'hooks/misc/useLatest' -import useTableRowsPrefetchWrapper from './TableEditorLayout.utils' export interface TableEditorLayoutProps { onAddTable: () => void @@ -32,11 +24,7 @@ const TableEditorLayout = ({ children, }: PropsWithChildren) => { const { vault, meta, ui } = useStore() - const router = useRouter() - const { ref, id: _id } = useParams() - const id = _id ? Number(_id) : undefined - const snap = useTableEditorGlobalStateSnapshot() const canReadTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables') const vaultExtension = meta.extensions.byId('supabase_vault') @@ -51,56 +39,6 @@ const TableEditorLayout = ({ } }, [ui.selectedProject?.ref]) - 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() @@ -127,7 +65,7 @@ const TableEditorLayout = ({ /> } > - {router.isReady && id !== undefined ? isLoaded ? children : : children} + {children}
) } diff --git a/studio/data/foreign-tables/foreign-table-query.ts b/studio/data/foreign-tables/foreign-table-query.ts new file mode 100644 index 00000000000..b10d74e0928 --- /dev/null +++ b/studio/data/foreign-tables/foreign-table-query.ts @@ -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> +export type ForeignTableError = unknown + +export const useForeignTableQuery = ( + { projectRef, id }: ForeignTableVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + foreignTableKeys.foreignTable(projectRef, id), + ({ signal }) => getForeignTable({ projectRef, id }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined', + ...options, + } + ) diff --git a/studio/data/foreign-tables/foreign-tables-query.ts b/studio/data/foreign-tables/foreign-tables-query.ts new file mode 100644 index 00000000000..99ce73a2a0f --- /dev/null +++ b/studio/data/foreign-tables/foreign-tables-query.ts @@ -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> +export type ForeignTablesError = unknown + +export const useForeignTablesQuery = ( + { projectRef, connectionString }: ForeignTablesVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + foreignTableKeys.list(projectRef), + ({ signal }) => getForeignTables({ projectRef, connectionString }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } + ) diff --git a/studio/data/foreign-tables/keys.ts b/studio/data/foreign-tables/keys.ts new file mode 100644 index 00000000000..869cd2ccfad --- /dev/null +++ b/studio/data/foreign-tables/keys.ts @@ -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, +} diff --git a/studio/data/materialized-views/keys.ts b/studio/data/materialized-views/keys.ts new file mode 100644 index 00000000000..6e34f29b960 --- /dev/null +++ b/studio/data/materialized-views/keys.ts @@ -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, +} diff --git a/studio/data/materialized-views/materialized-view-query.ts b/studio/data/materialized-views/materialized-view-query.ts new file mode 100644 index 00000000000..6efd95ded42 --- /dev/null +++ b/studio/data/materialized-views/materialized-view-query.ts @@ -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> +export type MaterializedViewError = unknown + +export const useMaterializedViewQuery = ( + { projectRef, connectionString, id }: MaterializedViewVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + materializedViewKeys.materializedView(projectRef, id), + ({ signal }) => getMaterializedView({ projectRef, connectionString, id }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined', + ...options, + } + ) diff --git a/studio/data/materialized-views/materialized-views-query.ts b/studio/data/materialized-views/materialized-views-query.ts new file mode 100644 index 00000000000..5f84c001834 --- /dev/null +++ b/studio/data/materialized-views/materialized-views-query.ts @@ -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> +export type MaterializedViewsError = unknown + +export const useMaterializedViewsQuery = ( + { projectRef, connectionString }: MaterializedViewsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + materializedViewKeys.list(projectRef), + ({ signal }) => getMaterializedViews({ projectRef, connectionString }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } + ) diff --git a/studio/data/tables/keys.ts b/studio/data/tables/keys.ts new file mode 100644 index 00000000000..f40847ed5a5 --- /dev/null +++ b/studio/data/tables/keys.ts @@ -0,0 +1,5 @@ +export const tableKeys = { + list: (projectRef: string | undefined) => ['projects', projectRef, 'tables'] as const, + table: (projectRef: string | undefined, id: number | undefined) => + ['projects', projectRef, 'tables', id] as const, +} diff --git a/studio/data/tables/table-query.ts b/studio/data/tables/table-query.ts new file mode 100644 index 00000000000..447a371a43d --- /dev/null +++ b/studio/data/tables/table-query.ts @@ -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 { tableKeys } from './keys' + +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> +export type TableError = unknown + +export const useTableQuery = ( + { projectRef, connectionString, id }: TableVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + tableKeys.table(projectRef, id), + ({ signal }) => getTable({ projectRef, connectionString, id }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined', + ...options, + } + ) diff --git a/studio/data/tables/tables-query.ts b/studio/data/tables/tables-query.ts new file mode 100644 index 00000000000..c258a168e3b --- /dev/null +++ b/studio/data/tables/tables-query.ts @@ -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 { tableKeys } from './keys' +import { Table } from './table-query' + +export type TablesVariables = { + projectRef?: string + connectionString?: string +} + +export type TablesResponse = Table[] | { error?: any } + +export async function getTables( + { projectRef, connectionString }: TablesVariables, + 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}/tables`, { + headers: Object.fromEntries(headers), + signal, + })) as TablesResponse + + if (!Array.isArray(response) && response.error) { + throw response.error + } + + return response as PostgresTable[] +} + +export type TablesData = Awaited> +export type TablesError = unknown + +export const useTablesQuery = ( + { projectRef, connectionString }: TablesVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + tableKeys.list(projectRef), + ({ signal }) => getTables({ projectRef, connectionString }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } + ) diff --git a/studio/data/views/keys.ts b/studio/data/views/keys.ts new file mode 100644 index 00000000000..943f32500f1 --- /dev/null +++ b/studio/data/views/keys.ts @@ -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, +} diff --git a/studio/data/views/view-query.ts b/studio/data/views/view-query.ts new file mode 100644 index 00000000000..7b7b12a4793 --- /dev/null +++ b/studio/data/views/view-query.ts @@ -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> +export type ViewError = unknown + +export const useViewQuery = ( + { projectRef, connectionString, id }: ViewVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + viewKeys.view(projectRef, id), + ({ signal }) => getView({ projectRef, connectionString, id }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined', + ...options, + } + ) diff --git a/studio/data/views/views-query.ts b/studio/data/views/views-query.ts new file mode 100644 index 00000000000..08300ba9dfd --- /dev/null +++ b/studio/data/views/views-query.ts @@ -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> +export type ViewsError = unknown + +export const useViewsQuery = ( + { projectRef, connectionString }: ViewsVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + viewKeys.list(projectRef), + ({ signal }) => getViews({ projectRef, connectionString }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } + ) diff --git a/studio/hooks/misc/useTable.ts b/studio/hooks/misc/useTable.ts new file mode 100644 index 00000000000..505ecf23e95 --- /dev/null +++ b/studio/hooks/misc/useTable.ts @@ -0,0 +1,76 @@ +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' +import { useForeignTableQuery } from 'data/foreign-tables/foreign-table-query' +import { useMaterializedViewQuery } from 'data/materialized-views/materialized-view-query' +import { useTableQuery } from 'data/tables/table-query' +import { useViewQuery } from 'data/views/view-query' +import useEntityType from './useEntityType' + +/** + * 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 diff --git a/studio/pages/project/[ref]/editor/[id].tsx b/studio/pages/project/[ref]/editor/[id].tsx index 3415d635902..93d2646c4fc 100644 --- a/studio/pages/project/[ref]/editor/[id].tsx +++ b/studio/pages/project/[ref]/editor/[id].tsx @@ -1,9 +1,9 @@ import { useQueryClient } from '@tanstack/react-query' -import { useEffect, useState } from 'react' +import { useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' -import { isUndefined, isNaN } from 'lodash' +import { isUndefined } from 'lodash' import { Alert, Button, Checkbox, IconExternalLink, Modal } from 'ui' import type { PostgresTable, PostgresColumn } from '@supabase/postgres-meta' @@ -25,11 +25,13 @@ import { import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector' import { useTheme } from 'common' import { useTableEditorStateSnapshot } from 'state/table-editor' +import useTable from 'hooks/misc/useTable' const TableEditorPage: NextPageWithLayout = () => { const router = useRouter() const { isDarkMode } = useTheme() - const { id, ref: projectRef } = useParams() + const { id: _id, ref: projectRef } = useParams() + const id = _id ? Number(_id) : undefined const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] }) const { project } = useProjectContext() @@ -59,28 +61,7 @@ const TableEditorPage: NextPageWithLayout = () => { column: any }>() - const tables: PostgresTable[] = meta.tables.list() - const views: SchemaView[] = meta.views.list() - const materializedViews = meta.materializedViews.list() - const foreignTables: Partial[] = 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 { data: selectedTable, isLoading } = useTable(id) const onAddRow = () => { setSidePanelKey('row') @@ -278,6 +259,7 @@ const TableEditorPage: NextPageWithLayout = () => { onDuplicateTable={onDuplicateTable} > Date: Wed, 3 May 2023 16:37:35 +1000 Subject: [PATCH 05/17] refactor to valtio table editor state --- studio/components/grid/SupabaseGrid.tsx | 312 ++++++++------- .../DeleteConfirmationDialogs.tsx | 236 +++++++++++ .../interfaces/TableGridEditor/EmptyState.tsx | 10 +- .../SidePanelEditor/SidePanelEditor.tsx | 111 +++--- .../TableGridEditor/TableGridEditor.tsx | 134 ++----- .../TableEditorLayout/EntityListItem.tsx | 26 +- .../TableEditorLayout/TableEditorLayout.tsx | 37 +- .../TableEditorLayout/TableEditorMenu.tsx | 31 +- studio/hooks/misc/useTable.ts | 13 +- .../pages/project/[ref]/database/tables.tsx | 230 +++-------- studio/pages/project/[ref]/editor/[id].tsx | 375 +----------------- studio/pages/project/[ref]/editor/index.tsx | 110 +---- studio/state/table-editor.tsx | 141 +++++++ 13 files changed, 740 insertions(+), 1026 deletions(-) create mode 100644 studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx diff --git a/studio/components/grid/SupabaseGrid.tsx b/studio/components/grid/SupabaseGrid.tsx index 94e65bfa996..17434cb5457 100644 --- a/studio/components/grid/SupabaseGrid.tsx +++ b/studio/components/grid/SupabaseGrid.tsx @@ -25,7 +25,10 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte /** Supabase Grid: React component to render database table */ -export const SupabaseGrid = forwardRef((props, ref) => { +export const SupabaseGrid = forwardRef(function SupabaseGrid( + props, + ref +) { const monaco = useMonaco() const _props = cleanupProps(props) const { theme } = _props @@ -57,166 +60,171 @@ export const SupabaseGrid = forwardRef((prop ) }) -const SupabaseGridLayout = forwardRef((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( + 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(null) - const [mounted, setMounted] = useState(false) + const gridRef = useRef(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, 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) { dispatch({ - type: 'SET_ROWS_COUNT', - payload: data.rows.length, + type: 'ADD_NEW_ROW', + payload: row, }) }, - } - ) - - useImperativeHandle(ref, () => ({ - rowAdded(row: Dictionary) { - dispatch({ - type: 'ADD_NEW_ROW', - payload: row, - }) - }, - rowEdited(row: Dictionary, 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, 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 ( -
-
- {showCustomChildren && children !== undefined ? ( - <>{children} - ) : ( - <> - -
- - - )} + useEffect(() => { + dispatch({ + type: 'INIT_CALLBACK', + payload: { ...props }, + }) + }, []) - {mounted && - createPortal(, document.body)} -
- ) -}) + 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 ( +
+
+ {showCustomChildren && children !== undefined ? ( + <>{children} + ) : ( + <> + +
+ + + )} + + {mounted && + createPortal( + , + document.body + )} +
+ ) + } +) diff --git a/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx b/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx new file mode 100644 index 00000000000..6e373c9c774 --- /dev/null +++ b/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx @@ -0,0 +1,236 @@ +import type { PostgresTable } from '@supabase/postgres-meta' +import { useQueryClient } from '@tanstack/react-query' +import Link from 'next/link' +import { useRouter } from 'next/router' + +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 { useStore, useUrlState } from 'hooks' +import { TableLike } from 'hooks/misc/useTable' +import { useTableEditorStateSnapshot } from 'state/table-editor' +import { Alert, Button, Checkbox, IconExternalLink, Modal } from 'ui' + +export type DeleteConfirmationDialogsProps = { + projectRef?: string + selectedTable?: TableLike +} + +const DeleteConfirmationDialogs = ({ + projectRef, + selectedTable, +}: DeleteConfirmationDialogsProps) => { + const { project } = useProjectContext() + const snap = useTableEditorStateSnapshot() + + const router = useRouter() + const [, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] }) + + const queryClient = useQueryClient() + + const { meta, ui } = useStore() + + 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}"`, + }) + + 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 (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 = meta.tables.list( + (table: PostgresTable) => table.schema === snap.selectedSchemaName + ) + + 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 (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 ( + <> + { + snap.closeConfirmationDialog() + }} + onSelectConfirm={onConfirmDeleteColumn} + > + +
+

+ Are you sure you want to delete the selected column? This action cannot be undone. +

+ snap.toggleConfirmationIsWithCascade()} + /> + {isDeleteWithCascade && ( + +

+ All dependent objects will be removed, as will any objects that depend on them, + recursively. +

+ + + + + +
+ )} +
+
+
+ {`Confirm deletion of table "${selectedTable?.name}"`} + } + buttonLabel="Delete" + buttonLoadingLabel="Deleting" + onSelectCancel={() => { + snap.closeConfirmationDialog() + }} + onSelectConfirm={onConfirmDeleteTable} + > + +
+

+ Are you sure you want to delete the selected table? This action cannot be undone. +

+ snap.toggleConfirmationIsWithCascade(!isDeleteWithCascade)} + /> + {isDeleteWithCascade && ( + +

+ All dependent objects will be removed, as will any objects that depend on them, + recursively. +

+ + + + + +
+ )} +
+
+
+ + ) +} + +export default DeleteConfirmationDialogs diff --git a/studio/components/interfaces/TableGridEditor/EmptyState.tsx b/studio/components/interfaces/TableGridEditor/EmptyState.tsx index 8aeb7128028..2972d3ae944 100644 --- a/studio/components/interfaces/TableGridEditor/EmptyState.tsx +++ b/studio/components/interfaces/TableGridEditor/EmptyState.tsx @@ -6,11 +6,9 @@ import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-que import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -export interface EmptyStateProps { - onAddTable: () => void -} +export interface EmptyStateProps {} -const EmptyState = ({ onAddTable }: EmptyStateProps) => { +const EmptyState = ({}: EmptyStateProps) => { const snap = useTableEditorStateSnapshot() const isProtectedSchema = EXCLUDED_SCHEMAS.includes(snap.selectedSchemaName) const canCreateTables = @@ -42,7 +40,7 @@ const EmptyState = ({ onAddTable }: EmptyStateProps) => {

There are no tables available in this schema.

@@ -51,7 +49,7 @@ const EmptyState = ({ onAddTable }: EmptyStateProps) => {

Select a table from the navigation panel on the left to view its data diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index b2d544f4a1e..379d83f2d02 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -25,21 +25,10 @@ import { sqlKeys } from 'data/sql/keys' import ForeignRowSelector, { ForeignRowSelectorProps, } from './RowEditor/ForeignRowSelector/ForeignRowSelector' +import { useTableEditorStateSnapshot } from 'state/table-editor' export interface SidePanelEditorProps { selectedTable?: PostgresTable - selectedRowToEdit?: Dictionary - selectedColumnToEdit?: PostgresColumn - selectedTableToEdit?: PostgresTable - selectedValueForJsonEdit?: JsonEditValue - selectedForeignKeyToEdit?: { - foreignKey: NonNullable - row: any - column: any - } - sidePanelKey?: 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import' - isDuplicating?: boolean - closePanel: () => void onRowCreated?: (row: Dictionary) => void onRowUpdated?: (row: Dictionary, idx: number) => void @@ -51,19 +40,12 @@ export interface SidePanelEditorProps { const SidePanelEditor = ({ 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() @@ -82,7 +64,7 @@ const SidePanelEditor = ({ const { mutateAsync: createTableRows } = useTableRowCreateMutation() const { mutateAsync: updateTableRow } = useTableRowUpdateMutation({ async onMutate({ projectRef, table, configuration, payload }) { - closePanel() + snap.closeSidePanel() const primaryKeyColumns = new Set(Object.keys(configuration.identifiers)) @@ -199,12 +181,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 @@ -220,7 +203,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 @@ -242,6 +226,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( @@ -285,7 +271,7 @@ const SidePanelEditor = ({ ]) onColumnSaved(configuration.isEncrypted) setIsEdited(false) - closePanel() + snap.closeSidePanel() } if (configuration.isEncrypted && selectedTable?.schema) { @@ -336,8 +322,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', @@ -384,15 +374,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 @@ -408,13 +398,13 @@ 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)), @@ -434,7 +424,7 @@ const SidePanelEditor = ({ if (!saveTableError) { setIsEdited(false) - closePanel() + snap.closeSidePanel() } resolve() @@ -514,14 +504,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() } } @@ -529,9 +519,9 @@ const SidePanelEditor = ({ <> {!isUndefined(selectedTable) && ( setIsEdited(true)} @@ -539,41 +529,58 @@ const SidePanelEditor = ({ )} {!isUndefined(selectedTable) && ( setIsEdited(true)} /> )} setIsEdited(true)} /> { setIsClosingPanel(false) setIsEdited(false) - closePanel() + snap.closeSidePanel() }} > diff --git a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index d87be9046c1..ebeeb3b974e 100644 --- a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -1,7 +1,7 @@ import { useRef, useEffect, useState } from 'react' import { observer } from 'mobx-react-lite' import { useRouter } from 'next/router' -import { find, isUndefined, noop } from 'lodash' +import { find, isUndefined } from 'lodash' import type { PostgresColumn, PostgresRelationship, PostgresTable } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { QueryKey, useQueryClient } from '@tanstack/react-query' @@ -34,70 +34,23 @@ import { useTableEditorStateSnapshot } from 'state/table-editor' import TwoOptionToggle from 'components/ui/TwoOptionToggle' import { ERROR_PRIMARY_KEY_NOTFOUND } from 'components/grid/constants' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { JsonEditValue } from './SidePanelEditor/RowEditor/RowEditor.types' -import { ForeignRowSelectorProps } from './SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector' import TableDefinition from './TableDefinition' import APIDocumentationPanel from './APIDocumentationPanel' import Connecting from 'components/ui/Loading/Loading' +import { TableLike } from 'hooks/misc/useTable' export interface TableGridEditorProps { /** Theme for the editor */ theme?: 'dark' | 'light' isLoadingSelectedTable?: boolean - 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 - selectedColumnToEdit?: PostgresColumn - selectedTableToEdit?: PostgresTable - selectedValueForJsonEdit?: JsonEditValue - selectedForeignKeyToEdit?: { - foreignKey: NonNullable - row: any - column: any - } - - onAddRow: () => void - onEditRow: (row: Dictionary) => void - onAddColumn: () => void - onEditColumn: (column: PostgresColumn) => void - onDeleteColumn: (column: PostgresColumn) => void - onExpandJSONEditor: (column: string, row: any) => void - onEditForeignKeyColumnValue: (args: { - foreignKey: NonNullable - row: any - column: any - }) => void - onClosePanel: () => void - onImportData: () => void + selectedTable?: TableLike } const TableGridEditor = ({ theme = 'dark', 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() @@ -214,8 +167,6 @@ const TableGridEditor = ({ return } - const tableId = selectedTable?.id - const isViewSelected = entityType?.type === ENTITY_TYPE.VIEW || entityType?.type === ENTITY_TYPE.MATERIALIZED_VIEW const isTableSelected = entityType?.type === ENTITY_TYPE.TABLE @@ -226,17 +177,18 @@ const TableGridEditor = ({ // [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_DELETION_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_DELETION_ACTION.NO_ACTION, } - ) + }) const gridTable = !isViewSelected && !isForeignTableSelected @@ -284,26 +236,21 @@ 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 + const column = find(selectedTable?.columns ?? [], { name }) as PostgresColumn if (column) { - onEditColumn(column) + snap.onEditColumn(column) } else { - console.error(`Unable to find column ${name} in ${table?.name}`) + console.error(`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 column = find(selectedTable?.columns ?? [], { name }) as PostgresColumn + if (column) { + snap.onDeleteColumn(column) + } else { + console.error(`Unable to find column ${name} in ${selectedTable?.name}`) + } } const onError = (error: any) => { @@ -316,11 +263,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 ;(selectedTable as PostgresTable).primary_keys.forEach( @@ -392,17 +342,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' ? ( @@ -420,19 +372,11 @@ const TableGridEditor = ({ {!isUndefined(snap.selectedSchemaName) && ( )} diff --git a/studio/components/layouts/TableEditorLayout/EntityListItem.tsx b/studio/components/layouts/TableEditorLayout/EntityListItem.tsx index 2e0d5fdeb19..2af073982ed 100644 --- a/studio/components/layouts/TableEditorLayout/EntityListItem.tsx +++ b/studio/components/layouts/TableEditorLayout/EntityListItem.tsx @@ -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={} onClick={(e) => { e.stopPropagation() - onEditTable(entity) + snap.onEditTable() }} - disabled={isLoadingTableMetadata} > Edit Table , @@ -148,9 +136,8 @@ const EntityListItem = ({ icon={} onClick={(e) => { e.stopPropagation() - onDuplicateTable(entity) + snap.onDuplicateTable() }} - disabled={isLoadingTableMetadata} > Duplicate Table , @@ -170,9 +157,8 @@ const EntityListItem = ({ icon={} onClick={(e) => { e.stopPropagation() - onDeleteTable(entity) + snap.onDeleteTable() }} - disabled={isLoadingTableMetadata} > Delete Table , diff --git a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx index d27a43a4c85..a3e626ee48e 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -1,28 +1,15 @@ import { PropsWithChildren, useEffect } from 'react' -import { noop } from 'lodash' import { observer } from 'mobx-react-lite' import { PermissionAction } from '@supabase/shared-types/out/constants' import { checkPermissions, useStore } from 'hooks' -import { Entity } from 'data/entity-types/entity-type-query' -import ProjectLayout from '../' +import { ProjectLayoutWithAuth } from '../' import TableEditorMenu from './TableEditorMenu' import NoPermission from 'components/ui/NoPermission' -export interface TableEditorLayoutProps { - onAddTable: () => void - onEditTable: (table: Entity) => void - onDeleteTable: (table: Entity) => void - onDuplicateTable: (table: Entity) => void -} +export interface TableEditorLayoutProps {} -const TableEditorLayout = ({ - onAddTable = noop, - onEditTable = noop, - onDeleteTable = noop, - onDuplicateTable = noop, - children, -}: PropsWithChildren) => { +const TableEditorLayout = ({ children }: PropsWithChildren) => { const { vault, meta, ui } = useStore() const canReadTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables') @@ -47,26 +34,16 @@ const TableEditorLayout = ({ if (!canReadTables) { return ( - + - + ) } return ( - - } - > + }> {children} - + ) } diff --git a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index a4a3ee92cf6..958ef89d8ce 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -1,6 +1,5 @@ import { useMemo, useState } from 'react' -import { noop, partition } from 'lodash' -import { observer } from 'mobx-react-lite' +import { partition } from 'lodash' import { Button, Dropdown, @@ -20,29 +19,17 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common/hooks' import { useTableEditorStateSnapshot } from 'state/table-editor' -import { checkPermissions, useLocalStorage, useStore } from 'hooks' +import { checkPermissions, useLocalStorage } from 'hooks' import InfiniteList from 'components/ui/InfiniteList' import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query' -import { Entity } from 'data/entity-types/entity-type-query' import { useSchemasQuery } from 'data/database/schemas-query' import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' import { useProjectContext } from '../ProjectLayout/ProjectContext' import EntityListItem from './EntityListItem' -export interface TableEditorMenuProps { - onAddTable: () => void - onEditTable: (table: Entity) => void - onDeleteTable: (table: Entity) => void - onDuplicateTable: (table: Entity) => void -} +export interface TableEditorMenuProps {} -const TableEditorMenu = ({ - onAddTable = noop, - onEditTable = noop, - onDeleteTable = noop, - onDuplicateTable = noop, -}: TableEditorMenuProps) => { - const { meta } = useStore() +const TableEditorMenu = ({}: TableEditorMenuProps) => { const { id } = useParams() const snap = useTableEditorStateSnapshot() @@ -93,8 +80,6 @@ const TableEditorMenu = ({ const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName) const canCreateTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') - const isLoadingTableMetadata = id ? !meta.tables.byId(id) : true - const refreshTables = async () => { await refetch() } @@ -189,7 +174,7 @@ const TableEditorMenu = ({ } type="default" style={{ justifyContent: 'start' }} - onClick={onAddTable} + onClick={snap.onAddTable} > New table @@ -353,10 +338,6 @@ const TableEditorMenu = ({ itemProps={{ projectRef: project?.ref, id: Number(id), - onEditTable, - onDeleteTable, - onDuplicateTable, - isLoadingTableMetadata, }} getItemSize={() => 28} hasNextPage={hasNextPage} @@ -370,4 +351,4 @@ const TableEditorMenu = ({ ) } -export default observer(TableEditorMenu) +export default TableEditorMenu diff --git a/studio/hooks/misc/useTable.ts b/studio/hooks/misc/useTable.ts index 505ecf23e95..19647e03c88 100644 --- a/studio/hooks/misc/useTable.ts +++ b/studio/hooks/misc/useTable.ts @@ -1,11 +1,16 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' -import { useForeignTableQuery } from 'data/foreign-tables/foreign-table-query' -import { useMaterializedViewQuery } from 'data/materialized-views/materialized-view-query' -import { useTableQuery } from 'data/tables/table-query' -import { useViewQuery } from 'data/views/view-query' +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... */ diff --git a/studio/pages/project/[ref]/database/tables.tsx b/studio/pages/project/[ref]/database/tables.tsx index 60d7a31acd1..f8d723a8c81 100644 --- a/studio/pages/project/[ref]/database/tables.tsx +++ b/studio/pages/project/[ref]/database/tables.tsx @@ -1,206 +1,82 @@ -import { useQueryClient } from '@tanstack/react-query' -import { useState, useEffect } from 'react' import { observer } from 'mobx-react-lite' -import { isUndefined } from 'lodash' -import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta' -import { Modal } from 'ui' +import { useRouter } from 'next/router' +import { useEffect } from 'react' -import { useStore } from 'hooks' -import { sqlKeys } from 'data/sql/keys' - -import { DatabaseLayout } from 'components/layouts' -import ConfirmationModal from 'components/ui/ConfirmationModal' -import { TableList, ColumnList } from 'components/interfaces/Database' +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 { Table, useTableQuery } 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 { meta } = useStore() - const queryClient = useQueryClient() + const router = useRouter() + const { id: _id, ref: projectRef } = useParams() + const id = _id ? Number(_id) : undefined - const [isDeleting, setIsDeleting] = useState(false) - const [selectedTable, setSelectedTable] = useState() - const [sidePanelKey, setSidePanelKey] = useState<'column' | 'table'>() - - const [selectedColumnToEdit, setSelectedColumnToEdit] = useState() - const [selectedTableToEdit, setSelectedTableToEdit] = useState() - - const [selectedColumnToDelete, setSelectedColumnToDelete] = useState() - const [selectedTableToDelete, setSelectedTableToDelete] = useState() + const { data: selectedTable } = useTableQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + id, + }) useEffect(() => { - if (ui.selectedProject?.ref) { + if (project?.ref) { meta.types.load() } - }, [ui.selectedProject?.ref]) + }, [project?.ref]) - 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 setSelectedTable = (table: Table | undefined) => { + router.replace({ + pathname: router.pathname, + query: table + ? { + ...router.query, + id: table.id, + } + : { + ...router.query, + id: undefined, // TODO(alaister): needs full removal of ?id= from the url + }, + }) } return ( <>

- {isUndefined(selectedTable) ? ( - - ) : ( + {id !== undefined && selectedTable !== undefined ? ( setSelectedTable(undefined)} /> + ) : ( + { + setSelectedTable(table) + snap.onEditTable() + }} + onDeleteTable={(table) => { + setSelectedTable(table) + snap.onDeleteTable() + }} + onOpenTable={setSelectedTable} + /> )}
- {`Confirm deletion of table "${selectedTableToDelete?.name}"`} - } - buttonLabel="Delete" - buttonLoadingLabel="Deleting" - onSelectCancel={() => setIsDeleting(false)} - onSelectConfirm={onConfirmDeleteTable} - > - -

- Are you sure you want to delete the selected table? This action cannot be undone. -

-
-
- setIsDeleting(false)} - onSelectConfirm={onConfirmDeleteColumn} - > - -

- Are you sure you want to delete the selected column? This action cannot be undone. -

-
-
- + + ) } diff --git a/studio/pages/project/[ref]/editor/[id].tsx b/studio/pages/project/[ref]/editor/[id].tsx index 93d2646c4fc..31b390bbf67 100644 --- a/studio/pages/project/[ref]/editor/[id].tsx +++ b/studio/pages/project/[ref]/editor/[id].tsx @@ -1,384 +1,35 @@ -import { useQueryClient } from '@tanstack/react-query' -import { useState } from 'react' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { observer } from 'mobx-react-lite' -import { isUndefined } from 'lodash' -import { Alert, Button, Checkbox, IconExternalLink, Modal } from 'ui' -import type { PostgresTable, PostgresColumn } from '@supabase/postgres-meta' - -import { useStore, withAuth, useUrlState } from 'hooks' -import { useParams } from 'common/hooks' -import { entityTypeKeys } from 'data/entity-types/keys' -import { Entity } from 'data/entity-types/entity-type-query' -import { sqlKeys } from 'data/sql/keys' -import { Dictionary } from 'components/grid' -import { TableEditorLayout } from 'components/layouts' -import { TableGridEditor } from 'components/interfaces' -import ConfirmationModal from 'components/ui/ConfirmationModal' -import { NextPageWithLayout, SchemaView } from 'types' -import { JsonEditValue } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types' -import { - ProjectContextFromParamsProvider, - useProjectContext, -} from 'components/layouts/ProjectLayout/ProjectContext' -import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector' import { useTheme } from 'common' -import { useTableEditorStateSnapshot } from 'state/table-editor' +import { useParams } from 'common/hooks' +import { TableGridEditor } from 'components/interfaces' +import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs' +import { TableEditorLayout } from 'components/layouts' +import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext' import useTable from 'hooks/misc/useTable' +import { NextPageWithLayout } from 'types' const TableEditorPage: NextPageWithLayout = () => { - const router = useRouter() const { isDarkMode } = useTheme() const { id: _id, ref: projectRef } = useParams() const id = _id ? Number(_id) : undefined - const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] }) - - const { project } = useProjectContext() - const snap = useTableEditorStateSnapshot() - - const queryClient = useQueryClient() - - const { meta, ui } = useStore() - - const [isDeleting, setIsDeleting] = useState(false) - const [isDuplicating, setIsDuplicating] = useState(false) - const [isDeleteWithCascade, setIsDeleteWithCascade] = useState(false) - - const [selectedColumnToDelete, setSelectedColumnToDelete] = useState() - const [selectedTableToDelete, setSelectedTableToDelete] = useState() - - const [sidePanelKey, setSidePanelKey] = useState< - 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import' - >() - const [selectedRowToEdit, setSelectedRowToEdit] = useState>() - const [selectedColumnToEdit, setSelectedColumnToEdit] = useState() - const [selectedTableToEdit, setSelectedTableToEdit] = useState() - const [selectedValueForJsonEdit, setSelectedValueForJsonEdit] = useState() - const [selectedForeignKeyToEdit, setSelectedForeignKeyToEdit] = useState<{ - foreignKey: NonNullable - row: any - column: any - }>() const { data: selectedTable, isLoading } = useTable(id) - const onAddRow = () => { - setSidePanelKey('row') - setSelectedRowToEdit(undefined) - } - - const onEditRow = (row: Dictionary) => { - 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 - 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 (snap.selectedSchemaName) await meta.views.loadBySchema(snap.selectedSchemaName) - } 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 === snap.selectedSchemaName - ) - - 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 (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 { - setIsDeleting(false) - setSelectedTableToDelete(undefined) - } - } - return ( - + <> - { - setIsDeleting(false) - setSelectedColumnToDelete(undefined) - }} - onSelectConfirm={onConfirmDeleteColumn} - > - -
-

- Are you sure you want to delete the selected column? This action cannot be undone. -

- setIsDeleteWithCascade(!isDeleteWithCascade)} - /> - {isDeleteWithCascade && ( - -

- All dependent objects will be removed, as will any objects that depend on them, - recursively. -

- - - - - -
- )} -
-
-
- {`Confirm deletion of table "${selectedTableToDelete?.name}"`} - } - buttonLabel="Delete" - buttonLoadingLabel="Deleting" - onSelectCancel={() => { - setIsDeleting(false) - setSelectedTableToDelete(undefined) - }} - onSelectConfirm={onConfirmDeleteTable} - > - -
-

- Are you sure you want to delete the selected table? This action cannot be undone. -

- setIsDeleteWithCascade(!isDeleteWithCascade)} - /> - {isDeleteWithCascade && ( - -

- All dependent objects will be removed, as will any objects that depend on them, - recursively. -

- - - - - -
- )} -
-
-
-
+ + ) } TableEditorPage.getLayout = (page) => ( - {page} + + {page} + ) -export default withAuth(observer(TableEditorPage)) +export default TableEditorPage diff --git a/studio/pages/project/[ref]/editor/index.tsx b/studio/pages/project/[ref]/editor/index.tsx index b93b42e7bfb..0588fece332 100644 --- a/studio/pages/project/[ref]/editor/index.tsx +++ b/studio/pages/project/[ref]/editor/index.tsx @@ -1,112 +1,16 @@ -import { useState } from 'react' -import router from 'next/router' -import { observer } from 'mobx-react-lite' -import { isUndefined } from 'lodash' -import type { PostgresTable } from '@supabase/postgres-meta' - -import { NextPageWithLayout } from 'types' -import { useStore, withAuth } from 'hooks' -import { Entity } from 'data/entity-types/entity-type-query' +import { EmptyState } from 'components/interfaces/TableGridEditor' import { TableEditorLayout } from 'components/layouts' -import { EmptyState, SidePanelEditor } from 'components/interfaces/TableGridEditor' -import ConfirmationModal from 'components/ui/ConfirmationModal' import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext' -import { useTableEditorStateSnapshot } from 'state/table-editor' +import { NextPageWithLayout } from 'types' const TableEditorPage: NextPageWithLayout = () => { - const snap = useTableEditorStateSnapshot() - const { meta, ui } = useStore() - const projectRef = ui.selectedProject?.ref - const [sidePanelKey, setSidePanelKey] = useState<'row' | 'column' | 'table'>() - const [isDeleting, setIsDeleting] = useState(false) - const [isDuplicating, setIsDuplicating] = useState(false) - const [selectedTableToEdit, setSelectedTableToEdit] = useState() - const [selectedTableToDelete, setSelectedTableToDelete] = useState() - - 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(snap.selectedSchemaName) - } catch (error: any) { - ui.setNotification({ - category: 'error', - message: `Failed to delete ${selectedTableToDelete!.name}`, - }) - } - } - - return ( - - - {/* On this page it'll only handle tables */} - {`Confirm deletion of table "${selectedTableToDelete?.name}"`} - } - 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} - /> - router.push(`/project/${projectRef}/editor/${table.id}`)} - /> - - ) + return } TableEditorPage.getLayout = (page) => ( - {page} + + {page} + ) -export default withAuth(observer(TableEditorPage)) +export default TableEditorPage diff --git a/studio/state/table-editor.tsx b/studio/state/table-editor.tsx index 94135e0d035..3b38c94f5e1 100644 --- a/studio/state/table-editor.tsx +++ b/studio/state/table-editor.tsx @@ -1,12 +1,153 @@ +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 + row: Dictionary + column: PostgresColumn +} + +export type SidePanel = + | { type: 'row'; row?: Dictionary } + | { 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) => { + 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 From 075e94102c3e52afd3690d1e0b4fabd4ebfcc054 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 3 May 2023 18:57:34 +1000 Subject: [PATCH 06/17] tables page use local state --- .../pages/project/[ref]/database/tables.tsx | 41 +++++-------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/studio/pages/project/[ref]/database/tables.tsx b/studio/pages/project/[ref]/database/tables.tsx index f8d723a8c81..249c91525eb 100644 --- a/studio/pages/project/[ref]/database/tables.tsx +++ b/studio/pages/project/[ref]/database/tables.tsx @@ -1,6 +1,5 @@ import { observer } from 'mobx-react-lite' -import { useRouter } from 'next/router' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { useParams } from 'common' import { ColumnList, TableList } from 'components/interfaces/Database' @@ -8,7 +7,7 @@ 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 { Table, useTableQuery } from 'data/tables/table-query' +import { Table } from 'data/tables/table-query' import { useStore } from 'hooks' import { useTableEditorStateSnapshot } from 'state/table-editor' import { NextPageWithLayout } from 'types' @@ -18,15 +17,7 @@ const DatabaseTables: NextPageWithLayout = () => { const snap = useTableEditorStateSnapshot() const { meta } = useStore() - const router = useRouter() - const { id: _id, ref: projectRef } = useParams() - const id = _id ? Number(_id) : undefined - - const { data: selectedTable } = useTableQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - id, - }) + const { ref: projectRef } = useParams() useEffect(() => { if (project?.ref) { @@ -34,25 +25,13 @@ const DatabaseTables: NextPageWithLayout = () => { } }, [project?.ref]) - const setSelectedTable = (table: Table | undefined) => { - router.replace({ - pathname: router.pathname, - query: table - ? { - ...router.query, - id: table.id, - } - : { - ...router.query, - id: undefined, // TODO(alaister): needs full removal of ?id= from the url - }, - }) - } + const [selectedTable, setSelectedTable] = useState(undefined) + const [selectedTableToEdit, setSelectedTableToEdit] = useState
(undefined) return ( <>
- {id !== undefined && selectedTable !== undefined ? ( + {selectedTable !== undefined ? ( { { - setSelectedTable(table) + setSelectedTableToEdit(table) snap.onEditTable() }} onDeleteTable={(table) => { - setSelectedTable(table) + setSelectedTableToEdit(table) snap.onDeleteTable() }} onOpenTable={setSelectedTable} /> )}
- - + + ) } From 7e9079af22e1b205977f5e3411e1dd0971adf8f7 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 3 May 2023 22:35:45 +1000 Subject: [PATCH 07/17] swaps meta store read methods for react-query --- .../formatter/ForeignKeyFormatter.tsx | 44 ++- .../components/header/RLSBannerWarning.tsx | 33 +- .../Database/Hooks/EditHookPanel.tsx | 41 ++- .../Publications/PublicationsTables.tsx | 98 +++--- .../interfaces/Database/Tables/TableList.tsx | 315 +++++++++--------- .../Database/Triggers/CreateTrigger.tsx | 25 +- .../TableGridEditor/APIDocumentationPanel.tsx | 28 +- .../DeleteConfirmationDialogs.tsx | 16 +- .../ForeignKeySelector/ForeignKeySelector.tsx | 31 +- .../ForeignRowSelector/ForeignRowSelector.tsx | 28 +- .../SidePanelEditor/SidePanelEditor.tsx | 35 +- .../TableGridEditor/TableGridEditor.tsx | 2 +- .../layouts/AuthLayout/AuthLayout.tsx | 27 +- .../layouts/DatabaseLayout/DatabaseLayout.tsx | 14 +- studio/data/tables/keys.ts | 3 +- studio/data/tables/table-query.ts | 24 +- studio/data/tables/tables-query.ts | 48 ++- studio/pages/project/[ref]/auth/policies.tsx | 42 ++- studio/pages/project/[ref]/editor/index.tsx | 20 +- studio/stores/pgmeta/MetaStore.ts | 45 ++- 20 files changed, 539 insertions(+), 380 deletions(-) diff --git a/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx b/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx index cab2f3416a2..d6a91029ddf 100644 --- a/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx +++ b/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx @@ -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>) => { - 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
Name, - Schema, - - Description - , - - {/* Temporarily disable All tables toggle for publications. See https://github.com/supabase/supabase/pull/7233. + {isLoading && ( +
+ +
+ )} + {isSuccess && + (tables.length === 0 ? ( + + ) : ( +
+
Name, + Schema, + + Description + , + + {/* Temporarily disable All tables toggle for publications. See https://github.com/supabase/supabase/pull/7233.
All Tables @@ -117,20 +137,20 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT onChange={() => toggleReplicationForAllTables(publication, enabledForAllTables)} />
*/} - , - ]} - body={tables.map((table: any, i: number) => ( - - ))} - /> -
- )} +
, + ]} + body={tables.map((table: any, i: number) => ( + + ))} + /> + + ))} ) } -export default observer(PublicationsTables) +export default PublicationsTables diff --git a/studio/components/interfaces/Database/Tables/TableList.tsx b/studio/components/interfaces/Database/Tables/TableList.tsx index a63291cbdd7..604104db37f 100644 --- a/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/studio/components/interfaces/Database/Tables/TableList.tsx @@ -1,30 +1,30 @@ -import { useState } from 'react' -import { observer } from 'mobx-react-lite' -import { - Button, - IconPlus, - Input, - IconSearch, - IconTrash, - IconEdit3, - IconColumns, - Listbox, - IconLock, - IconCheck, -} from 'ui' -import { noop, partition } from 'lodash' import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' -import type { PostgresTable, PostgresSchema } from '@supabase/postgres-meta' +import { noop, partition } from 'lodash' +import { observer } from 'mobx-react-lite' +import { useState } from 'react' +import { + Button, + IconCheck, + IconColumns, + IconEdit3, + IconLock, + IconPlus, + IconSearch, + IconTrash, + Input, + Listbox, +} from 'ui' -import { useStore, checkPermissions } from 'hooks' -import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' -import { useTableEditorStateSnapshot } from 'state/table-editor' -import Table from 'components/to-be-cleaned/Table' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' +import Table from 'components/to-be-cleaned/Table' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useSchemasQuery } from 'data/database/schemas-query' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useTablesQuery } from 'data/tables/tables-query' +import { checkPermissions, useStore } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { useTableEditorStateSnapshot } from 'state/table-editor' export interface TableListProps { onAddTable: () => void @@ -42,7 +42,6 @@ const TableList = ({ const { project } = useProjectContext() const snap = useTableEditorStateSnapshot() const { meta } = useStore() - const { isLoading } = meta.tables const [filterString, setFilterString] = useState('') const canUpdateTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') @@ -54,14 +53,24 @@ const TableList = ({ EXCLUDED_SCHEMAS.includes(schema?.name ?? '') ) - const allTables: PostgresTable[] = meta.tables.list( - (table: PostgresTable) => table.schema === snap.selectedSchemaName + const { + data: tables, + isLoading, + isSuccess, + } = 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( @@ -159,129 +168,133 @@ const TableList = ({ )} - {isLoading ? ( + {isLoading && (
- ) : tables.length === 0 ? ( - - ) : ( -
-
Name, - - Description - , - - Rows (Estimated) - , - - Size (Estimated) - , - - Realtime Enabled - , - , - ]} - body={tables.map((x: any, i: any) => ( - - -

{x.name}

-
- - {x.comment !== null ? ( -

{x.comment}

- ) : ( -

No description

- )} -
- - {x.live_rows_estimate ?? x.live_row_count} - - - {x.size} - - - {(realtimePublication?.tables ?? []).find((table: any) => table.id === x.id) && ( -
- -
- )} -
- -
- + )} + {isSuccess && + (tables.length === 0 ? ( + + ) : ( +
+
Name, + + Description + , + + Rows (Estimated) + , + + Size (Estimated) + , + + Realtime Enabled + , + , + ]} + body={tables.map((x: any, i: any) => ( + + +

{x.name}

+
+ + {x.comment !== null ? ( +

{x.comment}

+ ) : ( +

No description

+ )} +
+ + {x.live_rows_estimate ?? x.live_row_count} + + + {x.size} + + + {(realtimePublication?.tables ?? []).find( + (table: any) => table.id === x.id + ) && ( +
+ +
+ )} +
+ +
+ - - -
-
-
- ))} - /> - - )} + + + + ))} + /> + + ))} ) } diff --git a/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx b/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx index 335f05f6021..2084cc4ff17 100644 --- a/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx +++ b/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx @@ -22,6 +22,8 @@ import FormEmptyBox from 'components/ui/FormBoxEmpty' import NoTableState from 'components/ui/States/NoTableState' import { useStore } from 'hooks' import { BASE_PATH } from 'lib/constants' +import { useTablesQuery } from 'data/tables/tables-query' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' class CreateTriggerFormState { id: number | undefined @@ -248,27 +250,32 @@ type CreateTriggerProps = { } & any const CreateTrigger: FC = ({ trigger, visible, setVisible }) => { + const { project } = useProjectContext() const { ui, meta } = useStore() 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() }, []) diff --git a/studio/components/interfaces/TableGridEditor/APIDocumentationPanel.tsx b/studio/components/interfaces/TableGridEditor/APIDocumentationPanel.tsx index 4565ba6ee28..405528446f3 100644 --- a/studio/components/interfaces/TableGridEditor/APIDocumentationPanel.tsx +++ b/studio/components/interfaces/TableGridEditor/APIDocumentationPanel.tsx @@ -1,15 +1,16 @@ import { useState } from 'react' -import { SidePanel, IconBookOpen } from 'ui' -import { useStore } from 'hooks' import { useParams } from 'common/hooks' -import { GeneralContent, 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 { GeneralContent, ResourceContent } from '../Docs' +import GeneratingTypes from '../Docs/GeneratingTypes' +import LangSelector from '../Docs/LangSelector' +import ActionBar from './SidePanelEditor/ActionBar' interface APIDocumentationPanelProps { visible: boolean @@ -17,8 +18,16 @@ interface APIDocumentationPanelProps { } 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, @@ -30,7 +39,6 @@ const APIDocumentationPanel = ({ visible, onClose }: APIDocumentationPanelProps) const [selectedLang, setSelectedLang] = useState('js') const [showApiKey, setShowApiKey] = useState(DEFAULT_KEY) - const tables = meta.tables.list() const autoApiService = { ...settings?.autoApiService, endpoint: `${settings?.autoApiService.protocol ?? 'https'}://${ @@ -118,7 +126,7 @@ const APIDocumentationPanel = ({ visible, onClose }: APIDocumentationPanelProps) table.id === Number(id))?.name} + resourceId={table?.name} resources={resources} definitions={jsonSchema.definitions} paths={jsonSchema.paths} diff --git a/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx b/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx index 6e373c9c774..a8587768038 100644 --- a/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx +++ b/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx @@ -1,4 +1,3 @@ -import type { PostgresTable } from '@supabase/postgres-meta' import { useQueryClient } from '@tanstack/react-query' import Link from 'next/link' import { useRouter } from 'next/router' @@ -7,10 +6,12 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte import ConfirmationModal from 'components/ui/ConfirmationModal' import { entityTypeKeys } from 'data/entity-types/keys' import { sqlKeys } from 'data/sql/keys' +import { useGetTables } from 'data/tables/tables-query' import { useStore, useUrlState } from 'hooks' import { TableLike } from 'hooks/misc/useTable' import { useTableEditorStateSnapshot } from 'state/table-editor' import { Alert, Button, Checkbox, IconExternalLink, Modal } from 'ui' +import { tableKeys } from 'data/tables/keys' export type DeleteConfirmationDialogsProps = { projectRef?: string @@ -31,6 +32,11 @@ const DeleteConfirmationDialogs = ({ 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[] @@ -71,7 +77,9 @@ const DeleteConfirmationDialogs = ({ queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])) await Promise.all([ - meta.tables.loadById(selectedColumnToDelete!.table_id), + queryClient.invalidateQueries( + tableKeys.table(project?.ref, selectedColumnToDelete!.table_id) + ), queryClient.invalidateQueries( sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name]) ), @@ -105,9 +113,7 @@ const DeleteConfirmationDialogs = ({ 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 === snap.selectedSchemaName - ) + const tables = await getTables(snap.selectedSchemaName) await queryClient.invalidateQueries(entityTypeKeys.list(projectRef)) diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx index aaa15bec5fe..8538923372a 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx @@ -1,18 +1,18 @@ -import { useEffect, useState } from 'react' -import { get, find, isEmpty, sortBy } from 'lodash' +import type { PostgresColumn, PostgresSchema, PostgresTable } from '@supabase/postgres-meta' import { Dictionary } from 'components/grid' -import { SidePanel, Input, Listbox, IconHelpCircle, IconDatabase } from 'ui' -import type { PostgresTable, PostgresColumn, PostgresSchema } from '@supabase/postgres-meta' +import { find, get, isEmpty, sortBy } from 'lodash' +import { useEffect, useState } from 'react' +import { IconDatabase, IconHelpCircle, Input, Listbox, SidePanel } from 'ui' -import { useStore } from 'hooks' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import InformationBox from 'components/ui/InformationBox' import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants' import { useSchemasQuery } from 'data/database/schemas-query' -import InformationBox from 'components/ui/InformationBox' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useTablesQuery } from 'data/tables/tables-query' import ActionBar from '../ActionBar' -import { ForeignKey } from './ForeignKeySelector.types' import { ColumnField } from '../SidePanelEditor.types' import { FOREIGN_KEY_DELETION_OPTIONS } from './ForeignKeySelector.constants' +import { ForeignKey } from './ForeignKeySelector.types' import { generateDeletionActionDescription } from './ForeignKeySelector.utils' export interface ForeignKeySelectorProps { @@ -32,7 +32,6 @@ const ForeignKeySelector = ({ saveChanges, }: ForeignKeySelectorProps) => { const { project } = useProjectContext() - const { meta } = useStore() const [errors, setErrors] = useState({}) const [selectedForeignKey, setSelectedForeignKey] = useState({ schema: 'public', @@ -45,9 +44,11 @@ const ForeignKeySelector = ({ projectRef: project?.ref, connectionString: project?.connectionString, }) - const tables = meta.tables.list( - (table: PostgresTable) => table.schema === selectedForeignKey.schema - ) + const { data: tables } = useTablesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + schema: selectedForeignKey.schema, + }) const foreignKey = column?.foreignKey const selectedTable: PostgresTable | undefined = find(tables, { @@ -58,11 +59,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) { @@ -87,7 +83,6 @@ const ForeignKeySelector = ({ }, [visible]) const updateSelectedSchema = (schema: string) => { - meta.tables.loadBySchema(schema) const updatedForeignKey = { schema, table: '', diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx index add2ac309c1..4a3620853f9 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx @@ -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 }) diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index 379d83f2d02..194759561e5 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -1,31 +1,29 @@ -import { useState } from 'react' +import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta' import { QueryKey, useQueryClient } from '@tanstack/react-query' -import { find, isEmpty, isUndefined, noop } from 'lodash' -import { Dictionary } from 'components/grid' -import { Modal } from 'ui' -import type { PostgresTable, PostgresColumn } from '@supabase/postgres-meta' +import { isEmpty, isUndefined, noop } from 'lodash' +import { useState } from 'react' -import { useStore, useUrlState } from 'hooks' +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 { RowEditor, ColumnEditor, TableEditor, SpreadsheetImport } from '.' -import { ImportContent } from './TableEditor/TableEditor.types' +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 from './RowEditor/ForeignRowSelector/ForeignRowSelector' +import JsonEdit from './RowEditor/JsonEditor/JsonEditor' import { ColumnField, CreateColumnPayload, ExtendedPostgresRelationship, UpdateColumnPayload, } from './SidePanelEditor.types' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import ConfirmationModal from 'components/ui/ConfirmationModal' -import JsonEdit from './RowEditor/JsonEditor/JsonEditor' -import { JsonEditValue } from './RowEditor/RowEditor.types' -import { sqlKeys } from 'data/sql/keys' -import ForeignRowSelector, { - ForeignRowSelectorProps, -} from './RowEditor/ForeignRowSelector/ForeignRowSelector' -import { useTableEditorStateSnapshot } from 'state/table-editor' +import { ImportContent } from './TableEditor/TableEditor.types' export interface SidePanelEditorProps { selectedTable?: PostgresTable @@ -53,7 +51,6 @@ const SidePanelEditor = ({ const [isEdited, setIsEdited] = useState(false) const [isClosingPanel, setIsClosingPanel] = useState(false) - const tables = meta.tables.list() const enumArrayColumns = (selectedTable?.columns ?? []) .filter((column) => { return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array' @@ -256,7 +253,7 @@ const SidePanelEditor = ({ } queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])) await Promise.all([ - meta.tables.loadById(selectedTable!.id), + queryClient.invalidateQueries(tableKeys.table(project?.ref, selectedTable!.id)), queryClient.invalidateQueries( sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name]) ), diff --git a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index ebeeb3b974e..0fe19b5fab9 100644 --- a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -370,7 +370,7 @@ const TableGridEditor = ({ {(isViewSelected || isTableSelected) && } - {!isUndefined(snap.selectedSchemaName) && ( + {snap.selectedSchemaName !== undefined && ( ) => { + const { ref: projectRef } = useParams() const { ui, meta } = useStore() - const { isInitialized, isLoading, error } = meta.tables - const projectRef = ui.selectedProject?.ref ?? 'default' const router = useRouter() const page = router.pathname.split('/')[4] - const [loaded, setLoaded] = useState(isInitialized) - useEffect(() => { if (ui.selectedProject?.ref) { meta.policies.load() - meta.tables.load() meta.roles.load() } }, [ui.selectedProject?.ref]) - useEffect(() => { - if (!isLoading && !loaded) { - setLoaded(true) - } - }, [isLoading]) - - if (error) { - return ( - - - - ) - } - return ( } + productMenu={} >
{children} diff --git a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index 5c7ae50b811..ec9b1ab18a5 100644 --- a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -1,10 +1,9 @@ -import { FC, ReactNode, useEffect, useState } from 'react' +import { FC, ReactNode, useEffect } from 'react' import { observer } from 'mobx-react-lite' import { useRouter } from 'next/router' import { useFlag, useStore, withAuth } from 'hooks' import ProjectLayout from '../' -import Error from 'components/ui/Error' import ProductMenu from 'components/ui/ProductMenu' import { generateDatabaseMenu } from './DatabaseMenu.utils' import { IS_PLATFORM } from 'lib/constants' @@ -16,7 +15,6 @@ interface Props { const DatabaseLayout: FC = ({ title, children }) => { const { meta, ui, vault, backups } = useStore() - const { error } = meta.tables const project = ui.selectedProject const router = useRouter() @@ -28,8 +26,6 @@ const DatabaseLayout: FC = ({ title, children }) => { useEffect(() => { if (ui.selectedProject?.ref) { - meta.tables.load() - meta.roles.load() meta.triggers.load() meta.extensions.load() @@ -47,14 +43,6 @@ const DatabaseLayout: FC = ({ title, children }) => { } }, [ui.selectedProject?.ref, isVaultEnabled]) - if (error) { - return ( - - - - ) - } - return ( ['projects', projectRef, 'tables'] as const, + 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, } diff --git a/studio/data/tables/table-query.ts b/studio/data/tables/table-query.ts index 447a371a43d..eaa7a417e5b 100644 --- a/studio/data/tables/table-query.ts +++ b/studio/data/tables/table-query.ts @@ -1,8 +1,9 @@ import { PostgresTable } from '@supabase/postgres-meta' -import { useQuery, UseQueryOptions } from '@tanstack/react-query' +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 @@ -55,3 +56,24 @@ export const useTableQuery = ( ...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) { + const queryClient = useQueryClient() + + return useCallback( + (id: NonNullable) => { + return queryClient.fetchQuery({ + queryKey: tableKeys.table(projectRef, id), + queryFn: ({ signal }) => getTable({ id, projectRef, connectionString }, signal), + }) + }, + [connectionString, projectRef, queryClient] + ) +} diff --git a/studio/data/tables/tables-query.ts b/studio/data/tables/tables-query.ts index c258a168e3b..ea18061372b 100644 --- a/studio/data/tables/tables-query.ts +++ b/studio/data/tables/tables-query.ts @@ -1,19 +1,21 @@ import { PostgresTable } from '@supabase/postgres-meta' -import { useQuery, UseQueryOptions } from '@tanstack/react-query' +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 { 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 }: TablesVariables, + { projectRef, connectionString, schema }: TablesVariables, signal?: AbortSignal ) { if (!projectRef) { @@ -23,10 +25,17 @@ export async function getTables( let headers = new Headers() if (connectionString) headers.set('x-connection-encrypted', connectionString) - const response = (await get(`${API_URL}/pg-meta/${projectRef}/tables`, { - headers: Object.fromEntries(headers), - signal, - })) as TablesResponse + 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 @@ -39,11 +48,32 @@ export type TablesData = Awaited> export type TablesError = unknown export const useTablesQuery = ( - { projectRef, connectionString }: TablesVariables, + { projectRef, connectionString, schema }: TablesVariables, { enabled = true, ...options }: UseQueryOptions = {} ) => useQuery( - tableKeys.list(projectRef), - ({ signal }) => getTables({ projectRef, connectionString }, signal), + 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) { + 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] + ) +} diff --git a/studio/pages/project/[ref]/auth/policies.tsx b/studio/pages/project/[ref]/auth/policies.tsx index 7ae77e49e06..0835f805548 100644 --- a/studio/pages/project/[ref]/auth/policies.tsx +++ b/studio/pages/project/[ref]/auth/policies.tsx @@ -1,20 +1,22 @@ -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 { checkPermissions, 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 Connecting from 'components/ui/Loading/Loading' import NoPermission from 'components/ui/NoPermission' import { useSchemasQuery } from 'data/database/schemas-query' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { useTableEditorStateSnapshot } from 'state/table-editor' +import { useTablesQuery } from 'data/tables/tables-query' +import { checkPermissions, 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 @@ -74,10 +76,17 @@ const AuthPoliciesPage: NextPageWithLayout = () => { const policies = meta.policies.list() - const tables = meta.tables.list( - (table: { schema: string }) => table.schema === snap.selectedSchemaName - ) - const filteredTables = onFilterTables(tables, policies, searchString) + const { + data: tables, + isLoading, + isSuccess, + } = useTablesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + schema: snap.selectedSchemaName, + }) + + const filteredTables = onFilterTables(tables ?? [], policies, searchString) const canReadPolicies = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'policies') @@ -159,7 +168,10 @@ const AuthPoliciesPage: NextPageWithLayout = () => { - 0} isLocked={isLocked} /> + {isLoading && } + {isSuccess && ( + 0} isLocked={isLocked} /> + )} ) } diff --git a/studio/pages/project/[ref]/editor/index.tsx b/studio/pages/project/[ref]/editor/index.tsx index 0588fece332..55c2cfe0189 100644 --- a/studio/pages/project/[ref]/editor/index.tsx +++ b/studio/pages/project/[ref]/editor/index.tsx @@ -1,10 +1,26 @@ -import { EmptyState } from 'components/interfaces/TableGridEditor' +import { useRouter } from 'next/router' + +import { useParams } from 'common' +import { EmptyState, SidePanelEditor } from 'components/interfaces/TableGridEditor' import { TableEditorLayout } from 'components/layouts' import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext' +import { Table } from 'data/tables/table-query' import { NextPageWithLayout } from 'types' const TableEditorPage: NextPageWithLayout = () => { - return + const { ref: projectRef } = useParams() + const router = useRouter() + + const onTableCreated = (table: Table) => { + router.push(`/project/${projectRef}/editor/${table.id}`) + } + + return ( + <> + + + + ) } TableEditorPage.getLayout = (page) => ( diff --git a/studio/stores/pgmeta/MetaStore.ts b/studio/stores/pgmeta/MetaStore.ts index 905c3f16f2c..98b3e6538b2 100644 --- a/studio/stores/pgmeta/MetaStore.ts +++ b/studio/stores/pgmeta/MetaStore.ts @@ -44,6 +44,10 @@ import ForeignTableStore, { IForeignTableStore } from './ForeignTableStore' import ViewStore, { IViewStore } from './ViewStore' import MaterializedViewStore, { IMaterializedViewStore } from './MaterializedViewStore' import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants' +import { getQueryClient } from 'data/query-client' +import { tableKeys } from 'data/tables/keys' +import { getTables } from 'data/tables/tables-query' +import { getTable } from 'data/tables/table-query' const BATCH_SIZE = 1000 const CHUNK_SIZE = 1024 * 1024 * 0.1 // 0.1MB @@ -313,7 +317,15 @@ 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 projectRef = this.rootStore.ui.selectedProject?.ref + const connectionString = this.rootStore.ui.selectedProject?.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) { @@ -560,8 +572,14 @@ export default class MetaStore implements IMetaStore { }) } - await this.tables.load() - const tables = this.tables.list() + const queryClient = getQueryClient() + const projectRef = this.rootStore.ui.selectedProject?.ref + const connectionString = this.rootStore.ui.selectedProject?.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) { @@ -585,8 +603,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 @@ -709,7 +727,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 @@ -829,7 +847,20 @@ 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 projectRef = this.rootStore.ui.selectedProject?.ref + const connectionString = this.rootStore.ui.selectedProject?.connectionString + + queryClient.invalidateQueries(tableKeys.table(projectRef, table.id)) + + return { + table: await getTable({ + projectRef, + connectionString, + id: table.id, + }), + hasError, + } } async insertRowsViaSpreadsheet( From 4100321c3e04f7ea44690b48c2df61633982531f Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Thu, 7 Sep 2023 17:03:18 +1000 Subject: [PATCH 08/17] address feedback --- .../TableEditor/HeaderTitle.tsx | 6 ++-- .../TableEditor/TableEditor.tsx | 6 +--- .../TableGridEditor/TableGridEditor.tsx | 2 +- studio/data/database/schemas-query.ts | 28 +++++++++++-------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx index 81e4bb4566c..9c71dcce12c 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx @@ -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 {table.name} + Duplicate table {table?.name} ) } return ( <> - Update table {table.name} + Update table {table?.name} ) } diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index 72262f47ec3..dd97c467436 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -170,11 +170,7 @@ const TableEditor = ({ key="TableEditor" visible={visible} header={ - + } className={`transition-all duration-100 ease-in ${isImportingSpreadsheet ? ' mr-32' : ''}`} onCancel={closePanel} diff --git a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index 323c337ad90..79565b1d54e 100644 --- a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -411,7 +411,7 @@ const TableGridEditor = ({ {(isViewSelected || isTableSelected) && } - {!isUndefined(snap.selectedSchemaName) && ( + {snap.selectedSchemaName !== undefined && ( > From e8dc502bd4d89f248f96bccfc6bd879bd8fffbf2 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Thu, 7 Sep 2023 17:13:21 +1000 Subject: [PATCH 09/17] cleanup --- .../interfaces/Database/Extensions/EnableExtensionModal.tsx | 4 ++-- .../components/interfaces/TableGridEditor/TableGridEditor.tsx | 3 +-- studio/pages/project/[ref]/database/hooks/index.tsx | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx b/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx index 619de9a8e88..9a80bb1ee56 100644 --- a/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx +++ b/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx @@ -1,4 +1,4 @@ -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' @@ -162,7 +162,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM Create a new schema "{extension.name}" - {schemas?.map((schema: PostgresSchema) => { + {schemas?.map((schema) => { return ( (null) - const isVaultEnabled = useFlag('vaultExtension') const [encryptedColumns, setEncryptedColumns] = useState([]) const [apiPreviewPanelOpen, setApiPreviewPanelOpen] = useState(false) diff --git a/studio/pages/project/[ref]/database/hooks/index.tsx b/studio/pages/project/[ref]/database/hooks/index.tsx index 79db6860b2f..80843408841 100644 --- a/studio/pages/project/[ref]/database/hooks/index.tsx +++ b/studio/pages/project/[ref]/database/hooks/index.tsx @@ -25,7 +25,7 @@ const HooksPage: NextPageWithLayout = () => { const { data: schemas, - isLoading: isSchemasLoading, + isLoading: isLoadingSchemas, refetch, } = useSchemasQuery({ projectRef: project?.ref, @@ -78,7 +78,7 @@ const HooksPage: NextPageWithLayout = () => { return } - if (isSchemasLoading) { + if (isLoadingSchemas) { return (
From d551d52212694d1270bd5769b993ae6f7dfc7a30 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Fri, 8 Sep 2023 18:00:49 +1000 Subject: [PATCH 10/17] toast column not found errors --- .../interfaces/TableGridEditor/TableGridEditor.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index cd40b4d7658..22db3524264 100644 --- a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -239,7 +239,10 @@ const TableGridEditor = ({ if (column) { snap.onEditColumn(column) } else { - console.error(`Unable to find column ${name} in ${selectedTable?.name}`) + ui.setNotification({ + category: 'error', + message: `Unable to find column ${name} in ${selectedTable?.name}`, + }) } } @@ -248,7 +251,10 @@ const TableGridEditor = ({ if (column) { snap.onDeleteColumn(column) } else { - console.error(`Unable to find column ${name} in ${selectedTable?.name}`) + ui.setNotification({ + category: 'error', + message: `Unable to find column ${name} in ${selectedTable?.name}`, + }) } } From 2796f5ef5ff5eadfcede88176e22b5c528285cb5 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 12 Sep 2023 22:43:25 +1200 Subject: [PATCH 11/17] address feedback --- .../Publications/PublicationsTables.tsx | 6 ++ .../Database/Schemas/SchemaGraph.tsx | 31 ++++--- .../interfaces/Database/Tables/TableList.tsx | 20 +--- .../DeleteConfirmationDialogs.tsx | 13 ++- .../SidePanelEditor/SidePanelEditor.tsx | 11 ++- .../TableEditorLayout/TableEditorLayout.tsx | 4 +- .../TableEditorLayout/TableEditorMenu.tsx | 4 +- studio/data/tables/tables-query.ts | 3 +- studio/pages/project/[ref]/auth/policies.tsx | 8 +- .../project/[ref]/database/functions.tsx | 10 +- .../pages/project/[ref]/database/schemas.tsx | 91 +++++++++++-------- studio/pages/project/[ref]/editor/[id].tsx | 15 ++- 12 files changed, 127 insertions(+), 89 deletions(-) diff --git a/studio/components/interfaces/Database/Publications/PublicationsTables.tsx b/studio/components/interfaces/Database/Publications/PublicationsTables.tsx index 70e44d7c19e..0e6b650674b 100644 --- a/studio/components/interfaces/Database/Publications/PublicationsTables.tsx +++ b/studio/components/interfaces/Database/Publications/PublicationsTables.tsx @@ -5,6 +5,7 @@ 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 AlertError from 'components/ui/AlertError' import InformationBox from 'components/ui/InformationBox' import Connecting from 'components/ui/Loading/Loading' import { useTablesQuery } from 'data/tables/tables-query' @@ -31,6 +32,8 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT data: tables, isLoading, isSuccess, + isError, + error, } = useTablesQuery( { projectRef: project?.ref, @@ -110,6 +113,9 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT
)} + + {isError && } + {isSuccess && (tables.length === 0 ? ( diff --git a/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx b/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx index d76abe61581..c2701e5e5b2 100644 --- a/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx +++ b/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx @@ -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 (
-

Loading schemas...

+

Loading table...

) } - if (meta.tables.hasError) { + if (isError) { return (

Error connecting to API

-

{`${meta.tables.error?.message ?? 'Unknown error'}`}

+

{`${error?.message ?? 'Unknown error'}`}

) } diff --git a/studio/components/interfaces/Database/Tables/TableList.tsx b/studio/components/interfaces/Database/Tables/TableList.tsx index 624ea54e786..1e190ba7eb8 100644 --- a/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/studio/components/interfaces/Database/Tables/TableList.tsx @@ -25,6 +25,7 @@ import { Input, Listbox, } from 'ui' +import AlertError from 'components/ui/AlertError' interface TableListProps { onAddTable: () => void @@ -58,6 +59,8 @@ const TableList = ({ data: tables, isLoading, isSuccess, + isError, + error, } = useTablesQuery( { projectRef: project?.ref, @@ -180,6 +183,8 @@ const TableList = ({ )} + {isError && } + {isSuccess && (tables.length === 0 ? ( @@ -323,21 +328,6 @@ const TableList = ({ )} - - {/* */} - {onAddRow !== undefined && onImportData !== undefined && ( - - )} - + {(filters ?? []).length === 0 ? ( +
+

This table is empty

+ {onAddRow !== undefined && onImportData !== undefined && ( + <> +

+ Add or generate rows to get started. +

+
+ {/* [Joshen] Leaving this as a placeholder */} + {/* */} + {onAddRow !== undefined && onImportData !== undefined && ( + + )} +
+ + )} +
+ ) : ( +
+

+ The filters applied has returned no results from this table +

+
+ )} )} - + } /> From 6d0a72492d1a0d40ec25bb07e8615e0b6b509e89 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 18 Sep 2023 16:04:02 +0800 Subject: [PATCH 17/17] Add remove filters cta if filters returned no results --- studio/components/grid/SupabaseGrid.tsx | 1 + .../components/grid/components/grid/Grid.tsx | 20 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/studio/components/grid/SupabaseGrid.tsx b/studio/components/grid/SupabaseGrid.tsx index daea832730d..11082c85985 100644 --- a/studio/components/grid/SupabaseGrid.tsx +++ b/studio/components/grid/SupabaseGrid.tsx @@ -192,6 +192,7 @@ const SupabaseGridLayout = forwardRef( isSuccess={isSuccess} isError={isError} filters={filters} + setParams={setParams} updateRow={updateTableRow} onAddRow={onAddRow} onImportData={onImportData} diff --git a/studio/components/grid/components/grid/Grid.tsx b/studio/components/grid/components/grid/Grid.tsx index 0ceec407f85..4e937cce8d4 100644 --- a/studio/components/grid/components/grid/Grid.tsx +++ b/studio/components/grid/components/grid/Grid.tsx @@ -7,13 +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 { useUrlState } from 'hooks' +import { Button } from 'ui' import { useDispatch, useTrackedState } from '../../store' import { Filter, GridProps, SupaRow } from '../../types' import RowRenderer from './RowRenderer' -import { Button } from 'ui' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' -import AlertError from 'components/ui/AlertError' const rowKeyGetter = (row: SupaRow) => { return row?.idx ?? -1 @@ -34,6 +35,7 @@ interface IGrid extends GridProps { isSuccess: boolean isError: boolean filters: Filter[] + setParams: ReturnType[1] updateRow: (previousRow: any, updatedData: any) => void onAddRow?: () => void onImportData?: () => void @@ -60,6 +62,7 @@ export const Grid = memo( isSuccess, isError, filters, + setParams, updateRow, onAddRow, onImportData, @@ -133,6 +136,12 @@ export const Grid = memo( } } + const removeAllFilters = () => { + setParams((prevParams) => { + return { ...prevParams, filter: [] } + }) + } + return (
The filters applied has returned no results from this table

+
+ +
)}