Skip to content

Commit 4855bfe

Browse files
committed
Add tooltips for grid column foreign key
1 parent 5248435 commit 4855bfe

File tree

9 files changed

+110
-37
lines changed

9 files changed

+110
-37
lines changed

studio/components/grid/SupabaseGrid.utils.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,12 @@ export function parseSupaTable(
215215
isEncrypted: encryptedColumns.includes(column.name),
216216
enum: column.enums,
217217
comment: column.comment,
218-
targetTableSchema: null,
219-
targetTableName: null,
220-
targetColumnName: null,
218+
foreignKey: {
219+
targetTableSchema: null,
220+
targetTableName: null,
221+
targetColumnName: null,
222+
deletionAction: undefined,
223+
},
221224
}
222225
const primaryKey = primaryKeys.find((pk) => pk.name == column.name)
223226
temp.isPrimaryKey = !!primaryKey
@@ -230,9 +233,10 @@ export function parseSupaTable(
230233
)
231234
})
232235
if (relationship) {
233-
temp.targetTableSchema = relationship.target_table_schema
234-
temp.targetTableName = relationship.target_table_name
235-
temp.targetColumnName = relationship.target_column_name
236+
temp.foreignKey.targetTableSchema = relationship.target_table_schema
237+
temp.foreignKey.targetTableName = relationship.target_table_name
238+
temp.foreignKey.targetColumnName = relationship.target_column_name
239+
temp.foreignKey.deletionAction = relationship.deletion_action
236240
}
237241
return temp
238242
})

