Merge pull request #13667 from supabase/chore/fix-table-editor-filter-removing

fix: table editor filter removing
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-04-13 14:28:08 +07:00
commit 0a02024772
10 files changed
+97 -461

No files matched your search

+9 -21
View File
@@ -152,21 +152,13 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((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<SupabaseGridRef, SupabaseGridProps>((props
filter as string[]
)
console.log('savedState:', savedState)
if (savedState.sorts || savedState.filters) {
setParams((prevParams) => {
return {
@@ -187,17 +180,12 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((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 (
<div className="sb-grid">
+37 -110
View File
@@ -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<SupaTable | null> {
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<SupaTable | null> {
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<any>
@@ -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 (
<div
className="sb-grid-grid--loading"
style={{ width: width || '100%', height: height || '50vh' }}
>
<div className="sb-grid-grid--loading__inner flex items-center gap-2">
<div className="animate-spin text-scale-900">
<IconLoader />
</div>
<div className="text-sm text-scale-1100">Loading...</div>
</div>
</div>
)
}
return (
<div
className={containerClass}
@@ -133,7 +113,7 @@ export const Grid = memo(
>
<DataGrid
ref={ref}
columns={gridColumns}
columns={state.gridColumns}
rows={rows ?? []}
rowRenderer={RowRenderer}
rowKeyGetter={rowKeyGetter}
@@ -1,189 +0,0 @@
import { format } from '@scaleleap/pg-format'
export function getColumnsSql(filter_schema: string, filter_name: string) {
return format(columnsSql, filter_schema, filter_name)
}
const columnsSql = `
SELECT
c.oid :: int8 AS table_id,
nc.nspname AS schema,
c.relname AS table,
(c.oid || '.' || a.attnum) AS id,
a.attnum AS ordinal_position,
a.attname AS name,
CASE
WHEN a.atthasdef THEN pg_get_expr(ad.adbin, ad.adrelid)
ELSE NULL
END AS default_value,
CASE
WHEN t.typtype = 'd' THEN CASE
WHEN bt.typelem <> 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')
)
`
@@ -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 }
}
}
}
@@ -1,22 +0,0 @@
import { Dictionary, ServiceError } from '../../types'
export interface IMetaService {
fetchInfo: (
name: string,
schema?: string
) => Promise<{ data?: Dictionary<any>; error?: ServiceError }>
fetchColumns: (
name: string,
schema?: string
) => Promise<{ data?: Dictionary<any>[]; error?: ServiceError }>
fetchPrimaryKeys: (
name: string,
schema?: string
) => Promise<{ data?: Dictionary<any>[]; error?: ServiceError }>
fetchRelationships: (
name: string,
schema?: string
) => Promise<{ data?: Dictionary<any>[]; error?: ServiceError }>
}
export * from './SqlMetaService'
@@ -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,
@@ -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<TableEditorLayoutProps>) => {
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<Set<number>>(() => 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) {
+12 -9
View File
@@ -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 ? (
<SessionContextProvider supabaseClient={supabase as any}>
const AuthContainer = useMemo(
() => (props: any) => {
return IS_PLATFORM ? (
<SessionContextProvider supabaseClient={supabase as any}>
<AuthProvider>{props.children}</AuthProvider>
</SessionContextProvider>
) : (
<AuthProvider>{props.children}</AuthProvider>
</SessionContextProvider>
) : (
<AuthProvider>{props.children}</AuthProvider>
)
}
)
},
[supabase]
)
return (
<QueryClientProvider client={queryClient}>
+28
View File
@@ -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<number>([]),
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<typeof useSnapshot>[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)
}