Files
2026-01-02 15:01:06 +10:00

295 lines
9.5 KiB
TypeScript

import { useParams } from 'common'
import { AlertCircle } from 'lucide-react'
import Link from 'next/link'
import { useMemo, useState } from 'react'
import { GeneralContent } from 'components/interfaces/Docs/GeneralContent'
import { ResourceContent } from 'components/interfaces/Docs/ResourceContent'
import { RpcContent } from 'components/interfaces/Docs/RpcContent'
import { generateDocsMenu } from 'components/layouts/DocsLayout/DocsLayout.utils'
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query'
import { useOpenAPISpecQuery } from 'data/open-api/api-spec-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from 'lib/constants'
import { snakeToCamel } from 'lib/helpers'
import { AlertTitle_Shadcn_, Alert_Shadcn_, cn } from 'ui'
import { ShimmeringLoader } from 'ui-patterns'
import { LangSelector } from '../../Docs/LangSelector'
export const DataApiDocsTab = () => {
const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' }
const { ref: projectRef, page, resource, rpc } = useParams()
const { data: project } = useSelectedProjectQuery()
const { projectAuthAll: authEnabled } = useIsFeatureEnabled(['project_auth:all'])
const isPaused = project?.status === PROJECT_STATUS.INACTIVE
const [selectedLang, setSelectedLang] = useState<'js' | 'bash'>('js')
const [selectedApikey, setSelectedApiKey] = useState<any>(DEFAULT_KEY)
const { data: config, isPending: isConfigLoading } = useProjectPostgrestConfigQuery({
projectRef,
})
const isEnabled = !!config?.db_schema?.trim()
const { data: openApiSpec } = useOpenAPISpecQuery(
{ projectRef },
{
enabled: !!projectRef && !isPaused && isEnabled,
}
)
const tableNames = useMemo(
() => (openApiSpec?.tables ?? []).map((table: any) => table.name),
[openApiSpec]
)
const functionNames = useMemo(
() => (openApiSpec?.functions ?? []).map((fn: any) => fn.name),
[openApiSpec]
)
const activePage = useMemo(() => {
if (!page && !resource && !rpc) return 'introduction'
return (page || rpc || resource) as string
}, [page, resource, rpc])
const docsBasePath = projectRef ? `/project/${projectRef}/integrations/data_api/docs` : undefined
const menu = useMemo(() => {
if (!projectRef) return []
return generateDocsMenu(projectRef, tableNames, functionNames, { authEnabled }, docsBasePath)
}, [projectRef, tableNames, functionNames, authEnabled, docsBasePath])
if (isConfigLoading) {
return (
<div className="flex w-full bg-surface-100 flex-1 items-stretch p-10">
<ShimmeringLoader />
</div>
)
}
if (!isEnabled) {
return (
<div className="flex w-full bg-surface-100 flex-1 items-center justify-center p-10">
<Alert_Shadcn_ className="max-w-md">
<AlertCircle size={16} />
<AlertTitle_Shadcn_>Data API is disabled</AlertTitle_Shadcn_>
<div className="text-foreground-light text-sm mt-2">
You must enable the Data API in the{' '}
<Link
href={`/project/${projectRef}/integrations/data_api/overview`}
className="text-primary hover:underline"
>
Overview
</Link>{' '}
tab to view the documentation.
</div>
</Alert_Shadcn_>
</div>
)
}
return (
<div className="flex w-full bg-surface-100 flex-1 items-stretch">
<aside className="hidden lg:flex flex-col gap-y-6 w-60 shrink-0 p-10">
<LangSelector
selectedLang={selectedLang}
selectedApiKey={selectedApikey}
setSelectedLang={(lang: 'js' | 'bash') => setSelectedLang(lang)}
setSelectedApiKey={setSelectedApiKey}
/>
<DocsMenu activePage={activePage} menu={menu} />
</aside>
<div className="flex-1 min-w-0 relative">
<DocView
selectedLang={selectedLang}
setSelectedLang={setSelectedLang}
selectedApikey={selectedApikey}
setSelectedApiKey={setSelectedApiKey}
/>
</div>
</div>
)
}
const DocView = ({
selectedLang,
selectedApikey,
}: {
selectedLang: 'js' | 'bash'
setSelectedLang: (lang: 'js' | 'bash') => void
selectedApikey: any
setSelectedApiKey: (key: any) => void
}) => {
const functionPath = 'rpc/'
const { ref: projectRef, page, resource, rpc } = useParams()
const { data: settings, error: settingsError } = useProjectSettingsV2Query({ projectRef })
const {
data: jsonSchema,
error: jsonSchemaError,
isPending: isLoading,
refetch,
} = useProjectJsonSchemaQuery({ projectRef })
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const refreshDocs = async () => await refetch()
const protocol = settings?.app_config?.protocol ?? 'https'
const hostEndpoint = settings?.app_config?.endpoint
const endpoint =
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain?.hostname}`
: `${protocol}://${hostEndpoint ?? '-'}`
const { paths } = jsonSchema || {}
const PAGE_KEY: any = resource || rpc || page || 'index'
const { resources, rpcs } = Object.entries(paths || {}).reduce(
(a, [name]) => {
const trimmedName = name.slice(1)
const id = trimmedName.replace(functionPath, '')
const displayName = id.replace(/_/g, ' ')
const camelCase = snakeToCamel(id)
const enriched = { id, displayName, camelCase }
if (!trimmedName.length) {
return a
}
return {
resources: {
...a.resources,
...(!trimmedName.includes(functionPath)
? {
[id]: enriched,
}
: {}),
},
rpcs: {
...a.rpcs,
...(trimmedName.includes(functionPath)
? {
[id]: enriched,
}
: {}),
},
}
},
{ resources: {}, rpcs: {} }
)
if (settingsError || jsonSchemaError) {
return (
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
<p className="text-foreground-light">
<p>Error connecting to API</p>
<p>{`${settingsError || jsonSchemaError}`}</p>
</p>
</div>
)
}
if (isLoading || !settings || !jsonSchema) {
return (
<div className="w-full h-full overflow-y-auto flex flex-col py-10 space-y-2">
<ShimmeringLoader className="h-2 w-24" />
<ShimmeringLoader className="h-2 w-96" />
<ShimmeringLoader className="h-2 w-96" />
<ShimmeringLoader className="h-2 w-96" />
</div>
)
}
return (
<div className="w-full h-full overflow-y-auto flex flex-col" key={PAGE_KEY}>
<div className="flex-1 flex flex-col">
{resource ? (
<ResourceContent
apiEndpoint={endpoint}
selectedLang={selectedLang}
resourceId={resource}
resources={resources}
showApiKey={selectedApikey.key}
refreshDocs={refreshDocs}
/>
) : rpc ? (
<RpcContent
selectedLang={selectedLang}
rpcId={rpc}
paths={paths}
rpcs={rpcs}
showApiKey={selectedApikey.key}
refreshDocs={refreshDocs}
/>
) : (
<GeneralContent selectedLang={selectedLang} showApiKey={selectedApikey.key} page={page} />
)}
</div>
</div>
)
}
const DocsMenu = ({ menu, activePage }: { menu: ProductMenuGroup[]; activePage?: string }) => {
return (
<nav className="space-y-6 text-xs">
{menu.map((group, idx) => (
<div key={group.key || group.title || idx}>
{group.title && (
<div className="heading-meta mb-2 text-foreground-lighter">{group.title}</div>
)}
<div className="space-y-2">
{group.items.map((item) => {
const isActive = item.pages
? item.pages.includes(activePage ?? '')
: activePage === item.key
const isDisabled = !!item.disabled
const content = (
<span
className={cn(
'flex items-center',
isActive ? 'text-foreground' : 'text-foreground-light hover:text-foreground',
isDisabled && 'pointer-events-none opacity-50'
)}
>
<span className="truncate">{item.name}</span>
{item.rightIcon && (
<span className="ml-auto text-foreground-lighter">{item.rightIcon}</span>
)}
</span>
)
if (item.isExternal) {
return (
<a
key={item.key}
href={item.url}
target="_blank"
rel="noreferrer"
className="block"
>
{content}
</a>
)
}
return (
<Link key={item.key} href={item.url} className="block">
{content}
</Link>
)
})}
</div>
</div>
))}
</nav>
)
}