studio/components/grid/components/common/ForeignTableModal/index.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const ForeignTableModal: React.FC<ForeignTableModalProps> = ({
3131

3232
if (defaultValue && columnDefinition) {
3333
fetchData({
34-
column: columnDefinition.targetColumnName!,
34+
column: columnDefinition?.foreignKey?.targetColumnName!,
3535
operator: '=',
3636
value: defaultValue,
3737
})
@@ -43,14 +43,14 @@ export const ForeignTableModal: React.FC<ForeignTableModalProps> = ({
4343
async function fetchColumns() {
4444
if (
4545
!state.metaService ||
46-
!columnDefinition?.targetTableName ||
47-
!columnDefinition?.targetTableSchema
46+
!columnDefinition?.foreignKey?.targetTableName ||
47+
!columnDefinition?.foreignKey?.targetTableSchema
4848
)
4949
return
5050

5151
const { data } = await state.metaService.fetchColumns(
52-
columnDefinition?.targetTableName,
53-
columnDefinition?.targetTableSchema
52+
columnDefinition?.foreignKey?.targetTableName,
53+
columnDefinition?.foreignKey?.targetTableSchema
5454
)
5555
if (data && data.length > 0) {
5656
const columnNames = data.map((x) => x.name)
@@ -59,11 +59,14 @@ export const ForeignTableModal: React.FC<ForeignTableModalProps> = ({
5959
}
6060

6161
async function fetchData(filter?: Filter) {
62-
if (!state.onSqlQuery || !columnDefinition?.targetTableName) return
62+
if (!state.onSqlQuery || !columnDefinition?.foreignKey?.targetTableName) return
6363

6464
const query = new Query()
6565
let queryChains = query
66-
.from(columnDefinition?.targetTableName, columnDefinition?.targetTableSchema ?? undefined)
66+
.from(
67+
columnDefinition?.foreignKey?.targetTableName,
68+
columnDefinition?.foreignKey?.targetTableSchema ?? undefined
69+
)
6770
.select()
6871

6972
if (filter && filter.value && filter.value != '') {
@@ -88,8 +91,8 @@ export const ForeignTableModal: React.FC<ForeignTableModalProps> = ({
8891
}
8992

9093
function onItemSelect(item: Dictionary<any>) {
91-
if (item && columnDefinition && columnDefinition.targetColumnName) {
92-
const value = item[columnDefinition.targetColumnName]
94+
if (item && columnDefinition && columnDefinition.foreignKey?.targetColumnName) {
95+
const value = item[columnDefinition.foreignKey?.targetColumnName]
9396
onChange(value)
9497
}
9598

@@ -140,7 +143,7 @@ export const ForeignTableModal: React.FC<ForeignTableModalProps> = ({
140143
>
141144
<Modal.Content>
142145
<FilterHeader
143-
defaultColumnName={columnDefinition?.targetColumnName ?? undefined}
146+
defaultColumnName={columnDefinition?.foreignKey?.targetColumnName ?? undefined}
144147
defaultValue={defaultValue}
145148
foreignColumnNames={foreignColumnNames}
146149
onChange={onFilterChange}

studio/components/grid/components/grid/ColumnHeader.tsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import * as React from 'react'
2-
import { IconKey, IconLink, IconLock } from 'ui'
2+
import { IconArrowRight, IconKey, IconLink, IconLock } from 'ui'
33
import * as Tooltip from '@radix-ui/react-tooltip'
44
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'
55
import { XYCoord } from 'dnd-core'
66
import { useDispatch } from '../../store'
7-
import { ColumnHeaderProps, ColumnType, DragItem } from '../../types'
7+
import { ColumnHeaderProps, ColumnType, DragItem, GridForeignKey } from '../../types'
88
import { ColumnMenu } from '../menu'
99
import { useTrackedState } from '../../store'
10+
import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants'
11+
import { getForeignKeyDeletionAction } from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils'
1012

1113
export function ColumnHeader<R>({
1214
column,
1315
columnType,
1416
isPrimaryKey,
1517
isEncrypted,
1618
format,
19+
foreignKey,
1720
}: ColumnHeaderProps<R>) {
1821
const ref = React.useRef<HTMLDivElement>(null)
1922
const dispatch = useDispatch()
@@ -119,7 +122,7 @@ export function ColumnHeader<R>({
119122
<div ref={ref} data-handler-id={handlerId} style={{ opacity }} className="w-full">
120123
<div className={`sb-grid-column-header ${cursor}`}>
121124
<div className="sb-grid-column-header__inner">
122-
{renderColumnIcon(columnType)}
125+
{renderColumnIcon(columnType, { name: column.name as string, foreignKey })}
123126
{isPrimaryKey && (
124127
<Tooltip.Root delayDuration={0}>
125128
<Tooltip.Trigger>
@@ -169,13 +172,45 @@ export function ColumnHeader<R>({
169172
)
170173
}
171174

172-
function renderColumnIcon(type: ColumnType) {
175+
function renderColumnIcon(
176+
type: ColumnType,
177+
columnMeta: { name?: string; foreignKey?: GridForeignKey }
178+
) {
179+
const { name, foreignKey } = columnMeta
173180
switch (type) {
174181
case 'foreign_key':
175182
return (
176-
<div>
177-
<IconLink size="tiny" strokeWidth={2} />
178-
</div>
183+
<Tooltip.Root delayDuration={0}>
184+
<Tooltip.Trigger>
185+
<IconLink size="tiny" strokeWidth={2} />
186+
</Tooltip.Trigger>
187+
<Tooltip.Content side="bottom">
188+
<Tooltip.Arrow className="radix-tooltip-arrow" />
189+
<div
190+
className={[
191+
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
192+
'border border-scale-200',
193+
].join(' ')}
194+
>
195+
<div>
196+
<p className="text-xs text-scale-1100">Foreign key relation:</p>
197+
<div className="flex items-center space-x-1">
198+
<p className="text-xs text-scale-1200">{name}</p>
199+
<IconArrowRight size="tiny" strokeWidth={1.5} />
200+
<p className="text-xs text-scale-1200">
201+
{foreignKey?.targetTableSchema}.{foreignKey?.targetTableName}.
202+
{foreignKey?.targetColumnName}
203+
</p>
204+
</div>
205+
{foreignKey?.deletionAction !== FOREIGN_KEY_DELETION_ACTION.NO_ACTION && (
206+
<p className="text-xs text-scale-1200 mt-1">
207+
On delete: {getForeignKeyDeletionAction(foreignKey?.deletionAction)}
208+
</p>
209+
)}
210+
</div>
211+
</div>
212+
</Tooltip.Content>
213+
</Tooltip.Root>
179214
)
180215
default:
181216
return null

studio/components/grid/types/base.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,17 @@ export type ColumnType =
5555
| 'time'
5656
| 'unknown'
5757

58+
export interface GridForeignKey {
59+
targetTableSchema?: string | null
60+
targetTableName?: string | null
61+
targetColumnName?: string | null
62+
deletionAction?: string
63+
}
64+
5865
export interface ColumnHeaderProps<R> extends HeaderRendererProps<R> {
5966
columnType: ColumnType
6067
isPrimaryKey: boolean | undefined
6168
isEncrypted: boolean | undefined
6269
format: string
70+
foreignKey?: GridForeignKey
6371
}

studio/components/grid/types/table.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Dictionary } from './base'
1+
import { Dictionary, GridForeignKey } from './base'
22

33
export interface SupaColumn {
44
readonly dataType: string
@@ -13,9 +13,7 @@ export interface SupaColumn {
1313
readonly isNullable?: boolean
1414
readonly isUpdatable?: boolean
1515
readonly isEncrypted?: boolean
16-
readonly targetTableSchema?: string | null
17-
readonly targetTableName?: string | null
18-
readonly targetColumnName?: string | null
16+
readonly foreignKey?: GridForeignKey
1917
position: number
2018
}
2119

studio/components/grid/utils/gridColumns.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export function getGridColumns(
4747
): any[] {
4848
const columns = table.columns.map((x, idx) => {
4949
const columnType = getColumnType(x)
50-
5150
const columnDefaultWidth = getColumnDefaultWidth(x)
5251
const columnWidthBasedOnName =
5352
(x.name.length + x.format.length) * ESTIMATED_CHARACTER_PIXEL_WIDTH
@@ -75,6 +74,7 @@ export function getGridColumns(
7574
isPrimaryKey={x.isPrimaryKey}
7675
isEncrypted={x.isEncrypted}
7776
format={x.format}
77+
foreignKey={x.foreignKey}
7878
/>
7979
),
8080
editor: options?.editable

studio/components/grid/utils/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,6 @@ export function isEnumColumn(type: string) {
6565
}
6666

6767
export function isForeignKeyColumn(columnDef: SupaColumn) {
68-
const { targetTableSchema, targetTableName, targetColumnName } = columnDef
68+
const { targetTableSchema, targetTableName, targetColumnName } = columnDef?.foreignKey ?? {}
6969
return !!targetTableSchema && !!targetTableName && !!targetColumnName
7070
}

studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,10 @@ export const getColumnForeignKey = (
222222
if (foreignKey === undefined) return foreignKey
223223
else {
224224
const foreignKeyMeta = foreignKeys.find((fk) => fk.id === foreignKey.id)
225-
return { ...foreignKey, deletion_action: foreignKeyMeta?.deletion_action ?? 'a' }
225+
return {
226+
...foreignKey,
227+
deletion_action: foreignKeyMeta?.deletion_action ?? FOREIGN_KEY_DELETION_ACTION.NO_ACTION,
228+
}
226229
}
227230
}
228231

studio/components/interfaces/TableGridEditor/TableGridEditor.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { FC, useRef, useEffect, useState } from 'react'
22
import { observer } from 'mobx-react-lite'
33
import { useRouter } from 'next/router'
44
import { find, isUndefined } from 'lodash'
5-
import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
5+
import type { PostgresColumn, PostgresRelationship, PostgresTable } from '@supabase/postgres-meta'
66
import { PermissionAction } from '@supabase/shared-types/out/constants'
77
import { QueryKey, useQueryClient } from '@tanstack/react-query'
88

@@ -30,6 +30,11 @@ import { JsonEditValue } from './SidePanelEditor/RowEditor/RowEditor.types'
3030
import LangSelector from '../Docs/LangSelector'
3131
import GeneratingTypes from '../Docs/GeneratingTypes'
3232
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
33+
import {
34+
ForeignKeyConstraint,
35+
useForeignKeyConstraintsQuery,
36+
} from 'data/database/foreign-key-constraints-query'
37+
import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants'
3338

3439
interface Props {
3540
/** Theme for the editor */
@@ -198,14 +203,17 @@ const TableGridEditor: FC<Props> = ({
198203
error: jsonSchemaError,
199204
refetch,
200205
} = useProjectJsonSchemaQuery({ projectRef })
201-
202-
if (jsonSchemaError) console.log('jsonSchemaError', jsonSchemaError)
206+
if (jsonSchemaError) console.error('jsonSchemaError', jsonSchemaError)
203207

204208
const resources = getResourcesFromJsonSchema(jsonSchema)
209+
const refreshDocs = async () => await refetch()
205210

206-
const refreshDocs = async () => {
207-
await refetch()
208-
}
211+
const { data } = useForeignKeyConstraintsQuery({
212+
projectRef: project?.ref,
213+
connectionString: project?.connectionString,
214+
schema: selectedTable?.schema,
215+
})
216+
const foreignKeyMeta = data?.result ?? []
209217

210218
useEffect(() => {
211219
if (selectedTable !== undefined && selectedTable.id !== undefined && isVaultEnabled) {
@@ -228,14 +236,28 @@ const TableGridEditor: FC<Props> = ({
228236
const canUpdateTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
229237
const canEditViaTableEditor = !isViewSelected && !isForeignTableSelected && !isLocked
230238

239+
// [Joshen] We can tweak below to eventually support composite keys as the data
240+
// returned from foreignKeyMeta should be easy to deal with, rather than pg-meta
241+
const formattedRelationships = (selectedTable?.relationships ?? []).map(
242+
(relationship: PostgresRelationship) => {
243+
const relationshipMeta = foreignKeyMeta.find(
244+
(fk: ForeignKeyConstraint) => fk.id === relationship.id
245+
)
246+
return {
247+
...relationship,
248+
deletion_action: relationshipMeta?.deletion_action ?? FOREIGN_KEY_DELETION_ACTION.NO_ACTION,
249+
}
250+
}
251+
)
252+
231253
const gridTable =
232254
!isViewSelected && !isForeignTableSelected
233255
? parseSupaTable(
234256
{
235257
table: selectedTable as PostgresTable,
236258
columns: (selectedTable as PostgresTable).columns ?? [],
237259
primaryKeys: (selectedTable as PostgresTable).primary_keys,
238-
relationships: (selectedTable as PostgresTable).relationships,
260+
relationships: formattedRelationships,
239261
},
240262
encryptedColumns
241263
)

0 commit comments

Comments
 (0)