checkpoint 1

This commit is contained in:
Francesco Sansalvadore committed 2024-09-02 18:24:37 +02:00
1 parent 3fa447d57d
commit 3a4e4ad1ef
26 files changed
+2802 -3

No files matched your search

@@ -0,0 +1,110 @@
import { useParams } from 'common'
import { Badge } from 'ui'
import { useBucketsQuery } from 'data/storage/buckets-query'
import { formatBytes } from 'lib/helpers'
import { useAppStateSnapshot } from 'state/app-state'
import { DOCS_RESOURCE_CONTENT } from '../ProjectAPIDocs.constants'
import ResourceContent from '../ResourceContent'
import type { ContentProps } from './Content.types'
const Bucket = ({ language, apikey, endpoint }: ContentProps) => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
const { data } = useBucketsQuery({ projectRef: ref })
const resource = snap.activeDocsSection[1]
const buckets = data ?? []
const bucket = buckets.find((b) => b.name === resource)
const allowedMimeTypes = bucket?.allowed_mime_types
const maxFileSizeLimit = bucket?.file_size_limit
if (bucket === undefined) return null
return (
<div className="divide-y">
<div className="space-y-1 px-4 py-4">
<div className="flex items-center space-x-2">
<h2 className="text-xl">{bucket.name}</h2>
<Badge variant={bucket.public ? 'warning' : 'default'}>
{bucket.public ? 'Public' : 'Private'}
</Badge>
</div>
<p className="text-sm text-foreground-light">
Allowed MIME types:{' '}
{allowedMimeTypes === null
? 'All types are allowed'
: (allowedMimeTypes ?? []).length === 0
? 'No types are allowed'
: (allowedMimeTypes ?? []).length > 1
? (allowedMimeTypes ?? []).join(', ')
: 'Unknown'}
</p>
<p className="text-sm text-foreground-light">
Max file size limit:{' '}
{maxFileSizeLimit === null ? 'No limit' : `${formatBytes(maxFileSizeLimit)}`}
</p>
</div>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.uploadFile}
codeSnippets={DOCS_RESOURCE_CONTENT.uploadFile.code({
name: resource,
apikey,
endpoint,
})}
/>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.deleteFiles}
codeSnippets={DOCS_RESOURCE_CONTENT.deleteFiles.code({
name: resource,
apikey,
endpoint,
})}
/>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.listFiles}
codeSnippets={DOCS_RESOURCE_CONTENT.listFiles.code({
name: resource,
apikey,
endpoint,
})}
/>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.downloadFile}
codeSnippets={DOCS_RESOURCE_CONTENT.downloadFile.code({
name: resource,
apikey,
endpoint,
})}
/>
{bucket.public ? (
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.retrievePublicURL}
codeSnippets={DOCS_RESOURCE_CONTENT.retrievePublicURL.code({
name: resource,
apikey,
endpoint,
})}
/>
) : (
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.createSignedURL}
codeSnippets={DOCS_RESOURCE_CONTENT.createSignedURL.code({
name: resource,
apikey,
endpoint,
})}
/>
)}
</div>
)
}
export default Bucket
@@ -0,0 +1,6 @@
export interface ContentProps {
showKeys?: boolean
language: 'js' | 'bash'
endpoint?: string
apikey?: string
}
@@ -0,0 +1,20 @@
export const navigateToSection = (key: string) => {
if (typeof window !== 'undefined') {
const el = document.getElementById(key)
if (el) el.scrollIntoView({ behavior: 'smooth' })
}
}
// Removes some auto-generated Postgrest text
// Ideally PostgREST wouldn't add this if there is already a comment
export const tempRemovePostgrestText = (content: string) => {
const postgrestTextPk = `Note:\nThis is a Primary Key.<pk/>`
const postgrestTextFk = `Note:\nThis is a Foreign Key to`
const pkTextPos = content.lastIndexOf(postgrestTextPk)
const fkTextPos = content.lastIndexOf(postgrestTextFk)
let cleansed = content
if (pkTextPos >= 0) cleansed = cleansed.substring(0, pkTextPos)
if (fkTextPos >= 0) cleansed = cleansed.substring(0, fkTextPos)
return cleansed
}
@@ -0,0 +1,41 @@
import { useParams } from 'common'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import { useAppStateSnapshot } from 'state/app-state'
import { DOCS_RESOURCE_CONTENT } from '../ProjectAPIDocs.constants'
import ResourceContent from '../ResourceContent'
import type { ContentProps } from './Content.types'
const Bucket = ({ language, apikey = 'API_KEY', endpoint }: ContentProps) => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
const { data } = useEdgeFunctionsQuery({ projectRef: ref })
const resource = snap.activeDocsSection[1]
const edgeFunctions = data ?? []
const edgeFunction = edgeFunctions.find((fn) => fn.name === resource)
if (edgeFunction === undefined) return null
return (
<div className="divide-y">
<div className="space-y-1 px-4 py-4">
<div className="flex items-center space-x-2">
<h2 className="text-xl">{edgeFunction.name}</h2>
</div>
</div>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.invokeEdgeFunction}
codeSnippets={DOCS_RESOURCE_CONTENT.invokeEdgeFunction.code({
name: resource,
endpoint: `${endpoint}`,
apikey: apikey,
})}
/>
</div>
)
}
export default Bucket
@@ -0,0 +1,16 @@
import ContentSnippet from '../ContentSnippet'
import { DOCS_CONTENT } from '../ProjectAPIDocs.constants'
import type { ContentProps } from './Content.types'
const EdgeFunctions = ({ language }: ContentProps) => {
return (
<>
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.edgeFunctions} />
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.edgeFunctionsPreReq} />
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.createEdgeFunction} />
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.deployEdgeFunction} />
</>
)
}
export default EdgeFunctions
@@ -0,0 +1,69 @@
import Link from 'next/link'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { generateTypes } from 'data/projects/project-type-generation-query'
import { Button, IconDownload, IconExternalLink } from 'ui'
import ContentSnippet from '../ContentSnippet'
import { DOCS_CONTENT } from '../ProjectAPIDocs.constants'
import type { ContentProps } from './Content.types'
const Entities = ({ language }: ContentProps) => {
const { ref } = useParams()
const [isGeneratingTypes, setIsGeneratingTypes] = useState(false)
const onClickGenerateTypes = async () => {
try {
setIsGeneratingTypes(true)
const res = await generateTypes({ ref })
let element = document.createElement('a')
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(res.types))
element.setAttribute('download', 'supabase.ts')
element.style.display = 'none'
document.body.appendChild(element)
element.click()
document.body.removeChild(element)
toast.success(`Successfully generated types! File is being downloaded`)
} catch (error: any) {
toast.error(`Failed to generate types: ${error.message}`)
} finally {
setIsGeneratingTypes(false)
}
}
return (
<>
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.entitiesIntroduction} />
<div>
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.generatingTypes} />
<div className="flex items-center space-x-2 px-4 mt-3">
<Button asChild type="default" icon={<IconExternalLink />}>
<Link
href="https://supabase.com/docs/guides/database/api/generating-types"
target="_blank"
rel="noreferrer"
>
Documentation
</Link>
</Button>
<Button
type="default"
disabled={isGeneratingTypes}
loading={isGeneratingTypes}
icon={<IconDownload strokeWidth={1.5} />}
onClick={onClickGenerateTypes}
>
Generate and download types
</Button>
</div>
<p className="text-xs text-foreground-light px-4 mt-2">
Remember to re-generate and download this file as you make changes to your tables.
</p>
</div>
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.graphql} />
</>
)
}
export default Entities
@@ -0,0 +1,148 @@
import { useParams } from 'common'
import { useEffect } from 'react'
import Table from 'components/to-be-cleaned/Table'
import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query'
import { useAppStateSnapshot } from 'state/app-state'
import { DOCS_RESOURCE_CONTENT } from '../ProjectAPIDocs.constants'
import ResourceContent from '../ResourceContent'
import type { ContentProps } from './Content.types'
import { tempRemovePostgrestText } from './Content.utils'
import LanguageSelector from '../LanguageSelector'
function getColumnType(type: string, format: string) {
// json and jsonb both have type=undefined, so check format instead
if (type === undefined && (format === 'jsonb' || format === 'json')) return 'json'
switch (type) {
case 'string':
return 'string'
case 'integer':
return 'number'
case 'json':
return 'json'
case 'number':
return 'number'
case 'boolean':
return 'boolean'
default:
return type
}
}
const Entity = ({ language, apikey = '', endpoint = '' }: ContentProps) => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
const resource = snap.activeDocsSection[1]
const { data: jsonSchema, refetch } = useProjectJsonSchemaQuery({ projectRef: ref })
const definition = jsonSchema?.definitions?.[resource]
const columns =
definition !== undefined
? Object.entries(definition.properties).map(([id, val]: any) => ({
...val,
id,
required: (definition?.required ?? []).includes(id),
}))
: []
useEffect(() => {
if (resource !== undefined) refetch()
}, [resource])
if (resource === undefined) return null
return (
<div className="divide-y relative">
<div className="flex items-center justify-between px-4 py-4 sticky top-0 bg-surface-100 z-10 border-b shadow-md">
<div className="flex flex-col gap-y-1">
<h2 className="text-xl">{resource}</h2>
<p className="text-sm text-foreground-light">
{definition?.description ?? 'No description available'}
</p>
</div>
<LanguageSelector />
</div>
<div className="space-y-2 px-4 py-4 !border-t-0">
<p className="text-sm text-foreground-light">Columns</p>
<Table
head={[
<Table.th key="name">Name</Table.th>,
<Table.th key="format">Format</Table.th>,
<Table.th key="type">Type</Table.th>,
<Table.th key="description">Description</Table.th>,
]}
body={columns.map((column) => {
const formattedColumnType = getColumnType(column.type, column.format)
return (
<Table.tr key={column.id}>
<Table.td title={column.id}>{column.id}</Table.td>
<Table.td title={column.format}>
<p className="truncate">{column.format}</p>
</Table.td>
<Table.td title={formattedColumnType}>{formattedColumnType}</Table.td>
<Table.td title={column.description}>
{tempRemovePostgrestText(column.description ?? '').trim()}
</Table.td>
</Table.tr>
)
})}
/>
</div>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.readRows}
codeSnippets={DOCS_RESOURCE_CONTENT.readRows.code({
resourceId: resource,
endpoint,
apikey,
})}
/>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.filtering}
codeSnippets={DOCS_RESOURCE_CONTENT.filtering.code({
resourceId: resource,
endpoint,
apikey,
})}
/>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.insertRows}
codeSnippets={DOCS_RESOURCE_CONTENT.insertRows.code({
resourceId: resource,
endpoint,
apikey,
})}
/>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.updateRows}
codeSnippets={DOCS_RESOURCE_CONTENT.updateRows.code({
resourceId: resource,
endpoint,
apikey,
})}
/>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.deleteRows}
codeSnippets={DOCS_RESOURCE_CONTENT.deleteRows.code({
resourceId: resource,
endpoint,
apikey,
})}
/>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.subscribeChanges}
codeSnippets={DOCS_RESOURCE_CONTENT.subscribeChanges.code({ resourceId: resource })}
/>
</div>
)
}
export default Entity
@@ -0,0 +1,115 @@
import { useParams } from 'common'
import { Button, IconCopy, Input } from 'ui'
import { useProjectApiQuery } from 'data/config/project-api-query'
import ContentSnippet from '../ContentSnippet'
import { DOCS_CONTENT } from '../ProjectAPIDocs.constants'
import type { ContentProps } from './Content.types'
import { copyToClipboard } from 'lib/helpers'
import { useEffect, useState } from 'react'
const Introduction = ({ showKeys, language, apikey, endpoint }: ContentProps) => {
const { ref } = useParams()
const { data } = useProjectApiQuery({ projectRef: ref })
const [copied, setCopied] = useState<'anon' | 'service'>()
useEffect(() => {
if (copied !== undefined) setTimeout(() => setCopied(undefined), 2000)
}, [copied])
const serviceKey = showKeys
? data?.autoApiService.serviceApiKey ?? 'SUPABASE_CLIENT_SERVICE_KEY'
: 'SUPABASE_CLIENT_SERVICE_KEY'
return (
<>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.init}
>
<div className="px-4 space-y-6">
<div className="flex space-x-4 mt-8">
<p className="text-sm w-40">Project URL</p>
<Input disabled readOnly copy size="small" value={endpoint} className="w-full" />
</div>
<div className="flex space-x-4">
<p className="text-sm w-40">Client API key</p>
<Input
disabled
readOnly
size="small"
value={showKeys ? apikey : 'Reveal API keys via dropdown in the header'}
className="w-full"
descriptionText="This key is safe to use in a browser if you have enabled Row Level Security (RLS) for your tables and configured policies."
actions={[
<Button
key="copy"
type="default"
icon={<IconCopy />}
onClick={() => {
setCopied('anon')
copyToClipboard(
data?.autoApiService.defaultApiKey ?? 'SUPABASE_CLIENT_ANON_KEY'
)
}}
>
{copied === 'anon' ? 'Copied' : 'Copy'}
</Button>,
]}
/>
</div>
<div className="flex space-x-4">
<p className="text-sm w-40 mb-16">Service key</p>
<Input
disabled
readOnly
size="small"
value={showKeys ? serviceKey : 'Reveal API keys via dropdown in the header'}
className="w-full"
descriptionText={
<p>
This key has the ability to bypass Row Level Security.{' '}
<span className="text-amber-900">Never share it publicly.</span>
</p>
}
actions={[
<Button
key="copy"
type="default"
icon={<IconCopy />}
onClick={() => {
setCopied('service')
copyToClipboard(
data?.autoApiService.serviceApiKey ?? 'SUPABASE_CLIENT_SERVICE_KEY'
)
}}
>
{copied === 'service' ? 'Copied' : 'Copy'}
</Button>,
]}
/>
</div>
</div>
</ContentSnippet>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.clientApiKeys}
/>
<ContentSnippet
selectedLanguage={language}
apikey={serviceKey}
endpoint={endpoint}
snippet={DOCS_CONTENT.serviceApiKeys}
/>
</>
)
}
export default Introduction
@@ -0,0 +1,103 @@
import { useParams } from 'common'
import { useEffect } from 'react'
import { Badge } from 'ui'
import Table from 'components/to-be-cleaned/Table'
import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query'
import { useOpenAPISpecQuery } from 'data/open-api/api-spec-query'
import { useAppStateSnapshot } from 'state/app-state'
import { DOCS_RESOURCE_CONTENT } from '../ProjectAPIDocs.constants'
import ResourceContent from '../ResourceContent'
import type { ContentProps } from './Content.types'
export const RPC = ({ language }: ContentProps) => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
const { data: jsonSchema, refetch: refetchJsonSchema } = useProjectJsonSchemaQuery({
projectRef: ref,
})
const { data, refetch: refetchOpenAPISpec } = useOpenAPISpecQuery({ projectRef: ref })
const functions = data?.functions ?? []
const rpcName = snap.activeDocsSection[1]
const rpc = functions.find((fn) => fn.name === rpcName)
const {
post: { parameters: postParameters },
} = rpc ?? {}
const rpcJsonSchema = jsonSchema?.paths[rpc?.path]
const summary = rpcJsonSchema?.post?.summary
const rpcParamsObject =
postParameters &&
postParameters[0] &&
postParameters[0].schema &&
postParameters[0].schema.properties
? postParameters[0].schema.properties
: {}
const parameters: { name: string; format: string; type: string; required: boolean }[] =
Object.entries(rpcParamsObject)
.map(([k, v]: any) => ({
name: k,
...v,
required: postParameters[0].schema.required.includes(k),
}))
.filter((x) => !!x.name)
useEffect(() => {
if (rpcName !== undefined) {
refetchJsonSchema()
refetchOpenAPISpec()
}
}, [rpcName])
if (rpc === undefined) return null
return (
<div className="divide-y">
<div className="space-y-1 px-4 py-4">
<h2 className="text-xl">{rpc.name}</h2>
<p className="text-sm text-foreground-light">{summary ?? 'No description available'}</p>
</div>
<div className="space-y-2 px-4 py-4">
<p className="text-sm text-foreground-light">Function arguments</p>
<Table
head={[
<Table.th key="name">Name</Table.th>,
<Table.th key="format">Format</Table.th>,
<Table.th key="type">Type</Table.th>,
<Table.th key="required"></Table.th>,
]}
body={parameters.map((parameter) => (
<Table.tr key={parameter.name}>
<Table.td title={parameter.name}>
<p className="font-mono text-xs text-foreground truncate">{parameter.name}</p>
</Table.td>
<Table.td title={parameter.format}>{parameter.format}</Table.td>
<Table.td title={parameter.type}>{parameter.type}</Table.td>
<Table.td>
{parameter.required ? (
<Badge variant="warning">Required</Badge>
) : (
<Badge variant="default">Optional</Badge>
)}
</Table.td>
</Table.tr>
))}
/>
</div>
<ResourceContent
selectedLanguage={language}
snippet={DOCS_RESOURCE_CONTENT.rpcSingle}
codeSnippets={DOCS_RESOURCE_CONTENT.rpcSingle.code({
rpcName,
rpcParams: parameters,
endpoint: 'endpoint',
apiKey: 'apiKey',
showBearer: true,
})}
/>
</div>
)
}
@@ -0,0 +1,17 @@
import ContentSnippet from '../ContentSnippet'
import { DOCS_CONTENT } from '../ProjectAPIDocs.constants'
import type { ContentProps } from './Content.types'
const Realtime = ({ language }: ContentProps) => {
return (
<>
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.realtime} />
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.subscribeChannel} />
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.unsubscribeChannel} />
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.unsubscribeChannels} />
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.retrieveAllChannels} />
</>
)
}
export default Realtime
@@ -0,0 +1,13 @@
import ContentSnippet from '../ContentSnippet'
import { DOCS_CONTENT } from '../ProjectAPIDocs.constants'
import type { ContentProps } from './Content.types'
const Storage = ({ language }: ContentProps) => {
return (
<>
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.storage} />
</>
)
}
export default Storage
@@ -0,0 +1,16 @@
import ContentSnippet from '../ContentSnippet'
import { DOCS_CONTENT } from '../ProjectAPIDocs.constants'
import type { ContentProps } from './Content.types'
const StoredProcedures = ({ language }: ContentProps) => {
return (
<>
<ContentSnippet
selectedLanguage={language}
snippet={DOCS_CONTENT.storedProceduresIntroduction}
/>
</>
)
}
export default StoredProcedures
@@ -0,0 +1,85 @@
import ContentSnippet from '../ContentSnippet'
import { DOCS_CONTENT } from '../ProjectAPIDocs.constants'
import type { ContentProps } from './Content.types'
const UserManagement = ({ language, apikey, endpoint }: ContentProps) => {
return (
<>
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.userManagement} />
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.signUp}
/>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.emailLogin}
/>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.magicLinkLogin}
/>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.phoneLogin}
/>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.smsLogin}
/>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.smsVerify}
/>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.oauthLogin}
/>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.user}
/>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.forgotPassWordEmail}
/>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.updateUser}
/>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.logout}
/>
<ContentSnippet
selectedLanguage={language}
apikey={apikey}
endpoint={endpoint}
snippet={DOCS_CONTENT.emailInvite}
/>
</>
)
}
export default UserManagement
@@ -0,0 +1,12 @@
export { default as EdgeFunctions } from './EdgeFunctions'
export { default as Entities } from './Entities'
export { default as Introduction } from './Introduction'
export { default as Realtime } from './Realtime'
export { default as Storage } from './Storage'
export { default as StoredProcedures } from './StoredProcedures'
export { default as UserManagement } from './UserManagement'
export { default as Bucket } from './Bucket'
export { default as EdgeFunction } from './EdgeFunction'
export { default as Entity } from './Entity'
export { RPC } from './RPC'
@@ -0,0 +1,60 @@
import { useParams } from 'common'
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
import { Markdown } from '../Markdown'
import { PropsWithChildren } from 'react'
interface ContentSnippetProps {
apikey?: string
endpoint?: string
selectedLanguage: 'js' | 'bash'
snippet: {
key: string
category: string
title: string
description?: string
js?: (apikey?: string, endpoint?: string) => string
bash?: (apikey?: string, endpoint?: string) => string
}
}
const ContentSnippet = ({
apikey,
endpoint,
selectedLanguage,
snippet,
children,
}: PropsWithChildren<ContentSnippetProps>) => {
const { ref: projectRef } = useParams()
const codeSnippet = snippet[selectedLanguage]?.(apikey, endpoint).replaceAll(
'[ref]',
projectRef ?? ''
)
return (
<div id={snippet.key} className="space-y-4 py-6 pb-2 last:pb-6">
<div className="px-4 space-y-4">
<h2 className="doc-heading">{snippet.title}</h2>
{snippet.description !== undefined && (
<div className="doc-section">
<article className="text text-sm text-foreground-light">
<Markdown
className="max-w-none"
content={snippet.description.replaceAll('[ref]', projectRef ?? '_')}
/>
</article>
</div>
)}
</div>
{children}
{codeSnippet !== undefined && (
<div className="px-4 codeblock-container">
<div className="bg rounded p-2">
<SimpleCodeBlock className={selectedLanguage}>{codeSnippet}</SimpleCodeBlock>
</div>
</div>
)}
</div>
)
}
export default ContentSnippet
@@ -0,0 +1,209 @@
import { useParams } from 'common'
import Link from 'next/link'
import { Fragment } from 'react'
import SVG from 'react-inlinesvg'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import { useOpenAPISpecQuery } from 'data/open-api/api-spec-query'
import { useBucketsQuery } from 'data/storage/buckets-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { BASE_PATH } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { Button, IconBook, IconBookOpen } from 'ui'
import { navigateToSection } from './Content/Content.utils'
import { DOCS_CONTENT, DOCS_MENU } from './ProjectAPIDocs.constants'
const Separator = () => <div className="border-t !mt-3 pb-1 mx-3" />
const FirstLevelNav = () => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
const {
projectAuthAll: authEnabled,
projectStorageAll: storageEnabled,
projectEdgeFunctionAll: edgeFunctionsEnabled,
realtimeAll: realtimeEnabled,
} = useIsFeatureEnabled([
'project_auth:all',
'project_storage:all',
'project_edge_function:all',
'realtime:all',
])
const docsMenu = DOCS_MENU.filter((item) => {
if (item.key === 'user-management') return authEnabled
if (item.key === 'storage') return storageEnabled
if (item.key === 'edge-functions') return edgeFunctionsEnabled
if (item.key === 'realtime') return realtimeEnabled
return true
})
const { data } = useOpenAPISpecQuery({ projectRef: ref })
const tables = data?.tables ?? []
const functions = data?.functions ?? []
const { data: buckets } = useBucketsQuery({ projectRef: ref })
const { data: edgeFunctions } = useEdgeFunctionsQuery({ projectRef: ref })
return (
<>
<div className="px-2 py-4 border-b">
{docsMenu.map((item) => {
const isActive = snap.activeDocsSection[0] === item.key
const sections = Object.values(DOCS_CONTENT).filter(
(snippet) => snippet.category === item.key
)
// [Joshen] Need to find the right UI component for accessbility
return (
<Fragment key={item.key}>
<div
className={`cursor-pointer text-sm py-2 px-3 rounded-md transition ${
isActive ? 'bg-surface-300' : ''
}`}
onClick={() => snap.setActiveDocsSection([item.key])}
>
{item.name}
</div>
{isActive && sections.length > 0 && (
<div className="space-y-2 py-2">
{sections.map((section) => (
<p
key={section.key}
title={section.title}
className="text-sm text-foreground-light px-4 hover:text-foreground transition cursor-pointer"
onClick={() => {
snap.setActiveDocsSection([item.key])
navigateToSection(section.key)
}}
>
{section.title}
</p>
))}
{item.key === 'entities' && (
<>
{tables.length > 0 && <Separator />}
{tables.map((table) => (
<p
key={table.name}
title={table.name}
className="text-sm text-foreground-light px-4 hover:text-foreground transition cursor-pointer"
onClick={() => snap.setActiveDocsSection([item.key, table.name])}
>
{table.name}
</p>
))}
</>
)}
{item.key === 'stored-procedures' && (
<>
{functions.length > 0 && <Separator />}
{functions.map((fn) => (
<p
key={fn.name}
title={fn.name}
className="text-sm text-foreground-light px-4 hover:text-foreground transition cursor-pointer"
onClick={() => snap.setActiveDocsSection([item.key, fn.name])}
>
{fn.name}
</p>
))}
</>
)}
{item.key === 'storage' && (
<>
{(buckets ?? []).length > 0 && <Separator />}
{(buckets ?? []).map((bucket) => (
<p
key={bucket.name}
title={bucket.name}
className="text-sm text-foreground-light px-4 hover:text-foreground transition cursor-pointer"
onClick={() => snap.setActiveDocsSection([item.key, bucket.name])}
>
{bucket.name}
</p>
))}
</>
)}
{item.key === 'edge-functions' && (
<>
{(edgeFunctions ?? []).length > 0 && <Separator />}
{(edgeFunctions ?? []).map((fn) => (
<p
key={fn.name}
title={fn.name}
className="text-sm text-foreground-light px-4 hover:text-foreground transition cursor-pointer"
onClick={() => snap.setActiveDocsSection([item.key, fn.name])}
>
{fn.name}
</p>
))}
</>
)}
</div>
)}
</Fragment>
)
})}
</div>
<div className="px-2 py-4 border-b">
<Button
block
asChild
type="text"
size="small"
icon={
<SVG
src={`${BASE_PATH}/img/graphql.svg`}
style={{ width: `${16}px`, height: `${16}px` }}
className="text-foreground"
preProcessor={(code) => code.replace(/svg/, 'svg class="m-auto text-color-inherit"')}
/>
}
onClick={() => snap.setShowProjectApiDocs(false)}
>
<Link className="!justify-start" href={`/project/${ref}/api/graphiql`}>
GraphiQL
</Link>
</Button>
<Button block asChild type="text" size="small" icon={<IconBookOpen />}>
<Link
href="https://supabase.com/docs/guides/graphql"
target="_blank"
rel="noreferrer"
className="!justify-start"
>
GraphQL guide
</Link>
</Button>
</div>
<div className="px-2 py-4">
<Button block asChild type="text" size="small" icon={<IconBook />}>
<Link
href="https://supabase.com/docs"
target="_blank"
rel="noreferrer"
className="!justify-start"
>
Documentation
</Link>
</Button>
<Button block asChild type="text" size="small" icon={<IconBookOpen />}>
<Link
href="https://supabase.com/docs/guides/api"
target="_blank"
rel="noreferrer"
className="!justify-start"
>
REST guide
</Link>
</Button>
</div>
</>
)
}
export default FirstLevelNav
@@ -0,0 +1,87 @@
import { BASE_PATH } from 'lib/constants'
import { useState } from 'react'
import { useAppStateSnapshot } from 'state/app-state'
import {
Button,
CommandGroup_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Command_Shadcn_,
IconChevronDown,
IconTerminal,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
interface LanguageSelectorProps {
/**
* Only show icons to represent the language
*/
simplifiedVersion?: boolean
}
const LanguageSelector = ({ simplifiedVersion = false }: LanguageSelectorProps) => {
const snap = useAppStateSnapshot()
const [showLanguage, setShowLanguage] = useState(false)
const updateLanguage = (value: 'js' | 'bash') => {
snap.setDocsLanguage(value)
setShowLanguage(false)
}
return (
<div className="flex items-center gap-x-2">
<Popover_Shadcn_ modal={false} open={showLanguage} onOpenChange={setShowLanguage}>
<PopoverTrigger_Shadcn_ asChild>
<Button
type="default"
className={simplifiedVersion ? 'px-1' : ''}
icon={
simplifiedVersion ? (
snap.docsLanguage === 'js' ? (
<img
src={`${BASE_PATH}/img/libraries/javascript-icon.svg`}
alt={`javascript logo`}
width="14"
/>
) : (
<IconTerminal size={14} strokeWidth={2.5} />
)
) : undefined
}
iconRight={!simplifiedVersion && <IconChevronDown size={14} strokeWidth={2} />}
>
{!simplifiedVersion
? `Language: ${snap.docsLanguage === 'js' ? 'Javascript' : 'Bash'}`
: undefined}
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="p-0 w-24" side="bottom" align="end">
<Command_Shadcn_>
<CommandList_Shadcn_>
<CommandGroup_Shadcn_>
<CommandItem_Shadcn_
className="cursor-pointer"
onSelect={() => updateLanguage('js')}
onClick={() => updateLanguage('js')}
>
<p>Javascript</p>
</CommandItem_Shadcn_>
<CommandItem_Shadcn_
className="cursor-pointer"
onSelect={() => updateLanguage('bash')}
onClick={() => updateLanguage('bash')}
>
<p>Bash</p>
</CommandItem_Shadcn_>
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</div>
)
}
export default LanguageSelector
@@ -0,0 +1,40 @@
import { useParams } from 'common'
import type { CommandOptions } from 'ui-patterns/CommandMenu'
import { useRegisterCommands } from 'ui-patterns/CommandMenu'
import { COMMAND_MENU_SECTIONS } from '../App/CommandMenu/CommandMenu.utils'
export function useApiDocsGotoCommands(options?: CommandOptions) {
let { ref } = useParams()
ref ||= '_'
useRegisterCommands(
COMMAND_MENU_SECTIONS.NAVIGATE,
[
{
id: 'nav-api',
name: 'Project API Docs',
route: `/project/${ref}/api`,
defaultHidden: true,
},
{
id: 'nav-api-auth',
name: 'Auth Docs',
route: `/project/${ref}/api?page=auth`,
defaultHidden: true,
},
{
id: 'nav-api-user-management',
name: 'User Management Docs',
route: `/project/${ref}/api?page=users`,
defaultHidden: true,
},
{
id: 'nav-api-graphql',
name: 'GraphQL Docs',
route: `/project/${ref}/api/graphiql`,
defaultHidden: true,
},
],
{ ...options, deps: [ref] }
)
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,140 @@
import { useState } from 'react'
import { Button, SidePanel } from 'ui'
import { useParams } from 'common'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useAppStateSnapshot } from 'state/app-state'
import {
Bucket,
EdgeFunction,
EdgeFunctions,
Entities,
Entity,
Introduction,
RPC,
Realtime,
Storage,
StoredProcedures,
UserManagement,
} from './Content'
import FirstLevelNav from './FirstLevelNav'
import LanguageSelector from './LanguageSelector'
import SecondLevelNav from './SecondLevelNav'
/**
* [Joshen] Reminder: when we choose to release this as a main feature
* Ensure that UX is better than the existing, and make sure we do the
* necessary communications around releasing this.
*
* Problems:
* - Needs URL support
* - Language selector is not clear, users are missing the bash language option
* - GraphiQL needs a better home, cannot be placed under Database as its "API"
*/
const ProjectAPIDocs = () => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
const isEntityDocs =
snap.activeDocsSection.length === 2 && snap.activeDocsSection[0] === 'entities'
const [showKeys, setShowKeys] = useState(false)
const language = snap.docsLanguage
const { data } = useProjectApiQuery({ projectRef: ref })
const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref })
const apikey = showKeys
? data?.autoApiService.defaultApiKey ?? 'SUPABASE_CLIENT_ANON_KEY'
: 'SUPABASE_CLIENT_ANON_KEY'
const endpoint =
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain?.hostname}`
: `https://${data?.autoApiService.endpoint ?? ''}`
return (
<SidePanel
hideFooter
size="xxlarge"
className="max-w-5xl"
visible={snap.showProjectApiDocs}
onCancel={() => snap.setShowProjectApiDocs(false)}
>
<div className="flex items-start h-full">
<div className="w-72 border-r h-full">
<div className="border-b px-4 py-2 flex items-center justify-between">
<h4>API Docs</h4>
<div className="flex items-center space-x-1">
{!isEntityDocs && <LanguageSelector simplifiedVersion />}
<Button type="default" onClick={() => setShowKeys(!showKeys)}>
{showKeys ? 'Hide keys' : 'Show keys'}
</Button>
</div>
</div>
{snap.activeDocsSection.length === 1 ? <FirstLevelNav /> : <SecondLevelNav />}
</div>
<div className="flex-1 divide-y space-y-4 max-h-screen overflow-auto">
{snap.activeDocsSection[0] === 'introduction' && (
<Introduction
showKeys={showKeys}
language={language}
apikey={apikey}
endpoint={endpoint}
/>
)}
{snap.activeDocsSection[0] === 'user-management' && (
<UserManagement language={language} apikey={apikey} endpoint={endpoint} />
)}
{snap.activeDocsSection[0] === 'realtime' && <Realtime language={language} />}
{snap.activeDocsSection[0] === 'storage' && (
<>
{snap.activeDocsSection[1] !== undefined ? (
<Bucket language={language} apikey={apikey} endpoint={endpoint} />
) : (
<Storage language={language} />
)}
</>
)}
{snap.activeDocsSection[0] === 'edge-functions' && (
<>
{snap.activeDocsSection[1] !== undefined ? (
<EdgeFunction language={language} apikey={apikey} endpoint={endpoint} />
) : (
<EdgeFunctions language={language} />
)}
</>
)}
{snap.activeDocsSection[0] === 'entities' && (
<>
{snap.activeDocsSection[1] !== undefined ? (
<Entity language={language} apikey={apikey} endpoint={endpoint} />
) : (
<Entities language={language} />
)}
</>
)}
{snap.activeDocsSection[0] === 'stored-procedures' && (
<>
{snap.activeDocsSection[1] !== undefined ? (
<RPC language={language} />
) : (
<StoredProcedures language={language} />
)}
</>
)}
</div>
</div>
</SidePanel>
)
}
export default ProjectAPIDocs
@@ -0,0 +1,61 @@
import { useParams } from 'common'
import Link from 'next/link'
import { Button, IconExternalLink } from 'ui'
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
import { Markdown } from '../Markdown'
interface ResourceContentProps {
selectedLanguage: 'js' | 'bash'
snippet: {
key: string
title: string
description?: string
docsUrl?: string
}
codeSnippets: any[]
}
const ResourceContent = ({ selectedLanguage, snippet, codeSnippets }: ResourceContentProps) => {
const { ref: projectRef } = useParams()
return (
<div id={snippet.key} className="space-y-4 py-6">
<div className="px-4 space-y-2">
<div className="flex items-center justify-between">
<h2 className="doc-heading">{snippet.title}</h2>
{snippet.docsUrl !== undefined && (
<Button asChild type="default" icon={<IconExternalLink />}>
<Link href={snippet.docsUrl} target="_blank" rel="noreferrer">
Documentation
</Link>
</Button>
)}
</div>
{snippet.description !== undefined && (
<div className="doc-section">
<article className="text text-sm text-foreground-light">
<Markdown
className="max-w-none"
content={snippet.description.replaceAll('[ref]', projectRef ?? '_')}
/>
</article>
</div>
)}
</div>
{codeSnippets.map((codeSnippet) => (
<div key={codeSnippet.key} className="px-4 space-y-2">
<p className="text-sm text-foreground-light">{codeSnippet.title}</p>
<div className="codeblock-container">
<div className="bg rounded p-2">
<SimpleCodeBlock className={selectedLanguage}>
{codeSnippet[selectedLanguage]}
</SimpleCodeBlock>
</div>
</div>
</div>
))}
</div>
)
}
export default ResourceContent
@@ -0,0 +1,178 @@
import { useParams } from 'common'
import Link from 'next/link'
import { useState } from 'react'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
CommandGroup_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Command_Shadcn_,
IconChevronLeft,
IconCode,
IconExternalLink,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import { useOpenAPISpecQuery } from 'data/open-api/api-spec-query'
import { useBucketsQuery } from 'data/storage/buckets-query'
import { useAppStateSnapshot } from 'state/app-state'
import { useIsAPIDocsSidePanelEnabled } from '../App/FeaturePreview/FeaturePreviewContext'
import { navigateToSection } from './Content/Content.utils'
import { DOCS_RESOURCE_CONTENT } from './ProjectAPIDocs.constants'
const SecondLevelNav = () => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
const [open, setOpen] = useState(false)
const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled()
const { data } = useOpenAPISpecQuery({ projectRef: ref })
const tables = data?.tables ?? []
const functions = data?.functions ?? []
const [section, resource] = snap.activeDocsSection
const { data: buckets } = useBucketsQuery({ projectRef: ref })
const { data: edgeFunctions } = useEdgeFunctionsQuery({ projectRef: ref })
const bucket = (buckets ?? []).find((b) => b.name === resource)
const content: { [key: string]: { title: string; options: any[]; docsUrl: string } } = {
entities: {
title: 'Tables & Views',
options: tables,
docsUrl: 'https://supabase.com/docs/reference/javascript/select',
},
'stored-procedures': {
title: 'Stored Procedures',
options: functions,
docsUrl: 'https://supabase.com/docs/reference/javascript/rpc',
},
storage: {
title: 'Storage',
options: buckets ?? [],
docsUrl: 'https://supabase.com/docs/reference/javascript/storage-createbucket',
},
'edge-functions': {
title: 'Edge Functions',
options: edgeFunctions ?? [],
docsUrl: 'https://supabase.com/docs/reference/javascript/functions-invoke',
},
}
const updateSection = (value: string) => {
snap.setActiveDocsSection([snap.activeDocsSection[0], value])
setOpen(false)
}
const menuItems = Object.values(DOCS_RESOURCE_CONTENT).filter(
(content) => content.category === snap.activeDocsSection[0]
)
return (
<div className="py-4">
<div className="px-4 flex items-center space-x-2 mb-2">
{isNewAPIDocsEnabled && (
<Button
type="text"
icon={<IconChevronLeft />}
className="px-1"
onClick={() => snap.setActiveDocsSection([snap.activeDocsSection[0]])}
/>
)}
<p className="text-sm text-foreground-light capitalize">{content[section].title}</p>
</div>
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<div className="px-4">
<Button block type="default" size="small" className="[&>span]:w-full">
<div>
<div className="flex items-center justify-between w-full">
<p>{snap.activeDocsSection[1]}</p>
<div>
<IconCode className="rotate-90" strokeWidth={1.5} size={12} />
</div>
</div>
</div>
</Button>
</div>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="p-0 w-60" side="bottom" align="center">
<Command_Shadcn_>
<CommandList_Shadcn_>
<CommandGroup_Shadcn_>
{content[section].options.map((option) => (
<CommandItem_Shadcn_
key={option.name}
className="cursor-pointer"
onSelect={() => updateSection(option.name)}
onClick={() => updateSection(option.name)}
>
<p>{option.name}</p>
</CommandItem_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
<div className="px-2 py-4 space-y-2">
{menuItems.map((item) => {
if (section === 'storage' && bucket !== undefined) {
if (
(!bucket.public && item.key === 'retrieve-public-url') ||
(bucket.public && item.key === 'create-signed-url')
)
return null
}
return (
<p
key={item.key}
title={item.title}
className="text-sm text-foreground-light px-4 hover:text-foreground transition cursor-pointer"
onClick={() => navigateToSection(item.key)}
>
{item.title}
</p>
)
})}
</div>
<div className="px-4 py-4 border-t space-y-2">
<Alert_Shadcn_ className="p-3">
<AlertTitle_Shadcn_>
<p className="text-xs">Unable to find what you're looking for?</p>
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="space-y-1">
<p className="text-xs !leading-normal">
The API methods shown here are only the commonly used ones to get you started building
quickly.
</p>
<p className="text-xs !leading-normal">
Head over to our docs site for the full API documentation.
</p>
<Button
asChild
className="!mt-2"
size="tiny"
type="default"
icon={<IconExternalLink strokeWidth={1.5} />}
>
<Link href={content[section].docsUrl} target="_blank" rel="noreferrer">
Documentation
</Link>
</Button>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</div>
</div>
)
}
export default SecondLevelNav
@@ -90,7 +90,7 @@ const GlobalNavigationMenu: FC = () => {
<div className="p-3 md:p-1">
{section[0].menuItems?.map((menuItem, menuItemIndex) => (
<Fragment key={`desktop-docs-menu-section-${menuItemIndex}-${menuItemIndex}`}>
{menuItemIndex !== 0 && <MenubarSeparator className="bg-border-muted" />}
{menuItemIndex !== 0 && <MenubarSeparator className="bg-border" />}
{menuItem.map((item, itemIdx) =>
!item.href ? (
<div
@@ -27,6 +27,7 @@ import {
IconMenuDevCli,
IconSupport,
IconTroubleshooting,
IconAPIDocs,
} from './MenuIcons'
function getMenuIcon(menuKey: string, width: number = 16, height: number = 16, className?: string) {
@@ -87,6 +88,8 @@ function getMenuIcon(menuKey: string, width: number = 16, height: number = 16, c
return <IconSupport width={width} height={height} className={className} />
case 'contributing':
return <IconTroubleshooting width={width} height={height} className={className} />
case 'api-docs':
return <IconAPIDocs width={width} height={height} className={className} />
default:
return <IconMenuPlatform width={width} height={height} className={className} />
}
@@ -635,3 +635,26 @@ export function IconTroubleshooting({ width = 16, height = 16, className }: Home
</svg>
)
}
export function IconAPIDocs({ width = 16, height = 16, className }: HomeMenuIcon) {
return (
<svg
className={className}
width={width}
height={height}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"></path>
<path d="M14 2v4a2 2 0 0 0 2 2h4"></path>
<path d="M10 9H8"></path>
<path d="M16 13H8"></path>
<path d="M16 17H8"></path>
</svg>
)
}
@@ -140,13 +140,19 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [
label: 'CLI Commands',
icon: 'reference-cli',
href: '/reference/cli/introduction',
level: 'reference_javascript',
level: 'api',
},
{
label: 'Management API',
icon: 'reference-api',
href: '/reference/api/introduction',
level: 'reference_javascript',
level: 'api',
},
{
label: 'API Docs',
icon: 'api-docs',
href: '/reference/api/introduction',
level: 'api',
},
],
[