From 04575b65b14588e9727b4a73aa47ee9023f5be22 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 2 May 2023 00:13:39 +1000 Subject: [PATCH 01/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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 0775f2d6bdeb40f86381ea138d358c0c06ba4511 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Mon, 3 Jul 2023 16:41:11 +0800 Subject: [PATCH 08/39] fix --- .../IntegrationSettings/SidePanelGitHubRepoSelection.tsx | 2 +- .../components/interfaces/Organization/OAuthApps/OAuthApps.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/studio/components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoSelection.tsx b/studio/components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoSelection.tsx index 4b978dd0ae2..c520893dc7b 100644 --- a/studio/components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoSelection.tsx +++ b/studio/components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoSelection.tsx @@ -34,7 +34,7 @@ Check the details below before proceeding /> - +
Date: Sat, 16 Sep 2023 02:35:41 +0530 Subject: [PATCH 25/39] Fix tab accessibility with popup modal --- packages/ui/src/components/Toggle/Toggle.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/Toggle/Toggle.tsx b/packages/ui/src/components/Toggle/Toggle.tsx index 9ebbfbd6fff..3b9ad145f14 100644 --- a/packages/ui/src/components/Toggle/Toggle.tsx +++ b/packages/ui/src/components/Toggle/Toggle.tsx @@ -63,7 +63,9 @@ function Toggle({ }, []) function handleBlurEvent(e: React.FocusEvent) { - if (handleBlur) handleBlur(e) + setTimeout(() => { + if (handleBlur) handleBlur(e) + }, 100) if (onBlur) onBlur(e) } From 76d84cda7047c85f1e42d311d131036232f19d96 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Sat, 16 Sep 2023 09:42:27 +0200 Subject: [PATCH 26/39] fix can't edit column after creating error --- .../SidePanelEditor/SidePanelEditor.tsx | 3 ++- .../TableGridEditor/TableGridEditor.tsx | 17 +++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index e4a495e8fb5..dc9aa678a3c 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -244,8 +244,9 @@ const SidePanelEditor = ({ ) { reAddRenamedColumnSortAndFilter(selectedColumnToEdit.name, payload.name) } - queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])) + await Promise.all([ + queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])), queryClient.invalidateQueries(tableKeys.table(project?.ref, selectedTable!.id)), queryClient.invalidateQueries( sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name]) diff --git a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index 5f62aaafe73..a0f15ceb6fd 100644 --- a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -27,10 +27,11 @@ import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query' import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' import { sqlKeys } from 'data/sql/keys' import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation' -import { useCheckPermissions, useStore, useUrlState } from 'hooks' +import { useCheckPermissions, useLatest, useStore, useUrlState } from 'hooks' import useEntityType from 'hooks/misc/useEntityType' import { TableLike } from 'hooks/misc/useTable' import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { EMPTY_ARR } from 'lib/void' import { useTableEditorStateSnapshot } from 'state/table-editor' import { SchemaView } from 'types' import APIDocumentationPanel from './APIDocumentationPanel' @@ -157,12 +158,13 @@ const TableGridEditor = ({ }, [selectedTable?.id]) const entityType = useEntityType(selectedTable?.id) + const columnsRef = useLatest(selectedTable?.columns ?? EMPTY_ARR) + // NOTE: DO NOT PUT HOOKS AFTER THIS LINE if (isLoadingSelectedTable) { return } - // NOTE: DO NOT PUT HOOKS AFTER THIS LINE if (isUndefined(selectedTable)) { return } @@ -234,8 +236,11 @@ const TableGridEditor = ({ } } - const onSelectEditColumn = async (name: string) => { - const column = find(selectedTable?.columns ?? [], { name }) as PostgresColumn + // columns must be accessed via columnsRef.current as these two functions immediately become + // stale as they are accessed via some react-tracked madness + // [TODO]: refactor out all of react-tracked + const onSelectEditColumn = (name: string) => { + const column = find(columnsRef.current, { name }) as PostgresColumn if (column) { snap.onEditColumn(column) } else { @@ -246,8 +251,8 @@ const TableGridEditor = ({ } } - const onSelectDeleteColumn = async (name: string) => { - const column = find(selectedTable?.columns ?? [], { name }) as PostgresColumn + const onSelectDeleteColumn = (name: string) => { + const column = find(columnsRef.current ?? [], { name }) as PostgresColumn if (column) { snap.onDeleteColumn(column) } else { From 62aeb7aaba3ee38b83c8e6e60490d537c2044574 Mon Sep 17 00:00:00 2001 From: Ender Bonnet <13243693+enBonnet@users.noreply.github.com> Date: Sat, 16 Sep 2023 07:10:11 -0300 Subject: [PATCH 27/39] Update sveltekit.mdx Added missing "fail" import --- apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx index 38fec39a043..b38ac77adf7 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx @@ -464,6 +464,8 @@ export const actions = { ```js src/routes/login/+page.server.ts // src/routes/login/+page.server.ts +import { fail } from '@sveltejs/kit' + export const actions = { default: async ({ request, url, locals: { supabase } }) => { const formData = await request.formData() From 75863037f2d696f45421b8105c950c263a6b95ec Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Sun, 17 Sep 2023 00:15:40 +0800 Subject: [PATCH 28/39] chore: update section titles --- apps/docs/pages/guides/cli/config.tsx | 12 ++++----- spec/cli_v1_config.yaml | 38 +++++++++++++-------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/apps/docs/pages/guides/cli/config.tsx b/apps/docs/pages/guides/cli/config.tsx index 6789c50ce74..febc8fe69d5 100644 --- a/apps/docs/pages/guides/cli/config.tsx +++ b/apps/docs/pages/guides/cli/config.tsx @@ -7,17 +7,15 @@ import Head from 'next/head' import { CodeBlock } from 'ui' // Parameters are grouped on the page by tag -const TAGS = ['General', 'Auth', 'API', 'Database', 'Dashboard', 'Local', 'Edge-Functions'] - const tocList = [] -const content = TAGS.map((tag) => { - tocList.push({ text: tag, link: `${tag.toLowerCase()}-config`, level: 2 }) +const content = specFile.info.tags.map((tag) => { + tocList.push({ text: tag.title, link: `${tag.id}-config`, level: 2 }) return (
- {tag} Config + {tag.title} Config {specFile.parameters - .filter((param: Parameter) => param.tags[0] === tag.toLowerCase()) - .map((parameter: Parameter, index) => { + .filter((param: Parameter) => param.tags[0] === tag.id) + .map((parameter: Parameter) => { tocList.push({ text: parameter.id, link: `#${parameter.id}`, level: 3 }) return })} diff --git a/spec/cli_v1_config.yaml b/spec/cli_v1_config.yaml index dfc29bb92b7..b4e9cc67e52 100644 --- a/spec/cli_v1_config.yaml +++ b/spec/cli_v1_config.yaml @@ -21,32 +21,32 @@ info: title: General description: General settings. - id: auth - title: Auth Settings - description: Auth Settings. + title: Auth + description: Auth settings. - id: api - title: API Settings - description: Auth Settings. + title: API + description: Auth settings. - id: database - title: Database Settings - description: Database Settings. - - id: realtime - title: Realtime Settings - description: Dashboard Settings. - - id: storage - title: Storage Settings - description: Storage Settings. + title: Database + description: Database settings. - id: dashboard - title: Dashboard Settings - description: Dashboard Settings. + title: Dashboard + description: Dashboard settings. + - id: realtime + title: Realtime + description: Dashboard settings. + - id: storage + title: Storage + description: Storage settings. - id: edge-functions - title: Edge Functions Settings - description: Edge Functions Settings. + title: Edge-Functions + description: Edge-Functions settings. - id: analytics - title: Analytics Settings - description: Analytics Settings. + title: Analytics + description: Analytics settings. - id: local title: Local Development - description: Local Development. + description: Local Development settings. # This section is an array of public functions which a user might need to execute. parameters: From ed43f6ebdebf6804b34fd570ca3135a1c8160158 Mon Sep 17 00:00:00 2001 From: Sadeed pv Date: Sun, 17 Sep 2023 00:57:01 +0530 Subject: [PATCH 29/39] Add gap between framework icons on the docs get started page --- apps/docs/components/HomePageCover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx index 657083befd2..ef750758a95 100644 --- a/apps/docs/components/HomePageCover.tsx +++ b/apps/docs/components/HomePageCover.tsx @@ -82,7 +82,7 @@ const HomePageCover = (props) => { Discover how to set up a database to an app making queries in just a few minutes.

-
+
{frameworks.map((framework, i) => ( From 97bc98aa21b54c510407de27f836928bb457fdcd Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 18 Sep 2023 13:02:50 +0800 Subject: [PATCH 30/39] fix empty states of table editor --- studio/components/grid/SupabaseGrid.tsx | 9 ++- .../components/grid/components/grid/Grid.tsx | 77 ++++++++++++++----- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/studio/components/grid/SupabaseGrid.tsx b/studio/components/grid/SupabaseGrid.tsx index ba327db1e2b..daea832730d 100644 --- a/studio/components/grid/SupabaseGrid.tsx +++ b/studio/components/grid/SupabaseGrid.tsx @@ -65,7 +65,7 @@ const SupabaseGridLayout = forwardRef( const filters = formatFilterURLParams(filter as string[]) const { project } = useProjectContext() - const { data, isLoading, isRefetching } = useTableRowsQuery( + const { data, error, isSuccess, isError, isLoading, isRefetching } = useTableRowsQuery( { queryKey: [props.table.schema, props.table.name], projectRef: project?.ref, @@ -187,7 +187,14 @@ const SupabaseGridLayout = forwardRef( ref={gridRef} {...gridProps} rows={data?.rows ?? []} + error={error} + isLoading={isLoading} + isSuccess={isSuccess} + isError={isError} + filters={filters} updateRow={updateTableRow} + onAddRow={onAddRow} + onImportData={onImportData} onEditForeignKeyColumnValue={onEditForeignKeyColumnValue} />
diff --git a/studio/components/grid/components/grid/Grid.tsx b/studio/components/grid/components/grid/Grid.tsx index 3d28e84c4ad..0ceec407f85 100644 --- a/studio/components/grid/components/grid/Grid.tsx +++ b/studio/components/grid/components/grid/Grid.tsx @@ -9,9 +9,11 @@ import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/S import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query' import { useDispatch, useTrackedState } from '../../store' -import { GridProps, SupaRow } from '../../types' +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 @@ -27,6 +29,11 @@ const updateColumnResizeDebounced = AwesomeDebouncePromise(updateColumnResize, 5 interface IGrid extends GridProps { rows: any[] + error: any + isLoading: boolean + isSuccess: boolean + isError: boolean + filters: Filter[] updateRow: (previousRow: any, updatedData: any) => void onAddRow?: () => void onImportData?: () => void @@ -48,6 +55,11 @@ export const Grid = memo( gridClass, rowClass, rows, + error, + isLoading, + isSuccess, + isError, + filters, updateRow, onAddRow, onImportData, @@ -142,26 +154,55 @@ export const Grid = memo( rowClass={rowClass} style={{ height: '100%' }} noRowsFallback={ -
-

This table is empty

- {onAddRow !== undefined && onImportData !== undefined && ( + <> + {isLoading && ( +
+ +
+ )} + {isError && ( +
+ +
+ )} + {isSuccess && ( <> -

Add or generate rows to get started.

-
- {/* [Joshen] Leaving this as a placeholder */} - {/* */} - {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 31/39] 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

+
+ +
)} From f1baf06e493895242f1524b65653094c7869b3f9 Mon Sep 17 00:00:00 2001 From: Denys Nykula Date: Mon, 18 Sep 2023 14:01:59 +0300 Subject: [PATCH 32/39] Fix 404 when opening the Docker self-hosting guide in a new tab Relative to /docs, the Next.js root, /docs resolves as /docs/docs, doing a 404 when opening a new tab or refreshing the page that the client-side router manages to open on an ordinary click. --- apps/docs/pages/guides/self-hosting.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/self-hosting.mdx b/apps/docs/pages/guides/self-hosting.mdx index 4b631683686..b8468f8112a 100644 --- a/apps/docs/pages/guides/self-hosting.mdx +++ b/apps/docs/pages/guides/self-hosting.mdx @@ -26,7 +26,7 @@ export const official = [ { name: 'Docker', description: 'Deploy Supabase within your own infrastructure using Docker Compose.', - href: '/docs/guides/self-hosting/docker', + href: '/guides/self-hosting/docker', }, { name: 'BYO Cloud', From dfcaebcdc9d81a8445f7707204c02491a942c080 Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 18 Sep 2023 21:48:39 +0530 Subject: [PATCH 33/39] fix:#17512 --- apps/www/components/Nav/index.tsx | 104 ++++++++++++++++-------------- packages/common/auth.tsx | 2 + 2 files changed, 57 insertions(+), 49 deletions(-) diff --git a/apps/www/components/Nav/index.tsx b/apps/www/components/Nav/index.tsx index b36fd57c9dd..5b321c0318a 100644 --- a/apps/www/components/Nav/index.tsx +++ b/apps/www/components/Nav/index.tsx @@ -13,7 +13,7 @@ import Developers from '~/components/Nav/Developers' import ScrollProgress from '~/components/ScrollProgress' -import { useIsLoggedIn, useTheme } from 'common' +import { useIsLoggedIn, useIsUserLoading, useTheme } from 'common' import TextLink from '../TextLink' import Image from 'next/image' import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png' @@ -26,7 +26,9 @@ const Nav = () => { const [openProduct, setOpenProduct] = useState(false) const [openDevelopers, setOpenDevelopers] = useState(false) const isLoggedIn = useIsLoggedIn() - + const isUserLoading = useIsUserLoading() + + const isHomePage = router.pathname === '/' const isLaunchWeekPage = router.pathname.includes('launch-week') const showLaunchWeekNavMode = @@ -290,62 +292,66 @@ const Nav = () => {
- - } - > - - Star us on GitHub - - - + + Star us on GitHub + + + - {isLoggedIn ? ( - - - - - - ) : ( - <> - + {isLoggedIn ? ( + - - - - - - + + ) : ( + <> + + + + + + + + + + + + )} - )} + } diff --git a/packages/common/auth.tsx b/packages/common/auth.tsx index 9529f3cbe8c..4d5a943ab9d 100644 --- a/packages/common/auth.tsx +++ b/packages/common/auth.tsx @@ -103,6 +103,8 @@ export const useSession = () => useAuth().session export const useUser = () => useSession()?.user ?? null +export const useIsUserLoading = () => useAuth().isLoading + export const useIsLoggedIn = () => { const user = useUser() From ca2b7b85fef4a59d3a38f1a88177220f25ce4d79 Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 18 Sep 2023 22:33:05 +0530 Subject: [PATCH 34/39] fix:#17512 and formatted --- apps/www/components/Nav/index.tsx | 93 ++++++++++++++++--------------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/apps/www/components/Nav/index.tsx b/apps/www/components/Nav/index.tsx index 5b321c0318a..ac2f75dfdc2 100644 --- a/apps/www/components/Nav/index.tsx +++ b/apps/www/components/Nav/index.tsx @@ -27,8 +27,7 @@ const Nav = () => { const [openDevelopers, setOpenDevelopers] = useState(false) const isLoggedIn = useIsLoggedIn() const isUserLoading = useIsUserLoading() - - + const isHomePage = router.pathname === '/' const isLaunchWeekPage = router.pathname.includes('launch-week') const showLaunchWeekNavMode = @@ -292,17 +291,17 @@ const Nav = () => { - - } - > - - Star us on GitHub - - - + + Star us on GitHub + + + - {isLoggedIn ? ( - - - - - - ) : ( - <> - + {isLoggedIn ? ( + - + - - - - - - - )} + ) : ( + <> + + + + + + + + + + + + )} - } + )} From 715881c33e411758c334bdf5599345acbbf314b0 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Mon, 18 Sep 2023 15:36:19 -0230 Subject: [PATCH 35/39] Edits --- DEVELOPERS.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 45f43a69963..78cfcda3598 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -118,11 +118,11 @@ Now when you run a local development docs server you will see the new docs site. ## Running Docker -To effectively test your changes, it's essential to run Supabase using Docker. Docker enables you to access and utilize various features of your locally modified Supabase version. +To test your changes, you need to run Supabase using Docker. Docker lets you use features of your locally modified Supabase version. #### Prerequsites -Before you begin, ensure that you have the Docker application installed on your device. If you haven't installed Docker yet, you can download and install it from [here](https://docs.docker.com/get-docker/). +First, make sure you have the Docker application installed on your device. You can download and install it from [here](https://docs.docker.com/get-docker/). #### Getting Started @@ -131,7 +131,7 @@ Before you begin, ensure that you have the Docker application installed on your ```sh cd docker ``` - + 2. Copy the example `env` file ```sh @@ -144,19 +144,17 @@ Before you begin, ensure that you have the Docker application installed on your docker-compose up ``` -This command initializes the containers specified in the docker-compose.yml file. It might take a few moments to complete, depending on your system's speed and internet connection. +This command initializes the containers specified in the docker-compose.yml file. It might take a few moments to complete, depending on your computer and internet connection. -Once the docker-compose up process completes, you should have your local version of Supabase up and running within Docker containers. You can access and test the features you've modified or added. +Once the `docker-compose up` process completes, you should have your local version of Supabase up and running within Docker containers. You can access and test the features you've modified or added. -Remember to keep the Docker application open as long as you're working with your local Supabase instance. This will ensure that the containers remain active and accessible for testing. - ---- +Remember to keep the Docker application open as long as you're working with your local Supabase instance. ## Create a pull request -After making your changes, open a pull request (PR). Once you submit your pull request, others from the Supabase team/community will review it with you. +After making your changes, open a pull request. Once you submit your pull request, the Supabase team will review it with you. -If you have an issue, like a merge conflict, or don't know how to open a pull request then check out [GitHub's pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) tutorial on how to resolve merge conflicts and other issues. Once your PR has been merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/supabase/supabase/graphs/contributors). +Once your PR has been merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/supabase/supabase/graphs/contributors). --- From f86fd17e641a3b247108c2eee8b318d38b1c79a9 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Mon, 18 Sep 2023 12:44:16 -0600 Subject: [PATCH 36/39] feat(dashboard): button wording on discard modal warnings --- .../interfaces/Auth/Policies/PolicyEditorModal/index.tsx | 2 +- .../components/interfaces/Database/Functions/CreateFunction.tsx | 2 +- studio/components/interfaces/Database/Hooks/EditHookPanel.tsx | 2 +- .../components/interfaces/Database/Triggers/CreateTrigger.tsx | 2 +- .../TableGridEditor/SidePanelEditor/SidePanelEditor.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx b/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx index c6fc98dfefe..07e7dac366c 100644 --- a/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx +++ b/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx @@ -195,7 +195,7 @@ const PolicyEditorModal = ({ setIsClosingPolicyEditorModal(false)} onSelectConfirm={() => { onSelectCancel() diff --git a/studio/components/interfaces/Database/Functions/CreateFunction.tsx b/studio/components/interfaces/Database/Functions/CreateFunction.tsx index 6b321df2402..a59bccd84f5 100644 --- a/studio/components/interfaces/Database/Functions/CreateFunction.tsx +++ b/studio/components/interfaces/Database/Functions/CreateFunction.tsx @@ -453,7 +453,7 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => { setIsClosingPanel(false)} onSelectConfirm={() => { setIsClosingPanel(false) diff --git a/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx b/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx index fb9ddbb3b15..49bc46d9aac 100644 --- a/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx +++ b/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx @@ -317,7 +317,7 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) = setIsClosingPanel(false)} onSelectConfirm={() => { setIsClosingPanel(false) diff --git a/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx b/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx index 32a0c9d2138..70af9f8e28a 100644 --- a/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx +++ b/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx @@ -394,7 +394,7 @@ const CreateTrigger = ({ trigger, visible, setVisible }: CreateTriggerProps) => setIsClosingPanel(false)} onSelectConfirm={() => { setIsClosingPanel(false) diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index dc9aa678a3c..79df9b3b0d5 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -586,7 +586,7 @@ const SidePanelEditor = ({ setIsClosingPanel(false)} onSelectConfirm={() => { setIsClosingPanel(false) From 75e1f75551c15fa125f28e649e355004529e3a1c Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Mon, 18 Sep 2023 13:04:01 -0600 Subject: [PATCH 37/39] feat(dashboard): header wording on discard modal warnings --- .../interfaces/Auth/Policies/PolicyEditorModal/index.tsx | 2 +- .../components/interfaces/Database/Functions/CreateFunction.tsx | 2 +- studio/components/interfaces/Database/Hooks/EditHookPanel.tsx | 2 +- .../components/interfaces/Database/Triggers/CreateTrigger.tsx | 2 +- .../TableGridEditor/SidePanelEditor/SidePanelEditor.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx b/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx index 07e7dac366c..ec03755588d 100644 --- a/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx +++ b/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx @@ -194,7 +194,7 @@ const PolicyEditorModal = ({
setIsClosingPolicyEditorModal(false)} onSelectConfirm={() => { diff --git a/studio/components/interfaces/Database/Functions/CreateFunction.tsx b/studio/components/interfaces/Database/Functions/CreateFunction.tsx index a59bccd84f5..1a32bb22545 100644 --- a/studio/components/interfaces/Database/Functions/CreateFunction.tsx +++ b/studio/components/interfaces/Database/Functions/CreateFunction.tsx @@ -452,7 +452,7 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => { setIsClosingPanel(false)} onSelectConfirm={() => { diff --git a/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx b/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx index 49bc46d9aac..392aca7a2fe 100644 --- a/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx +++ b/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx @@ -316,7 +316,7 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) = setIsClosingPanel(false)} onSelectConfirm={() => { diff --git a/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx b/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx index 70af9f8e28a..91eebf02de4 100644 --- a/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx +++ b/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx @@ -393,7 +393,7 @@ const CreateTrigger = ({ trigger, visible, setVisible }: CreateTriggerProps) => setIsClosingPanel(false)} onSelectConfirm={() => { diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index 79df9b3b0d5..0e8e621b338 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -585,7 +585,7 @@ const SidePanelEditor = ({ /> setIsClosingPanel(false)} onSelectConfirm={() => { From 093a980678e1c7fb2e056a40c02f36d7cd423765 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Mon, 18 Sep 2023 16:25:54 -0400 Subject: [PATCH 38/39] fix docs getting started icon grid --- apps/docs/components/HomePageCover.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx index ef750758a95..55d62c39620 100644 --- a/apps/docs/components/HomePageCover.tsx +++ b/apps/docs/components/HomePageCover.tsx @@ -63,7 +63,7 @@ const HomePageCover = (props) => { const GettingStarted = () => (
-
+
@@ -82,7 +82,7 @@ const HomePageCover = (props) => { Discover how to set up a database to an app making queries in just a few minutes.

- -
+
From 2ee34de1815f0cb20dbea978e9290c0024b15694 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Mon, 18 Sep 2023 16:29:55 -0400 Subject: [PATCH 39/39] remove unnecessary space in docs homepage --- apps/docs/pages/index.mdx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/docs/pages/index.mdx b/apps/docs/pages/index.mdx index 77529472c4e..b8423ac2ab9 100644 --- a/apps/docs/pages/index.mdx +++ b/apps/docs/pages/index.mdx @@ -9,7 +9,6 @@ import { IconPlay, TextLink, } from 'ui' -import { LWAnnouncement } from 'ui' import HomeMenuIconPicker from '~/components/Navigation/NavigationMenu/HomeMenuIconPicker' @@ -21,10 +20,6 @@ export const meta = { }
- -
- -
## Products