mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
chore: aggressive prefetching (#29987)
* chore: aggressive prefetching * use abort signals in prefetchers * move encrypted columns to react-query * prefetching for filter and sort applied * prefetch remaining entity types * prefetch tables in more places * prefetch editor page on project panel * add feature flag * fix typescript * nit * Nit * fix imports * remove views check on encrypted schemas * use fetchQuery instead of prefetchQuery * fix useEncryptedColumnsQuery ts error * filter by schema on encrypted columns * don't use pg_get_tabledef for foreign tables * Remove unnecesary import --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
837b424c72
commit
5781937739
33 files changed
+717
-238
No files matched your search
@@ -28,8 +28,12 @@ import { InitialStateType } from './store/reducers'
|
||||
import type { SupabaseGridProps } from './types'
|
||||
import { getGridColumns } from './utils/gridColumns'
|
||||
|
||||
function onLoadStorage(storageRef: string, tableName: string, schema?: string | null) {
|
||||
const storageKey = getStorageKey(STORAGE_KEY_PREFIX, storageRef)
|
||||
export function loadTableEditorSortsAndFiltersFromLocalStorage(
|
||||
projectRef: string,
|
||||
tableName: string,
|
||||
schema?: string | null
|
||||
) {
|
||||
const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef)
|
||||
const jsonStr = localStorage.getItem(storageKey)
|
||||
if (!jsonStr) return
|
||||
const json = JSON.parse(jsonStr)
|
||||
@@ -45,7 +49,11 @@ async function initTable(
|
||||
filter?: string[] // Comes directly from URL param
|
||||
): Promise<{ savedState: { sorts?: string[]; filters?: string[] } }> {
|
||||
const savedState = props.projectRef
|
||||
? onLoadStorage(props.projectRef, props.table.name, props.table.schema)
|
||||
? loadTableEditorSortsAndFiltersFromLocalStorage(
|
||||
props.projectRef,
|
||||
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
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import type { PostgresRelationship, PostgresTable } from '@supabase/postgres-meta'
|
||||
import AwesomeDebouncePromise from 'awesome-debounce-promise'
|
||||
import type { Filter } from 'components/grid/types'
|
||||
import { compact } from 'lodash'
|
||||
import type { Dictionary } from 'types'
|
||||
|
||||
import type { Filter } from 'components/grid/types'
|
||||
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
|
||||
import {
|
||||
ForeignKeyConstraint,
|
||||
ForeignKeyConstraintsData,
|
||||
} from 'data/database/foreign-key-constraints-query'
|
||||
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
|
||||
import { TableLike } from 'hooks/misc/useTable'
|
||||
import type { Dictionary, SchemaView } from 'types'
|
||||
import { FilterOperatorOptions } from './components/header/filter/Filter.constants'
|
||||
import { STORAGE_KEY_PREFIX } from './constants'
|
||||
import { InitialStateType } from './store/reducers'
|
||||
import type { Sort, SupabaseGridProps, SupaColumn, SupaTable } from './types'
|
||||
|
||||
/**
|
||||
* Ensure that if editable is false, we should remove all editing actions
|
||||
* to prevent rare-case bugs with the UI
|
||||
@@ -124,6 +132,54 @@ export function parseSupaTable(
|
||||
}
|
||||
}
|
||||
|
||||
export function getSupaTable({
|
||||
selectedTable,
|
||||
entityType,
|
||||
foreignKeyMeta,
|
||||
encryptedColumns,
|
||||
}: {
|
||||
selectedTable: TableLike
|
||||
entityType?: ENTITY_TYPE
|
||||
foreignKeyMeta: ForeignKeyConstraintsData
|
||||
encryptedColumns: string[]
|
||||
}) {
|
||||
// [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 = (
|
||||
('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 isViewSelected =
|
||||
entityType === ENTITY_TYPE.VIEW || entityType === ENTITY_TYPE.MATERIALIZED_VIEW
|
||||
const isForeignTableSelected = entityType === ENTITY_TYPE.FOREIGN_TABLE
|
||||
|
||||
return !isViewSelected && !isForeignTableSelected
|
||||
? parseSupaTable(
|
||||
{
|
||||
table: selectedTable as PostgresTable,
|
||||
columns: (selectedTable as PostgresTable).columns ?? [],
|
||||
primaryKeys: (selectedTable as PostgresTable).primary_keys ?? [],
|
||||
relationships: formattedRelationships,
|
||||
},
|
||||
encryptedColumns
|
||||
)
|
||||
: parseSupaTable({
|
||||
table: selectedTable as SchemaView,
|
||||
columns: (selectedTable as SchemaView).columns ?? [],
|
||||
primaryKeys: [],
|
||||
relationships: [],
|
||||
})
|
||||
}
|
||||
|
||||
export const saveStorageDebounced = AwesomeDebouncePromise(saveStorage, 500)
|
||||
|
||||
function saveStorage(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import type { RenderCellProps } from 'react-data-grid'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
|
||||
import { useTableQuery } from 'data/tables/table-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
@@ -62,11 +62,20 @@ export const ForeignKeyFormatter = (props: Props) => {
|
||||
className="translate-y-[2px]"
|
||||
style={{ padding: '3px' }}
|
||||
>
|
||||
<Link
|
||||
<EditorTablePageLink
|
||||
href={`/project/${projectRef}/editor/${targetTable?.id}?schema=${selectedSchema}&filter=${relationship?.target_column_name}%3Aeq%3A${value}`}
|
||||
projectRef={projectRef}
|
||||
id={targetTable && String(targetTable?.id)}
|
||||
filters={[
|
||||
{
|
||||
column: relationship.target_column_name,
|
||||
operator: '=',
|
||||
value: String(value),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ArrowRight size={14} />
|
||||
</Link>
|
||||
</EditorTablePageLink>
|
||||
</Button>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="bottom">View referencing record</TooltipContent_Shadcn_>
|
||||
|
||||
+14
-10
@@ -1,12 +1,13 @@
|
||||
import type { PostgresTable } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { noop } from 'lodash'
|
||||
import { Lock, Unlock } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useQueryState } from 'nuqs'
|
||||
|
||||
import type { PostgresTable } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import {
|
||||
AiIconAnimation,
|
||||
Badge,
|
||||
@@ -29,8 +30,7 @@ const PolicyTableRowHeader = ({
|
||||
onSelectToggleRLS = noop,
|
||||
onSelectCreatePolicy,
|
||||
}: PolicyTableRowHeaderProps) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { ref } = useParams()
|
||||
|
||||
const canToggleRLS = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
|
||||
|
||||
@@ -42,7 +42,11 @@ const PolicyTableRowHeader = ({
|
||||
return (
|
||||
<div id={table.id.toString()} className="flex w-full items-center justify-between">
|
||||
<div className="flex gap-x-4 text-left">
|
||||
<Link href={`/project/${ref}/editor/${table.id}`} className="flex items-center gap-x-2">
|
||||
<EditorTablePageLink
|
||||
projectRef={ref}
|
||||
id={String(table.id)}
|
||||
className="flex items-center gap-x-2"
|
||||
>
|
||||
{table.rls_enabled ? (
|
||||
<div className="flex items-center gap-x-1 text-xs">
|
||||
<Lock size={14} strokeWidth={2} className="text-brand" />
|
||||
@@ -53,7 +57,7 @@ const PolicyTableRowHeader = ({
|
||||
</div>
|
||||
)}
|
||||
<h4 className="m-0">{table.name}</h4>
|
||||
</Link>
|
||||
</EditorTablePageLink>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{isTableLocked && (
|
||||
<Badge>
|
||||
|
||||
@@ -30,12 +30,12 @@ import { useDatabasePublicationsQuery } from 'data/database-publications/databas
|
||||
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
|
||||
import { useForeignTablesQuery } from 'data/foreign-tables/foreign-tables-query'
|
||||
import { useMaterializedViewsQuery } from 'data/materialized-views/materialized-views-query'
|
||||
import { usePrefetchEditorTablePage } from 'data/prefetchers/project.$ref.editor.$id'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useViewsQuery } from 'data/views/views-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import {
|
||||
Button,
|
||||
Checkbox_Shadcn_,
|
||||
@@ -74,6 +74,8 @@ const TableList = ({
|
||||
const { ref } = useParams()
|
||||
const { project } = useProjectContext()
|
||||
|
||||
const prefetchEditorTablePage = usePrefetchEditorTablePage()
|
||||
|
||||
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
|
||||
|
||||
const [filterString, setFilterString] = useState<string>('')
|
||||
@@ -476,6 +478,9 @@ const TableList = ({
|
||||
onClick={() =>
|
||||
router.push(`/project/${project?.ref}/editor/${x.id}`)
|
||||
}
|
||||
onMouseEnter={() =>
|
||||
prefetchEditorTablePage({ id: x.id ? String(x.id) : undefined })
|
||||
}
|
||||
>
|
||||
<Eye size={12} />
|
||||
<p>View in Table Editor</p>
|
||||
|
||||
@@ -7,6 +7,7 @@ import Link from 'next/link'
|
||||
import { useParams } from 'common'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import type { FDW } from 'data/fdw/fdws-query'
|
||||
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { Collapsible } from 'ui'
|
||||
import type { WrapperMeta } from './Wrappers.types'
|
||||
@@ -119,11 +120,15 @@ const WrapperRow = ({
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{wrapper.tables ? (
|
||||
wrapper.tables.map((table: any) => (
|
||||
<Link key={table.id} href={`/project/${ref}/editor/${table.id}`}>
|
||||
<EditorTablePageLink
|
||||
key={table.id}
|
||||
projectRef={ref}
|
||||
id={String(table.id)}
|
||||
>
|
||||
<div className="text-sm border rounded px-2 py-1 transition bg-surface-200 hover:bg-overlay-hover">
|
||||
{table.name}
|
||||
</div>
|
||||
</Link>
|
||||
</EditorTablePageLink>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-foreground-light">No tables available</p>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { Auth, EdgeFunctions, Realtime, SqlEditor, Storage, TableEditor } from 'icons'
|
||||
import { Button } from 'ui'
|
||||
@@ -10,8 +11,7 @@ import APIKeys from './APIKeys'
|
||||
import GetStartedHero from './GetStartedHero'
|
||||
|
||||
const NewProjectPanel = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { ref } = useParams()
|
||||
|
||||
const {
|
||||
projectAuthAll: authEnabled,
|
||||
@@ -47,7 +47,7 @@ const NewProjectPanel = () => {
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button asChild type="default" icon={<TableEditor strokeWidth={1.5} />}>
|
||||
<Link href={`/project/${ref}/editor`}>Table Editor</Link>
|
||||
<EditorIndexPageLink projectRef={ref}>Table Editor</EditorIndexPageLink>
|
||||
</Button>
|
||||
<Button asChild type="default" icon={<SqlEditor strokeWidth={1.5} />}>
|
||||
<Link href={`/project/${ref}/sql/new`}>SQL editor</Link>
|
||||
|
||||
+7
-2
@@ -13,13 +13,13 @@ import RefreshButton from 'components/grid/components/header/RefreshButton'
|
||||
import FilterPopover from 'components/grid/components/header/filter/FilterPopover'
|
||||
import { SortPopover } from 'components/grid/components/header/sort'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useEncryptedColumnsQuery } from 'data/encrypted-columns/encrypted-columns-query'
|
||||
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
|
||||
import { useTableQuery } from 'data/tables/table-query'
|
||||
import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state'
|
||||
import { SidePanel } from 'ui'
|
||||
import ActionBar from '../../ActionBar'
|
||||
import { ForeignKey } from '../../ForeignKeySelector/ForeignKeySelector.types'
|
||||
import { useEncryptedColumns } from '../../SidePanelEditor.utils'
|
||||
import Pagination from './Pagination'
|
||||
import SelectorGrid from './SelectorGrid'
|
||||
|
||||
@@ -47,7 +47,12 @@ const ForeignRowSelector = ({
|
||||
id: tableId,
|
||||
})
|
||||
|
||||
const encryptedColumns = useEncryptedColumns({ schemaName, tableName })
|
||||
const { data: encryptedColumns } = useEncryptedColumnsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: schemaName,
|
||||
tableName,
|
||||
})
|
||||
|
||||
const supaTable =
|
||||
table &&
|
||||
|
||||
-64
@@ -1,11 +1,9 @@
|
||||
import type { PostgresPrimaryKey, PostgresTable } from '@supabase/postgres-meta'
|
||||
import { chunk, find, isEmpty, isEqual } from 'lodash'
|
||||
import Papa from 'papaparse'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Query } from 'components/grid/query/Query'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import SparkBar from 'components/ui/SparkBar'
|
||||
import { createDatabaseColumn } from 'data/database-columns/database-column-create-mutation'
|
||||
import { deleteDatabaseColumn } from 'data/database-columns/database-column-delete-mutation'
|
||||
@@ -23,7 +21,6 @@ import { deleteTable as deleteTableMutation } from 'data/tables/table-delete-mut
|
||||
import { getTable } from 'data/tables/table-query'
|
||||
import { updateTable as updateTableMutation } from 'data/tables/table-update-mutation'
|
||||
import { getTables } from 'data/tables/tables-query'
|
||||
import { getViews } from 'data/views/views-query'
|
||||
import { timeout, tryParseJson } from 'lib/helpers'
|
||||
import {
|
||||
generateCreateColumnPayload,
|
||||
@@ -37,67 +34,6 @@ import type { ImportContent } from './TableEditor/TableEditor.types'
|
||||
const BATCH_SIZE = 1000
|
||||
const CHUNK_SIZE = 1024 * 1024 * 0.1 // 0.1MB
|
||||
|
||||
export interface UseEncryptedColumnsArgs {
|
||||
schemaName?: string
|
||||
tableName?: string
|
||||
}
|
||||
const listEncryptedColumns = async (
|
||||
projectRef: string,
|
||||
connectionString: string | undefined = undefined,
|
||||
schema: string,
|
||||
table: string
|
||||
) => {
|
||||
if (!table) return []
|
||||
|
||||
const views = await getViews({ projectRef, connectionString, schema })
|
||||
const decryptedView = views.find((view) => view.name === `decrypted_${table}`)
|
||||
if (!decryptedView) return []
|
||||
|
||||
try {
|
||||
const encryptedColumns = await executeSql({
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: `SELECT column_name as name FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'decrypted_${table}' and column_name like 'decrypted_%'`,
|
||||
})
|
||||
return encryptedColumns.result.map((column: any) => column.name.split('decrypted_')[1])
|
||||
} catch (error) {
|
||||
console.error('Error fetching encrypted columns', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function useEncryptedColumns({ schemaName, tableName }: UseEncryptedColumnsArgs) {
|
||||
const { project } = useProjectContext()
|
||||
const [encryptedColumns, setEncryptedColumns] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
|
||||
const getEncryptedColumns = async () => {
|
||||
if (schemaName !== undefined && tableName !== undefined) {
|
||||
const columns = await listEncryptedColumns(
|
||||
project?.ref!,
|
||||
project?.connectionString,
|
||||
schemaName,
|
||||
tableName
|
||||
)
|
||||
|
||||
if (isMounted) {
|
||||
setEncryptedColumns(columns)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getEncryptedColumns()
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [schemaName, tableName])
|
||||
|
||||
return encryptedColumns
|
||||
}
|
||||
|
||||
/**
|
||||
* The functions below are basically just queries but may be supported directly
|
||||
* from the pg-meta library in the future
|
||||
|
||||
+6
-5
@@ -1,10 +1,10 @@
|
||||
import clsx from 'clsx'
|
||||
import { useParams } from 'common'
|
||||
import Link from 'next/link'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { Badge, Button, cn } from 'ui'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
|
||||
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import type { ForeignKey } from '../../ForeignKeySelector/ForeignKeySelector.types'
|
||||
|
||||
@@ -77,12 +77,13 @@ export const ForeignKeyRow = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Link
|
||||
href={`/project/${ref}/editor/${foreignKey.tableId}`}
|
||||
<EditorTablePageLink
|
||||
projectRef={ref}
|
||||
id={String(foreignKey.tableId)}
|
||||
onClick={() => closePanel()}
|
||||
>
|
||||
{foreignKey.schema}.{foreignKey.table}
|
||||
</Link>
|
||||
</EditorTablePageLink>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PostgresColumn, PostgresRelationship, PostgresTable } from '@supabase/postgres-meta'
|
||||
import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { QueryKey, useQueryClient } from '@tanstack/react-query'
|
||||
import { find, isUndefined } from 'lodash'
|
||||
@@ -8,14 +8,11 @@ import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { SupabaseGrid } from 'components/grid/SupabaseGrid'
|
||||
import { parseSupaTable } from 'components/grid/SupabaseGrid.utils'
|
||||
import { getSupaTable } from 'components/grid/SupabaseGrid.utils'
|
||||
import { SupaTable } from 'components/grid/types'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
|
||||
import {
|
||||
ForeignKeyConstraint,
|
||||
useForeignKeyConstraintsQuery,
|
||||
} from 'data/database/foreign-key-constraints-query'
|
||||
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
|
||||
import { useEncryptedColumnsQuery } from 'data/encrypted-columns/encrypted-columns-query'
|
||||
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
|
||||
import { sqlKeys } from 'data/sql/keys'
|
||||
import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation'
|
||||
@@ -28,13 +25,12 @@ import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { EMPTY_ARR } from 'lib/void'
|
||||
import { useGetImpersonatedRole } from 'state/role-impersonation-state'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import type { Dictionary, SchemaView } from 'types'
|
||||
import type { Dictionary } from 'types'
|
||||
import { Button } from 'ui'
|
||||
import GridHeaderActions from './GridHeaderActions'
|
||||
import { TableGridSkeletonLoader } from './LoadingState'
|
||||
import NotFoundState from './NotFoundState'
|
||||
import SidePanelEditor from './SidePanelEditor/SidePanelEditor'
|
||||
import { useEncryptedColumns } from './SidePanelEditor/SidePanelEditor.utils'
|
||||
import TableDefinition from './TableDefinition'
|
||||
|
||||
export interface TableGridEditorProps {
|
||||
@@ -62,8 +58,10 @@ const TableGridEditor = ({
|
||||
const canEditColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
|
||||
const isReadOnly = !canEditTables && !canEditColumns
|
||||
|
||||
const encryptedColumns = useEncryptedColumns({
|
||||
schemaName: selectedTable?.schema,
|
||||
const { data: encryptedColumns } = useEncryptedColumnsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: selectedTable?.schema,
|
||||
tableName: selectedTable?.name,
|
||||
})
|
||||
|
||||
@@ -148,43 +146,16 @@ const TableGridEditor = ({
|
||||
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 = 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 = (
|
||||
('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 = getSupaTable({
|
||||
encryptedColumns,
|
||||
foreignKeyMeta,
|
||||
selectedTable,
|
||||
entityType: entityType?.type,
|
||||
})
|
||||
|
||||
const gridTable =
|
||||
!isViewSelected && !isForeignTableSelected
|
||||
? parseSupaTable(
|
||||
{
|
||||
table: selectedTable as PostgresTable,
|
||||
columns: (selectedTable as PostgresTable).columns ?? [],
|
||||
primaryKeys: (selectedTable as PostgresTable).primary_keys ?? [],
|
||||
relationships: formattedRelationships,
|
||||
},
|
||||
encryptedColumns
|
||||
)
|
||||
: parseSupaTable({
|
||||
table: selectedTable as SchemaView,
|
||||
columns: (selectedTable as SchemaView).columns ?? [],
|
||||
primaryKeys: [],
|
||||
relationships: [],
|
||||
})
|
||||
|
||||
const gridKey = `${selectedTable.schema}_${selectedTable.name}`
|
||||
|
||||
const onTableCreated = (table: PostgresTable) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Blocks, FileText, Lightbulb, List, Settings } from 'lucide-react'
|
||||
|
||||
import type { Route } from 'components/ui/ui.types'
|
||||
import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor'
|
||||
import type { Project } from 'data/projects/project-detail-query'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import {
|
||||
Auth,
|
||||
Database,
|
||||
@@ -11,8 +13,8 @@ import {
|
||||
Storage,
|
||||
TableEditor,
|
||||
} from 'icons'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { ICON_SIZE, ICON_STROKE_WIDTH } from './NavigationBar'
|
||||
import { Settings, FileText, List, Lightbulb, Blocks } from 'lucide-react'
|
||||
|
||||
export const generateToolRoutes = (ref?: string, project?: Project): Route[] => {
|
||||
const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
|
||||
@@ -24,6 +26,7 @@ export const generateToolRoutes = (ref?: string, project?: Project): Route[] =>
|
||||
label: 'Table Editor',
|
||||
icon: <TableEditor size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
||||
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/editor`),
|
||||
linkComponent: <EditorIndexPageLink projectRef={ref} />,
|
||||
},
|
||||
{
|
||||
key: 'sql',
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { noop } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { AnchorHTMLAttributes, forwardRef } from 'react'
|
||||
import { cn } from 'ui'
|
||||
import { Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui'
|
||||
import {
|
||||
AnchorHTMLAttributes,
|
||||
cloneElement,
|
||||
ComponentPropsWithoutRef,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
} from 'react'
|
||||
import { cn, Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui'
|
||||
|
||||
import type { Route } from 'components/ui/ui.types'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
|
||||
interface NavigationIconButtonProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
route: Route
|
||||
@@ -41,8 +46,18 @@ const NavigationIconLink = forwardRef<HTMLAnchorElement, NavigationIconButtonPro
|
||||
`${isActive && '!bg-selection shadow-sm'}`,
|
||||
]
|
||||
|
||||
const LinkComponent = forwardRef<HTMLAnchorElement, ComponentPropsWithoutRef<typeof Link>>(
|
||||
function LinkComponent(props, ref) {
|
||||
if (route.linkComponent && isValidElement(route.linkComponent)) {
|
||||
return cloneElement<any>(route.linkComponent, { ...props, ref })
|
||||
}
|
||||
|
||||
return <Link ref={ref} {...props} />
|
||||
}
|
||||
)
|
||||
|
||||
const linkContent = (
|
||||
<Link
|
||||
<LinkComponent
|
||||
role="button"
|
||||
aria-current={isActive}
|
||||
ref={ref}
|
||||
@@ -73,7 +88,7 @@ const NavigationIconLink = forwardRef<HTMLAnchorElement, NavigationIconButtonPro
|
||||
>
|
||||
{route.label}
|
||||
</span>
|
||||
</Link>
|
||||
</LinkComponent>
|
||||
)
|
||||
|
||||
if (!allowNavPanelToExpand) {
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
} from 'ui'
|
||||
import { useProjectContext } from '../ProjectLayout/ProjectContext'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
|
||||
|
||||
export interface EntityListItemProps {
|
||||
id: number
|
||||
@@ -277,8 +278,9 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
<EditorTablePageLink
|
||||
title={entity.name}
|
||||
id={String(entity.id)}
|
||||
href={`/project/${projectRef}/editor/${entity.id}?schema=${selectedSchema}`}
|
||||
role="button"
|
||||
aria-label={`View ${entity.name}`}
|
||||
@@ -447,7 +449,7 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</Link>
|
||||
</EditorTablePageLink>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ import SchemaSelector from 'components/ui/SchemaSelector'
|
||||
import { useSchemasQuery } from 'data/database/schemas-query'
|
||||
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
|
||||
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
|
||||
import { useTableQuery } from 'data/tables/table-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import useTable from 'hooks/misc/useTable'
|
||||
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import {
|
||||
@@ -96,15 +96,7 @@ const TableEditorMenu = () => {
|
||||
)
|
||||
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
|
||||
|
||||
const { data: selectedTable } = useTableQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
id: Number(id),
|
||||
},
|
||||
// only run if we have a selected table
|
||||
{ enabled: Boolean(id) }
|
||||
)
|
||||
const { data: selectedTable } = useTable(id !== undefined ? Number(id) : undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTable) {
|
||||
|
||||
@@ -6,4 +6,5 @@ export interface Route {
|
||||
icon: ReactNode
|
||||
link?: string
|
||||
disabled?: boolean
|
||||
linkComponent?: ReactNode
|
||||
}
|
||||
@@ -32,13 +32,7 @@ with records as (
|
||||
'create materialized view ', concat(nc.nspname, '.', c.relname), ' as',
|
||||
pg_get_viewdef(concat(nc.nspname, '.', c.relname), true)
|
||||
)
|
||||
when 'f' then pg_temp.pg_get_tabledef(
|
||||
concat(nc.nspname),
|
||||
concat(c.relname),
|
||||
false,
|
||||
'FKEYS_INTERNAL',
|
||||
'NO_TRIGGERS'
|
||||
)
|
||||
when 'f' then concat('create foreign table ', nc.nspname, '.', c.relname, ' ( ... )')
|
||||
when 'p' then pg_temp.pg_get_tabledef(
|
||||
concat(nc.nspname),
|
||||
concat(c.relname),
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { UseQueryOptions } from '@tanstack/react-query'
|
||||
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
|
||||
import { QueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { sqlKeys } from 'data/sql/keys'
|
||||
import {
|
||||
executeSql,
|
||||
ExecuteSqlData,
|
||||
ExecuteSqlError,
|
||||
useExecuteSqlQuery,
|
||||
} from '../sql/execute-sql-query'
|
||||
|
||||
type GetForeignKeyConstraintsVariables = {
|
||||
schema?: string
|
||||
@@ -96,6 +103,22 @@ export type ForeignKeyConstraintsVariables = GetForeignKeyConstraintsVariables &
|
||||
export type ForeignKeyConstraintsData = ForeignKeyConstraint[]
|
||||
export type ForeignKeyConstraintsError = ExecuteSqlError
|
||||
|
||||
function select(
|
||||
data:
|
||||
| {
|
||||
result: any
|
||||
}
|
||||
| undefined
|
||||
) {
|
||||
return (data?.result ?? []).map((foreignKey: ForeignKeyConstraintRaw) => {
|
||||
return {
|
||||
...foreignKey,
|
||||
source_columns: foreignKey.source_columns.replace('{', '').replace('}', '').split(','),
|
||||
target_columns: foreignKey.target_columns.replace('{', '').replace('}', '').split(','),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useForeignKeyConstraintsQuery = <
|
||||
TData extends ForeignKeyConstraintsData = ForeignKeyConstraintsData,
|
||||
>(
|
||||
@@ -107,19 +130,30 @@ export const useForeignKeyConstraintsQuery = <
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: getForeignKeyConstraintsQuery({ schema }),
|
||||
queryKey: ['foreign-key-constraints'],
|
||||
queryKey: ['foreign-key-constraints', schema],
|
||||
},
|
||||
{
|
||||
select(data) {
|
||||
return ((data as any)?.result ?? []).map((foreignKey: ForeignKeyConstraintRaw) => {
|
||||
return {
|
||||
...foreignKey,
|
||||
source_columns: foreignKey.source_columns.replace('{', '').replace('}', '').split(','),
|
||||
target_columns: foreignKey.target_columns.replace('{', '').replace('}', '').split(','),
|
||||
}
|
||||
})
|
||||
},
|
||||
select,
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function prefetchForeignKeyConstraints(
|
||||
client: QueryClient,
|
||||
{ projectRef, connectionString, schema }: ForeignKeyConstraintsVariables
|
||||
) {
|
||||
return client
|
||||
.fetchQuery(sqlKeys.query(projectRef, ['foreign-key-constraints', schema]), ({ signal }) =>
|
||||
executeSql(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: getForeignKeyConstraintsQuery({ schema }),
|
||||
queryKey: ['foreign-key-constraints', schema],
|
||||
},
|
||||
signal
|
||||
)
|
||||
)
|
||||
.then((data) => select(data) as ForeignKeyConstraintsData)
|
||||
}
|
||||
@@ -2,7 +2,12 @@ import pgMeta from '@supabase/pg-meta'
|
||||
import { QueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from 'data/sql/execute-sql-query'
|
||||
import {
|
||||
executeSql,
|
||||
ExecuteSqlData,
|
||||
ExecuteSqlError,
|
||||
useExecuteSqlQuery,
|
||||
} from 'data/sql/execute-sql-query'
|
||||
import { sqlKeys } from 'data/sql/keys'
|
||||
|
||||
export type SchemasVariables = {
|
||||
@@ -39,3 +44,20 @@ export const useSchemasQuery = <TData = SchemasData>(
|
||||
export function invalidateSchemasQuery(client: QueryClient, projectRef: string | undefined) {
|
||||
return client.invalidateQueries(sqlKeys.query(projectRef, ['schemas', 'list']))
|
||||
}
|
||||
|
||||
export function prefetchSchemas(
|
||||
client: QueryClient,
|
||||
{ projectRef, connectionString }: SchemasVariables
|
||||
) {
|
||||
return client.fetchQuery(sqlKeys.query(projectRef, ['schemas', 'list']), ({ signal }) =>
|
||||
executeSql(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: pgMetaSchemasList.sql,
|
||||
queryKey: ['schemas', 'list'],
|
||||
},
|
||||
signal
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import type { ResponseError } from 'types'
|
||||
import { encryptedColumnKeys } from './keys'
|
||||
|
||||
export type EncryptedColumnsVariables = {
|
||||
projectRef?: string
|
||||
connectionString?: string
|
||||
schema?: string
|
||||
tableName?: string
|
||||
}
|
||||
|
||||
export async function getEncryptedColumns(
|
||||
{ projectRef, connectionString, schema, tableName }: EncryptedColumnsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!tableName) return []
|
||||
|
||||
try {
|
||||
const encryptedColumns = await executeSql(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: `select column_name as name from information_schema.columns where table_schema = '${schema}' and table_name = 'decrypted_${tableName}' and column_name like 'decrypted_%'`,
|
||||
queryKey: ['encrypted-columns', tableName],
|
||||
},
|
||||
signal
|
||||
)
|
||||
return encryptedColumns.result.map((column: any) => column.name.split('decrypted_')[1])
|
||||
} catch (error) {
|
||||
console.error('Error fetching encrypted columns', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export type EncryptedColumnsData = Awaited<ReturnType<typeof getEncryptedColumns>>
|
||||
export type EncryptedColumnsError = ResponseError
|
||||
|
||||
export const useEncryptedColumnsQuery = <TData = EncryptedColumnsData>(
|
||||
{ projectRef, connectionString, schema, tableName }: EncryptedColumnsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<EncryptedColumnsData, EncryptedColumnsError, TData> = {}
|
||||
) =>
|
||||
useQuery<EncryptedColumnsData, EncryptedColumnsError, TData>(
|
||||
encryptedColumnKeys.list(projectRef, schema, tableName),
|
||||
({ signal }) =>
|
||||
getEncryptedColumns({ projectRef, connectionString, schema, tableName }, signal),
|
||||
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
|
||||
)
|
||||
|
||||
export function prefetchEncryptedColumns(
|
||||
client: QueryClient,
|
||||
{ projectRef, connectionString, schema, tableName }: EncryptedColumnsVariables
|
||||
) {
|
||||
return client.fetchQuery(encryptedColumnKeys.list(projectRef, schema, tableName), ({ signal }) =>
|
||||
getEncryptedColumns(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
schema,
|
||||
tableName,
|
||||
},
|
||||
signal
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const encryptedColumnKeys = {
|
||||
list: (
|
||||
projectRef: string | undefined,
|
||||
schema: string | undefined,
|
||||
tableName: string | undefined
|
||||
) => ['projects', projectRef, 'encrypted-columns', schema, tableName] as const,
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import { UseQueryOptions } from '@tanstack/react-query'
|
||||
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
|
||||
import { QueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { sqlKeys } from 'data/sql/keys'
|
||||
import {
|
||||
executeSql,
|
||||
ExecuteSqlData,
|
||||
ExecuteSqlError,
|
||||
useExecuteSqlQuery,
|
||||
} from '../sql/execute-sql-query'
|
||||
import { ENTITY_TYPE } from './entity-type-constants'
|
||||
|
||||
type EntityTypeArgs = {
|
||||
@@ -75,3 +82,22 @@ export const useEntityTypeQuery = <TData extends EntityTypeData = EntityTypeData
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export function prefetchEntityType(
|
||||
client: QueryClient,
|
||||
{ projectRef, connectionString, id }: EntityTypeVariables
|
||||
) {
|
||||
return client
|
||||
.fetchQuery(sqlKeys.query(projectRef, ['entity-type', id]), ({ signal }) =>
|
||||
executeSql(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: entityTypeSqlQuery({ id }),
|
||||
queryKey: ['entity-type', id],
|
||||
},
|
||||
signal
|
||||
)
|
||||
)
|
||||
.then((data: { result: EntityTypeData[] }) => data?.result?.[0] ?? null)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query'
|
||||
import { QueryClient, useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query'
|
||||
import { executeSql, ExecuteSqlVariables } from 'data/sql/execute-sql-query'
|
||||
import type { Entity } from './entity-type-query'
|
||||
import { entityTypeKeys } from './keys'
|
||||
import { ENTITY_TYPE } from './entity-type-constants'
|
||||
|
||||
export type EntityTypesVariables = {
|
||||
projectRef?: string
|
||||
@@ -29,7 +30,7 @@ export async function getEntityTypes(
|
||||
limit = 100,
|
||||
page = 0,
|
||||
sort = 'alphabetical',
|
||||
filterTypes,
|
||||
filterTypes = Object.values(ENTITY_TYPE),
|
||||
}: EntityTypesVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
@@ -57,7 +58,7 @@ export async function getEntityTypes(
|
||||
pg_namespace nc
|
||||
join pg_class c on nc.oid = c.relnamespace
|
||||
where
|
||||
c.relkind in (${filterTypes === undefined ? `'r', 'v', 'm', 'f', 'p'` : filterTypes.map((x) => `'${x}'`).join(', ')})
|
||||
c.relkind in (${filterTypes.map((x) => `'${x}'`).join(', ')})
|
||||
and not pg_is_other_temp_schema(nc.oid)
|
||||
and (
|
||||
pg_has_role(c.relowner, 'USAGE')
|
||||
@@ -155,3 +156,34 @@ export const useEntityTypesQuery = <TData = EntityTypesData>(
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export function prefetchEntityTypes(
|
||||
client: QueryClient,
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
schemas = ['public'],
|
||||
search,
|
||||
limit = 100,
|
||||
sort,
|
||||
filterTypes,
|
||||
}: Omit<EntityTypesVariables, 'page'>
|
||||
) {
|
||||
return client.prefetchInfiniteQuery(
|
||||
entityTypeKeys.list(projectRef, { schemas, search, sort, limit, filterTypes }),
|
||||
({ signal, pageParam }) =>
|
||||
getEntityTypes(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
schemas,
|
||||
search,
|
||||
limit,
|
||||
page: pageParam,
|
||||
sort,
|
||||
filterTypes,
|
||||
},
|
||||
signal
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PostgresTable } from '@supabase/postgres-meta'
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { foreignTableKeys } from './keys'
|
||||
@@ -56,3 +56,12 @@ export const useForeignTableQuery = <TData = ForeignTableData>(
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export function prefetchForeignTable(
|
||||
client: QueryClient,
|
||||
{ projectRef, connectionString, id }: ForeignTableVariables
|
||||
) {
|
||||
return client.fetchQuery(foreignTableKeys.foreignTable(projectRef, id), ({ signal }) =>
|
||||
getForeignTable({ id, projectRef, connectionString }, signal)
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PostgresMaterializedView } from '@supabase/postgres-meta'
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { materializedViewKeys } from './keys'
|
||||
@@ -59,3 +60,12 @@ export const useMaterializedViewQuery = <TData = MaterializedViewData>(
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export function prefetchMaterializedView(
|
||||
client: QueryClient,
|
||||
{ projectRef, connectionString, id }: MaterializedViewVariables
|
||||
) {
|
||||
return client.fetchQuery(materializedViewKeys.materializedView(projectRef, id), ({ signal }) =>
|
||||
getMaterializedView({ id, projectRef, connectionString }, signal)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ComponentProps, PropsWithChildren, useCallback } from 'react'
|
||||
|
||||
import { loadTableEditorSortsAndFiltersFromLocalStorage } from 'components/grid/SupabaseGrid'
|
||||
import {
|
||||
formatFilterURLParams,
|
||||
formatSortURLParams,
|
||||
getSupaTable,
|
||||
} from 'components/grid/SupabaseGrid.utils'
|
||||
import { Filter, Sort } from 'components/grid/types'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import {
|
||||
ForeignKeyConstraintsData,
|
||||
prefetchForeignKeyConstraints,
|
||||
} from 'data/database/foreign-key-constraints-query'
|
||||
import { prefetchEncryptedColumns } from 'data/encrypted-columns/encrypted-columns-query'
|
||||
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
|
||||
import { Entity, prefetchEntityType } from 'data/entity-types/entity-type-query'
|
||||
import { ForeignTableData, prefetchForeignTable } from 'data/foreign-tables/foreign-table-query'
|
||||
import {
|
||||
MaterializedViewData,
|
||||
prefetchMaterializedView,
|
||||
} from 'data/materialized-views/materialized-view-query'
|
||||
import { prefetchTableRows } from 'data/table-rows/table-rows-query'
|
||||
import { prefetchTable, TableData } from 'data/tables/table-query'
|
||||
import { prefetchView, ViewData } from 'data/views/view-query'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state'
|
||||
import { TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE } from 'state/table-editor'
|
||||
|
||||
export function usePrefetchEditorTablePage() {
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const { project } = useProjectContext()
|
||||
const roleImpersonationState = useRoleImpersonationStateSnapshot()
|
||||
const tableEditorPrefetchingEnabled = useFlag('tableEditorPrefetching')
|
||||
|
||||
return useCallback(
|
||||
({ id: _id, filters, sorts }: { id?: string; filters?: Filter[]; sorts?: Sort[] }) => {
|
||||
if (!tableEditorPrefetchingEnabled) return
|
||||
|
||||
const id = _id ? Number(_id) : undefined
|
||||
if (!project || !id || isNaN(id)) return
|
||||
|
||||
// Prefetch the code
|
||||
router.prefetch(`/project/${project.ref}/editor/${id}`)
|
||||
|
||||
// Prefetch the data
|
||||
prefetchEntityType(queryClient, {
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
id,
|
||||
})
|
||||
.then((entity) => {
|
||||
let promises: [
|
||||
Promise<Entity | null>,
|
||||
Promise<ForeignKeyConstraintsData>,
|
||||
Promise<string[]>,
|
||||
(
|
||||
| Promise<TableData | ViewData | MaterializedViewData | ForeignTableData | undefined>
|
||||
| undefined
|
||||
),
|
||||
] = [
|
||||
Promise.resolve(entity),
|
||||
prefetchForeignKeyConstraints(queryClient, {
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
schema: entity?.schema,
|
||||
}),
|
||||
prefetchEncryptedColumns(queryClient, {
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
schema: entity?.schema,
|
||||
tableName: entity?.name,
|
||||
}),
|
||||
undefined,
|
||||
]
|
||||
|
||||
switch (entity?.type) {
|
||||
case ENTITY_TYPE.TABLE:
|
||||
case ENTITY_TYPE.PARTITIONED_TABLE:
|
||||
promises[3] = prefetchTable(queryClient, {
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
id,
|
||||
})
|
||||
break
|
||||
|
||||
case ENTITY_TYPE.VIEW:
|
||||
promises[3] = prefetchView(queryClient, {
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
id,
|
||||
})
|
||||
break
|
||||
|
||||
case ENTITY_TYPE.MATERIALIZED_VIEW:
|
||||
promises[3] = prefetchMaterializedView(queryClient, {
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
id,
|
||||
})
|
||||
break
|
||||
|
||||
case ENTITY_TYPE.FOREIGN_TABLE:
|
||||
promises[3] = prefetchForeignTable(queryClient, {
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
id,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
})
|
||||
.then(([entity, foreignKeyConstraints, encryptedColumns, table]) => {
|
||||
if (entity && table) {
|
||||
const supaTable = getSupaTable({
|
||||
selectedTable: table,
|
||||
encryptedColumns,
|
||||
foreignKeyMeta: foreignKeyConstraints,
|
||||
entityType: entity.type,
|
||||
})
|
||||
|
||||
const { sorts: localSorts = [], filters: localFilters = [] } =
|
||||
loadTableEditorSortsAndFiltersFromLocalStorage(
|
||||
project.ref,
|
||||
entity.name,
|
||||
entity.schema
|
||||
) ?? {}
|
||||
|
||||
prefetchTableRows(queryClient, {
|
||||
queryKey: [supaTable.schema, supaTable.name],
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
table: supaTable,
|
||||
sorts: sorts ?? formatSortURLParams(supaTable.name, localSorts),
|
||||
filters: filters ?? formatFilterURLParams(localFilters),
|
||||
page: 1,
|
||||
limit: TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE,
|
||||
impersonatedRole: roleImpersonationState.role,
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
[project, queryClient, router]
|
||||
)
|
||||
}
|
||||
|
||||
type LinkProps = ComponentProps<typeof Link>
|
||||
|
||||
interface EditorTablePageLinkProps extends Omit<LinkProps, 'href'> {
|
||||
projectRef?: string
|
||||
id?: string
|
||||
sorts?: Sort[]
|
||||
filters?: Filter[]
|
||||
href?: LinkProps['href']
|
||||
}
|
||||
|
||||
export function EditorTablePageLink({
|
||||
projectRef,
|
||||
id,
|
||||
sorts,
|
||||
filters,
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<EditorTablePageLinkProps>) {
|
||||
const prefetch = usePrefetchEditorTablePage()
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href || `/project/${projectRef}/editor/${id}`}
|
||||
onMouseEnter={() => prefetch({ id, sorts, filters })}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import Link from 'next/link'
|
||||
import { ComponentProps, PropsWithChildren, useCallback } from 'react'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { prefetchSchemas } from 'data/database/schemas-query'
|
||||
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
|
||||
import { prefetchEntityTypes } from 'data/entity-types/entity-types-infinite-query'
|
||||
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export function usePrefetchEditorIndexPage() {
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const { project } = useProjectContext()
|
||||
const tableEditorPrefetchingEnabled = useFlag('tableEditorPrefetching')
|
||||
|
||||
const [entityTypesSort] = useLocalStorage<'alphabetical' | 'grouped-alphabetical'>(
|
||||
'table-editor-sort',
|
||||
'alphabetical'
|
||||
)
|
||||
|
||||
return useCallback(() => {
|
||||
if (!tableEditorPrefetchingEnabled || !project) return
|
||||
|
||||
// Prefetch code
|
||||
router.prefetch(`/project/${project.ref}/editor`)
|
||||
|
||||
// Prefetch data
|
||||
prefetchSchemas(queryClient, {
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
})
|
||||
prefetchEntityTypes(queryClient, {
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
sort: entityTypesSort,
|
||||
filterTypes: Object.values(ENTITY_TYPE),
|
||||
})
|
||||
}, [entityTypesSort, project, queryClient, router])
|
||||
}
|
||||
|
||||
type LinkProps = ComponentProps<typeof Link>
|
||||
|
||||
interface EditorIndexPageLinkProps extends Omit<LinkProps, 'href'> {
|
||||
projectRef?: string
|
||||
href?: LinkProps['href']
|
||||
}
|
||||
|
||||
export function EditorIndexPageLink({
|
||||
href,
|
||||
projectRef,
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<EditorIndexPageLinkProps>) {
|
||||
const prefetch = usePrefetchEditorIndexPage()
|
||||
|
||||
return (
|
||||
<Link href={href || `/project/${projectRef}/editor`} onMouseEnter={prefetch} {...props}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueryKey, UseQueryOptions } from '@tanstack/react-query'
|
||||
import type { QueryClient, QueryKey, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { IS_PLATFORM } from 'common'
|
||||
import { Query } from 'components/grid/query/Query'
|
||||
@@ -7,13 +7,17 @@ import {
|
||||
JSON_TYPES,
|
||||
TEXT_TYPES,
|
||||
} from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.constants'
|
||||
import { sqlKeys } from 'data/sql/keys'
|
||||
import { KB } from 'lib/constants'
|
||||
import {
|
||||
ImpersonationRole,
|
||||
ROLE_IMPERSONATION_NO_RESULTS,
|
||||
wrapWithRoleImpersonation,
|
||||
} from 'lib/role-impersonation'
|
||||
import { useIsRoleImpersonationEnabled } from 'state/role-impersonation-state'
|
||||
import {
|
||||
isRoleImpersonationEnabled,
|
||||
useIsRoleImpersonationEnabled,
|
||||
} from 'state/role-impersonation-state'
|
||||
import {
|
||||
ExecuteSqlData,
|
||||
ExecuteSqlError,
|
||||
@@ -204,6 +208,21 @@ export type TableRowsVariables = GetTableRowsArgs & {
|
||||
export type TableRowsData = TableRows
|
||||
export type TableRowsError = ExecuteSqlError
|
||||
|
||||
function getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args }: TableRowsVariables) {
|
||||
return [
|
||||
...(queryKey ?? []),
|
||||
{
|
||||
table: {
|
||||
name: table?.name,
|
||||
schema: table?.schema,
|
||||
columns: table?.columns.map((c) => c.name),
|
||||
},
|
||||
impersonatedRole,
|
||||
...args,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const useTableRowsQuery = <TData extends TableRowsData = TableRowsData>(
|
||||
{ projectRef, connectionString, queryKey, table, impersonatedRole, ...args }: TableRowsVariables,
|
||||
options: UseQueryOptions<ExecuteSqlData, TableRowsError, TData> = {}
|
||||
@@ -218,18 +237,7 @@ export const useTableRowsQuery = <TData extends TableRowsData = TableRowsData>(
|
||||
projectRef: projectRef ?? 'ref',
|
||||
role: impersonatedRole,
|
||||
}),
|
||||
queryKey: [
|
||||
...(queryKey ?? []),
|
||||
{
|
||||
table: {
|
||||
name: table?.name,
|
||||
schema: table?.schema,
|
||||
columns: table?.columns.map((c) => c.name),
|
||||
},
|
||||
impersonatedRole,
|
||||
...args,
|
||||
},
|
||||
],
|
||||
queryKey: getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args }),
|
||||
isRoleImpersonationEnabled,
|
||||
},
|
||||
{
|
||||
@@ -247,3 +255,26 @@ export const useTableRowsQuery = <TData extends TableRowsData = TableRowsData>(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function prefetchTableRows(
|
||||
client: QueryClient,
|
||||
{ projectRef, connectionString, queryKey, table, impersonatedRole, ...args }: TableRowsVariables
|
||||
) {
|
||||
return client.fetchQuery(
|
||||
sqlKeys.query(projectRef, getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args })),
|
||||
({ signal }) =>
|
||||
executeSql(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: wrapWithRoleImpersonation(getTableRowsSqlQuery({ table, ...args }), {
|
||||
projectRef: projectRef ?? 'ref',
|
||||
role: impersonatedRole,
|
||||
}),
|
||||
queryKey: getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args }),
|
||||
isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRole),
|
||||
},
|
||||
signal
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PostgresTable } from '@supabase/postgres-meta'
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { useCallback } from 'react'
|
||||
import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { tableKeys } from './keys'
|
||||
@@ -57,23 +56,11 @@ export const useTableQuery = <TData = TableData>(
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 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<TableVariables, 'projectRef' | 'connectionString'>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useCallback(
|
||||
(id: NonNullable<TableVariables['id']>) => {
|
||||
return queryClient.fetchQuery({
|
||||
queryKey: tableKeys.table(projectRef, id),
|
||||
queryFn: ({ signal }) => getTable({ id, projectRef, connectionString }, signal),
|
||||
})
|
||||
},
|
||||
[connectionString, projectRef, queryClient]
|
||||
export function prefetchTable(
|
||||
client: QueryClient,
|
||||
{ projectRef, connectionString, id }: TableVariables
|
||||
) {
|
||||
return client.fetchQuery(tableKeys.table(projectRef, id), ({ signal }) =>
|
||||
getTable({ id, projectRef, connectionString }, signal)
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PostgresView } from '@supabase/postgres-meta'
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
@@ -55,3 +55,12 @@ export const useViewQuery = <TData = ViewData>(
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export function prefetchView(
|
||||
client: QueryClient,
|
||||
{ projectRef, connectionString, id }: ViewVariables
|
||||
) {
|
||||
return client.fetchQuery(viewKeys.view(projectRef, id), ({ signal }) =>
|
||||
getView({ id, projectRef, connectionString }, signal)
|
||||
)
|
||||
}
|
||||
@@ -25,18 +25,7 @@ function useTable(id?: number) {
|
||||
id: entity?.id,
|
||||
},
|
||||
{
|
||||
enabled: entity?.type === ENTITY_TYPE.TABLE,
|
||||
}
|
||||
)
|
||||
|
||||
const partitionTableResult = useTableQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
id: entity?.id,
|
||||
},
|
||||
{
|
||||
enabled: entity?.type === ENTITY_TYPE.PARTITIONED_TABLE,
|
||||
enabled: entity?.type === ENTITY_TYPE.TABLE || entity?.type === ENTITY_TYPE.PARTITIONED_TABLE,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -75,9 +64,8 @@ function useTable(id?: number) {
|
||||
|
||||
switch (entity?.type) {
|
||||
case ENTITY_TYPE.TABLE:
|
||||
return tableResult
|
||||
case ENTITY_TYPE.PARTITIONED_TABLE:
|
||||
return partitionTableResult
|
||||
return tableResult
|
||||
case ENTITY_TYPE.VIEW:
|
||||
return viewResult
|
||||
case ENTITY_TYPE.MATERIALIZED_VIEW:
|
||||
|
||||
@@ -31,7 +31,7 @@ const TableEditorPage: NextPageWithLayout = () => {
|
||||
if (tables.length > 0) {
|
||||
router.push(`/project/${projectRef}/editor/${tables[0].id}`)
|
||||
} else {
|
||||
router.push(`/project/${projectRef}/editor/`)
|
||||
router.push(`/project/${projectRef}/editor`)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { PropsWithChildren, createContext, useContext, useRef } from 'react'
|
||||
import type { Dictionary } from 'types'
|
||||
import { proxy, useSnapshot } from 'valtio'
|
||||
|
||||
export const TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE = 100
|
||||
|
||||
type ForeignKeyState = {
|
||||
foreignKey: ForeignKey
|
||||
row: Dictionary<any>
|
||||
@@ -63,7 +65,7 @@ export const createTableEditorState = () => {
|
||||
setPage: (page: number) => {
|
||||
state.page = page
|
||||
},
|
||||
rowsPerPage: 100,
|
||||
rowsPerPage: TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE,
|
||||
setRowsPerPage: (rowsPerPage: number) => {
|
||||
state.rowsPerPage = rowsPerPage
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user