diff --git a/studio/components/grid/SupabaseGrid.tsx b/studio/components/grid/SupabaseGrid.tsx index 3a1f1fbaffb..299501c1073 100644 --- a/studio/components/grid/SupabaseGrid.tsx +++ b/studio/components/grid/SupabaseGrid.tsx @@ -152,21 +152,13 @@ const SupabaseGridLayout = forwardRef((props ]) useEffect(() => { - if (!state.metaService) { - dispatch({ - type: 'INIT_CLIENT', - payload: { onSqlQuery: props.onSqlQuery }, - }) - dispatch({ - type: 'INIT_CALLBACK', - payload: { ...props }, - }) - } - }, [state.metaService]) + dispatch({ + type: 'INIT_CALLBACK', + payload: { ...props }, + }) + }, []) useEffect(() => { - if (!state.metaService) return - const initializeData = async () => { const { savedState } = await initTable( props, @@ -176,6 +168,7 @@ const SupabaseGridLayout = forwardRef((props filter as string[] ) + console.log('savedState:', savedState) if (savedState.sorts || savedState.filters) { setParams((prevParams) => { return { @@ -187,17 +180,12 @@ const SupabaseGridLayout = forwardRef((props } } - const refreshView = - typeof props.table === 'string' && - state?.table?.name !== props.table && - state?.table?.schema !== props.schema - const refreshTable = - typeof props.table !== 'string' && JSON.stringify(props.table) !== JSON.stringify(state.table) + const refreshTable = JSON.stringify(props.table) !== JSON.stringify(state.table) - if (!state.table || refreshView || refreshTable) { + if (!state.table || refreshTable) { initializeData() } - }, [state.metaService, state.table, props.table, props.schema]) + }, [state.table, props.table, props.schema]) return (
diff --git a/studio/components/grid/SupabaseGrid.utils.ts b/studio/components/grid/SupabaseGrid.utils.ts index 2a0a4f29cb2..7f2e72a678f 100644 --- a/studio/components/grid/SupabaseGrid.utils.ts +++ b/studio/components/grid/SupabaseGrid.utils.ts @@ -1,6 +1,5 @@ import AwesomeDebouncePromise from 'awesome-debounce-promise' import { STORAGE_KEY_PREFIX } from './constants' -import { IMetaService } from './services/meta' import { InitialStateType } from './store/reducers' import { Dictionary, Sort, SupabaseGridProps, SupaColumn, SupaTable } from './types' import { getGridColumns } from './utils/gridColumns' @@ -71,124 +70,52 @@ export async function initTable( props: SupabaseGridProps, state: InitialStateType, dispatch: (value: any) => void, - sort?: string[], // Come directly from URL param - filter?: string[] // Come directly from URL param + sort?: string[], // Comes directly from URL param + filter?: string[] // Comes directly from URL param ): Promise<{ savedState: { sorts?: string[]; filters?: string[] } }> { - function onInitTable(table: SupaTable, props: SupabaseGridProps) { - const savedState = props.storageRef - ? onLoadStorage(props.storageRef, table.name, table.schema) - : undefined + const savedState = props.storageRef + ? onLoadStorage(props.storageRef, props.table.name, props.table.schema) + : undefined - // Check for saved state on initial load and also, load sort and filters via URL param only if given - // Otherwise load from local storage to resume user session - if ( - !state.isInitialComplete && - sort === undefined && - filter === undefined && - (savedState?.sorts || savedState?.filters) - ) { - return { - savedState: { - sorts: savedState.sorts, - filters: savedState.filters, - }, - } - } - - const gridColumns = getGridColumns(table, { - editable: props.editable, - defaultWidth: props.gridProps?.defaultColumnWidth, - onAddColumn: props.editable ? props.onAddColumn : undefined, - onExpandJSONEditor: props.editable ? props.onExpandJSONEditor : () => {}, - }) - - dispatch({ - type: 'INIT_TABLE', - payload: { - table, - gridProps: props.gridProps, - gridColumns, - savedState, - editable: props.editable, - onSqlQuery: props.onSqlQuery, - onError: props.onError ?? defaultErrorHandler, + // Check for saved state on initial load and also, load sort and filters via URL param only if given + // Otherwise load from local storage to resume user session + if ( + !state.isInitialComplete && + sort === undefined && + filter === undefined && + (savedState?.sorts || savedState?.filters) + ) { + return { + savedState: { + sorts: savedState.sorts, + filters: savedState.filters, }, - }) - - return { savedState: {} } - } - - if (typeof props.table === 'string') { - const viewData = props.editable - ? await fetchEditableInfo(state.metaService!, props.table, props.schema) - : await fetchReadOnlyInfo(state.metaService!, props.table, props.schema) - - if (viewData) { - return onInitTable(viewData, props) - } else { - if (props.onError) props.onError({ message: 'Failed to fetch data from view' }) } - } else { - return onInitTable(props.table, props) } + const gridColumns = getGridColumns(props.table, { + editable: props.editable, + defaultWidth: props.gridProps?.defaultColumnWidth, + onAddColumn: props.editable ? props.onAddColumn : undefined, + onExpandJSONEditor: props.editable ? props.onExpandJSONEditor : () => {}, + }) + + dispatch({ + type: 'INIT_TABLE', + payload: { + table: props.table, + gridProps: props.gridProps, + gridColumns, + savedState, + editable: props.editable, + onSqlQuery: props.onSqlQuery, + onError: props.onError ?? defaultErrorHandler, + }, + }) + return { savedState: {} } } -async function fetchEditableInfo( - service: IMetaService, - tableName: string, - schema?: string -): Promise { - const resTable = await service.fetchInfo(tableName, schema) - const resColumns = await service.fetchColumns(tableName, schema) - const resPrimaryKeys = await service.fetchPrimaryKeys(tableName, schema) - const resRelationships = await service.fetchRelationships(tableName, schema) - if ( - resTable.data && - resColumns.data && - resPrimaryKeys.data && - resRelationships.data && - resColumns.data.length > 0 - ) { - const supaTable = parseSupaTable({ - table: resTable.data, - columns: resColumns.data, - primaryKeys: resPrimaryKeys.data, - relationships: resRelationships.data, - }) - return supaTable - } - return null -} - -async function fetchReadOnlyInfo( - service: IMetaService, - name: string, - schema?: string -): Promise { - const { data } = await service.fetchColumns(name, schema) - - if (data) { - const supaColumns: SupaColumn[] = data.map((x, index) => { - return { - name: x.name, - dataType: x.format, - format: x.format, - position: index, - isUpdatable: false, - } - }) - - return { - name: name, - schema: schema, - columns: supaColumns, - } - } - return null -} - export function parseSupaTable( data: { table: Dictionary diff --git a/studio/components/grid/components/grid/Grid.tsx b/studio/components/grid/components/grid/Grid.tsx index 98612a258d1..c88d3143e0d 100644 --- a/studio/components/grid/components/grid/Grid.tsx +++ b/studio/components/grid/components/grid/Grid.tsx @@ -43,10 +43,6 @@ export const Grid = memo( const dispatch = useDispatch() const state = useTrackedState() - // workaround to force state tracking on state.gridColumns - const columnHeaders = state.gridColumns.map((x) => `${x.key}_${x.frozen}`) - const { gridColumns, onError: onErrorFunc } = state - function onColumnResize(index: number, width: number) { updateColumnResizeDebounced(index, width, dispatch) } @@ -110,22 +106,6 @@ export const Grid = memo( } } - if (!columnHeaders || columnHeaders.length == 0) { - return ( -
-
-
- -
-
Loading...
-
-
- ) - } - return (
0 :: oid - AND bt.typlen = -1 THEN 'ARRAY' - WHEN nbt.nspname = 'pg_catalog' THEN format_type(t.typbasetype, NULL) - ELSE 'USER-DEFINED' - END - ELSE CASE - WHEN t.typelem <> 0 :: oid - AND t.typlen = -1 THEN 'ARRAY' - WHEN nt.nspname = 'pg_catalog' THEN format_type(a.atttypid, NULL) - ELSE 'USER-DEFINED' - END - END AS data_type, - COALESCE(bt.typname, t.typname) AS format, - CASE - WHEN a.attidentity IN ('a', 'd') THEN TRUE - ELSE FALSE - END AS is_identity, - CASE - a.attidentity - WHEN 'a' THEN 'ALWAYS' - WHEN 'd' THEN 'BY DEFAULT' - ELSE NULL - END AS identity_generation, - CASE - WHEN a.attnotnull - OR t.typtype = 'd' - AND t.typnotnull THEN FALSE - ELSE TRUE - END AS is_nullable, - CASE - WHEN ( - c.relkind IN ('r', 'p') - ) - OR ( - c.relkind IN ('v', 'f') - ) - AND pg_column_is_updatable(c.oid, a.attnum, FALSE) THEN TRUE - ELSE FALSE - END AS is_updatable, - array_to_json( - array( - SELECT - enumlabel - FROM - pg_catalog.pg_enum enums - WHERE - quote_ident(COALESCE(bt.typname, t.typname)) = format_type(enums.enumtypid, NULL) - ORDER BY - enums.enumsortorder - ) - ) AS enums, - col_description(c.oid, a.attnum) AS comment -FROM - pg_attribute a - LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid - AND a.attnum = ad.adnum - JOIN ( - pg_class c - JOIN pg_namespace nc ON c.relnamespace = nc.oid - ) ON a.attrelid = c.oid - JOIN ( - pg_type t - JOIN pg_namespace nt ON t.typnamespace = nt.oid - ) ON a.atttypid = t.oid - LEFT JOIN ( - pg_type bt - JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid - ) ON t.typtype = 'd' - AND t.typbasetype = bt.oid -WHERE - NOT pg_is_other_temp_schema(nc.oid) - AND nc.nspname = %L - AND c.relname = %L - AND a.attnum > 0 - AND NOT a.attisdropped - AND (c.relkind IN ('r', 'v', 'f', 'p')) - AND ( - pg_has_role(c.relowner, 'USAGE') - OR has_column_privilege( - c.oid, - a.attnum, - 'SELECT, INSERT, UPDATE, REFERENCES' - ) - ) -` - -export function getPrimaryKeysSql(filter_schema: string, filter_name: string) { - return format(primaryKeysSql, filter_schema, filter_name) -} -const primaryKeysSql = ` -SELECT - n.nspname AS schema, - c.relname AS table_name, - a.attname AS name, - c.oid :: int8 AS table_id -FROM - pg_index i, - pg_class c, - pg_attribute a, - pg_namespace n -WHERE - n.nspname = %L - AND c.relname = %L - AND i.indrelid = c.oid - AND c.relnamespace = n.oid - AND a.attrelid = c.oid - AND a.attnum = ANY (i.indkey) - AND i.indisprimary -` - -export function getRelationshipsSql(filter_schema: string, filter_name: string) { - return format(relationshipsSql, filter_schema, filter_name) -} -const relationshipsSql = ` -SELECT - c.oid :: int8 AS id, - c.conname AS constraint_name, - nsa.nspname AS source_schema, - csa.relname AS source_table_name, - sa.attname AS source_column_name, - nta.nspname AS target_table_schema, - cta.relname AS target_table_name, - ta.attname AS target_column_name -FROM - pg_constraint c - JOIN ( - pg_attribute sa - JOIN pg_class csa ON sa.attrelid = csa.oid - JOIN pg_namespace nsa ON csa.relnamespace = nsa.oid - ) ON sa.attrelid = c.conrelid - AND sa.attnum = ANY (c.conkey) - JOIN ( - pg_attribute ta - JOIN pg_class cta ON ta.attrelid = cta.oid - JOIN pg_namespace nta ON cta.relnamespace = nta.oid - ) ON ta.attrelid = c.confrelid - AND ta.attnum = ANY (c.confkey) -WHERE - c.contype = 'f' - AND nsa.nspname = %L - AND csa.relname = %L; -` - -export function getTableSql(filter_schema: string, filter_name: string) { - return format(tableSql, filter_schema, filter_name) -} -const tableSql = ` -SELECT - c.oid :: int8 AS id, - nc.nspname AS schema, - c.relname AS name, - pg_stat_get_live_tuples(c.oid) AS live_rows_estimate, - obj_description(c.oid) AS comment -FROM - pg_namespace nc - JOIN pg_class c ON nc.oid = c.relnamespace -WHERE - nc.nspname = %L - AND c.relname = %L - AND c.relkind IN ('r', 'p') - AND NOT pg_is_other_temp_schema(nc.oid) - AND ( - pg_has_role(c.relowner, 'USAGE') - OR has_table_privilege( - c.oid, - 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER' - ) - OR has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') - ) -` diff --git a/studio/components/grid/services/meta/SqlMetaService.ts b/studio/components/grid/services/meta/SqlMetaService.ts deleted file mode 100644 index 7e91ca00105..00000000000 --- a/studio/components/grid/services/meta/SqlMetaService.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { IMetaService } from '.' -import { - getColumnsSql, - getPrimaryKeysSql, - getRelationshipsSql, - getTableSql, -} from './SqlMetaService.sqls' - -export class SqlMetaService implements IMetaService { - constructor(protected onSqlQuery: (query: string) => Promise<{ data?: any; error?: any }>) {} - - async fetchInfo(name: string, schema?: string | undefined) { - const sql = getTableSql(schema ?? 'public', name) - const { data, error } = await this.onSqlQuery(sql) - if (error) { - return { error } - } else { - if (data?.length == 1) { - return { data: data[0] } - } else { - return { error: { message: 'fetch table info failed' } } - } - } - } - async fetchColumns(name: string, schema?: string | undefined) { - const sql = getColumnsSql(schema ?? 'public', name) - const { data, error } = await this.onSqlQuery(sql) - if (error) { - return { error } - } else { - return { data } - } - } - async fetchPrimaryKeys(name: string, schema?: string | undefined) { - const sql = getPrimaryKeysSql(schema ?? 'public', name) - const { data, error } = await this.onSqlQuery(sql) - if (error) { - return { error } - } else { - return { data } - } - } - async fetchRelationships(name: string, schema?: string | undefined) { - const sql = getRelationshipsSql(schema ?? 'public', name) - const { data, error } = await this.onSqlQuery(sql) - if (error) { - return { error } - } else { - return { data } - } - } -} diff --git a/studio/components/grid/services/meta/index.ts b/studio/components/grid/services/meta/index.ts deleted file mode 100644 index 5b328984c04..00000000000 --- a/studio/components/grid/services/meta/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Dictionary, ServiceError } from '../../types' - -export interface IMetaService { - fetchInfo: ( - name: string, - schema?: string - ) => Promise<{ data?: Dictionary; error?: ServiceError }> - fetchColumns: ( - name: string, - schema?: string - ) => Promise<{ data?: Dictionary[]; error?: ServiceError }> - fetchPrimaryKeys: ( - name: string, - schema?: string - ) => Promise<{ data?: Dictionary[]; error?: ServiceError }> - fetchRelationships: ( - name: string, - schema?: string - ) => Promise<{ data?: Dictionary[]; error?: ServiceError }> -} - -export * from './SqlMetaService' diff --git a/studio/components/grid/store/reducers/base.ts b/studio/components/grid/store/reducers/base.ts index 1307eb23007..69c2f35f7cd 100644 --- a/studio/components/grid/store/reducers/base.ts +++ b/studio/components/grid/store/reducers/base.ts @@ -2,11 +2,9 @@ import { CalculatedColumn } from '@supabase/react-data-grid' import { GridProps, SavedState, SupaTable } from '../../types' import { REFRESH_PAGE_IMMEDIATELY, TOTAL_ROWS_RESET } from '../../constants' import { IRowService, SqlRowService } from '../../services/row' -import { IMetaService, SqlMetaService } from '../../services/meta' export interface BaseInitialState { table: SupaTable | null - metaService: IMetaService | null rowService: IRowService | null refreshPageFlag: number isInitialComplete: boolean @@ -16,7 +14,6 @@ export interface BaseInitialState { export const baseInitialState: BaseInitialState = { table: null, - metaService: null, rowService: null, refreshPageFlag: 0, isInitialComplete: false, @@ -25,12 +22,6 @@ export const baseInitialState: BaseInitialState = { } export type INIT_ACTIONTYPE = - | { - type: 'INIT_CLIENT' - payload: { - onSqlQuery: (query: string) => Promise<{ data?: any; error?: any }> - } - } | { type: 'INIT_TABLE' payload: { @@ -56,12 +47,6 @@ type BASE_ACTIONTYPE = INIT_ACTIONTYPE const BaseReducer = (state: BaseInitialState, action: BASE_ACTIONTYPE) => { switch (action.type) { - case 'INIT_CLIENT': { - return { - ...state, - metaService: new SqlMetaService(action.payload.onSqlQuery), - } - } case 'INIT_TABLE': { return { ...state, diff --git a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx index 3d3c15330ea..5af32a7110e 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -8,6 +8,7 @@ import { checkPermissions, useStore } from 'hooks' import { useParams } from 'common/hooks' import { Entity } from 'data/entity-types/entity-type-query' import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' +import { useIsTableLoaded, useTableEditorStateSnapshot } from 'state/table-editor' import ProjectLayout from '../ProjectLayout/ProjectLayout' import TableEditorMenu from './TableEditorMenu' import NoPermission from 'components/ui/NoPermission' @@ -36,9 +37,10 @@ const TableEditorLayout = ({ }: PropsWithChildren) => { const { vault, meta, ui } = useStore() const router = useRouter() - const { id: _id } = useParams() + const { ref, id: _id } = useParams() const id = _id ? Number(_id) : undefined + const snap = useTableEditorStateSnapshot() const canReadTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables') const vaultExtension = meta.extensions.byId('supabase_vault') @@ -54,15 +56,10 @@ const TableEditorLayout = ({ } }, [ui.selectedProject?.ref]) - const [loadedIds, setLoadedIds] = useState>(() => new Set()) - const isLoaded = id !== undefined && loadedIds.has(id) + const isLoaded = useIsTableLoaded(ref, id) const entity = useEntityType(id, function onNotFound(id) { - setLoadedIds((loadedIds) => { - const newLoadedIds = new Set(loadedIds) - newLoadedIds.add(id) - return newLoadedIds - }) + if (ref) snap.addLoadedId(ref, id) }) const prefetch = useLatest(useTableRowsPrefetchWrapper()) @@ -91,32 +88,23 @@ const TableEditorLayout = ({ loadTable() ?.then(async (entity: any) => { await prefetch.current(entity) - return entity }) .then((entity: any) => { - if (mounted) { - setLoadedIds((loadedIds) => { - const newLoadedIds = new Set(loadedIds) - newLoadedIds.add(entity.id ?? entity?.id) - return newLoadedIds - }) + if (mounted && ref) { + snap.addLoadedId(ref, entity.id) } }) .catch(() => { - if (mounted && entity?.id) { - setLoadedIds((loadedIds) => { - const newLoadedIds = new Set(loadedIds) - newLoadedIds.add(entity.id) - return newLoadedIds - }) + if (mounted && entity?.id && ref) { + snap.addLoadedId(ref, entity.id) } }) return () => { mounted = false } - }, [entity]) + }, [entity?.id]) useEffect(() => { if (isVaultEnabled) { diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index 37d061e0f6d..c800be4a192 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -26,7 +26,7 @@ import Script from 'next/script' import { AppPropsWithLayout } from 'types' import { ThemeProvider } from 'common' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Hydrate, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { RootStore } from 'stores' @@ -108,15 +108,18 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { const getLayout = Component.getLayout ?? ((page) => page) - const AuthContainer = (props: any) => { - return IS_PLATFORM ? ( - + const AuthContainer = useMemo( + () => (props: any) => { + return IS_PLATFORM ? ( + + {props.children} + + ) : ( {props.children} - - ) : ( - {props.children} - ) - } + ) + }, + [supabase] + ) return ( diff --git a/studio/state/table-editor.ts b/studio/state/table-editor.ts new file mode 100644 index 00000000000..99fe3309946 --- /dev/null +++ b/studio/state/table-editor.ts @@ -0,0 +1,28 @@ +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) +}