mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## What kind of change does this PR introduce? nitpick ui bug fix in docs of the collapsible details component + commentary ## What is the current behavior? 1. data / response / notes collapsibles on reference pages grow taller when you expand them + also get double padding: the panel pads the content, and the code block pads itself again inside it 2. commentary that follows a snippet in the example column renders unstyled, since that column has no prose context. it comes out larger than the description column and inline code stays as plain text ## What is the new behavior? ├ adds `CodeBlock` a `compact` variant that get appropriate styling when used within collapsible | state | preview | | -------|------| | before | <video src="https://github.com/user-attachments/assets/8b70e1c6-9e0e-4371-a2b2-eb4a3d580247" /> | | after | <video src="https://github.com/user-attachments/assets/1e5cdd46-ce7d-4d2d-ac6a-6da680df37e5" /> | ├ wraps example column in prose so trailing commentary matches the description font size + inline code styling | state | preview | | -------|------| | before | <img width="1142" height="404" alt="image" src="https://github.com/user-attachments/assets/8d0f1ac0-087d-47f7-b35d-b8798d589fdb" /> | | after | <img width="1142" height="404" alt="image" src="https://github.com/user-attachments/assets/b9ba4202-a8dd-407d-b535-f34d83f5b24b" /> | ## Test - visits `/docs/reference/javascript/using-filters-gt` - visits `/docs/reference/server/middleware-withsupabaseadminclient` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Documentation code blocks can now be displayed in a compact format without borders or extra spacing. - Reference documentation supports customizing code block presentation. - **Style** - Improved formatting for example content, including prose wrapping, spacing, and code block margins. - Refined collapsible documentation sections with clearer spacing, hover and focus states, and open/close animations. - Code-only collapsible content now uses a more compact layout, while text content receives consistent typography and padding. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
585 lines
20 KiB
TypeScript
585 lines
20 KiB
TypeScript
import ApiSchema from '~/components/ApiSchema'
|
|
import { clientSdkIds, REFERENCES } from '~/content/navigation.references'
|
|
import {
|
|
getApiEndpointById,
|
|
getCliSpec,
|
|
getFlattenedSections,
|
|
getFunctionsList,
|
|
getSelfHostedApiEndpointById,
|
|
getTypeSpec,
|
|
} from '~/features/docs/Reference.generated.singleton'
|
|
import { getRefMarkdown, MDXRemoteRefs } from '~/features/docs/Reference.mdx'
|
|
import type { MethodTypes, VariableTypes } from '~/features/docs/Reference.typeSpec'
|
|
import { formatMethodSignature } from '~/features/docs/Reference.typeSpec'
|
|
import {
|
|
ApiOperationRequestBodyDetails,
|
|
ApiSchemaParamDetails,
|
|
CollapsibleDetails,
|
|
FnParameterDetails,
|
|
RefSubLayout,
|
|
ReturnTypeDetails,
|
|
StickyHeader,
|
|
} from '~/features/docs/Reference.ui'
|
|
import type { AbbrevApiReferenceSection } from '~/features/docs/Reference.utils'
|
|
import { normalizeMarkdown } from '~/features/docs/Reference.utils'
|
|
import { CodeBlock } from '~/features/ui/CodeBlock/CodeBlock'
|
|
import { isFeatureEnabled } from 'common'
|
|
import { Fragment } from 'react'
|
|
import ReactMarkdown from 'react-markdown'
|
|
import { Badge, cn, Tabs, TabsContent, TabsList, TabsTrigger } from 'ui'
|
|
|
|
import { type IApiEndPoint } from './Reference.api.utils'
|
|
import { RefInternalLink } from './Reference.navigation.client'
|
|
import { ApiOperationBodySchemeSelector } from './Reference.ui.client'
|
|
|
|
type RefSectionsProps = {
|
|
libraryId: string
|
|
version: string
|
|
}
|
|
|
|
async function RefSections({ libraryId, version }: RefSectionsProps) {
|
|
let flattenedSections = await getFlattenedSections(libraryId, version)
|
|
if (flattenedSections) {
|
|
flattenedSections = trimIntro(flattenedSections)
|
|
}
|
|
|
|
if (!isFeatureEnabled('sdk:auth') && clientSdkIds.includes(libraryId)) {
|
|
flattenedSections = flattenedSections?.filter(
|
|
(section) =>
|
|
'product' in section && section.product !== 'auth' && section.product !== 'auth-admin'
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col my-16 gap-16">
|
|
{(flattenedSections || [])
|
|
.filter((section) => section.type !== 'category')
|
|
.map((section, idx) => (
|
|
<Fragment key={`${section.id}-${idx}`}>
|
|
<SectionDivider />
|
|
<SectionSwitch libraryId={libraryId} version={version} section={section} />
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function trimIntro(sections: Array<AbbrevApiReferenceSection>) {
|
|
const hasIntro = sections[0]?.type === 'markdown' && sections[0]?.slug === 'introduction'
|
|
if (hasIntro) {
|
|
return sections.slice(1)
|
|
}
|
|
return sections
|
|
}
|
|
|
|
function SectionDivider() {
|
|
return <hr />
|
|
}
|
|
|
|
type SectionSwitchProps = {
|
|
libraryId: string
|
|
version: string
|
|
section: AbbrevApiReferenceSection
|
|
}
|
|
|
|
export function SectionSwitch({ libraryId, version, section }: SectionSwitchProps) {
|
|
const libPath = REFERENCES[libraryId.replaceAll('-', '_')].libPath
|
|
const allAvailableVersions = REFERENCES[libraryId.replaceAll('-', '_')].versions
|
|
const isLatestVersion = allAvailableVersions.length === 0 || version === allAvailableVersions[0]
|
|
|
|
const sectionLink = `/docs/reference/${libPath}/${isLatestVersion ? '' : `${version}/`}${section.slug}`
|
|
|
|
switch (section.type) {
|
|
case 'markdown':
|
|
return (
|
|
<MarkdownSection
|
|
libPath={libPath}
|
|
version={version}
|
|
isLatestVersion={isLatestVersion}
|
|
link={sectionLink}
|
|
section={section}
|
|
/>
|
|
)
|
|
case 'function':
|
|
return (
|
|
<FunctionSection
|
|
sdkId={libraryId}
|
|
version={version}
|
|
link={sectionLink}
|
|
section={section}
|
|
useTypeSpec={REFERENCES[libraryId].typeSpec}
|
|
/>
|
|
)
|
|
case 'cli-command':
|
|
return <CliCommandSection link={sectionLink} section={section} />
|
|
case 'operation':
|
|
return <ApiEndpointSection link={sectionLink} section={section} />
|
|
case 'self-hosted-operation':
|
|
return <ApiEndpointSection servicePath={libraryId} link={sectionLink} section={section} />
|
|
default:
|
|
console.error(`Unhandled type in reference sections: ${section.type}`)
|
|
return null
|
|
}
|
|
}
|
|
|
|
interface MarkdownSectionProps {
|
|
libPath: string
|
|
version: string
|
|
isLatestVersion: boolean
|
|
link: string
|
|
section: AbbrevApiReferenceSection
|
|
}
|
|
|
|
async function MarkdownSection({
|
|
libPath,
|
|
version,
|
|
isLatestVersion,
|
|
link,
|
|
section,
|
|
}: MarkdownSectionProps) {
|
|
const content = await getRefMarkdown(
|
|
section.meta?.shared
|
|
? `shared/${section.id}`
|
|
: `${libPath}/${isLatestVersion ? '' : `${version}/`}${section.id}`
|
|
)
|
|
|
|
return (
|
|
<RefSubLayout.EducationSection link={link} {...section}>
|
|
<StickyHeader {...section} />
|
|
<MDXRemoteRefs source={content} />
|
|
</RefSubLayout.EducationSection>
|
|
)
|
|
}
|
|
|
|
interface CliCommandSectionProps {
|
|
link: string
|
|
section: AbbrevApiReferenceSection
|
|
}
|
|
|
|
async function CliCommandSection({ link, section }: CliCommandSectionProps) {
|
|
const cliSpec = await getCliSpec()
|
|
const command = ((cliSpec as any).commands ?? []).find((cmd) => cmd.id === section.id)
|
|
|
|
if (!command) return null
|
|
|
|
return (
|
|
<RefSubLayout.Section columns="double" link={link} {...section}>
|
|
<StickyHeader title={command.title} className="col-span-full" monoFont={true} />
|
|
<div className="w-full min-w-0 prose wrap-break-word mb-8">
|
|
{command.description && <ReactMarkdown>{command.description}</ReactMarkdown>}
|
|
{command.usage && (
|
|
<div className="mb-8">
|
|
<h3 className="mb-2 text-base text-foreground">Usage</h3>
|
|
<CodeBlock lang="bash">{command.usage}</CodeBlock>
|
|
</div>
|
|
)}
|
|
{(command.subcommands ?? []).length > 0 && (
|
|
<>
|
|
<h3 className="mb-3 text-base text-foreground">Subcommands</h3>
|
|
<ul className="prose">
|
|
{command.subcommands.map((subcommand, index) => {
|
|
const subcommandDetails = (cliSpec as any).commands.find(
|
|
(cmd) => cmd.id === subcommand
|
|
)
|
|
if (!subcommandDetails) return null
|
|
return (
|
|
<li key={index} className="ml-4">
|
|
<RefInternalLink
|
|
href={`/reference/cli/${subcommandDetails.id}`}
|
|
sectionSlug={subcommandDetails.id}
|
|
>
|
|
{subcommandDetails.title}
|
|
</RefInternalLink>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</>
|
|
)}
|
|
{(command.flags ?? []).length > 0 && (
|
|
<>
|
|
<h3 className="mb-3 text-base text-foreground">Flags</h3>
|
|
<ul className="not-prose">
|
|
{command.flags.map((flag, index) => (
|
|
<li key={index} className="border-t last-of-type:border-b py-5 flex flex-col gap-3">
|
|
<div className="flex flex-wrap items-baseline gap-3">
|
|
<span className="font-mono text-sm font-medium text-foreground">
|
|
{flag.name}
|
|
</span>
|
|
{flag.required ? (
|
|
<Badge variant="warning">Required</Badge>
|
|
) : (
|
|
<Badge variant="default">Optional</Badge>
|
|
)}
|
|
</div>
|
|
{flag.description && (
|
|
<div className="prose wrap-break-word text-sm">
|
|
<ReactMarkdown>{flag.description}</ReactMarkdown>
|
|
</div>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="overflow-auto">
|
|
{'examples' in command &&
|
|
Array.isArray(command.examples) &&
|
|
command.examples.length > 0 && (
|
|
<Tabs defaultValue={command.examples[0].id}>
|
|
<TabsList className="flex-wrap gap-2 border-0">
|
|
{command.examples.map((example) => (
|
|
<TabsTrigger
|
|
key={example.id}
|
|
value={example.id}
|
|
className={cn(
|
|
'px-2.5 py-1 rounded-full',
|
|
'border-0 bg-surface-200 hover:bg-surface-300',
|
|
'text-xs text-foreground-lighter',
|
|
// Undoing styles from primitive component
|
|
'data-[state=active]:border-0 data-[state=active]:shadow-0',
|
|
'data-[state=active]:bg-foreground data-[state=active]:text-background',
|
|
'transition'
|
|
)}
|
|
>
|
|
{example.name}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
{command.examples.map((example) => (
|
|
<TabsContent key={example.id} value={example.id}>
|
|
<CodeBlock lang="bash" className="mb-6">
|
|
{example.code}
|
|
</CodeBlock>
|
|
<h3 className="text-foreground-lighter text-sm mb-2">Response</h3>
|
|
<CodeBlock lang="txt">{example.response}</CodeBlock>
|
|
</TabsContent>
|
|
))}
|
|
</Tabs>
|
|
)}
|
|
</div>
|
|
</RefSubLayout.Section>
|
|
)
|
|
}
|
|
|
|
interface ApiEndpointSectionProps {
|
|
link: string
|
|
section: AbbrevApiReferenceSection
|
|
servicePath?: string
|
|
}
|
|
|
|
async function ApiEndpointSection({ link, section, servicePath }: ApiEndpointSectionProps) {
|
|
const endpointDetails = servicePath
|
|
? await getSelfHostedApiEndpointById(servicePath, section.id)
|
|
: await getApiEndpointById(section.id)
|
|
if (!endpointDetails) return null
|
|
|
|
const endpointFgaPermissionGroups = endpointDetails['x-fga-permissions'] ?? []
|
|
const pathParameters = (endpointDetails.parameters ?? []).filter((param) => param.in === 'path')
|
|
const queryParameters = (endpointDetails.parameters ?? []).filter((param) => param.in === 'query')
|
|
const bodyParameters =
|
|
endpointDetails.requestBody ??
|
|
(endpointDetails.parameters ?? [])
|
|
.filter((param) => param.in === 'body')
|
|
.map(
|
|
(bodyParam) =>
|
|
({
|
|
content: {
|
|
'application/json': {
|
|
schema: bodyParam.schema,
|
|
},
|
|
},
|
|
}) satisfies IApiEndPoint['requestBody']
|
|
)[0]
|
|
|
|
const first2xxCode = Object.keys(endpointDetails.responses ?? {})
|
|
.filter((code) => code.startsWith('2'))
|
|
.sort()[0]
|
|
|
|
return (
|
|
<RefSubLayout.Section columns="double" link={link} {...section}>
|
|
<StickyHeader
|
|
title={
|
|
<>
|
|
{endpointDetails.summary}
|
|
{endpointDetails.deprecated && (
|
|
<Badge variant="warning" className="ml-2">
|
|
deprecated
|
|
</Badge>
|
|
)}
|
|
</>
|
|
}
|
|
className="col-span-full"
|
|
/>
|
|
<div className="flex flex-col gap-12">
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={cn(
|
|
'uppercase text-sm whitespace-nowrap bg-foreground text-background rounded-full font-mono font-medium px-2 py-0.5',
|
|
endpointDetails.deprecated && 'line-through'
|
|
)}
|
|
>
|
|
{endpointDetails.method}
|
|
</span>
|
|
<code
|
|
className={cn(
|
|
'text-foreground-lighter break-all',
|
|
endpointDetails.deprecated && 'line-through'
|
|
)}
|
|
>
|
|
{endpointDetails.path}
|
|
</code>
|
|
</div>
|
|
{endpointDetails.description && (
|
|
<div className="prose wrap-break-word mb-8">
|
|
<ReactMarkdown>{endpointDetails.description}</ReactMarkdown>
|
|
</div>
|
|
)}
|
|
{endpointDetails['x-oauth-scope'] && (
|
|
<section>
|
|
<h3 className="mb-3 text-base text-foreground">OAuth scopes</h3>
|
|
<ul>
|
|
<li key={endpointDetails['x-oauth-scope']} className="list-['-'] ml-2 pl-2">
|
|
<span className="font-mono text-sm font-medium text-foreground">
|
|
{endpointDetails['x-oauth-scope']}
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
)}
|
|
{endpointDetails['x-allowed-plans'] && (
|
|
<section>
|
|
<h3 className="mb-3 text-base text-foreground">
|
|
This endpoint is only available on the following plans:
|
|
</h3>
|
|
<ul>
|
|
{endpointDetails['x-allowed-plans'].map((plan) => (
|
|
<li key={plan} className="list-['-'] ml-2 pl-2">
|
|
<span className="font-mono text-sm font-medium text-foreground">{plan}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
)}
|
|
{endpointFgaPermissionGroups.length > 0 && (
|
|
<section>
|
|
<h3 className="mb-3 text-base text-foreground">
|
|
The fine-grained token must include the following permissions to access this endpoint:
|
|
</h3>
|
|
<ul>
|
|
{endpointFgaPermissionGroups.map((group, groupIndex) => (
|
|
<Fragment key={groupIndex}>
|
|
{groupIndex > 0 && (
|
|
<li className="my-2 text-foreground-lighter text-sm italic">or</li>
|
|
)}
|
|
{group.map((perm, permIndex) => (
|
|
<li key={permIndex} className="list-['-'] ml-2 pl-2">
|
|
<span className="font-mono text-sm font-medium text-foreground">{perm}</span>
|
|
</li>
|
|
))}
|
|
</Fragment>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
)}
|
|
{pathParameters.length > 0 && (
|
|
<section>
|
|
<h3 className="mb-3 text-base text-foreground">Path parameters</h3>
|
|
<ul>
|
|
{pathParameters.map((param, index) => (
|
|
<ApiSchemaParamDetails key={index} param={param} />
|
|
))}
|
|
</ul>
|
|
</section>
|
|
)}
|
|
{queryParameters.length > 0 && (
|
|
<section>
|
|
<h3 className="mb-3 text-base text-foreground">Query parameters</h3>
|
|
<ul>
|
|
{queryParameters.map((param, index) => (
|
|
<ApiSchemaParamDetails key={index} param={param} />
|
|
))}
|
|
</ul>
|
|
</section>
|
|
)}
|
|
{bodyParameters && (
|
|
<section>
|
|
<ApiOperationBodySchemeSelector requestBody={bodyParameters} className="mb-3" />
|
|
<ApiOperationRequestBodyDetails requestBody={bodyParameters} />
|
|
</section>
|
|
)}
|
|
{endpointDetails.responses && (
|
|
<section>
|
|
<h3 className="mb-3 text-base text-foreground">Response codes</h3>
|
|
<ul>
|
|
{Object.keys(endpointDetails.responses).map((code) => (
|
|
<li key={code} className="list-['-'] ml-2 pl-2">
|
|
<span className="font-mono text-sm font-medium text-foreground">{code}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
)}
|
|
</div>
|
|
{endpointDetails.responses && first2xxCode && (
|
|
<div className="overflow-auto">
|
|
<h3 className="mb-3 text-base text-foreground">{`Response (${first2xxCode})`}</h3>
|
|
<ApiSchema
|
|
id={`${section.id}-2xx-response`}
|
|
schema={
|
|
endpointDetails.responses[first2xxCode].content?.['application/json']?.schema ??
|
|
// @ts-ignore - schema is here in older versions
|
|
endpointDetails.responses[first2xxCode].schema ??
|
|
{}
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
</RefSubLayout.Section>
|
|
)
|
|
}
|
|
|
|
interface FunctionSectionProps {
|
|
sdkId: string
|
|
version: string
|
|
link: string
|
|
section: AbbrevApiReferenceSection
|
|
useTypeSpec: boolean
|
|
}
|
|
|
|
async function FunctionSection({
|
|
sdkId,
|
|
version,
|
|
link,
|
|
section,
|
|
useTypeSpec,
|
|
}: FunctionSectionProps) {
|
|
const fns = await getFunctionsList(sdkId, version)
|
|
|
|
const fn = fns?.find((fn) => fn.id === section.id)
|
|
if (!fn) return null
|
|
|
|
let types: MethodTypes | VariableTypes | undefined
|
|
if (useTypeSpec && '$ref' in fn) {
|
|
types = await getTypeSpec(sdkId, version, fn['$ref'] as string)
|
|
}
|
|
|
|
const fullDescription = [
|
|
types?.comment?.shortText,
|
|
types?.comment?.text,
|
|
'description' in fn && (fn.description as string),
|
|
'notes' in fn && (fn.notes as string),
|
|
]
|
|
.filter(Boolean)
|
|
.map(normalizeMarkdown)
|
|
.join('\n\n')
|
|
|
|
return (
|
|
<RefSubLayout.Section columns="double" link={link} {...section}>
|
|
<StickyHeader {...section} className="col-span-full" />
|
|
|
|
{/* Display method signature below title */}
|
|
{types && 'params' in types && formatMethodSignature(types) && (
|
|
<div className="col-span-full -mt-2 mb-4">
|
|
<code className="text-sm text-foreground-muted font-mono">
|
|
{formatMethodSignature(types)}
|
|
</code>
|
|
</div>
|
|
)}
|
|
|
|
<div className="overflow-hidden flex flex-col gap-8">
|
|
<div className="prose wrap-break-word text-sm">
|
|
<MDXRemoteRefs source={fullDescription} />
|
|
</div>
|
|
<FnParameterDetails
|
|
parameters={
|
|
'overwriteParams' in fn
|
|
? (fn.overwriteParams as Array<object>).map((overwrittenParams) => ({
|
|
...overwrittenParams,
|
|
__overwritten: true,
|
|
}))
|
|
: 'params' in fn
|
|
? (fn.params as Array<object>).map((param) => ({ ...param, __overwritten: true }))
|
|
: types && 'params' in types
|
|
? types.params
|
|
: undefined
|
|
}
|
|
altParameters={
|
|
types && 'altSignatures' in types
|
|
? types.altSignatures?.map(({ params }) => params)
|
|
: undefined
|
|
}
|
|
className="max-w-[80ch]"
|
|
/>
|
|
{types && 'ret' in types && !!types.ret && <ReturnTypeDetails returnType={types.ret} />}
|
|
</div>
|
|
<div className="overflow-auto">
|
|
{(() => {
|
|
// Prefer YAML examples, fallback to TypeDoc examples
|
|
const yamlExamples =
|
|
'examples' in fn && Array.isArray(fn.examples) && fn.examples.length > 0
|
|
? fn.examples
|
|
: []
|
|
const examples = yamlExamples.length > 0 ? yamlExamples : types?.comment?.examples || []
|
|
|
|
if (examples.length === 0) return null
|
|
|
|
return (
|
|
<Tabs defaultValue={examples[0].id}>
|
|
<TabsList className="flex-wrap gap-2 border-0">
|
|
{examples.map((example) => (
|
|
<TabsTrigger
|
|
key={example.id}
|
|
value={example.id}
|
|
className={cn(
|
|
'px-2.5 py-1 rounded-full',
|
|
'border-0 bg-surface-200 hover:bg-surface-300',
|
|
'text-xs text-foreground-lighter',
|
|
// Undoing styles from primitive component
|
|
'data-[state=active]:border-0 data-[state=active]:shadow-0',
|
|
'data-[state=active]:bg-foreground data-[state=active]:text-background',
|
|
'transition'
|
|
)}
|
|
>
|
|
{example.name}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
{examples.map((example) => (
|
|
<TabsContent key={example.id} value={example.id}>
|
|
<div
|
|
className={cn(
|
|
'prose wrap-break-word max-w-none',
|
|
'[&_.shiki]:!my-0 [&_.shiki:not(:last-child)]:!mb-4',
|
|
'[&_p]:!whitespace-normal'
|
|
)}
|
|
>
|
|
<MDXRemoteRefs source={example.code} />
|
|
</div>
|
|
<div className="flex flex-col gap-2 mt-2">
|
|
{'data' in example && !!example.data?.sql && (
|
|
<CollapsibleDetails title="Data source" content={example.data.sql} />
|
|
)}
|
|
{'response' in example && !!example.response && (
|
|
<CollapsibleDetails title="Response" content={example.response} />
|
|
)}
|
|
{'description' in example && !!example.description && (
|
|
<CollapsibleDetails
|
|
title="Notes"
|
|
content={normalizeMarkdown(example.description)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
))}
|
|
</Tabs>
|
|
)
|
|
})()}
|
|
</div>
|
|
</RefSubLayout.Section>
|
|
)
|
|
}
|
|
|
|
export { RefSections }
|