mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Merge pull request #13284 from supabase/feat/show-view-definition
Feat/show view definition
This commit is contained in:
12 files changed
+420
-166
No files matched your search
@@ -58,7 +58,15 @@ export const SupabaseGrid = forwardRef<SupabaseGridRef, SupabaseGridProps>((prop
|
||||
})
|
||||
|
||||
const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props, ref) => {
|
||||
const { editable, storageRef, gridProps, headerActions } = props
|
||||
const {
|
||||
editable,
|
||||
storageRef,
|
||||
gridProps,
|
||||
headerActions,
|
||||
showCustomChildren,
|
||||
customHeader,
|
||||
children,
|
||||
} = props
|
||||
const dispatch = useDispatch()
|
||||
const state = useTrackedState()
|
||||
|
||||
@@ -194,19 +202,28 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
table={table}
|
||||
sorts={sorts}
|
||||
filters={filters}
|
||||
isRefetching={isRefetching}
|
||||
onAddRow={editable ? props.onAddRow : undefined}
|
||||
onAddColumn={editable ? props.onAddColumn : undefined}
|
||||
headerActions={headerActions}
|
||||
customHeader={customHeader}
|
||||
/>
|
||||
<Grid
|
||||
ref={gridRef}
|
||||
{...gridProps}
|
||||
rows={data?.rows ?? []}
|
||||
updateRow={props.updateTableRow}
|
||||
onEditForeignKeyColumnValue={props.onEditForeignKeyColumnValue}
|
||||
/>
|
||||
<Footer isLoading={isLoading || isRefetching} />
|
||||
<Shortcuts gridRef={gridRef} />
|
||||
{showCustomChildren && children !== undefined ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<>
|
||||
<Grid
|
||||
ref={gridRef}
|
||||
{...gridProps}
|
||||
rows={data?.rows ?? []}
|
||||
updateRow={props.updateTableRow}
|
||||
onEditForeignKeyColumnValue={props.onEditForeignKeyColumnValue}
|
||||
/>
|
||||
<Footer isLoading={isLoading || isRefetching} />
|
||||
<Shortcuts gridRef={gridRef} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{mounted &&
|
||||
createPortal(<RowContextMenu table={table} rows={data?.rows ?? []} />, document.body)}
|
||||
</div>
|
||||
|
||||
@@ -28,22 +28,44 @@ export type HeaderProps = {
|
||||
table: SupaTable
|
||||
sorts: Sort[]
|
||||
filters: Filter[]
|
||||
isRefetching: boolean
|
||||
onAddColumn?: () => void
|
||||
onAddRow?: () => void
|
||||
headerActions?: ReactNode
|
||||
customHeader: ReactNode
|
||||
}
|
||||
|
||||
const Header = ({ table, sorts, filters, onAddColumn, onAddRow, headerActions }: HeaderProps) => {
|
||||
const Header = ({
|
||||
table,
|
||||
sorts,
|
||||
filters,
|
||||
onAddColumn,
|
||||
onAddRow,
|
||||
headerActions,
|
||||
customHeader,
|
||||
isRefetching,
|
||||
}: HeaderProps) => {
|
||||
const state = useTrackedState()
|
||||
const { selectedRows } = state
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex h-10 items-center justify-between bg-scale-100 px-5 py-1.5 dark:bg-scale-300">
|
||||
{selectedRows.size > 0 ? (
|
||||
<RowHeader table={table} sorts={sorts} filters={filters} />
|
||||
{customHeader ? (
|
||||
<>{customHeader}</>
|
||||
) : (
|
||||
<DefaultHeader table={table} onAddColumn={onAddColumn} onAddRow={onAddRow} />
|
||||
<>
|
||||
{selectedRows.size > 0 ? (
|
||||
<RowHeader table={table} sorts={sorts} filters={filters} />
|
||||
) : (
|
||||
<DefaultHeader
|
||||
table={table}
|
||||
isRefetching={isRefetching}
|
||||
onAddColumn={onAddColumn}
|
||||
onAddRow={onAddRow}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="sb-grid-header__inner">{headerActions}</div>
|
||||
</div>
|
||||
@@ -56,10 +78,11 @@ export default Header
|
||||
|
||||
type DefaultHeaderProps = {
|
||||
table: SupaTable
|
||||
isRefetching: boolean
|
||||
onAddColumn?: () => void
|
||||
onAddRow?: () => void
|
||||
}
|
||||
const DefaultHeader = ({ table, onAddColumn, onAddRow }: DefaultHeaderProps) => {
|
||||
const DefaultHeader = ({ table, isRefetching, onAddColumn, onAddRow }: DefaultHeaderProps) => {
|
||||
const canAddNew = onAddRow !== undefined || onAddColumn !== undefined
|
||||
|
||||
// [Joshen] Using this logic to block both column and row creation/update/delete
|
||||
@@ -72,7 +95,7 @@ const DefaultHeader = ({ table, onAddColumn, onAddRow }: DefaultHeaderProps) =>
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshButton table={table} />
|
||||
<RefreshButton table={table} isRefetching={isRefetching} />
|
||||
<FilterDropdown table={table} filters={filters as string[]} setParams={setParams} />
|
||||
<SortPopover table={table} sorts={sorts as string[]} setParams={setParams} />
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQueryClient, useIsFetching } from '@tanstack/react-query'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { SupaTable } from 'components/grid/types'
|
||||
import { sqlKeys } from 'data/sql/keys'
|
||||
import { useParams } from 'hooks'
|
||||
@@ -8,16 +8,14 @@ import { SupabaseGridQueue } from '../../constants'
|
||||
|
||||
export type RefreshButtonProps = {
|
||||
table: SupaTable
|
||||
isRefetching: boolean
|
||||
}
|
||||
|
||||
const RefreshButton = ({ table }: RefreshButtonProps) => {
|
||||
const RefreshButton = ({ table, isRefetching }: RefreshButtonProps) => {
|
||||
const { ref } = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const queryKey = sqlKeys.query(ref, [table.schema, table.name])
|
||||
|
||||
const loading = useIsFetching({ queryKey }) > 0
|
||||
|
||||
const [status, setStatus] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -61,7 +59,7 @@ const RefreshButton = ({ table }: RefreshButtonProps) => {
|
||||
onClick={() => {
|
||||
if (!status) onClick()
|
||||
}}
|
||||
loading={loading || status === 'saving'}
|
||||
loading={isRefetching || status === 'saving'}
|
||||
>
|
||||
{!status ? 'Refresh' : status === 'saved' ? 'Changes saved' : 'Saving changes'}
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
|
||||
import React from 'react'
|
||||
import React, { ReactNode } from 'react'
|
||||
import { Dictionary } from './base'
|
||||
import { SupaRow, SupaTable } from './table'
|
||||
|
||||
@@ -81,6 +81,21 @@ export interface SupabaseGridProps {
|
||||
row: any
|
||||
column: any
|
||||
}) => void
|
||||
|
||||
/**
|
||||
* Show custom component passed as children instead of the grid editor
|
||||
*/
|
||||
showCustomChildren?: boolean
|
||||
|
||||
/**
|
||||
* Custom header left most actions component
|
||||
*/
|
||||
customHeader?: ReactNode
|
||||
|
||||
/**
|
||||
* Custom component passed as children
|
||||
*/
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export interface SupabaseGridRef {
|
||||
|
||||
@@ -163,7 +163,7 @@ const ProviderForm: FC<Props> = ({ provider }) => {
|
||||
<Button
|
||||
htmlType="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={!canUpdateConfig || noChanges}
|
||||
disabled={isSubmitting || !canUpdateConfig || noChanges}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { useState } from 'react'
|
||||
import { SidePanel, IconBookOpen } from 'ui'
|
||||
|
||||
import { useParams, useStore } from 'hooks'
|
||||
import { GeneralContent, ResourceContent } from '../Docs'
|
||||
import LangSelector from '../Docs/LangSelector'
|
||||
import GeneratingTypes from '../Docs/GeneratingTypes'
|
||||
import ActionBar from './SidePanelEditor/ActionBar'
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query'
|
||||
import { snakeToCamel } from 'lib/helpers'
|
||||
|
||||
interface APIDocumentationPanelProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const APIDocumentationPanel = ({ visible, onClose }: APIDocumentationPanelProps) => {
|
||||
const { meta } = useStore()
|
||||
const { ref, page, id } = useParams()
|
||||
const {
|
||||
data: jsonSchema,
|
||||
error: jsonSchemaError,
|
||||
refetch,
|
||||
} = useProjectJsonSchemaQuery({ projectRef: ref })
|
||||
const { data: settings } = useProjectApiQuery({ projectRef: ref })
|
||||
|
||||
const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' }
|
||||
const [selectedLang, setSelectedLang] = useState<any>('js')
|
||||
const [showApiKey, setShowApiKey] = useState<any>(DEFAULT_KEY)
|
||||
|
||||
const tables = meta.tables.list()
|
||||
const autoApiService = {
|
||||
...settings?.autoApiService,
|
||||
endpoint: `${settings?.autoApiService.protocol ?? 'https'}://${
|
||||
settings?.autoApiService.endpoint ?? '-'
|
||||
}`,
|
||||
}
|
||||
const apiService = settings?.autoApiService
|
||||
const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
|
||||
? apiService.defaultApiKey
|
||||
: undefined
|
||||
const resources = getResourcesFromJsonSchema(jsonSchema)
|
||||
|
||||
function getResourcesFromJsonSchema(value: any) {
|
||||
const { paths } = value || {}
|
||||
const functionPath = 'rpc/'
|
||||
let resources: any = {}
|
||||
|
||||
Object.entries(paths || []).forEach(([name, val]) => {
|
||||
let trimmed = name.slice(1)
|
||||
let id = trimmed.replace(functionPath, '')
|
||||
let displayName = id.replace(/_/g, ' ')
|
||||
let camelCase = snakeToCamel(id)
|
||||
let enriched = { id, displayName, camelCase }
|
||||
if (!trimmed.length) return
|
||||
else resources[id] = enriched
|
||||
})
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
key="WrapperTableEditor"
|
||||
size="xxlarge"
|
||||
visible={visible}
|
||||
onCancel={() => onClose()}
|
||||
header={
|
||||
<span className="flex items-center gap-2">
|
||||
<IconBookOpen size="tiny" />
|
||||
API
|
||||
</span>
|
||||
}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
backButtonLabel="Close"
|
||||
hideApply={true}
|
||||
formId="wrapper-table-editor-form"
|
||||
closePanel={() => onClose()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="Docs Docs--table-editor">
|
||||
<SidePanel.Content>
|
||||
{jsonSchemaError ? (
|
||||
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
|
||||
<div className="text-scale-1000">
|
||||
<p>Error connecting to API</p>
|
||||
<p>{`${jsonSchemaError}`}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{jsonSchema ? (
|
||||
<>
|
||||
<div className="sticky top-0 z-10 bg-scale-100 dark:bg-scale-300">
|
||||
<LangSelector
|
||||
selectedLang={selectedLang}
|
||||
setSelectedLang={setSelectedLang}
|
||||
showApiKey={showApiKey}
|
||||
setShowApiKey={setShowApiKey}
|
||||
apiKey={anonKey}
|
||||
autoApiService={autoApiService}
|
||||
/>
|
||||
</div>
|
||||
<GeneralContent
|
||||
autoApiService={autoApiService}
|
||||
selectedLang={selectedLang}
|
||||
showApiKey={true}
|
||||
page={page}
|
||||
/>
|
||||
|
||||
<GeneratingTypes selectedLang={selectedLang} />
|
||||
|
||||
{jsonSchema?.definitions && (
|
||||
<ResourceContent
|
||||
autoApiService={autoApiService}
|
||||
selectedLang={selectedLang}
|
||||
resourceId={tables.find((table) => table.id === Number(id))?.name}
|
||||
resources={resources}
|
||||
definitions={jsonSchema.definitions}
|
||||
paths={jsonSchema.paths}
|
||||
showApiKey={showApiKey.key}
|
||||
refreshDocs={async () => await refetch()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
|
||||
<h3 className="text-lg">Building docs ...</h3>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SidePanel.Content>
|
||||
</div>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export default APIDocumentationPanel
|
||||
+1
-1
@@ -127,7 +127,7 @@ const ForeignRowSelector = ({
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center justify-between my-2 mx-3">
|
||||
<div className="flex items-center">
|
||||
<RefreshButton table={supaTable} />
|
||||
<RefreshButton table={supaTable} isRefetching={isRefetching} />
|
||||
<FilterPopover
|
||||
table={supaTable}
|
||||
filters={params.filter ?? []}
|
||||
|
||||
@@ -18,17 +18,10 @@ import {
|
||||
SupabaseGridRef,
|
||||
SupaTable,
|
||||
} from 'components/grid'
|
||||
import { IconBookOpen, SidePanel } from 'ui'
|
||||
import ActionBar from './SidePanelEditor/ActionBar'
|
||||
import { GeneralContent, ResourceContent } from '../Docs'
|
||||
import { sqlKeys } from 'data/sql/keys'
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query'
|
||||
import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation'
|
||||
import { snakeToCamel } from 'lib/helpers'
|
||||
import { JsonEditValue } from './SidePanelEditor/RowEditor/RowEditor.types'
|
||||
import LangSelector from '../Docs/LangSelector'
|
||||
import GeneratingTypes from '../Docs/GeneratingTypes'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import {
|
||||
ForeignKeyConstraint,
|
||||
@@ -36,6 +29,9 @@ import {
|
||||
} from 'data/database/foreign-key-constraints-query'
|
||||
import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants'
|
||||
import { ForeignRowSelectorProps } from './SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
|
||||
import TwoOptionToggle from 'components/ui/TwoOptionToggle'
|
||||
import ViewDefinition from './ViewDefinition'
|
||||
import APIDocumentationPanel from './APIDocumentationPanel'
|
||||
|
||||
export interface TableGridEditorProps {
|
||||
/** Theme for the editor */
|
||||
@@ -95,30 +91,18 @@ const TableGridEditor = ({
|
||||
onEditForeignKeyColumnValue = noop,
|
||||
onClosePanel = noop,
|
||||
}: TableGridEditorProps) => {
|
||||
const { project } = useProjectContext()
|
||||
const { meta, ui, vault } = useStore()
|
||||
const router = useRouter()
|
||||
const { ref: projectRef, page, id } = useParams()
|
||||
const { ref: projectRef, id } = useParams()
|
||||
const gridRef = useRef<SupabaseGridRef>(null)
|
||||
|
||||
const tables = meta.tables.list()
|
||||
|
||||
const { data: settings } = useProjectApiQuery({ projectRef: projectRef })
|
||||
|
||||
const autoApiService = {
|
||||
...settings?.autoApiService,
|
||||
endpoint: `${settings?.autoApiService.protocol ?? 'https'}://${
|
||||
settings?.autoApiService.endpoint ?? '-'
|
||||
}`,
|
||||
}
|
||||
const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' }
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const isVaultEnabled = useFlag('vaultExtension')
|
||||
const [encryptedColumns, setEncryptedColumns] = useState([])
|
||||
const [apiPreviewPanelOpen, setApiPreviewPanelOpen] = useState(false)
|
||||
|
||||
const [selectedLang, setSelectedLang] = useState<any>('js')
|
||||
const [showApiKey, setShowApiKey] = useState<any>(DEFAULT_KEY)
|
||||
// When rendering a view in the grid
|
||||
const [selectedView, setSelectedView] = useState<'data' | 'definition'>('data')
|
||||
|
||||
const isReadOnly =
|
||||
!checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') &&
|
||||
@@ -188,37 +172,7 @@ const TableGridEditor = ({
|
||||
},
|
||||
})
|
||||
|
||||
function getResourcesFromJsonSchema(value: any) {
|
||||
const { paths } = value || {}
|
||||
const functionPath = 'rpc/'
|
||||
let resources: any = {}
|
||||
|
||||
Object.entries(paths || []).forEach(([name, val]) => {
|
||||
let trimmed = name.slice(1)
|
||||
let id = trimmed.replace(functionPath, '')
|
||||
let displayName = id.replace(/_/g, ' ')
|
||||
let camelCase = snakeToCamel(id)
|
||||
let enriched = { id, displayName, camelCase }
|
||||
if (!trimmed.length) return
|
||||
else resources[id] = enriched
|
||||
})
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
const apiService = settings?.autoApiService
|
||||
const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
|
||||
? apiService.defaultApiKey
|
||||
: undefined
|
||||
|
||||
const {
|
||||
data: jsonSchema,
|
||||
error: jsonSchemaError,
|
||||
refetch,
|
||||
} = useProjectJsonSchemaQuery({ projectRef })
|
||||
if (jsonSchemaError) console.error('jsonSchemaError', jsonSchemaError)
|
||||
|
||||
const resources = getResourcesFromJsonSchema(jsonSchema)
|
||||
const { refetch } = useProjectJsonSchemaQuery({ projectRef })
|
||||
const refreshDocs = async () => await refetch()
|
||||
|
||||
const { data } = useForeignKeyConstraintsQuery({
|
||||
@@ -364,6 +318,11 @@ const TableGridEditor = ({
|
||||
})
|
||||
}
|
||||
|
||||
/** [Joshen] We're going to need to refactor SupabaseGrid eventually to make the code here more readable
|
||||
* For context we previously built the SupabaseGrid as a reusable npm component, but eventually decided
|
||||
* to just integrate it directly into the dashboard. The header, and body (+footer) should be decoupled.
|
||||
*/
|
||||
|
||||
return (
|
||||
<>
|
||||
<SupabaseGrid
|
||||
@@ -377,14 +336,24 @@ const TableGridEditor = ({
|
||||
table={gridTable}
|
||||
refreshDocs={refreshDocs}
|
||||
headerActions={
|
||||
canEditViaTableEditor && (
|
||||
isViewSelected ? (
|
||||
<div>
|
||||
<TwoOptionToggle
|
||||
width={75}
|
||||
options={['definition', 'data']}
|
||||
activeOption={selectedView}
|
||||
borderOverride="border-gray-500"
|
||||
onClickOption={setSelectedView}
|
||||
/>
|
||||
</div>
|
||||
) : canEditViaTableEditor ? (
|
||||
<GridHeaderActions
|
||||
table={selectedTable as PostgresTable}
|
||||
apiPreviewPanelOpen={apiPreviewPanelOpen}
|
||||
setApiPreviewPanelOpen={setApiPreviewPanelOpen}
|
||||
refreshDocs={refreshDocs}
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
onAddColumn={onAddColumn}
|
||||
onEditColumn={onSelectEditColumn}
|
||||
@@ -396,7 +365,21 @@ const TableGridEditor = ({
|
||||
onSqlQuery={onSqlQuery}
|
||||
onExpandJSONEditor={onExpandJSONEditor}
|
||||
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
|
||||
/>
|
||||
showCustomChildren={isViewSelected && selectedView === 'definition'}
|
||||
customHeader={
|
||||
isViewSelected && selectedView === 'definition' ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>
|
||||
SQL Definition of <code className="text-sm">{selectedTable.name}</code>{' '}
|
||||
</p>
|
||||
<p className="text-scale-1000 text-sm">(Read only)</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{isViewSelected && <ViewDefinition name={selectedTable.name} />}
|
||||
</SupabaseGrid>
|
||||
|
||||
{!isUndefined(selectedSchema) && (
|
||||
<SidePanelEditor
|
||||
selectedSchema={selectedSchema}
|
||||
@@ -416,81 +399,10 @@ const TableGridEditor = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<SidePanel
|
||||
key="WrapperTableEditor"
|
||||
size="xxlarge"
|
||||
<APIDocumentationPanel
|
||||
visible={apiPreviewPanelOpen}
|
||||
onCancel={() => setApiPreviewPanelOpen(false)}
|
||||
header={
|
||||
<span className="flex items-center gap-2">
|
||||
<IconBookOpen size="tiny" />
|
||||
API
|
||||
</span>
|
||||
}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
backButtonLabel="Close"
|
||||
hideApply={true}
|
||||
formId="wrapper-table-editor-form"
|
||||
closePanel={() => setApiPreviewPanelOpen(false)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="Docs Docs--table-editor">
|
||||
<SidePanel.Content>
|
||||
{jsonSchemaError ? (
|
||||
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
|
||||
<div className="text-scale-1000">
|
||||
<p>Error connecting to API</p>
|
||||
<p>{`${jsonSchemaError}`}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{jsonSchema ? (
|
||||
<>
|
||||
<div className="sticky top-0 z-10 bg-scale-100 dark:bg-scale-300">
|
||||
<LangSelector
|
||||
selectedLang={selectedLang}
|
||||
setSelectedLang={setSelectedLang}
|
||||
showApiKey={showApiKey}
|
||||
setShowApiKey={setShowApiKey}
|
||||
apiKey={anonKey}
|
||||
autoApiService={autoApiService}
|
||||
/>
|
||||
</div>
|
||||
<GeneralContent
|
||||
autoApiService={autoApiService}
|
||||
selectedLang={selectedLang}
|
||||
showApiKey={true}
|
||||
page={page}
|
||||
/>
|
||||
|
||||
<GeneratingTypes selectedLang={selectedLang} />
|
||||
|
||||
{jsonSchema?.definitions && (
|
||||
<ResourceContent
|
||||
autoApiService={autoApiService}
|
||||
selectedLang={selectedLang}
|
||||
resourceId={tables.find((table) => table.id === Number(id))?.name}
|
||||
resources={resources}
|
||||
definitions={jsonSchema.definitions}
|
||||
paths={jsonSchema.paths}
|
||||
showApiKey={showApiKey.key}
|
||||
refreshDocs={refreshDocs}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
|
||||
<h3 className="text-lg">Building docs ...</h3>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SidePanel.Content>
|
||||
</div>
|
||||
</SidePanel>
|
||||
onClose={() => setApiPreviewPanelOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useRef } from 'react'
|
||||
import Editor from '@monaco-editor/react'
|
||||
import { timeout } from 'lib/helpers'
|
||||
import { useStore } from 'hooks'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useViewDefinitionQuery } from 'data/database/view-definition-query'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
|
||||
export interface ViewDefinitionProps {
|
||||
name: string
|
||||
}
|
||||
|
||||
const ViewDefinition = ({ name }: ViewDefinitionProps) => {
|
||||
const editorRef = useRef(null)
|
||||
const monacoRef = useRef(null)
|
||||
|
||||
const { ui } = useStore()
|
||||
const { isDarkTheme } = ui
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const { data: definition, isLoading } = useViewDefinitionQuery({
|
||||
name,
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
const handleEditorOnMount = async (editor: any, monaco: any) => {
|
||||
editorRef.current = editor
|
||||
monacoRef.current = monaco
|
||||
|
||||
// add margin above first line
|
||||
editor.changeViewZones((accessor: any) => {
|
||||
accessor.addZone({
|
||||
afterLineNumber: 0,
|
||||
heightInPx: 4,
|
||||
domNode: document.createElement('div'),
|
||||
})
|
||||
})
|
||||
|
||||
// when editor did mount, it will need a delay before focus() works properly
|
||||
await timeout(500)
|
||||
editor?.focus()
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="py-4 space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-grow overflow-y-auto border-t border-scale-400">
|
||||
<Editor
|
||||
className="monaco-editor"
|
||||
theme={isDarkTheme ? 'vs-dark' : 'vs'}
|
||||
onMount={handleEditorOnMount}
|
||||
defaultLanguage="pgsql"
|
||||
defaultValue={definition}
|
||||
path={''}
|
||||
options={{
|
||||
domReadOnly: true,
|
||||
readOnly: true,
|
||||
tabSize: 2,
|
||||
fontSize: 13,
|
||||
minimap: { enabled: false },
|
||||
wordWrap: 'on',
|
||||
fixedOverflowWidgets: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ViewDefinition
|
||||
@@ -28,15 +28,13 @@ const FolderContextMenu: FC<Props> = ({ id = '' }) => {
|
||||
<IconDownload size="tiny" />
|
||||
<span className="ml-2 text-xs">Download</span>
|
||||
</Item>
|
||||
{canUpdateFiles && (
|
||||
<>
|
||||
<Separator />
|
||||
<Item onClick={({ props }) => setSelectedItemsToDelete([props.item])}>
|
||||
<IconTrash2 size="tiny" />
|
||||
<span className="ml-2 text-xs">Delete</span>
|
||||
</Item>
|
||||
</>
|
||||
)}
|
||||
{canUpdateFiles && [
|
||||
<Separator />,
|
||||
<Item onClick={({ props }) => setSelectedItemsToDelete([props.item])}>
|
||||
<IconTrash2 size="tiny" />
|
||||
<span className="ml-2 text-xs">Delete</span>
|
||||
</Item>,
|
||||
]}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const TwoOptionToggle: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative border ${borderOverride} rounded-md h-8`}
|
||||
className={`relative border ${borderOverride} rounded-md h-7`}
|
||||
style={{ padding: 1, width: (width + 1) * 2 }}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { UseQueryOptions } from '@tanstack/react-query'
|
||||
import { useCallback } from 'react'
|
||||
import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query'
|
||||
|
||||
type getViewDefinition = {
|
||||
name?: string
|
||||
}
|
||||
|
||||
type ViewDefinition = {
|
||||
id: number
|
||||
constraint_name: string
|
||||
deletion_action: string
|
||||
source_id: string
|
||||
source_schema: string
|
||||
source_table: string
|
||||
source_columns: string
|
||||
target_id: string
|
||||
target_schema: string
|
||||
target_table: string
|
||||
target_columns: string
|
||||
}
|
||||
|
||||
export const getViewDefinitionQuery = ({ name }: getViewDefinition) => {
|
||||
const sql = /* SQL */ `
|
||||
select pg_get_viewdef('${name}', true) as definition
|
||||
`.trim()
|
||||
return sql
|
||||
}
|
||||
|
||||
export type ViewDefinitionVariables = getViewDefinition & {
|
||||
projectRef?: string
|
||||
connectionString?: string
|
||||
}
|
||||
|
||||
export type ViewDefinitionData = any
|
||||
export type ViewDefinitionError = unknown
|
||||
|
||||
export const useViewDefinitionQuery = <TData extends ViewDefinitionData = ViewDefinitionData>(
|
||||
{ projectRef, connectionString, name }: ViewDefinitionVariables,
|
||||
options: UseQueryOptions<ExecuteSqlData, ViewDefinitionError, TData> = {}
|
||||
) => {
|
||||
return useExecuteSqlQuery(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: getViewDefinitionQuery({ name }),
|
||||
queryKey: ['view-definition', name],
|
||||
},
|
||||
{
|
||||
select(data) {
|
||||
return data.result[0].definition.trim()
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const useViewDefinitionQueryPrefetch = () => {
|
||||
const prefetch = useExecuteSqlPrefetch()
|
||||
|
||||
return useCallback(
|
||||
({ projectRef, connectionString, name }: ViewDefinitionVariables) =>
|
||||
prefetch({
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: getViewDefinitionQuery({ name }),
|
||||
queryKey: ['view-definition', name],
|
||||
}),
|
||||
[prefetch]
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user