diff --git a/studio/components/grid/SupabaseGrid.tsx b/studio/components/grid/SupabaseGrid.tsx index 0cd01351b10..11082c85985 100644 --- a/studio/components/grid/SupabaseGrid.tsx +++ b/studio/components/grid/SupabaseGrid.tsx @@ -36,168 +36,179 @@ 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, error, isSuccess, isError, isLoading, isRefetching } = useTableRowsQuery( + { + queryKey: [props.table.schema, props.table.name], + projectRef: project?.ref, + connectionString: project?.connectionString, + table: props.table, + sorts, + filters, + page: state.page, + limit: state.rowsPerPage, + }, + { + keepPreviousData: true, + onSuccess(data) { + dispatch({ + type: 'SET_ROWS_COUNT', + payload: data.rows.length, + }) + }, + } + ) + + useImperativeHandle(ref, () => ({ + rowAdded(row: Dictionary) { 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/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 */} - {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 +

+
+ +
+
+ )} )} - + } /> diff --git a/studio/components/grid/components/header/RLSBannerWarning.tsx b/studio/components/grid/components/header/RLSBannerWarning.tsx index 0695e64a4b2..6f27e1821de 100644 --- a/studio/components/grid/components/header/RLSBannerWarning.tsx +++ b/studio/components/grid/components/header/RLSBannerWarning.tsx @@ -1,24 +1,38 @@ -import { useCallback, useState } from 'react' -import { Button, IconAlertCircle, Modal } from 'ui' import Link from 'next/link' -import { useStore } from 'hooks' +import { useCallback, useState } from 'react' + import { useParams } from 'common/hooks' -import ConfirmationModal from 'components/ui/ConfirmationModal' -import type { PostgresTable } from '@supabase/postgres-meta' import { rlsAcknowledgedKey } from 'components/grid/constants' import RLSDisableModalContent from 'components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/RLSDisableModal' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import ConfirmationModal from 'components/ui/ConfirmationModal' +import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' +import { useTableQuery } from 'data/tables/table-query' +import useEntityType from 'hooks/misc/useEntityType' +import { Button, IconAlertCircle, Modal } from 'ui' export default function RLSBannerWarning() { - const { meta } = useStore() - const { ref: projectRef, id: tableID } = useParams() + const { project } = useProjectContext() + const { ref: projectRef, id: _id } = useParams() + const tableID = _id ? Number(_id) : undefined const rlsKey = rlsAcknowledgedKey(tableID) const isAcknowledged = localStorage?.getItem(rlsKey) === 'true' ?? false const [isOpen, setIsOpen] = useState(false) - const tables: PostgresTable[] = meta.tables.list() - const currentTable = tables.find((table) => table.id.toString() === tableID) + const entityType = useEntityType(tableID) + const { data: currentTable } = useTableQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + id: tableID, + }, + { + enabled: entityType?.type === ENTITY_TYPE.TABLE, + } + ) + const rlsEnabled = currentTable?.rls_enabled const isPublicTable = currentTable?.schema === 'public' diff --git a/studio/components/grid/constants.ts b/studio/components/grid/constants.ts index 5fed7c3ef88..0e98ad84764 100644 --- a/studio/components/grid/constants.ts +++ b/studio/components/grid/constants.ts @@ -18,4 +18,5 @@ export const ERROR_PRIMARY_KEY_NOTFOUND = export const RLS_ACKNOWLEDGED_KEY = 'supabase-acknowledge-rls-warning' -export const rlsAcknowledgedKey = (tableID?: string) => `${RLS_ACKNOWLEDGED_KEY}-${tableID}` +export const rlsAcknowledgedKey = (tableID?: string | number) => + `${RLS_ACKNOWLEDGED_KEY}-${String(tableID)}` diff --git a/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx b/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx index bd1cfb234ba..9a80bb1ee56 100644 --- a/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx +++ b/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx @@ -1,8 +1,10 @@ -import type { PostgresExtension, PostgresSchema } from '@supabase/postgres-meta' +import type { PostgresExtension } from '@supabase/postgres-meta' import { useEffect, useState } from 'react' import { Button, Form, IconDatabase, IconPlus, Input, Listbox, Modal } from 'ui' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useSchemasQuery } from 'data/database/schemas-query' import { useStore } from 'hooks' interface EnableExtensionModalProps { @@ -12,11 +14,15 @@ interface EnableExtensionModalProps { } const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionModalProps) => { + const { project } = useProjectContext() const { ui, meta } = useStore() const [defaultSchema, setDefaultSchema] = useState() const [fetchingSchemaInfo, setFetchingSchemaInfo] = useState(false) - const schemas = meta.schemas.list() + const { data: schemas, isLoading: isSchemasLoading } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) // [Joshen] Worth checking in with users - whether having this schema selection // might be confusing, and if we should have a tooltip to explain that schemas @@ -44,7 +50,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM return () => { cancel = true } - }, [visible]) + }, [visible, extension.name]) const validate = (values: any) => { const errors: any = {} @@ -124,7 +130,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM return (
- {fetchingSchemaInfo ? ( + {fetchingSchemaInfo || isSchemasLoading ? (
@@ -156,8 +162,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM Create a new schema "{extension.name}" - {/* @ts-ignore */} - {schemas.map((schema: PostgresSchema) => { + {schemas?.map((schema) => { return ( { + const { project } = useProjectContext() const { ui, meta } = useStore() const _localState = useLocalObservable(() => new CreateFunctionStore()) _localState.meta = meta as any const [isClosingPanel, setIsClosingPanel] = useState(false) - useEffect(() => { - const fetchSchemas = async () => { - await (_localState!.meta as any).schemas.load() - const schemas = (_localState!.meta as any).schemas.list() - _localState.setSchemas(schemas) + useSchemasQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + }, + { + onSuccess(schemas) { + _localState.setSchemas(schemas) + }, } - - fetchSchemas() - }, []) + ) useEffect(() => { _localState.formState.reset(func) diff --git a/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx index 744ae63c80d..a50847e20b5 100644 --- a/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx +++ b/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx @@ -1,17 +1,20 @@ import * as Tooltip from '@radix-ui/react-tooltip' -import { PostgresFunction, PostgresSchema } from '@supabase/postgres-meta' +import { PostgresFunction } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { noop, partition } from 'lodash' import { observer } from 'mobx-react-lite' import { useState } from 'react' import { Button, IconLock, IconSearch, Input, Listbox } from 'ui' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import Table from 'components/to-be-cleaned/Table' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' -import { useCheckPermissions, useStore } from 'hooks' -import FunctionList from './FunctionList' import AlertError from 'components/ui/AlertError' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import { useSchemasQuery } from 'data/database/schemas-query' +import { useCheckPermissions, useStore } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import FunctionList from './FunctionList' interface FunctionsListProps { createFunction: () => void @@ -24,6 +27,7 @@ const FunctionsList = ({ editFunction = noop, deleteFunction = noop, }: FunctionsListProps) => { + const { project } = useProjectContext() const { meta } = useStore() const [selectedSchema, setSelectedSchema] = useState('public') const [filterString, setFilterString] = useState('') @@ -33,11 +37,14 @@ const FunctionsList = ({ 'functions' ) - const schemas: PostgresSchema[] = meta.schemas.list() - const [protectedSchemas, openSchemas] = partition(schemas, (schema) => - meta.excludedSchemas.includes(schema?.name ?? '') + const { data: schemas } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const [protectedSchemas, openSchemas] = partition(schemas ?? [], (schema) => + EXCLUDED_SCHEMAS.includes(schema?.name ?? '') ) - const schema = schemas.find((schema) => schema.name === selectedSchema) + const schema = schemas?.find((schema) => schema.name === selectedSchema) const isLocked = protectedSchemas.some((s) => s.id === schema?.id) const functions = meta.functions.list() diff --git a/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx b/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx index 830bd41c110..fb9ddbb3b15 100644 --- a/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx +++ b/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx @@ -1,6 +1,6 @@ import { PostgresTable, PostgresTrigger } from '@supabase/postgres-meta' import Image from 'next/image' -import { MutableRefObject, useEffect, useRef, useState } from 'react' +import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react' import { useParams } from 'common/hooks' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -12,12 +12,13 @@ import { EdgeFunctionsResponse, useEdgeFunctionsQuery, } from 'data/edge-functions/edge-functions-query' +import { getTable } from 'data/tables/table-query' +import { useTablesQuery } from 'data/tables/tables-query' import { useStore } from 'hooks' -import { tryParseJson, uuidv4 } from 'lib/helpers' +import { isValidHttpUrl, tryParseJson, uuidv4 } from 'lib/helpers' import { Button, Checkbox, Form, Input, Listbox, Modal, Radio, SidePanel } from 'ui' import HTTPRequestFields from './HTTPRequestFields' import { AVAILABLE_WEBHOOK_TYPES, HOOK_EVENTS } from './Hooks.constants' -import { isValidHttpUrl } from 'lib/helpers' export interface EditHookPanelProps { visible: boolean @@ -29,7 +30,7 @@ export type HTTPArgument = { id: string; name: string; value: string } const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) => { const { ref } = useParams() - const { meta, ui } = useStore() + const { ui } = useStore() const submitRef = useRef(null) const [isEdited, setIsEdited] = useState(false) const [isClosingPanel, setIsClosingPanel] = useState(false) @@ -44,6 +45,10 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) = const [httpParameters, setHttpParameters] = useState([]) const { project } = useProjectContext() + const { data } = useTablesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) const { data: functions } = useEdgeFunctionsQuery({ projectRef: ref }) const { mutate: createDatabaseTrigger, isLoading: isCreatingDatabaseTrigger } = useDatabaseTriggerCreateMutation({ @@ -81,7 +86,10 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) = }) const isSubmitting = isCreatingDatabaseTrigger || isUpdatingDatabaseTrigger - const tables = meta.tables.list().sort((a, b) => (a.schema > b.schema ? 0 : -1)) + const tables = useMemo( + () => [...(data ?? [])].sort((a, b) => (a.schema > b.schema ? 0 : -1)), + [data] + ) const restUrl = project?.restUrl const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() @@ -185,7 +193,7 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) = return setEventsError('Please select at least one event') } - const selectedTable = meta.tables.byId(values.table_id) + const selectedTable = await getTable(values.table_id) if (!selectedTable) { return ui.setNotification({ category: 'error', message: 'Unable to find selected table' }) } diff --git a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx index 80a825545af..21a07a3471b 100644 --- a/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx +++ b/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -134,7 +134,7 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp value={selectedSchema} onChange={setSelectedSchema} > - {(schemas?.result ?? []).map((schema) => ( + {(schemas ?? []).map((schema) => ( {schema.name} diff --git a/studio/components/interfaces/Database/Indexes/Indexes.tsx b/studio/components/interfaces/Database/Indexes/Indexes.tsx index 1037a534d6b..ca350bbc1d1 100644 --- a/studio/components/interfaces/Database/Indexes/Indexes.tsx +++ b/studio/components/interfaces/Database/Indexes/Indexes.tsx @@ -137,7 +137,7 @@ const Indexes = () => { onChange={setSelectedSchema} className="w-[260px]" > - {(schemas?.result ?? []).map((schema) => ( + {(schemas ?? []).map((schema) => ( { - const { meta } = useStore() + const { project } = useProjectContext() const [filterString, setFilterString] = useState('') const canUpdatePublications = useCheckPermissions( @@ -24,13 +28,27 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT 'publications' ) - const tables = - filterString.length === 0 - ? meta.tables.list((table: any) => !meta.excludedSchemas.includes(table.schema)) - : meta.tables.list( - (table: any) => - !meta.excludedSchemas.includes(table.schema) && table.name.includes(filterString) + const { + data: tables, + isLoading, + isSuccess, + isError, + error, + } = useTablesQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + }, + { + select(tables) { + return tables.filter((table) => + filterString.length === 0 + ? !EXCLUDED_SCHEMAS.includes(table.schema) + : !EXCLUDED_SCHEMAS.includes(table.schema) && table.name.includes(filterString) ) + }, + } + ) // const publication = selectedPublication // const enabledForAllTables = publication.tables == null @@ -90,19 +108,28 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT )}
- {tables.length === 0 ? ( - - ) : ( -
- Name, - Schema, - - Description - , - - {/* Temporarily disable All tables toggle for publications. See https://github.com/supabase/supabase/pull/7233. + {isLoading && ( +
+ +
+ )} + + {isError && } + + {isSuccess && + (tables.length === 0 ? ( + + ) : ( +
+
Name, + Schema, + + Description + , + + {/* Temporarily disable All tables toggle for publications. See https://github.com/supabase/supabase/pull/7233.
All Tables @@ -116,20 +143,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/Schemas/SchemaGraph.tsx b/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx index d76abe61581..c2701e5e5b2 100644 --- a/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx +++ b/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx @@ -1,7 +1,8 @@ import dagre from '@dagrejs/dagre' import clsx from 'clsx' -import { observer } from 'mobx-react-lite' import { uniqBy } from 'lodash' +import { Diamond, Fingerprint } from 'lucide-react' +import { observer } from 'mobx-react-lite' import { useEffect, useMemo } from 'react' import ReactFlow, { Background, @@ -17,11 +18,10 @@ import ReactFlow, { import { PostgresTable } from '@supabase/postgres-meta' import { useTheme } from 'common/Providers' -import { useStore } from 'hooks' -import { IconHash, IconKey, IconLoader, IconLock } from 'ui' -import { Diamond, Fingerprint } from 'lucide-react' - +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useTablesQuery } from 'data/tables/tables-query' import 'reactflow/dist/style.css' +import { IconHash, IconKey, IconLoader, IconLock } from 'ui' type TableNodeData = { name: string @@ -347,23 +347,32 @@ const TablesGraph = ({ tables }: { tables: PostgresTable[] }) => { } const SchemaGraph = ({ schema }: { schema: string }) => { - const { meta } = useStore() - const tables = meta.tables.list((table: { schema: string }) => table.schema === schema) + const { project } = useProjectContext() + const { + data: tables, + isLoading, + isError, + error, + } = useTablesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + schema, + }) - if (meta.tables.isLoading) { + if (isLoading) { return (
-

Loading schemas...

+

Loading table...

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

Error connecting to API

-

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

+

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

) } diff --git a/studio/components/interfaces/Database/Tables/ColumnList.tsx b/studio/components/interfaces/Database/Tables/ColumnList.tsx index ab3888fcde2..05e06cead8b 100644 --- a/studio/components/interfaces/Database/Tables/ColumnList.tsx +++ b/studio/components/interfaces/Database/Tables/ColumnList.tsx @@ -4,6 +4,11 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { noop } from 'lodash' import { observer } from 'mobx-react-lite' import { useState } from 'react' + +import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' +import Table from 'components/to-be-cleaned/Table' +import { useCheckPermissions, useStore } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' import { Button, IconChevronLeft, @@ -15,11 +20,6 @@ import { Input, } from 'ui' -import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' -import Table from 'components/to-be-cleaned/Table' -import { useCheckPermissions, useStore } from 'hooks' -import { ScaffoldHeader } from 'components/layouts/Scaffold' - interface ColumnListProps { selectedTable: PostgresTable onSelectBack: () => void @@ -42,7 +42,7 @@ const ColumnList = ({ ? selectedTable.columns : selectedTable.columns?.filter((column: any) => column.name.includes(filterString))) ?? [] - const isLocked = meta.excludedSchemas.includes(selectedTable.schema ?? '') + const isLocked = EXCLUDED_SCHEMAS.includes(selectedTable.schema ?? '') const canUpdateColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') return ( diff --git a/studio/components/interfaces/Database/Tables/TableList.tsx b/studio/components/interfaces/Database/Tables/TableList.tsx index 999b193bb92..1e190ba7eb8 100644 --- a/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/studio/components/interfaces/Database/Tables/TableList.tsx @@ -1,9 +1,18 @@ import * as Tooltip from '@radix-ui/react-tooltip' -import type { PostgresSchema, PostgresTable } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { noop, partition } from 'lodash' import { observer } from 'mobx-react-lite' import { useState } from 'react' + +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' +import Table from 'components/to-be-cleaned/Table' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import { useSchemasQuery } from 'data/database/schemas-query' +import { useTablesQuery } from 'data/tables/tables-query' +import { useCheckPermissions, useStore } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { useTableEditorStateSnapshot } from 'state/table-editor' import { Button, IconCheck, @@ -16,15 +25,9 @@ import { Input, Listbox, } from 'ui' - -import Table from 'components/to-be-cleaned/Table' -import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' -import { useCheckPermissions, useStore } from 'hooks' -import { ScaffoldHeader } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' interface TableListProps { - selectedSchema: string - onSelectSchema: (schema: string) => void onAddTable: () => void onEditTable: (table: any) => void onDeleteTable: (table: any) => void @@ -32,38 +35,53 @@ interface TableListProps { } const TableList = ({ - selectedSchema, - onSelectSchema = noop, onAddTable = noop, onEditTable = noop, onDeleteTable = noop, onOpenTable = noop, }: TableListProps) => { const { meta } = useStore() - const { isLoading } = meta.tables + const { project } = useProjectContext() + const snap = useTableEditorStateSnapshot() + const [filterString, setFilterString] = useState('') const canUpdateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') - const schemas: PostgresSchema[] = meta.schemas.list() - const [protectedSchemas, openSchemas] = partition(schemas, (schema) => - meta.excludedSchemas.includes(schema?.name ?? '') + const { data: schemas } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const [protectedSchemas, openSchemas] = partition(schemas ?? [], (schema) => + EXCLUDED_SCHEMAS.includes(schema?.name ?? '') ) - const allTables: PostgresTable[] = meta.tables.list( - (table: PostgresTable) => table.schema === selectedSchema + const { + data: tables, + isLoading, + isSuccess, + isError, + error, + } = useTablesQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + schema: snap.selectedSchemaName, + }, + { + select(tables) { + return filterString.length === 0 + ? tables + : tables.filter((table) => table.name.includes(filterString)) + }, + } ) - const tables = - filterString.length === 0 - ? allTables - : // @ts-ignore - allTables.filter((table: PostgresTable) => table.name.includes(filterString)) const publications = meta.publications.list() const realtimePublication = publications.find( (publication) => publication.name === 'supabase_realtime' ) - // @ts-ignore - const schema = schemas.find((schema) => schema.name === selectedSchema) + + const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName) const isLocked = protectedSchemas.some((s) => s.id === schema?.id) return ( @@ -77,14 +95,13 @@ const TableList = ({
} >

Schemas

- {/* @ts-ignore */} {openSchemas.map((schema) => (
+ {!isLocked && (
@@ -159,11 +177,19 @@ const TableList = ({ )}
-
- {isLoading ? ( + {isLoading && ( +
+
+ )} + + {isError && } + + {isSuccess && + (tables.length === 0 ? ( + ) : ( -
+
Name, @@ -184,17 +210,17 @@ const TableList = ({ body={ <> {tables.length === 0 && filterString.length === 0 && ( - +

No tables created yet

- There are no tables found in the schema "{selectedSchema}" + There are no tables found in the schema "{snap.selectedSchemaName}"

)} {tables.length === 0 && filterString.length > 0 && ( - +

No results found

@@ -302,21 +328,6 @@ const TableList = ({ )} - - {/* + + + + )} + + + + {`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 66bfdfa157e..e39d4d72bae 100644 --- a/studio/components/interfaces/TableGridEditor/EmptyState.tsx +++ b/studio/components/interfaces/TableGridEditor/EmptyState.tsx @@ -1,19 +1,17 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { observer } from 'mobx-react-lite' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query' -import { useCheckPermissions, useLocalStorage, useStore } from 'hooks' +import { useCheckPermissions, useLocalStorage } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { useTableEditorStateSnapshot } from 'state/table-editor' -interface EmptyStateProps { - selectedSchema: string - onAddTable: () => void -} +export interface EmptyStateProps {} -const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => { - const { meta } = useStore() - const isProtectedSchema = meta.excludedSchemas.includes(selectedSchema) +const EmptyState = ({}: EmptyStateProps) => { + const snap = useTableEditorStateSnapshot() + const isProtectedSchema = EXCLUDED_SCHEMAS.includes(snap.selectedSchemaName) const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') && !isProtectedSchema @@ -27,7 +25,7 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => { { projectRef: project?.ref, connectionString: project?.connectionString, - schema: selectedSchema, + schema: snap.selectedSchemaName, sort, }, { @@ -43,7 +41,7 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {

There are no tables available in this schema.

@@ -52,7 +50,7 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {

Select a table from the navigation panel on the left to view its data @@ -65,4 +63,4 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => { ) } -export default observer(EmptyState) +export default EmptyState diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx index 3e049682945..ba2a066eb09 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx @@ -4,18 +4,19 @@ import type { PostgresTable, PostgresType, } from '@supabase/postgres-meta' -import { useParams } from 'common' -import { Dictionary } from 'components/grid' import { isEmpty, noop } from 'lodash' import Link from 'next/link' import { useEffect, useState } from 'react' -import { Button, Checkbox, IconExternalLink, Input, SidePanel, Toggle } from 'ui' +import { useParams } from 'common' +import { Dictionary } from 'components/grid' import { EncryptionKeySelector } from 'components/interfaces/Settings/Vault' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms' import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query' import { useStore } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { Button, Checkbox, IconExternalLink, Input, SidePanel, Toggle } from 'ui' import { ForeignKeySelector } from '..' import ActionBar from '../ActionBar' import { TEXT_TYPES } from '../SidePanelEditor.constants' @@ -82,9 +83,7 @@ const ColumnEditor = ({ const foreignKeyMeta = data || [] const keys = vault.listKeys() - const enumTypes = meta.types.list( - (type: PostgresType) => !meta.excludedSchemas.includes(type.schema) - ) + const enumTypes = meta.types.list((type: PostgresType) => !EXCLUDED_SCHEMAS.includes(type.schema)) const [pgsodiumExtension] = meta.extensions.list( (ext: PostgresExtension) => ext.name.toLowerCase() === 'pgsodium' diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx index df17d3f14e7..ff06e68fd95 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx @@ -1,7 +1,13 @@ import type { PostgresColumn, PostgresSchema, PostgresTable } from '@supabase/postgres-meta' -import { Dictionary } from 'components/grid' import { find, get, isEmpty, sortBy } from 'lodash' import { useEffect, useState } from 'react' + +import { Dictionary } from 'components/grid' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import InformationBox from 'components/ui/InformationBox' +import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' +import { useSchemasQuery } from 'data/database/schemas-query' +import { useTablesQuery } from 'data/tables/tables-query' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, @@ -13,10 +19,6 @@ import { Listbox, SidePanel, } from 'ui' - -import InformationBox from 'components/ui/InformationBox' -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' -import { useStore } from 'hooks' import ActionBar from '../ActionBar' import { ColumnField } from '../SidePanelEditor.types' import { FOREIGN_KEY_CASCADE_OPTIONS } from './ForeignKeySelector.constants' @@ -46,7 +48,7 @@ const ForeignKeySelector = ({ closePanel, saveChanges, }: ForeignKeySelectorProps) => { - const { meta } = useStore() + const { project } = useProjectContext() const [errors, setErrors] = useState({}) const [selectedForeignKey, setSelectedForeignKey] = useState({ schema: 'public', @@ -56,10 +58,15 @@ const ForeignKeySelector = ({ updateAction: FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, }) - const schemas = meta.schemas.list() - const tables = meta.tables.list( - (table: PostgresTable) => table.schema === selectedForeignKey.schema - ) + const { data: schemas } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const { data: tables } = useTablesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + schema: selectedForeignKey.schema, + }) const foreignKey = column?.foreignKey const selectedTable: PostgresTable | undefined = find(tables, { @@ -70,11 +77,6 @@ const ForeignKeySelector = ({ name: selectedForeignKey?.column, }) - useEffect(() => { - // make sure the public schemas are loaded initially - meta.tables.loadBySchema('public') - }, []) - useEffect(() => { // Reset the state of the side panel if (visible) { @@ -101,7 +103,6 @@ const ForeignKeySelector = ({ }, [visible]) const updateSelectedSchema = (schema: string) => { - meta.tables.loadBySchema(schema) const updatedForeignKey = { schema, table: '', @@ -200,8 +201,6 @@ const ForeignKeySelector = ({ customFooter={ updateSelectedSchema(value)} > - {schemas.map((schema: PostgresSchema) => { + {schemas?.map((schema: PostgresSchema) => { return ( { - 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 d84c021d106..dc9aa678a3c 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -1,23 +1,22 @@ import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta' import { QueryKey, useQueryClient } from '@tanstack/react-query' -import { Dictionary } from 'components/grid' -import { find, isEmpty, isUndefined, noop } from 'lodash' +import { isEmpty, isUndefined, noop } from 'lodash' import { useState } from 'react' -import { Modal } from 'ui' +import { Dictionary } from 'components/grid' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import ConfirmationModal from 'components/ui/ConfirmationModal' import { entityTypeKeys } from 'data/entity-types/keys' import { sqlKeys } from 'data/sql/keys' import { useTableRowCreateMutation } from 'data/table-rows/table-row-create-mutation' import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation' +import { tableKeys } from 'data/tables/keys' import { useStore, useUrlState } from 'hooks' +import { useTableEditorStateSnapshot } from 'state/table-editor' +import { Modal } from 'ui' import { ColumnEditor, RowEditor, SpreadsheetImport, TableEditor } from '.' -import ForeignRowSelector, { - ForeignRowSelectorProps, -} from './RowEditor/ForeignRowSelector/ForeignRowSelector' +import ForeignRowSelector from './RowEditor/ForeignRowSelector/ForeignRowSelector' import JsonEdit from './RowEditor/JsonEditor/JsonEditor' -import { JsonEditValue } from './RowEditor/RowEditor.types' import { ColumnField, CreateColumnPayload, @@ -27,20 +26,7 @@ import { import { ImportContent } from './TableEditor/TableEditor.types' export interface SidePanelEditorProps { - selectedSchema: string selectedTable?: PostgresTable - selectedRowToEdit?: Dictionary - 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,21 +37,13 @@ export interface SidePanelEditorProps { } const SidePanelEditor = ({ - selectedSchema, selectedTable, - selectedRowToEdit, - selectedColumnToEdit, - selectedTableToEdit, - selectedValueForJsonEdit, - selectedForeignKeyToEdit, - sidePanelKey, - isDuplicating = false, - closePanel, onRowCreated = noop, onRowUpdated = noop, onTableCreated = noop, onColumnSaved = noop, }: SidePanelEditorProps) => { + const snap = useTableEditorStateSnapshot() const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] }) const { meta, ui } = useStore() const queryClient = useQueryClient() @@ -73,7 +51,6 @@ const SidePanelEditor = ({ const [isEdited, setIsEdited] = useState(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' @@ -84,6 +61,8 @@ const SidePanelEditor = ({ const { mutateAsync: createTableRows } = useTableRowCreateMutation() const { mutateAsync: updateTableRow } = useTableRowUpdateMutation({ async onMutate({ projectRef, table, configuration, payload }) { + snap.closeSidePanel() + const primaryKeyColumns = new Set(Object.keys(configuration.identifiers)) const queryKey = sqlKeys.query(projectRef, [ table.schema, @@ -192,12 +171,13 @@ const SidePanelEditor = ({ onComplete() if (!saveRowError) { setIsEdited(false) - closePanel() + snap.closeSidePanel() } } const onSaveJSON = async (value: string | number) => { - if (selectedTable === undefined || selectedValueForJsonEdit === undefined) return + if (selectedTable === undefined || !(snap.sidePanel?.type === 'json')) return + const selectedValueForJsonEdit = snap.sidePanel.jsonValue try { const { row, column } = selectedValueForJsonEdit @@ -213,7 +193,8 @@ const SidePanelEditor = ({ } const onSaveForeignRow = async (value: any) => { - if (selectedTable === undefined || selectedForeignKeyToEdit === undefined) return + if (selectedTable === undefined || !(snap.sidePanel?.type === 'foreign-row-selector')) return + const selectedForeignKeyToEdit = snap.sidePanel.foreignKey try { const { row, column } = selectedForeignKeyToEdit @@ -235,6 +216,8 @@ const SidePanelEditor = ({ configuration: { columnId?: string; isEncrypted: boolean; keyId?: string; keyName?: string }, resolve: any ) => { + const selectedColumnToEdit = snap.sidePanel?.type === 'column' && snap.sidePanel.column + const { columnId, ...securityConfig } = configuration const response = isNewRecord ? await meta.createColumn( @@ -261,9 +244,10 @@ const SidePanelEditor = ({ ) { reAddRenamedColumnSortAndFilter(selectedColumnToEdit.name, payload.name) } - queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])) + await Promise.all([ - meta.tables.loadById(selectedTable!.id), + queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])), + queryClient.invalidateQueries(tableKeys.table(project?.ref, selectedTable!.id)), queryClient.invalidateQueries( sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name]) ), @@ -278,7 +262,7 @@ const SidePanelEditor = ({ ]) onColumnSaved(configuration.isEncrypted) setIsEdited(false) - closePanel() + snap.closeSidePanel() } if (configuration.isEncrypted && selectedTable?.schema) { @@ -329,8 +313,12 @@ const SidePanelEditor = ({ configuration try { - if (isDuplicating) { - const duplicateTable = find(tables, { id: tableId }) as PostgresTable + if ( + snap.sidePanel?.type === 'table' && + snap.sidePanel.mode === 'duplicate' && + selectedTable + ) { + const duplicateTable = selectedTable toastId = ui.setNotification({ category: 'loading', @@ -344,7 +332,10 @@ const SidePanelEditor = ({ duplicateTable, }) - await queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)) + await Promise.all([ + queryClient.invalidateQueries(tableKeys.list(project?.ref, table.schema)), + queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)), + ]) ui.setNotification({ id: toastId, @@ -368,7 +359,10 @@ const SidePanelEditor = ({ importContent ) - await queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)) + await Promise.all([ + queryClient.invalidateQueries(tableKeys.list(project?.ref, table.schema)), + queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)), + ]) ui.setNotification({ id: toastId, @@ -377,15 +371,15 @@ const SidePanelEditor = ({ }) onTableCreated(table) - } else if (selectedTableToEdit) { + } else if (selectedTable) { toastId = ui.setNotification({ category: 'loading', - message: `Updating table: ${selectedTableToEdit?.name}...`, + message: `Updating table: ${selectedTable?.name}...`, }) const { table, hasError }: any = await meta.updateTable( toastId, - selectedTableToEdit, + selectedTable, payload, columns, isRealtimeEnabled @@ -401,16 +395,17 @@ const SidePanelEditor = ({ queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])) await Promise.all([ queryClient.invalidateQueries( - sqlKeys.query(project?.ref, [selectedTableToEdit.schema, selectedTableToEdit.name]) + sqlKeys.query(project?.ref, [selectedTable.schema, selectedTable.name]) ), queryClient.invalidateQueries( sqlKeys.query(project?.ref, [ 'table-definition', - selectedTableToEdit.schema, - selectedTableToEdit.name, + selectedTable.schema, + selectedTable.name, ]) ), queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)), + queryClient.invalidateQueries(tableKeys.table(project?.ref, table.schema)), ]) ui.setNotification({ @@ -427,7 +422,7 @@ const SidePanelEditor = ({ if (!saveTableError) { setIsEdited(false) - closePanel() + snap.closeSidePanel() } resolve() @@ -507,14 +502,14 @@ const SidePanelEditor = ({ message: `Successfully imported ${rowCount} rows of data into ${selectedTable.name}`, }) resolve() - closePanel() + snap.closeSidePanel() } const onClosePanel = () => { if (isEdited) { setIsClosingPanel(true) } else { - closePanel() + snap.closeSidePanel() } } @@ -522,9 +517,9 @@ const SidePanelEditor = ({ <> {!isUndefined(selectedTable) && ( setIsEdited(true)} @@ -532,42 +527,58 @@ const SidePanelEditor = ({ )} {!isUndefined(selectedTable) && ( setIsEdited(true)} /> )} setIsEdited(true)} /> { setIsClosingPanel(false) setIsEdited(false) - closePanel() + snap.closeSidePanel() }} > diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx index 81e4bb4566c..9c71dcce12c 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx @@ -2,7 +2,7 @@ import type { PostgresTable } from '@supabase/postgres-meta' interface HeaderTitleProps { schema: string - table: PostgresTable + table?: { name: string } isDuplicating: boolean } @@ -17,13 +17,13 @@ const HeaderTitle = ({ schema, table, isDuplicating }: HeaderTitleProps) => { if (isDuplicating) { return ( <> - Duplicate table {table.name} + Duplicate table {table?.name} ) } return ( <> - Update table {table.name} + Update table {table?.name} ) } diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index c2b14a957a1..dd97c467436 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -2,12 +2,14 @@ import type { PostgresTable, PostgresType } from '@supabase/postgres-meta' import { isEmpty, isUndefined, noop } from 'lodash' import Link from 'next/link' import { useEffect, useState } from 'react' -import { Alert, Badge, Button, Checkbox, IconBookOpen, Input, Modal, SidePanel } from 'ui' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import ConfirmationModal from 'components/ui/ConfirmationModal' import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query' import { useStore } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { useTableEditorStateSnapshot } from 'state/table-editor' +import { Alert, Badge, Button, Checkbox, IconBookOpen, Input, Modal, SidePanel } from 'ui' import { SpreadsheetImport } from '../' import ActionBar from '../ActionBar' import { ColumnField, CreateTablePayload, UpdateTablePayload } from '../SidePanelEditor.types' @@ -23,9 +25,8 @@ import { validateFields, } from './TableEditor.utils' -interface TableEditorProps { +export interface TableEditorProps { table?: PostgresTable - selectedSchema: string isDuplicating: boolean visible: boolean closePanel: () => void @@ -47,20 +48,18 @@ interface TableEditorProps { const TableEditor = ({ table, - selectedSchema, isDuplicating, visible = false, closePanel = noop, saveChanges = noop, updateEditorDirty = noop, }: TableEditorProps) => { + const snap = useTableEditorStateSnapshot() const { ui, meta } = useStore() const { project } = useProjectContext() const isNewRecord = isUndefined(table) - const enumTypes = meta.types.list( - (type: PostgresType) => !meta.excludedSchemas.includes(type.schema) - ) + const enumTypes = meta.types.list((type: PostgresType) => !EXCLUDED_SCHEMAS.includes(type.schema)) const publications = meta.publications.list() const realtimePublication = publications.find( @@ -135,7 +134,7 @@ const TableEditor = ({ if (isEmpty(errors)) { const payload: CreateTablePayload | UpdateTablePayload = { name: tableFields.name, - schema: selectedSchema, + schema: snap.selectedSchemaName, comment: tableFields.comment, ...(!isNewRecord && { rls_enabled: tableFields.isRLSEnabled }), } @@ -170,8 +169,9 @@ const TableEditor = ({ size="large" key="TableEditor" visible={visible} - // @ts-ignore - header={} + header={ + + } className={`transition-all duration-100 ease-in ${isImportingSpreadsheet ? ' mr-32' : ''}`} onCancel={closePanel} onConfirm={() => (resolve: () => void) => onSaveChanges(resolve)} @@ -297,7 +297,7 @@ const TableEditor = ({

{!isDuplicating && ( - 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 + isLoadingSelectedTable?: boolean + selectedTable?: TableLike } const TableGridEditor = ({ theme = 'dark', - - selectedSchema, + isLoadingSelectedTable = false, selectedTable, - sidePanelKey, - isDuplicating, - selectedRowToEdit, - selectedColumnToEdit, - selectedTableToEdit, - selectedValueForJsonEdit, - selectedForeignKeyToEdit, - - onAddRow = noop, - onEditRow = noop, - onAddColumn = noop, - onEditColumn = noop, - onDeleteColumn = noop, - onExpandJSONEditor = noop, - onEditForeignKeyColumnValue = noop, - onClosePanel = noop, - onImportData = noop, }: TableGridEditorProps) => { + const { project } = useProjectContext() + const snap = useTableEditorStateSnapshot() const { meta, ui, vault } = useStore() const router = useRouter() const { ref: projectRef, id } = useParams() const gridRef = useRef(null) - const { project } = useProjectContext() const [encryptedColumns, setEncryptedColumns] = useState([]) const [apiPreviewPanelOpen, setApiPreviewPanelOpen] = useState(false) @@ -201,34 +158,38 @@ const TableGridEditor = ({ }, [selectedTable?.id]) const entityType = useEntityType(selectedTable?.id) + const columnsRef = useLatest(selectedTable?.columns ?? EMPTY_ARR) // NOTE: DO NOT PUT HOOKS AFTER THIS LINE + if (isLoadingSelectedTable) { + return + } + if (isUndefined(selectedTable)) { 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 const isForeignTableSelected = entityType?.type === ENTITY_TYPE.FOREIGN_TABLE - const isLocked = meta.excludedSchemas.includes(entityType?.schema ?? '') + const isLocked = EXCLUDED_SCHEMAS.includes(entityType?.schema ?? '') const canEditViaTableEditor = isTableSelected && !isLocked // [Joshen] We can tweak below to eventually support composite keys as the data // returned from foreignKeyMeta should be easy to deal with, rather than pg-meta - const formattedRelationships = (selectedTable?.relationships ?? []).map( - (relationship: PostgresRelationship) => { - const relationshipMeta = foreignKeyMeta.find( - (fk: ForeignKeyConstraint) => fk.id === relationship.id - ) - return { - ...relationship, - deletion_action: relationshipMeta?.deletion_action ?? FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, - } + const formattedRelationships = ( + ('relationships' in selectedTable && selectedTable.relationships) || + [] + ).map((relationship: PostgresRelationship) => { + const relationshipMeta = foreignKeyMeta.find( + (fk: ForeignKeyConstraint) => fk.id === relationship.id + ) + return { + ...relationship, + deletion_action: relationshipMeta?.deletion_action ?? FOREIGN_KEY_CASCADE_ACTION.NO_ACTION, } - ) + }) const gridTable = !isViewSelected && !isForeignTableSelected @@ -275,27 +236,31 @@ const TableGridEditor = ({ } } - const onSelectEditColumn = async (name: string) => { - // For some reason, selectedTable here is stale after adding a table - // temporary workaround is to list grab the selected table again - const tables: PostgresTable[] = meta.tables.list() - // @ts-ignore - const table = tables.find((table) => table.id === Number(tableId)) - const column = find(table!.columns, { name }) as PostgresColumn + // columns must be accessed via columnsRef.current as these two functions immediately become + // stale as they are accessed via some react-tracked madness + // [TODO]: refactor out all of react-tracked + const onSelectEditColumn = (name: string) => { + const column = find(columnsRef.current, { name }) as PostgresColumn if (column) { - onEditColumn(column) + snap.onEditColumn(column) } else { - console.error(`Unable to find column ${name} in ${table?.name}`) + ui.setNotification({ + category: 'error', + message: `Unable to find column ${name} in ${selectedTable?.name}`, + }) } } - const onSelectDeleteColumn = async (name: string) => { - // For some reason, selectedTable here is stale after adding a table - // temporary workaround is to list grab the selected table again - const tables: PostgresTable[] = meta.tables.list() - const table = tables.find((table) => table.id === Number(tableId)) - const column = find(table!.columns, { name }) as PostgresColumn - onDeleteColumn(column) + const onSelectDeleteColumn = (name: string) => { + const column = find(columnsRef.current ?? [], { name }) as PostgresColumn + if (column) { + snap.onDeleteColumn(column) + } else { + ui.setNotification({ + category: 'error', + message: `Unable to find column ${name} in ${selectedTable?.name}`, + }) + } } const onError = (error: any) => { @@ -308,11 +273,14 @@ const TableGridEditor = ({ const updateTableRow = (previousRow: any, updatedData: any) => { if (!project) return - const enumArrayColumns = selectedTable.columns - .filter((column: any) => { - return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array' - }) - .map((column: any) => column.name) + const enumArrayColumns = + ('columns' in selectedTable && + selectedTable.columns + ?.filter((column) => { + return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array' + }) + .map((column) => column.name)) || + [] const identifiers = {} as Dictionary ;(selectedTable as PostgresTable).primary_keys.forEach( @@ -384,17 +352,19 @@ const TableGridEditor = ({ ) : null } - onAddColumn={onAddColumn} + onAddColumn={snap.onAddColumn} onEditColumn={onSelectEditColumn} onDeleteColumn={onSelectDeleteColumn} - onAddRow={onAddRow} + onAddRow={snap.onAddRow} updateTableRow={updateTableRow} - onEditRow={onEditRow} - onImportData={onImportData} + onEditRow={snap.onEditRow} + onImportData={snap.onImportData} onError={onError} onSqlQuery={onSqlQuery} - onExpandJSONEditor={onExpandJSONEditor} - onEditForeignKeyColumnValue={onEditForeignKeyColumnValue} + onExpandJSONEditor={(column, row) => + snap.onExpandJSONEditor({ column, row, jsonString: JSON.stringify(row[column]) || '' }) + } + onEditForeignKeyColumnValue={snap.onEditForeignKeyColumnValue} showCustomChildren={(isViewSelected || isTableSelected) && selectedView === 'definition'} customHeader={ (isViewSelected || isTableSelected) && selectedView === 'definition' ? ( @@ -410,22 +380,13 @@ const TableGridEditor = ({ {(isViewSelected || isTableSelected) && } - {!isUndefined(selectedSchema) && ( + {snap.selectedSchemaName !== undefined && ( )} diff --git a/studio/components/layouts/AuthLayout/AuthLayout.tsx b/studio/components/layouts/AuthLayout/AuthLayout.tsx index dd060084a53..2dd7e989901 100644 --- a/studio/components/layouts/AuthLayout/AuthLayout.tsx +++ b/studio/components/layouts/AuthLayout/AuthLayout.tsx @@ -1,9 +1,8 @@ import { observer } from 'mobx-react-lite' import { useRouter } from 'next/router' -import { PropsWithChildren, useEffect, useState } from 'react' +import { PropsWithChildren, useEffect } from 'react' import { useParams } from 'common' -import Error from 'components/ui/Error' import ProductMenu from 'components/ui/ProductMenu' import { useAuthConfigPrefetch } from 'data/auth/auth-config-query' import { useStore, withAuth } from 'hooks' @@ -15,45 +14,26 @@ export interface AuthLayoutProps { } const AuthLayout = ({ title, children }: PropsWithChildren) => { - const { ui, meta } = useStore() - const { isInitialized, isLoading, error } = meta.tables const { ref: projectRef = 'default' } = useParams() + const { ui, meta } = useStore() + useAuthConfigPrefetch({ projectRef }) const router = useRouter() const page = router.pathname.split('/')[4] - const [loaded, setLoaded] = useState(isInitialized) - useEffect(() => { if (ui.selectedProjectRef) { meta.policies.load() - meta.tables.load() meta.roles.load() - meta.schemas.load() } }, [ui.selectedProjectRef]) - useEffect(() => { - if (!isLoading && !loaded) { - setLoaded(true) - } - }, [isLoading]) - - if (error) { - return ( - - - - ) - } - return ( } + productMenu={} >
{children} diff --git a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index 68d87ffa499..d5d33ad212e 100644 --- a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -1,8 +1,7 @@ import { observer } from 'mobx-react-lite' import { useRouter } from 'next/router' -import { PropsWithChildren, useEffect, useState } from 'react' +import { PropsWithChildren, useEffect } from 'react' -import Error from 'components/ui/Error' import ProductMenu from 'components/ui/ProductMenu' import { useSelectedProject, useStore, withAuth } from 'hooks' import ProjectLayout from '../' @@ -14,10 +13,6 @@ export interface DatabaseLayoutProps { const DatabaseLayout = ({ children }: PropsWithChildren) => { const { ui, meta, vault } = useStore() - const { isLoading: isSchemasLoading } = meta.schemas - const { isLoading: isVaultLoading } = vault - - const { isInitialized, error } = meta.tables const project = useSelectedProject() const router = useRouter() @@ -27,15 +22,8 @@ const DatabaseLayout = ({ children }: PropsWithChildren) => const isVaultEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null const pgNetExtensionExists = meta.extensions.byId('pg_net') !== undefined - const isLoading = isSchemasLoading || (isVaultEnabled && isVaultLoading) - const [loaded, setLoaded] = useState(isInitialized) - useEffect(() => { if (ui.selectedProjectRef) { - // Eventually should only load the required stores based on the pages - meta.schemas.load() - meta.tables.load() - meta.roles.load() meta.triggers.load() meta.extensions.load() @@ -49,25 +37,8 @@ const DatabaseLayout = ({ children }: PropsWithChildren) => } }, [ui.selectedProjectRef, isVaultEnabled]) - // Optimization required: load logic should be at the page level - // e.g backups page is waiting for meta.tables to load finish when it doesnt even need that data - useEffect(() => { - if (!isLoading && !loaded) { - setLoaded(true) - } - }, [isLoading]) - - if (error) { - return ( - - - - ) - } - return ( diff --git a/studio/components/layouts/ProjectLayout/ProjectContext.tsx b/studio/components/layouts/ProjectLayout/ProjectContext.tsx index b06457c84fe..c9d06b45b55 100644 --- a/studio/components/layouts/ProjectLayout/ProjectContext.tsx +++ b/studio/components/layouts/ProjectLayout/ProjectContext.tsx @@ -1,10 +1,12 @@ +import { createContext, PropsWithChildren, useContext, useMemo } from 'react' + import { useParams } from 'common/hooks' import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { PROJECT_STATUS } from 'lib/constants' -import { PropsWithChildren, createContext, useContext, useMemo } from 'react' +import { TableEditorStateContextProvider } from 'state/table-editor' import { Project } from 'types' -type ProjectContextType = { +export interface ProjectContextType { project: Project | undefined isLoading: boolean } @@ -35,7 +37,13 @@ export const ProjectContextProvider = ({ } }, [selectedProject, isLoading]) - return {children} + return ( + + + {children} + + + ) } export const ProjectContextFromParamsProvider = ({ children }: PropsWithChildren<{}>) => { 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 2cf50e4643f..bccfbc672bb 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -1,49 +1,15 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { noop } from 'lodash' import { observer } from 'mobx-react-lite' -import { useRouter } from 'next/router' import { PropsWithChildren, useEffect } from 'react' -import { useParams } from 'common/hooks' -import Connecting from 'components/ui/Loading/Loading' import NoPermission from 'components/ui/NoPermission' -import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' -import { Entity } from 'data/entity-types/entity-type-query' -import { useCheckPermissions, useSelectedProject, useStore } from 'hooks' -import useEntityType from 'hooks/misc/useEntityType' -import useLatest from 'hooks/misc/useLatest' -import { useIsTableLoaded, useTableEditorStateSnapshot } from 'state/table-editor' -import ProjectLayout from '../' -import useTableRowsPrefetchWrapper from './TableEditorLayout.utils' +import { useCheckPermissions, useStore } from 'hooks' +import { ProjectLayoutWithAuth } from '../' import TableEditorMenu from './TableEditorMenu' -export interface TableEditorLayoutProps { - selectedSchema?: string - selectedTable?: string - onSelectSchema: (schema: string) => void - onAddTable: () => void - onEditTable: (table: Entity) => void - onDeleteTable: (table: Entity) => void - onDuplicateTable: (table: Entity) => void -} +const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => { + const { vault, meta, ui } = useStore() -const TableEditorLayout = ({ - selectedSchema, - selectedTable, - onSelectSchema = noop, - onAddTable = noop, - onEditTable = noop, - onDeleteTable = noop, - onDuplicateTable = noop, - children, -}: PropsWithChildren) => { - const { ui, vault, meta } = useStore() - const selectedProject = useSelectedProject() - const router = useRouter() - const { ref, id: _id } = useParams() - const id = _id ? Number(_id) : undefined - - const snap = useTableEditorStateSnapshot() const canReadTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables') const vaultExtension = meta.extensions.byId('supabase_vault') @@ -51,7 +17,6 @@ const TableEditorLayout = ({ useEffect(() => { if (ui.selectedProjectRef) { - meta.schemas.load() meta.types.load() meta.policies.load() meta.publications.load() @@ -59,87 +24,24 @@ const TableEditorLayout = ({ } }, [ui.selectedProjectRef]) - const isLoaded = useIsTableLoaded(ref, id) - - const entity = useEntityType(id, function onNotFound(id) { - if (ref) snap.addLoadedId(ref, id) - }) - - const prefetch = useLatest(useTableRowsPrefetchWrapper()) - - useEffect(() => { - let mounted = true - - function loadTable() { - if (entity?.type) { - switch (entity.type) { - case ENTITY_TYPE.MATERIALIZED_VIEW: - return meta.materializedViews.loadById(entity.id) - - case ENTITY_TYPE.VIEW: - return meta.views.loadById(entity.id) - - case ENTITY_TYPE.FOREIGN_TABLE: - return meta.foreignTables.loadById(entity.id) - - default: - return meta.tables.loadById(entity.id) - } - } - } - - loadTable() - ?.then(async (entity: any) => { - await prefetch.current(entity) - return entity - }) - .then((entity: any) => { - if (mounted && ref) { - snap.addLoadedId(ref, entity.id) - } - }) - .catch(() => { - if (mounted && entity?.id && ref) { - snap.addLoadedId(ref, entity.id) - } - }) - - return () => { - mounted = false - } - }, [entity?.id]) - useEffect(() => { if (isVaultEnabled) { vault.load() } - }, [selectedProject?.ref, isVaultEnabled]) + }, [ui.selectedProjectRef, isVaultEnabled]) if (!canReadTables) { return ( - + - + ) } return ( - - } - > - {router.isReady && id !== undefined ? isLoaded ? children : : children} - + }> + {children} + ) } diff --git a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index 7e7190a73e9..ffe27f2fa52 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -1,6 +1,15 @@ +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { partition } from 'lodash' import { useMemo, useState } from 'react' -import { noop, partition } from 'lodash' -import { observer } from 'mobx-react-lite' + +import { useParams } from 'common/hooks' +import InfiniteList from 'components/ui/InfiniteList' +import { useSchemasQuery } from 'data/database/schemas-query' +import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query' +import { useCheckPermissions, useLocalStorage } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { useTableEditorStateSnapshot } from 'state/table-editor' import { Alert, Button, @@ -16,37 +25,12 @@ import { Listbox, Menu, } from 'ui' -import * as Tooltip from '@radix-ui/react-tooltip' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import type { PostgresSchema } from '@supabase/postgres-meta' - -import { useParams } from 'common/hooks' -import { useCheckPermissions, useStore, useLocalStorage } from 'hooks' -import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query' -import { Entity } from 'data/entity-types/entity-type-query' import { useProjectContext } from '../ProjectLayout/ProjectContext' -import InfiniteList from 'components/ui/InfiniteList' import EntityListItem from './EntityListItem' -export interface TableEditorMenuProps { - selectedSchema?: string - onSelectSchema: (schema: string) => void - onAddTable: () => void - onEditTable: (table: Entity) => void - onDeleteTable: (table: Entity) => void - onDuplicateTable: (table: Entity) => void -} - -const TableEditorMenu = ({ - selectedSchema, - onSelectSchema = noop, - onAddTable = noop, - onEditTable = noop, - onDeleteTable = noop, - onDuplicateTable = noop, -}: TableEditorMenuProps) => { - const { meta } = useStore() +const TableEditorMenu = () => { const { id } = useParams() + const snap = useTableEditorStateSnapshot() const [searchText, setSearchText] = useState('') const [sort, setSort] = useLocalStorage<'alphabetical' | 'grouped-alphabetical'>( @@ -68,7 +52,7 @@ const TableEditorMenu = ({ { projectRef: project?.ref, connectionString: project?.connectionString, - schema: selectedSchema, + schema: snap.selectedSchemaName, search: searchText || undefined, sort, }, @@ -83,18 +67,27 @@ const TableEditorMenu = ({ [data?.pages] ) - const schemas: PostgresSchema[] = meta.schemas.list() - const schema = schemas.find((schema) => schema.name === selectedSchema) - const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const { + data: schemas, + isLoading: isSchemasLoading, + isSuccess: isSchemasSuccess, + isError: isSchemasError, + error: schemasError, + refetch: refetchSchemas, + } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) - const isLoadingTableMetadata = id ? !meta.tables.byId(id) : true + const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName) + const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') const refreshTables = async () => { await refetch() } const [protectedSchemas, openSchemas] = partition(schemas, (schema) => - meta.excludedSchemas.includes(schema?.name ?? '') + EXCLUDED_SCHEMAS.includes(schema?.name ?? '') ) const isLocked = protectedSchemas.some((s) => s.id === schema?.id) @@ -105,25 +98,29 @@ const TableEditorMenu = ({ > {/* Schema selection dropdown */}
- {!meta.schemas.isInitialized ? ( + {isSchemasLoading && (
Loading schemas...
- ) : meta.schemas.hasError ? ( + )} + + {isSchemasError && ( -

Error: {meta.schemas?.error?.message}

-
- ) : ( + )} + + {isSchemasSuccess && ( { setSearchText('') - onSelectSchema(name) + snap.setSelectedSchemaName(name) }} > New table @@ -352,10 +349,6 @@ const TableEditorMenu = ({ itemProps={{ projectRef: project?.ref, id: Number(id), - onEditTable, - onDeleteTable, - onDuplicateTable, - isLoadingTableMetadata, }} getItemSize={() => 28} hasNextPage={hasNextPage} @@ -369,4 +362,4 @@ const TableEditorMenu = ({ ) } -export default observer(TableEditorMenu) +export default TableEditorMenu diff --git a/studio/data/database/keys.ts b/studio/data/database/keys.ts index 4d75d55bee4..dbaccaee8f9 100644 --- a/studio/data/database/keys.ts +++ b/studio/data/database/keys.ts @@ -1,4 +1,5 @@ export const databaseKeys = { + schemaList: (projectRef: string | undefined) => ['projects', projectRef, 'schemas'] as const, backups: (projectRef: string | undefined) => [projectRef, 'database', 'backups'] as const, poolingConfiguration: (projectRef: string | undefined) => [projectRef, 'database', 'pooling-configuration'] as const, diff --git a/studio/data/database/schemas-query.ts b/studio/data/database/schemas-query.ts index 896dd2df016..1d90cf9f95a 100644 --- a/studio/data/database/schemas-query.ts +++ b/studio/data/database/schemas-query.ts @@ -1,61 +1,61 @@ -import { useCallback } from 'react' -import { UseQueryOptions } from '@tanstack/react-query' -import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query' +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { components } from 'data/api' +import { get } from 'data/fetchers' import { ResponseError } from 'types' - -export type Schema = { - name: string -} - -export const getSchemasQuery = () => { - const sql = /* SQL */ ` - SELECT nspname as name - FROM pg_namespace - WHERE - nspname not in ('information_schema', 'pg_catalog', 'pg_toast') - AND nspname not like 'pg_temp_%' - AND nspname not like 'pg_toast_temp_%' - AND has_schema_privilege(oid, 'CREATE, USAGE') - ORDER BY nspname; -`.trim() - - return sql -} +import { databaseKeys } from './keys' export type SchemasVariables = { projectRef?: string connectionString?: string } -export type SchemasData = { result: Schema[] } -export type SchemasError = unknown +export type Schema = components['schemas']['PostgresSchema'] -export const useSchemasQuery = ( +export async function getSchemas( { projectRef, connectionString }: SchemasVariables, - options: UseQueryOptions = {} -) => { - return useExecuteSqlQuery( - { - projectRef, - connectionString, - sql: getSchemasQuery(), - queryKey: ['schemas'], + signal?: AbortSignal +) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + let headers = new Headers() + if (connectionString) headers.set('x-connection-encrypted', connectionString) + + const { data, error } = await get('/platform/pg-meta/{ref}/schemas', { + params: { + header: { + 'x-connection-encrypted': connectionString!, + }, + path: { + ref: projectRef, + }, }, - options - ) + headers: Object.fromEntries(headers), + signal, + }) + + if (error) { + throw error + } + + return data } -export const useSchemasPrefetch = () => { - const prefetch = useExecuteSqlPrefetch() +export type SchemasData = Awaited> +export type SchemasError = ResponseError - return useCallback( - ({ projectRef, connectionString }: SchemasVariables) => - prefetch({ - projectRef, - connectionString, - sql: getSchemasQuery(), - queryKey: ['schemas'], - }), - [prefetch] +export const useSchemasQuery = ( + { projectRef, connectionString }: SchemasVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + databaseKeys.schemaList(projectRef), + ({ signal }) => getSchemas({ projectRef, connectionString }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } ) + +export function invalidateSchemasQuery(client: QueryClient, projectRef: string | undefined) { + return client.invalidateQueries(databaseKeys.schemaList(projectRef)) } 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/projects/project-detail-query.ts b/studio/data/projects/project-detail-query.ts index cab2475d06c..282c5d5d546 100644 --- a/studio/data/projects/project-detail-query.ts +++ b/studio/data/projects/project-detail-query.ts @@ -35,3 +35,16 @@ export const useProjectDetailQuery = ( export function invalidateProjectDetailsQuery(client: QueryClient, ref: string) { return client.invalidateQueries(projectKeys.detail(ref)) } + +// get the cached value or fallback to fetching it +export async function getCachedProjectDetail( + client: QueryClient, + ref: string | undefined +): Promise { + if (!ref) return undefined + + const cached = client.getQueryData(projectKeys.detail(ref)) + if (cached) return cached + + return await client.fetchQuery(projectKeys.detail(ref)) +} diff --git a/studio/data/tables/keys.ts b/studio/data/tables/keys.ts new file mode 100644 index 00000000000..2947ecc11f8 --- /dev/null +++ b/studio/data/tables/keys.ts @@ -0,0 +1,6 @@ +export const tableKeys = { + list: (projectRef: string | undefined, schema: string | undefined) => + ['projects', projectRef, 'tables', { schema }] as const, + table: (projectRef: string | undefined, id: number | undefined) => + ['projects', projectRef, 'tables', id] as const, +} diff --git a/studio/data/tables/table-query.ts b/studio/data/tables/table-query.ts new file mode 100644 index 00000000000..eaa7a417e5b --- /dev/null +++ b/studio/data/tables/table-query.ts @@ -0,0 +1,79 @@ +import { PostgresTable } from '@supabase/postgres-meta' +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { tableKeys } from './keys' +import { useCallback } from 'react' + +export type TableVariables = { + id?: number + projectRef?: string + connectionString?: string +} + +export type Table = PostgresTable + +export type TableResponse = Table | { error?: any } + +export async function getTable( + { id, projectRef, connectionString }: TableVariables, + signal?: AbortSignal +) { + if (!projectRef) { + throw new Error('projectRef is required') + } + if (!id) { + throw new Error('id is required') + } + + let headers = new Headers() + if (connectionString) headers.set('x-connection-encrypted', connectionString) + + const response = (await get(`${API_URL}/pg-meta/${projectRef}/tables?id=${id}`, { + headers: Object.fromEntries(headers), + signal, + })) as TableResponse + + if ('error' in response) { + throw response.error + } + + return response as PostgresTable +} + +export type TableData = Awaited> +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, + } + ) + +/** + * 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 new file mode 100644 index 00000000000..eefe5c92879 --- /dev/null +++ b/studio/data/tables/tables-query.ts @@ -0,0 +1,80 @@ +import { PostgresTable } from '@supabase/postgres-meta' +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { useCallback } from 'react' +import { ResponseError } from 'types' +import { tableKeys } from './keys' +import { Table } from './table-query' + +export type TablesVariables = { + projectRef?: string + connectionString?: string + schema?: string +} + +export type TablesResponse = Table[] | { error?: any } + +export async function getTables( + { projectRef, connectionString, schema }: TablesVariables, + signal?: AbortSignal +) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + let headers = new Headers() + if (connectionString) headers.set('x-connection-encrypted', connectionString) + + let queryParams = new URLSearchParams() + if (schema) queryParams.set('included_schemas', schema) + const searchStr = queryParams.toString() + + const response = (await get( + `${API_URL}/pg-meta/${projectRef}/tables${searchStr ? `?${searchStr}` : ''}`, + { + headers: Object.fromEntries(headers), + signal, + } + )) as TablesResponse + + if (!Array.isArray(response) && response.error) { + throw response.error + } + + return response as PostgresTable[] +} + +export type TablesData = Awaited> +export type TablesError = ResponseError + +export const useTablesQuery = ( + { projectRef, connectionString, schema }: TablesVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + 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/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..19647e03c88 --- /dev/null +++ b/studio/hooks/misc/useTable.ts @@ -0,0 +1,81 @@ +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' +import { ForeignTable, useForeignTableQuery } from 'data/foreign-tables/foreign-table-query' +import { + MaterializedView, + useMaterializedViewQuery, +} from 'data/materialized-views/materialized-view-query' +import { Table, useTableQuery } from 'data/tables/table-query' +import { View, useViewQuery } from 'data/views/view-query' +import useEntityType from './useEntityType' + +export type TableLike = Table | View | MaterializedView | ForeignTable + +/** + * A hook that loads all table-like objects. e.g. tables, views, materialized views, etc... + */ +function useTable(id?: number) { + const { project } = useProjectContext() + const entity = useEntityType(id) + + const tableResult = useTableQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + id: entity?.id, + }, + { + enabled: entity?.type === ENTITY_TYPE.TABLE, + } + ) + + const viewResult = useViewQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + id: entity?.id, + }, + { + enabled: entity?.type === ENTITY_TYPE.VIEW, + } + ) + + const materializedViewResult = useMaterializedViewQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + id: entity?.id, + }, + { + enabled: entity?.type === ENTITY_TYPE.MATERIALIZED_VIEW, + } + ) + + const foreignTableResult = useForeignTableQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + id: entity?.id, + }, + { + enabled: entity?.type === ENTITY_TYPE.FOREIGN_TABLE, + } + ) + + switch (entity?.type) { + case ENTITY_TYPE.TABLE: + return tableResult + case ENTITY_TYPE.VIEW: + return viewResult + case ENTITY_TYPE.MATERIALIZED_VIEW: + return materializedViewResult + case ENTITY_TYPE.FOREIGN_TABLE: + return foreignTableResult + default: + // While entity is loading, we can fallback + // to the loading state of the table query. + return tableResult + } +} + +export default useTable diff --git a/studio/lib/constants/schemas.ts b/studio/lib/constants/schemas.ts new file mode 100644 index 00000000000..5f773e13b88 --- /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', + 'pgtle', + 'realtime', + 'storage', + 'supabase_functions', + 'supabase_migrations', + 'vault', + 'graphql', + 'graphql_public', +] diff --git a/studio/pages/project/[ref]/api/graphiql.tsx b/studio/pages/project/[ref]/api/graphiql.tsx index 5f4821e5e44..d1040650eca 100644 --- a/studio/pages/project/[ref]/api/graphiql.tsx +++ b/studio/pages/project/[ref]/api/graphiql.tsx @@ -34,7 +34,6 @@ const GraphiQLPage: NextPageWithLayout = () => { useEffect(() => { if (ui.selectedProjectRef) { // Schemas may be needed when enabling the GraphQL extension - meta.schemas.load() meta.extensions.load() } }, [ui.selectedProjectRef]) diff --git a/studio/pages/project/[ref]/auth/policies.tsx b/studio/pages/project/[ref]/auth/policies.tsx index 67e6995429c..c66ea97a317 100644 --- a/studio/pages/project/[ref]/auth/policies.tsx +++ b/studio/pages/project/[ref]/auth/policies.tsx @@ -1,16 +1,23 @@ -import { useState, useEffect } from 'react' -import { partition } from 'lodash' -import { Button, Listbox, IconSearch, Input, IconExternalLink, IconLock } from 'ui' -import { observer } from 'mobx-react-lite' -import { PostgresTable, PostgresPolicy } from '@supabase/postgres-meta' +import { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { partition } from 'lodash' +import { observer } from 'mobx-react-lite' +import { useEffect, useState } from 'react' -import { NextPageWithLayout } from 'types' -import { useCheckPermissions, useStore } from 'hooks' import { useParams } from 'common/hooks' -import { AuthLayout } from 'components/layouts' import { Policies } from 'components/interfaces/Auth/Policies' +import { AuthLayout } from 'components/layouts' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import { useSchemasQuery } from 'data/database/schemas-query' +import { useTablesQuery } from 'data/tables/tables-query' +import { useCheckPermissions, useStore } from 'hooks' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { useTableEditorStateSnapshot } from 'state/table-editor' +import { NextPageWithLayout } from 'types' +import { Button, IconExternalLink, IconLock, IconSearch, Input, Listbox } from 'ui' /** * Filter tables by table name and policy name @@ -48,27 +55,47 @@ const onFilterTables = ( } const AuthPoliciesPage: NextPageWithLayout = () => { + const { project } = useProjectContext() + const snap = useTableEditorStateSnapshot() const { meta } = useStore() const { search } = useParams() - const [selectedSchema, setSelectedSchema] = useState('public') const [searchString, setSearchString] = useState('') useEffect(() => { if (search) setSearchString(search) }, [search]) - const schemas = meta.schemas.list() + const { + data: schemas, + isLoading: isLoadingSchemas, + isSuccess: isSuccessSchemas, + isError: isErrorSchemas, + error: errorSchemas, + } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) const [protectedSchemas, openSchemas] = partition(schemas, (schema) => - meta.excludedSchemas.includes(schema?.name ?? '') + EXCLUDED_SCHEMAS.includes(schema?.name ?? '') ) - // @ts-ignore - const schema = schemas.find((schema) => schema.name === selectedSchema) + const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName) const isLocked = protectedSchemas.some((s) => s.id === schema?.id) const policies = meta.policies.list() - const tables = meta.tables.list((table: { schema: string }) => table.schema === selectedSchema) - const filteredTables = onFilterTables(tables, policies, searchString) + const { + data: tables, + isLoading, + isSuccess, + isError, + error, + } = useTablesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + schema: snap.selectedSchemaName, + }) + + const filteredTables = onFilterTables(tables ?? [], policies, searchString) const canReadPolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'policies') @@ -82,53 +109,62 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
- { - setSelectedSchema(schema) - setSearchString('') - }} - icon={isLocked && } - > - + )} + + {isErrorSchemas && ( + + )} + + {isSuccessSchemas && ( + { + snap.setSelectedSchemaName(schema) + setSearchString('') + }} + icon={isLocked && } > -

Schemas

-
- {/* @ts-ignore */} - {openSchemas.map((schema) => ( schema} + disabled + key="normal-schemas" + value="normal-schemas" + label="Schemas" > - {schema.name} +

Schemas

- ))} - -

Protected schemas

-
- {protectedSchemas.map((schema) => ( + {openSchemas.map((schema) => ( + schema} + > + {schema.name} + + ))} schema} + disabled + key="protected-schemas" + value="protected-schemas" + label="Protected schemas" > - {schema.name} +

Protected schemas

- ))} -
+ {protectedSchemas.map((schema) => ( + schema} + > + {schema.name} + + ))} + + )}
{
- 0} isLocked={isLocked} /> + {isLoading && } + {isError && } + {isSuccess && ( + 0} isLocked={isLocked} /> + )}
) } diff --git a/studio/pages/project/[ref]/database/functions.tsx b/studio/pages/project/[ref]/database/functions.tsx index f0dc7fa55f2..dfd219a8326 100644 --- a/studio/pages/project/[ref]/database/functions.tsx +++ b/studio/pages/project/[ref]/database/functions.tsx @@ -5,15 +5,13 @@ import { useEffect, useState } from 'react' import { CreateFunction, DeleteFunction } from 'components/interfaces/Database' import FunctionsList from 'components/interfaces/Database/Functions/FunctionsList/FunctionsList' import { DatabaseLayout } from 'components/layouts' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import { useCheckPermissions, useStore } from 'hooks' import { NextPageWithLayout } from 'types' -import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' const FunctionsPage: NextPageWithLayout = () => { - const { meta } = useStore() - const { project } = useProjectContext() + const { ui, meta } = useStore() const [selectedFunction, setSelectedFunction] = useState() const [showCreateFunctionForm, setShowCreateFunctionForm] = useState(false) const [showDeleteFunctionForm, setShowDeleteFunctionForm] = useState(false) @@ -21,10 +19,10 @@ const FunctionsPage: NextPageWithLayout = () => { const canReadFunctions = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'functions') useEffect(() => { - if (project?.ref) { + if (ui.selectedProjectRef) { fetchFunctions() } - }, [project?.ref]) + }, [ui.selectedProjectRef]) const fetchFunctions = async () => { meta.functions.load() diff --git a/studio/pages/project/[ref]/database/hooks.tsx b/studio/pages/project/[ref]/database/hooks.tsx index c7477d9d259..121004351a1 100644 --- a/studio/pages/project/[ref]/database/hooks.tsx +++ b/studio/pages/project/[ref]/database/hooks.tsx @@ -1,39 +1,48 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import clsx from 'clsx' -import { useParams } from 'common' import { observer } from 'mobx-react-lite' import { useEffect, useState } from 'react' -import { IconLoader } from 'ui' +import { useParams } from 'common' import DeleteHookModal from 'components/interfaces/Database/Hooks/DeleteHookModal' import EditHookPanel from 'components/interfaces/Database/Hooks/EditHookPanel' import HooksList from 'components/interfaces/Database/Hooks/HooksList/HooksList' import { DatabaseLayout } from 'components/layouts' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import NoPermission from 'components/ui/NoPermission' import { useHooksEnableMutation } from 'data/database/hooks-enable-mutation' +import { useSchemasQuery } from 'data/database/schemas-query' import { useCheckPermissions, useStore } from 'hooks' import { NextPageWithLayout } from 'types' -import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' +import { IconLoader } from 'ui' const HooksPage: NextPageWithLayout = () => { + const { project } = useProjectContext() const { meta, ui } = useStore() - const { ref } = useParams() - const schemas = meta.schemas.list() - const { isLoading: isLoadingSchemas } = meta.schemas + const { ref: projectRef } = useParams() + + const { + data: schemas, + isLoading: isLoadingSchemas, + refetch, + } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) const [selectedHook, setSelectedHook] = useState() 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) => schema.name === 'supabase_functions') const canReadWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers') const canCreateWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers') const { mutate: enableHooks, isLoading: isEnablingHooks } = useHooksEnableMutation({ onSuccess: () => { - meta.schemas.load() + refetch() ui.setNotification({ category: 'success', message: `Successfully enabled webhooks`, @@ -46,8 +55,8 @@ const HooksPage: NextPageWithLayout = () => { }, [ui.selectedProjectRef]) const enableHooksForProject = async () => { - if (!ref) return console.error('Project ref is required') - enableHooks({ ref }) + if (!projectRef) return console.error('Project ref is required') + enableHooks({ ref: projectRef }) } const createHook = () => { diff --git a/studio/pages/project/[ref]/database/schemas.tsx b/studio/pages/project/[ref]/database/schemas.tsx index 1f7058bd3dc..d9e67017988 100644 --- a/studio/pages/project/[ref]/database/schemas.tsx +++ b/studio/pages/project/[ref]/database/schemas.tsx @@ -1,66 +1,85 @@ -import { observer } from 'mobx-react-lite' import { partition } from 'lodash' import { useState } from 'react' import SchemaGraph from 'components/interfaces/Database/Schemas/SchemaGraph' import { DatabaseLayout } from 'components/layouts' -import { useStore } from 'hooks' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import AlertError from 'components/ui/AlertError' +import { useSchemasQuery } from 'data/database/schemas-query' +import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' import { NextPageWithLayout } from 'types' import { IconLock, Listbox } from 'ui' const SchemasPage: NextPageWithLayout = () => { - const { meta } = useStore() - const schemas = meta.schemas.list() + const { project } = useProjectContext() + const { + data: schemas, + isSuccess, + isLoading, + isError, + error, + } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) const [selectedSchema, setSelectedSchema] = useState('public') const [protectedSchemas, openSchemas] = partition(schemas, (schema) => - meta.excludedSchemas.includes(schema?.name ?? '') + EXCLUDED_SCHEMAS.includes(schema?.name ?? '') ) - const schema = schemas.find((schema) => schema.name === selectedSchema) + const schema = schemas?.find((schema) => schema.name === selectedSchema) const isLocked = protectedSchemas.some((s) => s.id === schema?.id) return ( <>
- } - > - -

Schemas

-
- {openSchemas.map((schema) => ( - schema} - > - {schema.name} - - ))} - + )} + + {isError && } + + {isSuccess && ( + } > -

Protected schemas

-
- {protectedSchemas.map((schema) => ( - schema} - > - {schema.name} + +

Schemas

- ))} -
+ {openSchemas.map((schema) => ( + schema} + > + {schema.name} + + ))} + +

Protected schemas

+
+ {protectedSchemas.map((schema) => ( + schema} + > + {schema.name} + + ))} + + )}
@@ -71,4 +90,4 @@ const SchemasPage: NextPageWithLayout = () => { SchemasPage.getLayout = (page) => {page} -export default observer(SchemasPage) +export default SchemasPage diff --git a/studio/pages/project/[ref]/database/tables.tsx b/studio/pages/project/[ref]/database/tables.tsx index a37cf910e14..088a06fde15 100644 --- a/studio/pages/project/[ref]/database/tables.tsx +++ b/studio/pages/project/[ref]/database/tables.tsx @@ -1,166 +1,56 @@ -import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta' -import { useQueryClient } from '@tanstack/react-query' -import { isUndefined } from 'lodash' import { observer } from 'mobx-react-lite' import { useEffect, useState } from 'react' -import { Modal } from 'ui' +import { useParams } from 'common' import { ColumnList, TableList } from 'components/interfaces/Database' import { SidePanelEditor } from 'components/interfaces/TableGridEditor' +import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs' import { DatabaseLayout } from 'components/layouts' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' -import ConfirmationModal from 'components/ui/ConfirmationModal' -import { sqlKeys } from 'data/sql/keys' +import { Table } from 'data/tables/table-query' import { useStore } from 'hooks' +import { useTableEditorStateSnapshot } from 'state/table-editor' import { NextPageWithLayout } from 'types' const DatabaseTables: NextPageWithLayout = () => { - const { meta, ui } = useStore() - const { project } = useProjectContext() + const snap = useTableEditorStateSnapshot() + const { ui, meta } = useStore() - const queryClient = useQueryClient() - - const [isDeleting, setIsDeleting] = useState(false) - const [selectedSchema, setSelectedSchema] = useState('public') - 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 { ref: projectRef } = useParams() useEffect(() => { - if (project?.ref) { + if (ui.selectedProjectRef) { meta.types.load() } - }, [project?.ref]) + }, [ui.selectedProjectRef]) - const onAddTable = () => { - setSidePanelKey('table') - setSelectedTableToEdit(undefined) - } - - const onEditTable = (table: PostgresTable) => { - setSidePanelKey('table') - setSelectedTableToEdit(table) - } - - const onDeleteTable = (table: PostgresTable) => { - setIsDeleting(true) - setSelectedTableToDelete(table) - } - - const onAddColumn = () => { - setSidePanelKey('column') - setSelectedColumnToEdit(undefined) - } - - const onEditColumn = (column: PostgresColumn) => { - setSidePanelKey('column') - setSelectedColumnToEdit(column) - } - - const onDeleteColumn = (column: PostgresColumn) => { - setIsDeleting(true) - setSelectedColumnToDelete(column) - } - - const onColumnUpdated = async () => { - if (selectedTable === undefined) return - - const updatedTable = await meta.tables.loadById(selectedTable.id) - setSelectedTable(updatedTable) - } - - const onClosePanel = () => setSidePanelKey(undefined) - - const onConfirmDeleteTable = async () => { - try { - if (isUndefined(selectedTableToDelete)) return - - const response: any = await meta.tables.del(selectedTableToDelete.id) - if (response.error) { - throw response.error - } else { - ui.setNotification({ - category: 'success', - message: `Successfully removed ${selectedTableToDelete.name}.`, - }) - } - } catch (error: any) { - ui.setNotification({ - category: 'error', - message: `Failed to delete ${selectedTableToDelete?.name}: ${error.message}`, - }) - } finally { - setIsDeleting(false) - setSelectedTableToDelete(undefined) - } - } - - const onConfirmDeleteColumn = async () => { - try { - if (isUndefined(selectedColumnToDelete)) return - - const response: any = await meta.columns.del(selectedColumnToDelete.id) - if (response.error) { - throw response.error - } else { - onColumnUpdated() - ui.setNotification({ - category: 'success', - message: `Successfully removed ${selectedColumnToDelete.name}.`, - }) - - queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])) - await Promise.all([ - meta.tables.loadById(selectedColumnToDelete!.table_id), - queryClient.invalidateQueries( - sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name]) - ), - queryClient.invalidateQueries( - sqlKeys.query(project?.ref, [ - 'table-definition', - selectedTable!.schema, - selectedTable!.name, - ]) - ), - ]) - } - } catch (error: any) { - ui.setNotification({ - category: 'error', - message: `Failed to delete ${selectedColumnToDelete?.name}: ${error.message}`, - }) - } finally { - setIsDeleting(false) - setSelectedColumnToDelete(undefined) - } - } + const [selectedTable, setSelectedTable] = useState
(undefined) + const [selectedTableToEdit, setSelectedTableToEdit] = useState
(undefined) return ( <>
- {isUndefined(selectedTable) ? ( + {selectedTable === undefined ? ( { + setSelectedTableToEdit(table) + snap.onEditTable() + }} + onDeleteTable={(table) => { + setSelectedTableToEdit(table) + snap.onDeleteTable() + }} onOpenTable={setSelectedTable} /> ) : ( setSelectedTable(undefined)} /> )} @@ -168,49 +58,8 @@ const DatabaseTables: NextPageWithLayout = () => { - {`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 9cf12e1ac2b..0b31931bf2b 100644 --- a/studio/pages/project/[ref]/editor/[id].tsx +++ b/studio/pages/project/[ref]/editor/[id].tsx @@ -1,402 +1,48 @@ -import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta' -import { useQueryClient } from '@tanstack/react-query' -import { useParams, useTheme } from 'common' -import { isNaN, isUndefined } from 'lodash' -import { observer } from 'mobx-react-lite' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import { Alert, Button, Checkbox, IconExternalLink, Modal } from 'ui' - -import { Dictionary } from 'components/grid' +import { useTheme } from 'common' +import { useParams } from 'common/hooks' import { TableGridEditor } from 'components/interfaces' -import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector' -import { JsonEditValue } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types' +import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs' import { TableEditorLayout } from 'components/layouts' -import { - ProjectContextFromParamsProvider, - useProjectContext, -} from 'components/layouts/ProjectLayout/ProjectContext' -import ConfirmationModal from 'components/ui/ConfirmationModal' -import { Entity } from 'data/entity-types/entity-type-query' -import { entityTypeKeys } from 'data/entity-types/keys' -import { sqlKeys } from 'data/sql/keys' -import { useStore, useUrlState, withAuth } from 'hooks' -import { NextPageWithLayout, SchemaView } from 'types' +import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext' +import useTable from 'hooks/misc/useTable' +import { useRouter } from 'next/router' +import { NextPageWithLayout } from 'types' const TableEditorPage: NextPageWithLayout = () => { const router = useRouter() const { isDarkMode } = useTheme() - const { id, ref: projectRef } = useParams() - const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] }) + const { id: _id, ref: projectRef } = useParams() + const id = _id ? Number(_id) : undefined - const { project } = useProjectContext() - - const queryClient = useQueryClient() - - const { meta, ui } = useStore() - const [selectedSchema, setSelectedSchema] = useState() - - 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 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 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 (selectedSchema) await meta.views.loadBySchema(selectedSchema) - } catch (error: any) { - ui.setNotification({ - category: 'error', - message: `Failed to delete ${selectedColumnToDelete!.name}: ${error.message}`, - }) - } finally { - setIsDeleting(false) - setSelectedColumnToDelete(undefined) - } - } - - const onConfirmDeleteTable = async () => { - try { - if (selectedTableToDelete === undefined) return - - const response: any = await meta.tables.del(selectedTableToDelete.id, isDeleteWithCascade) - if (response.error) throw response.error - - const tables = meta.tables.list((table: PostgresTable) => table.schema === selectedSchema) - - await queryClient.invalidateQueries(entityTypeKeys.list(projectRef)) - - // For simplicity for now, we just open the first table within the same schema - if (tables.length > 0) { - router.push(`/project/${projectRef}/editor/${tables[0].id}`) - } else { - router.push(`/project/${projectRef}/editor/`) - } - ui.setNotification({ - category: 'success', - message: `Successfully deleted table "${selectedTableToDelete.name}"`, - }) - if (selectedSchema) await meta.views.loadBySchema(selectedSchema) - } catch (error: any) { - ui.setNotification({ - error, - category: 'error', - message: `Failed to delete ${selectedTableToDelete?.name}: ${error.message}`, - }) - } finally { - setIsDeleting(false) - setSelectedTableToDelete(undefined) - } - } + const { data: selectedTable, isLoading } = useTable(id) return ( - + <> - { - setIsDeleting(false) - setSelectedColumnToDelete(undefined) + { + // For simplicity for now, we just open the first table within the same schema + if (tables.length > 0) { + router.push(`/project/${projectRef}/editor/${tables[0].id}`) + } else { + router.push(`/project/${projectRef}/editor/`) + } }} - onSelectConfirm={onConfirmDeleteColumn} - > - -
-

- 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 6cb80edcfdb..55c2cfe0189 100644 --- a/studio/pages/project/[ref]/editor/index.tsx +++ b/studio/pages/project/[ref]/editor/index.tsx @@ -1,119 +1,32 @@ -import type { PostgresTable } from '@supabase/postgres-meta' -import { isUndefined } from 'lodash' -import { observer } from 'mobx-react-lite' -import router from 'next/router' -import { useState } from 'react' +import { useRouter } from 'next/router' +import { useParams } from 'common' import { EmptyState, SidePanelEditor } from 'components/interfaces/TableGridEditor' import { TableEditorLayout } from 'components/layouts' -import { - ProjectContextFromParamsProvider, - useProjectContext, -} from 'components/layouts/ProjectLayout/ProjectContext' -import ConfirmationModal from 'components/ui/ConfirmationModal' -import { Entity } from 'data/entity-types/entity-type-query' -import { useStore, withAuth } from 'hooks' +import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext' +import { Table } from 'data/tables/table-query' import { NextPageWithLayout } from 'types' const TableEditorPage: NextPageWithLayout = () => { - const { meta, ui } = useStore() - const { project } = useProjectContext() - const projectRef = project?.ref + const { ref: projectRef } = useParams() + const router = useRouter() - const [sidePanelKey, setSidePanelKey] = useState<'row' | 'column' | 'table'>() - const [isDeleting, setIsDeleting] = useState(false) - const [isDuplicating, setIsDuplicating] = useState(false) - const [selectedSchema, setSelectedSchema] = useState('public') - 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(selectedSchema) - } catch (error: any) { - ui.setNotification({ - category: 'error', - message: `Failed to delete ${selectedTableToDelete!.name}`, - }) - } + const onTableCreated = (table: Table) => { + router.push(`/project/${projectRef}/editor/${table.id}`) } return ( - - - {/* 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}`)} - /> - + <> + + + ) } TableEditorPage.getLayout = (page) => ( - {page} + + {page} + ) -export default withAuth(observer(TableEditorPage)) +export default TableEditorPage diff --git a/studio/pages/project/[ref]/settings/api.tsx b/studio/pages/project/[ref]/settings/api.tsx index e430e0cb57c..3cbb7fddc8a 100644 --- a/studio/pages/project/[ref]/settings/api.tsx +++ b/studio/pages/project/[ref]/settings/api.tsx @@ -1,5 +1,5 @@ import { observer, useLocalObservable } from 'mobx-react-lite' -import { createContext, useEffect } from 'react' +import { createContext } from 'react' import { useParams } from 'common/hooks' import ServiceList from 'components/interfaces/Settings/API/ServiceList' @@ -31,12 +31,6 @@ const ApiSettings: NextPageWithLayout = () => { PageContext.project = project if (meta) PageState.meta = meta - useEffect(() => { - if (ui.selectedProjectRef) { - meta.schemas.load() - } - }, [ui.selectedProjectRef]) - return (
diff --git a/studio/pages/project/[ref]/sql/[id].tsx b/studio/pages/project/[ref]/sql/[id].tsx index d0b3ffb8828..fc9e513dd9d 100644 --- a/studio/pages/project/[ref]/sql/[id].tsx +++ b/studio/pages/project/[ref]/sql/[id].tsx @@ -67,7 +67,7 @@ const SqlEditor: NextPageWithLayout = () => { pgInfoRef.current = {} } pgInfoRef.current.tableColumns = tableColumns?.result - pgInfoRef.current.schemas = schemas?.result + pgInfoRef.current.schemas = schemas pgInfoRef.current.keywords = keywords?.result pgInfoRef.current.functions = functions?.result } 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..3b38c94f5e1 --- /dev/null +++ b/studio/state/table-editor.tsx @@ -0,0 +1,171 @@ +import { PostgresColumn } from '@supabase/postgres-meta' +import { Dictionary } from 'components/grid' +import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector' +import { JsonEditValue } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types' +import { PropsWithChildren, createContext, useContext, useRef } from 'react' +import { proxy, useSnapshot } from 'valtio' + +type ForeignKey = { + foreignKey: NonNullable + 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 +} + +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 d62d39c278b..b57d818bf49 100644 --- a/studio/stores/pgmeta/MetaStore.ts +++ b/studio/stores/pgmeta/MetaStore.ts @@ -1,62 +1,62 @@ -import Papa from 'papaparse' -import { makeObservable, observable } from 'mobx' -import { find, isUndefined, isEqual, isEmpty, chunk } from 'lodash' import { Query } from 'components/grid/query/Query' +import { chunk, find, isEmpty, isEqual, isUndefined } from 'lodash' +import { makeObservable } from 'mobx' +import Papa from 'papaparse' import type { PostgresColumn, - PostgresTable, - PostgresRelationship, PostgresPrimaryKey, - PostgresSchema, + PostgresRelationship, + PostgresTable, } from '@supabase/postgres-meta' -import { IS_PLATFORM, API_URL } from 'lib/constants' import { post } from 'lib/common/fetch' +import { API_URL, IS_PLATFORM } from 'lib/constants' import { timeout, tryParseJson } from 'lib/helpers' import { ResponseError } from 'types' import { IRootStore } from '../RootStore' -import ColumnStore from './ColumnStore' -import SchemaStore from './SchemaStore' -import TableStore, { ITableStore } from './TableStore' -import OpenApiStore, { IOpenApiStore } from './OpenApiStore' import { IPostgresMetaInterface } from '../common/PostgresMetaInterface' +import ColumnStore from './ColumnStore' +import OpenApiStore, { IOpenApiStore } from './OpenApiStore' +import TableStore, { ITableStore } from './TableStore' +import { + generateCreateColumnPayload, + generateUpdateColumnPayload, +} from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils' import { ColumnField, CreateColumnPayload, ExtendedPostgresRelationship, UpdateColumnPayload, } from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.types' -import { - generateCreateColumnPayload, - generateUpdateColumnPayload, -} from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils' import { ImportContent } from 'components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types' -import RolesStore, { IRolesStore } from './RolesStore' -import PoliciesStore from './PoliciesStore' -import TriggersStore from './TriggersStore' -import PublicationStore from './PublicationStore' +import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' +import { getCachedProjectDetail } from 'data/projects/project-detail-query' +import { getQueryClient } from 'data/query-client' +import { tableKeys } from 'data/tables/keys' +import { getTable } from 'data/tables/table-query' +import { getTables } from 'data/tables/tables-query' +import ExtensionsStore from './ExtensionsStore' +import ForeignTableStore, { IForeignTableStore } from './ForeignTableStore' import FunctionsStore from './FunctionsStore' import HooksStore from './HooksStore' -import ExtensionsStore from './ExtensionsStore' -import TypesStore from './TypesStore' -import ForeignTableStore, { IForeignTableStore } from './ForeignTableStore' -import ViewStore, { IViewStore } from './ViewStore' import MaterializedViewStore, { IMaterializedViewStore } from './MaterializedViewStore' -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' +import PoliciesStore from './PoliciesStore' +import PublicationStore from './PublicationStore' +import RolesStore, { IRolesStore } from './RolesStore' +import TriggersStore from './TriggersStore' +import TypesStore from './TypesStore' +import ViewStore, { IViewStore } from './ViewStore' const BATCH_SIZE = 1000 const CHUNK_SIZE = 1024 * 1024 * 0.1 // 0.1MB export interface IMetaStore { - excludedSchemas: string[] - openApi: IOpenApiStore tables: ITableStore columns: IPostgresMetaInterface - schemas: IPostgresMetaInterface views: IViewStore materializedViews: IMaterializedViewStore foreignTables: IForeignTableStore @@ -150,7 +150,6 @@ export default class MetaStore implements IMetaStore { openApi: OpenApiStore tables: TableStore columns: ColumnStore - schemas: SchemaStore views: ViewStore materializedViews: MaterializedViewStore foreignTables: ForeignTableStore @@ -169,25 +168,6 @@ export default class MetaStore implements IMetaStore { baseUrl: string headers: { [prop: string]: any } - // [Joshen] I'm going to treat this as a list of system schemas - excludedSchemas = [ - 'auth', - 'extensions', - 'information_schema', - 'net', - 'pgsodium', - 'pgsodium_masks', - 'pgbouncer', - 'pgtle', - 'realtime', - 'storage', - 'supabase_functions', - 'supabase_migrations', - 'vault', - 'graphql', - 'graphql_public', - ] - constructor(rootStore: IRootStore, options: { projectRef: string; connectionString?: string }) { const { projectRef, connectionString } = options this.rootStore = rootStore @@ -206,7 +186,6 @@ export default class MetaStore implements IMetaStore { ) this.tables = new TableStore(this.rootStore, `${this.baseUrl}/tables`, this.headers) this.columns = new ColumnStore(this.rootStore, `${this.baseUrl}/columns`, this.headers) - this.schemas = new SchemaStore(this.rootStore, `${this.baseUrl}/schemas`, this.headers) this.views = new ViewStore(this.rootStore, `${this.baseUrl}/views`, this.headers) this.materializedViews = new MaterializedViewStore( this.rootStore, @@ -239,9 +218,7 @@ export default class MetaStore implements IMetaStore { ) this.types = new TypesStore(this.rootStore, `${this.baseUrl}/types`, this.headers) - makeObservable(this, { - excludedSchemas: observable, - }) + makeObservable(this, {}) } /** @@ -349,7 +326,16 @@ export default class MetaStore implements IMetaStore { async updateTableRealtime(table: PostgresTable, enable: boolean) { let publicationUpdateError const publications = this.publications.list() - const publicTables = this.tables.list((table: PostgresTable) => table.schema === 'public') + + const queryClient = getQueryClient() + const project = await getCachedProjectDetail(queryClient, this.rootStore.ui.selectedProjectRef) + const projectRef = project?.ref + const connectionString = project?.connectionString + const publicTables = await queryClient.fetchQuery({ + queryKey: tableKeys.list(projectRef, 'public'), + queryFn: ({ signal }) => + getTables({ projectRef, connectionString, schema: 'public' }, signal), + }) let realtimePublication = publications.find((pub) => pub.name === 'supabase_realtime') if (realtimePublication === undefined) { @@ -596,8 +582,15 @@ export default class MetaStore implements IMetaStore { }) } - await this.tables.load() - const tables = this.tables.list() + const queryClient = getQueryClient() + const project = await getCachedProjectDetail(queryClient, this.rootStore.ui.selectedProjectRef) + const projectRef = project?.ref + const connectionString = project?.connectionString + const tables = await queryClient.fetchQuery({ + queryKey: tableKeys.list(projectRef, 'public'), + queryFn: ({ signal }) => getTables({ projectRef, connectionString }, signal), + }) + const duplicatedTable = find(tables, { schema: sourceTableSchema, name: duplicatedTableName }) if (isRLSEnabled) { @@ -621,8 +614,8 @@ export default class MetaStore implements IMetaStore { importContent?: ImportContent ) { // Create the table first - const table: any = await this.tables.create(payload) - if (table.error) throw table.error + const table = await this.tables.create(payload) + if ('error' in table) throw table.error // If we face any errors during this process after the actual table creation // We'll delete the table as a way to clean up and not leave behind bits that @@ -745,7 +738,7 @@ export default class MetaStore implements IMetaStore { } // Finally, return the created table - return await this.tables.loadById(table.id) + return table } catch (error: any) { this.tables.del(table.id) throw error @@ -865,7 +858,21 @@ export default class MetaStore implements IMetaStore { // Update table's realtime configuration await this.updateTableRealtime(table, isRealtimeEnabled) - return { table: await this.tables.loadById(table.id), hasError } + const queryClient = getQueryClient() + const project = await getCachedProjectDetail(queryClient, this.rootStore.ui.selectedProjectRef) + const projectRef = project?.ref + const connectionString = project?.connectionString + + queryClient.invalidateQueries(tableKeys.table(projectRef, table.id)) + + return { + table: await getTable({ + projectRef, + connectionString, + id: table.id, + }), + hasError, + } } async insertRowsViaSpreadsheet( @@ -1000,9 +1007,6 @@ export default class MetaStore implements IMetaStore { this.columns.setUrl(`${this.baseUrl}/columns`) this.columns.setHeaders(this.headers) - this.schemas.setUrl(`${this.baseUrl}/schemas`) - this.schemas.setHeaders(this.headers) - this.views.setUrl(`${this.baseUrl}/views`) this.views.setHeaders(this.headers) 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) - } -}