diff --git a/apps/docs/components/APIDocs/Content/Bucket.tsx b/apps/docs/components/APIDocs/Content/Bucket.tsx new file mode 100644 index 00000000000..6b7e6e8e804 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/Bucket.tsx @@ -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 ( +
+
+
+

{bucket.name}

+ + {bucket.public ? 'Public' : 'Private'} + +
+

+ Allowed MIME types:{' '} + {allowedMimeTypes === null + ? 'All types are allowed' + : (allowedMimeTypes ?? []).length === 0 + ? 'No types are allowed' + : (allowedMimeTypes ?? []).length > 1 + ? (allowedMimeTypes ?? []).join(', ') + : 'Unknown'} +

+

+ Max file size limit:{' '} + {maxFileSizeLimit === null ? 'No limit' : `${formatBytes(maxFileSizeLimit)}`} +

+
+ + + + + + {bucket.public ? ( + + ) : ( + + )} +
+ ) +} + +export default Bucket diff --git a/apps/docs/components/APIDocs/Content/Content.types.ts b/apps/docs/components/APIDocs/Content/Content.types.ts new file mode 100644 index 00000000000..7a624c4a606 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/Content.types.ts @@ -0,0 +1,6 @@ +export interface ContentProps { + showKeys?: boolean + language: 'js' | 'bash' + endpoint?: string + apikey?: string +} diff --git a/apps/docs/components/APIDocs/Content/Content.utils.ts b/apps/docs/components/APIDocs/Content/Content.utils.ts new file mode 100644 index 00000000000..da308eb6068 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/Content.utils.ts @@ -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.` + 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 +} diff --git a/apps/docs/components/APIDocs/Content/EdgeFunction.tsx b/apps/docs/components/APIDocs/Content/EdgeFunction.tsx new file mode 100644 index 00000000000..dca7cce0c52 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/EdgeFunction.tsx @@ -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 ( +
+
+
+

{edgeFunction.name}

+
+
+ + +
+ ) +} + +export default Bucket diff --git a/apps/docs/components/APIDocs/Content/EdgeFunctions.tsx b/apps/docs/components/APIDocs/Content/EdgeFunctions.tsx new file mode 100644 index 00000000000..c33ccd8a267 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/EdgeFunctions.tsx @@ -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 ( + <> + + + + + + ) +} + +export default EdgeFunctions diff --git a/apps/docs/components/APIDocs/Content/Entities.tsx b/apps/docs/components/APIDocs/Content/Entities.tsx new file mode 100644 index 00000000000..ccf88ec6f61 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/Entities.tsx @@ -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 ( + <> + +
+ +
+ + +
+

+ Remember to re-generate and download this file as you make changes to your tables. +

+
+ + + ) +} + +export default Entities diff --git a/apps/docs/components/APIDocs/Content/Entity.tsx b/apps/docs/components/APIDocs/Content/Entity.tsx new file mode 100644 index 00000000000..fda909b7295 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/Entity.tsx @@ -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 ( +
+
+
+

{resource}

+

+ {definition?.description ?? 'No description available'} +

+
+ +
+
+

Columns

+ Name, + Format, + Type, + Description, + ]} + body={columns.map((column) => { + const formattedColumnType = getColumnType(column.type, column.format) + return ( + + {column.id} + +

{column.format}

+
+ {formattedColumnType} + + {tempRemovePostgrestText(column.description ?? '').trim()} + +
+ ) + })} + /> + + + + + + + + + + ) +} + +export default Entity diff --git a/apps/docs/components/APIDocs/Content/Introduction.tsx b/apps/docs/components/APIDocs/Content/Introduction.tsx new file mode 100644 index 00000000000..a79d78b3ac4 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/Introduction.tsx @@ -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 ( + <> + +
+
+

Project URL

+ +
+
+

Client API key

+ } + onClick={() => { + setCopied('anon') + copyToClipboard( + data?.autoApiService.defaultApiKey ?? 'SUPABASE_CLIENT_ANON_KEY' + ) + }} + > + {copied === 'anon' ? 'Copied' : 'Copy'} + , + ]} + /> +
+
+

Service key

+ + This key has the ability to bypass Row Level Security.{' '} + Never share it publicly. +

+ } + actions={[ + , + ]} + /> +
+
+
+ + + + + + ) +} + +export default Introduction diff --git a/apps/docs/components/APIDocs/Content/RPC.tsx b/apps/docs/components/APIDocs/Content/RPC.tsx new file mode 100644 index 00000000000..24553a7c1ae --- /dev/null +++ b/apps/docs/components/APIDocs/Content/RPC.tsx @@ -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 ( +
+
+

{rpc.name}

+

{summary ?? 'No description available'}

+
+
+

Function arguments

+
Name, + Format, + Type, + , + ]} + body={parameters.map((parameter) => ( + + +

{parameter.name}

+
+ {parameter.format} + {parameter.type} + + {parameter.required ? ( + Required + ) : ( + Optional + )} + +
+ ))} + /> + + + + ) +} diff --git a/apps/docs/components/APIDocs/Content/Realtime.tsx b/apps/docs/components/APIDocs/Content/Realtime.tsx new file mode 100644 index 00000000000..e628ddca522 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/Realtime.tsx @@ -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 ( + <> + + + + + + + ) +} + +export default Realtime diff --git a/apps/docs/components/APIDocs/Content/Storage.tsx b/apps/docs/components/APIDocs/Content/Storage.tsx new file mode 100644 index 00000000000..18f7b851799 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/Storage.tsx @@ -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 ( + <> + + + ) +} + +export default Storage diff --git a/apps/docs/components/APIDocs/Content/StoredProcedures.tsx b/apps/docs/components/APIDocs/Content/StoredProcedures.tsx new file mode 100644 index 00000000000..20ef8e3767c --- /dev/null +++ b/apps/docs/components/APIDocs/Content/StoredProcedures.tsx @@ -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 ( + <> + + + ) +} + +export default StoredProcedures diff --git a/apps/docs/components/APIDocs/Content/UserManagement.tsx b/apps/docs/components/APIDocs/Content/UserManagement.tsx new file mode 100644 index 00000000000..22e762d9eb5 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/UserManagement.tsx @@ -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 ( + <> + + + + + + + + + + + + + + + ) +} + +export default UserManagement diff --git a/apps/docs/components/APIDocs/Content/index.ts b/apps/docs/components/APIDocs/Content/index.ts new file mode 100644 index 00000000000..5eff8ef2ab3 --- /dev/null +++ b/apps/docs/components/APIDocs/Content/index.ts @@ -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' diff --git a/apps/docs/components/APIDocs/ContentSnippet.tsx b/apps/docs/components/APIDocs/ContentSnippet.tsx new file mode 100644 index 00000000000..3e3c9bab310 --- /dev/null +++ b/apps/docs/components/APIDocs/ContentSnippet.tsx @@ -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) => { + const { ref: projectRef } = useParams() + const codeSnippet = snippet[selectedLanguage]?.(apikey, endpoint).replaceAll( + '[ref]', + projectRef ?? '' + ) + + return ( +
+
+

{snippet.title}

+ {snippet.description !== undefined && ( +
+
+ +
+
+ )} +
+ {children} + {codeSnippet !== undefined && ( +
+
+ {codeSnippet} +
+
+ )} +
+ ) +} +export default ContentSnippet diff --git a/apps/docs/components/APIDocs/FirstLevelNav.tsx b/apps/docs/components/APIDocs/FirstLevelNav.tsx new file mode 100644 index 00000000000..285ddfb8e1e --- /dev/null +++ b/apps/docs/components/APIDocs/FirstLevelNav.tsx @@ -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 = () =>
+ +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 ( + <> +
+ {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 ( + +
snap.setActiveDocsSection([item.key])} + > + {item.name} +
+ {isActive && sections.length > 0 && ( +
+ {sections.map((section) => ( +

{ + snap.setActiveDocsSection([item.key]) + navigateToSection(section.key) + }} + > + {section.title} +

+ ))} + {item.key === 'entities' && ( + <> + {tables.length > 0 && } + {tables.map((table) => ( +

snap.setActiveDocsSection([item.key, table.name])} + > + {table.name} +

+ ))} + + )} + {item.key === 'stored-procedures' && ( + <> + {functions.length > 0 && } + {functions.map((fn) => ( +

snap.setActiveDocsSection([item.key, fn.name])} + > + {fn.name} +

+ ))} + + )} + {item.key === 'storage' && ( + <> + {(buckets ?? []).length > 0 && } + {(buckets ?? []).map((bucket) => ( +

snap.setActiveDocsSection([item.key, bucket.name])} + > + {bucket.name} +

+ ))} + + )} + {item.key === 'edge-functions' && ( + <> + {(edgeFunctions ?? []).length > 0 && } + {(edgeFunctions ?? []).map((fn) => ( +

snap.setActiveDocsSection([item.key, fn.name])} + > + {fn.name} +

+ ))} + + )} +
+ )} +
+ ) + })} +
+ +
+ + +
+ +
+ + +
+ + ) +} + +export default FirstLevelNav diff --git a/apps/docs/components/APIDocs/LanguageSelector.tsx b/apps/docs/components/APIDocs/LanguageSelector.tsx new file mode 100644 index 00000000000..60b5bfca4e4 --- /dev/null +++ b/apps/docs/components/APIDocs/LanguageSelector.tsx @@ -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 ( +
+ + + + + + + + + updateLanguage('js')} + onClick={() => updateLanguage('js')} + > +

Javascript

+
+ updateLanguage('bash')} + onClick={() => updateLanguage('bash')} + > +

Bash

+
+
+
+
+
+
+
+ ) +} + +export default LanguageSelector diff --git a/apps/docs/components/APIDocs/ProjectAPIDocs.Commands.tsx b/apps/docs/components/APIDocs/ProjectAPIDocs.Commands.tsx new file mode 100644 index 00000000000..b64290d75ce --- /dev/null +++ b/apps/docs/components/APIDocs/ProjectAPIDocs.Commands.tsx @@ -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] } + ) +} diff --git a/apps/docs/components/APIDocs/ProjectAPIDocs.constants.ts b/apps/docs/components/APIDocs/ProjectAPIDocs.constants.ts new file mode 100644 index 00000000000..67b9adefa81 --- /dev/null +++ b/apps/docs/components/APIDocs/ProjectAPIDocs.constants.ts @@ -0,0 +1,1221 @@ +export const DOCS_MENU = [ + { name: 'Connect', key: 'introduction' }, + { name: 'User Management', key: 'user-management' }, + { name: 'Tables & Views', key: 'entities' }, + { name: 'Stored Procedures', key: 'stored-procedures' }, + { name: 'Storage', key: 'storage' }, + { name: 'Edge Functions', key: 'edge-functions' }, + { name: 'Realtime', key: 'realtime' }, +] + +export const DOCS_CONTENT = { + init: { + key: 'introduction', + category: 'introduction', + title: `Connect to your project`, + description: `Projects have a RESTful endpoint that you can use with your project's API key to query and manage your database. Put these keys in your .env file.`, + js: (apikey?: string, endpoint?: string) => ` +import { createClient } from '@supabase/supabase-js' + +const supabaseUrl = '${endpoint}' +const supabaseKey = process.env.SUPABASE_KEY +const supabase = createClient(supabaseUrl, supabaseKey)`, + bash: () => `# No client library required for Bash.`, + }, + clientApiKeys: { + key: 'client-api-keys', + category: 'introduction', + title: `Client API Keys`, + description: `Client keys allow "anonymous access" to your database, until the user has logged in. After logging in, the keys will switch to the user's own login token. + +In this documentation, we will refer to the key using the name \`SUPABASE_KEY\`. You can find the \`anon\` key in the [API settings](/project/[ref]/settings/api) page.`, + js: (apikey?: string, endpoint?: string) => ` +const SUPABASE_KEY = '${apikey}' +const SUPABASE_URL = '${endpoint}' +const supabase = createClient(SUPABASE_URL, process.env.SUPABASE_KEY);`, + bash: (apikey?: string, endpoint?: string) => `${apikey}`, + }, + serviceApiKeys: { + key: 'service-keys', + category: 'introduction', + title: `Service Keys`, + description: `Service keys have *FULL* access to your data, bypassing any security policies. Be VERY careful where you expose these keys. They should only be used on a server and never on a client or browser. + +In this documentation, we refer to the key using the name \`SERVICE_KEY\`. You can find the \`service_role\` key above or in the [API settings](/project/[ref]/settings/api) page.`, + js: (apikey?: string, endpoint?: string) => ` +const SUPABASE_KEY = '${apikey}' +const SUPABASE_URL = 'https://${endpoint}' +const supabase = createClient(SUPABASE_URL, process.env.SUPABASE_KEY);`, + bash: (apikey?: string, endpoint?: string) => `${apikey}`, + }, + // User Management + userManagement: { + key: 'user-management', + category: 'user-management', + title: `Introduction`, + description: `Supabase makes it easy to manage your users. + + Supabase assigns each user a unique ID. You can reference this ID anywhere in your database. For example, you might create a \`profiles\` table references the user using a \`user_id\` field. + + Supabase already has built in the routes to sign up, login, and log out for managing users in your apps and websites.`, + js: undefined, + bash: undefined, + }, + signUp: { + key: 'sign-up', + category: 'user-management', + title: `Sign up`, + description: `Allow your users to sign up and create a new account + + After they have signed up, all interactions using the Supabase client will be performed as "that user".`, + js: (apikey?: string, endpoint?: string) => ` +const { data, error } = await supabase.auth.signUp({ + email: 'someone@email.com', + password: 'some-secure-password' +})`, + bash: (apikey?: string, endpoint?: string) => ` +curl -X POST '${endpoint}/auth/v1/signup' \\ +-H "apikey: ${apikey}" \\ +-H "Content-Type: application/json" \\ +-d '{ + "email": "someone@email.com", + "password": "some-secure-password" +}'`, + }, + emailLogin: { + key: 'email-login', + category: 'user-management', + title: `Log in with Email/Password`, + description: ` +If an account is created, users can login to your app. + +After they have logged in, all interactions using the Supabase JS client will be performed as "that user".`, + js: (apikey?: string, endpoint?: string) => ` +const { data, error } = await supabase.auth.signInWithPassword({ + email: 'someone@email.com', + password: 'some-secure-password' +}) + `, + bash: (apikey?: string, endpoint?: string) => ` +curl -X POST '${endpoint}/auth/v1/token?grant_type=password' \\ +-H "apikey: ${apikey}" \\ +-H "Content-Type: application/json" \\ +-d '{ + "email": "someone@email.com", + "password": "some-secure-password" +}' + `, + }, + magicLinkLogin: { + key: 'magic-link-login', + category: 'user-management', + title: `Log in with Magic Link via Email`, + description: ` +Send a user a passwordless link which they can use to redeem an access_token. + +After they have clicked the link, all interactions using the Supabase JS client will be performed as "that user".`, + js: (apikey?: string, endpoint?: string) => ` +const { data, error } = await supabase.auth.signInWithOtp({ + email: 'someone@email.com' +}) + `, + bash: (apikey?: string, endpoint?: string) => ` +curl -X POST '${endpoint}/auth/v1/magiclink' \\ +-H "apikey: ${apikey}" \\ +-H "Content-Type: application/json" \\ +-d '{ + "email": "someone@email.com" +}' + `, + }, + phoneLogin: { + key: 'phone-log-in', + category: 'user-management', + title: `Sign up with Phone/Password`, + description: ` +A phone number can be used instead of an email as a primary account confirmation mechanism. + +The user will receive a mobile OTP via sms with which they can verify that they control the phone number. + +You must enter your own twilio credentials on the auth settings page to enable sms confirmations.`, + js: (apikey?: string, endpoint?: string) => ` +const { data, error } = await supabase.auth.signUp({ + phone: '+13334445555', + password: 'some-password' +}) + `, + bash: (apikey?: string, endpoint?: string) => ` +curl -X POST '${endpoint}/auth/v1/signup' \\ +-H "apikey: ${apikey}" \\ +-H "Content-Type: application/json" \\ +-d '{ + "phone": "+13334445555", + "password": "some-password" +}' + `, + }, + smsLogin: { + key: 'sms-otp-log-in', + category: 'user-management', + title: `Login via SMS OTP`, + description: ` +SMS OTPs work like magic links, except you have to provide an interface for the user to verify the 6 digit number they receive. + +You must enter your own twilio credentials on the auth settings page to enable SMS-based Logins.`, + js: (apikey?: string, endpoint?: string) => ` +const { data, error } = await supabase.auth.signInWithOtp({ + phone: '+13334445555' +}) + `, + bash: (apikey?: string, endpoint?: string) => ` +curl -X POST '${endpoint}/auth/v1/otp' \\ +-H "apikey: ${apikey}" \\ +-H "Content-Type: application/json" \\ +-d '{ + "phone": "+13334445555" +}' + `, + }, + smsVerify: { + key: 'sms-verify', + category: 'user-management', + title: `Verify an SMS OTP`, + description: ` +Once the user has received the OTP, have them enter it in a form and send it for verification + +You must enter your own twilio credentials on the auth settings page to enable SMS-based OTP verification.`, + js: (apikey?: string, endpoint?: string) => ` +const { data, error } = await supabase.auth.verifyOtp({ + phone: '+13334445555', + token: '123456', + type: 'sms' +}) + `, + bash: (apikey?: string, endpoint?: string) => ` +curl -X POST '${endpoint}/auth/v1/verify' \\ +-H "apikey: ${apikey}" \\ +-H "Content-Type: application/json" \\ +-d '{ + "type": "sms", + "phone": "+13334445555", + "token": "123456" +}' + `, + }, + oauthLogin: { + key: 'oauth-login', + category: 'user-management', + title: `Log in with Third Party OAuth`, + description: ` +Users can log in with Third Party OAuth like Google, Facebook, GitHub, and more. You must first enable each of these in the Auth Providers settings [here](https://supabase.com). + +View all the available [Third Party OAuth providers](https://supabase.com). + +After they have logged in, all interactions using the Supabase JS client will be performed as "that user". + +Generate your Client ID and secret from: [Google](https://console.developers.google.com/apis/credentials), [Github](https://github.com/settings/applications/new), [Gitlab](https://gitlab.com/oauth/applications), [Facebook](https://developers.facebook.com/apps), and [Bitbucket](https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud).`, + js: (apikey?: string, endpoint?: string) => ` +const { data, error } = await supabase.auth.signInWithOAuth({ + provider: 'github' +}) + `, + bash: (apikey?: string, endpoint?: string) => `No available command`, + }, + user: { + key: 'get-user', + category: 'user-management', + title: `Get user`, + description: `Get the JSON object for the logged in user.`, + js: (apikey?: string, endpoint?: string) => ` +const { data: { user } } = await supabase.auth.getUser() + `, + bash: (apikey?: string, endpoint?: string) => ` +curl -X GET '${endpoint}/auth/v1/user' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer USER_TOKEN" + `, + }, + forgotPassWordEmail: { + key: 'forgot-password-email', + category: 'user-management', + title: `Forgot password / email`, + description: `Sends the user a log in link via email. Once logged in you should direct the user to a new password form. And use "Update User" below to save the new password.`, + js: (apikey?: string, endpoint?: string) => ` +const { data, error } = await supabase.auth.resetPasswordForEmail(email) + `, + bash: (apikey?: string, endpoint?: string) => ` +curl -X POST '${endpoint}/auth/v1/recover' \\ +-H "apikey: ${apikey}" \\ +-H "Content-Type: application/json" \\ +-d '{ +"email": "someone@email.com" +}' +`, + }, + updateUser: { + key: 'update-user', + category: 'user-management', + title: `Update User`, + description: `Update the user with a new email or password. Each key (email, password, and data) is optional.`, + js: (apikey?: string, endpoint?: string) => ` +const { data, error } = await supabase.auth.updateUser({ + email: "new@email.com", + password: "new-password", + data: { hello: 'world' } +}) + `, + bash: (apikey?: string, endpoint?: string) => ` +curl -X PUT '${endpoint}/auth/v1/user' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer " \\ +-H "Content-Type: application/json" \\ +-d '{ +"email": "someone@email.com", +"password": "new-password", +"data": { + "key": "value" +} +}' +`, + }, + logout: { + key: 'log-out', + category: 'user-management', + title: `Log out`, + description: `After calling log out, all interactions using the Supabase JS client will be "anonymous".`, + js: (apikey?: string, endpoint?: string) => ` +const { error } = await supabase.auth.signOut() + `, + bash: (apikey?: string, endpoint?: string) => ` +curl -X POST '${endpoint}/auth/v1/logout' \\ +-H "apikey: ${apikey}" \\ +-H "Content-Type: application/json" \\ +-H "Authorization: Bearer USER_TOKEN" + `, + }, + emailInvite: { + key: 'email-invite', + category: 'user-management', + title: `Invite user over email`, + description: ` +Send a user a passwordless link which they can use to sign up and log in. + +After they have clicked the link, all interactions using the Supabase JS client will be performed as "that user". + +This endpoint requires you use the \`service_role_key\` when initializing the client, and should only be invoked from the server, never from the client.`, + js: (apikey?: string, endpoint?: string) => ` +const { data, error } = await supabase.auth.api.inviteUserByEmail('someone@email.com') + `, + bash: (apikey?: string, endpoint?: string) => ` +curl -X POST '${endpoint}/auth/v1/invite' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer ${apikey}" \\ +-H "Content-Type: application/json" \\ +-d '{ + "email": "someone@email.com" +}' + `, + }, + // Storage + storage: { + key: 'storage', + category: 'storage', + title: `Introduction`, + description: `Supabase Storage makes it simple to upload and serve files of any size, providing a robust framework for file access controls. + +You can use Supabase Storage to store images, videos, documents, and any other file type. Serve your assets with a global CDN to reduce latency from over 285 cities globally. Supabase Storage includes a built-in image optimizer, so you can resize and compress your media files on the fly.`, + js: undefined, + bash: undefined, + }, + // Edge functions + edgeFunctions: { + key: 'edge-function', + category: 'edge-functions', + title: 'Introduction', + description: ` +Edge Functions are server-side TypeScript functions, distributed globally at the edge—close to your users. They can be used for listening to webhooks or integrating your Supabase project with third-parties like Stripe. Edge Functions are developed using Deno, which offers a few benefits to you as a developer: +`, + js: undefined, + bash: undefined, + }, + edgeFunctionsPreReq: { + key: 'edge-function-pre-req', + category: 'edge-functions', + title: 'Pre-requisites', + description: ` +Follow the steps to prepare your Supabase project on your local machine. + +- Install the Supabase [CLI](https://supabase.com/docs/guides/cli). +- [Login to the CLI](https://supabase.com/docs/reference/cli/usage#supabase-login) using the command: \`supabase login\`.. +- [Initialize Supabase](https://supabase.com/docs/guides/getting-started/local-development#getting-started) inside your project using the command: \`supabase init\`.. +- [Link to your Remote Project](https://supabase.com/docs/reference/cli/usage#supabase-link) using the command \`supabase link --project-ref [ref]\`.. +- Setup your environment: Follow the steps [here](https://supabase.com/docs/guides/functions/quickstart#setting-up-your-environment). +`, + js: undefined, + bash: undefined, + }, + createEdgeFunction: { + key: 'create-edge-function', + category: 'edge-functions', + title: 'Create an Edge Function', + description: ` +Create a Supabase Edge Function locally via the Supabase CLI. +`, + js: () => `// Create an edge function via the Supabase CLI`, + bash: () => ` +supabase functions new hello-world +`, + }, + deployEdgeFunction: { + key: 'deploy-edge-function', + category: 'edge-functions', + title: 'Deploy an Edge Function', + description: ` +Deploy a Supabase Edge Function to your Supabase project via the Supabase CLI. +`, + js: () => `// Deploy an edge function via the Supabase CLI`, + bash: () => `supabase functions deploy hello-world --project-ref [ref] +`, + }, + // Entities + entitiesIntroduction: { + key: 'entities-introduction', + category: 'entities', + title: 'Introduction', + description: ` +All views and tables in the \`public\` schema, and those accessible by the active database role for a request are available for querying via the API. + +If you don't want to expose tables in your API, simply add them to a different schema (not the \`public\` schema). +`, + js: undefined, + bash: undefined, + }, + generatingTypes: { + key: 'generating-types', + category: 'entities', + title: 'Generating Types', + description: ` +Supabase APIs are generated from your database, which means that we can use database introspection to generate type-safe API definitions. + +You can generate types from your database either through the [Supabase CLI](https://supabase.com/docs/guides/database/api/generating-types), or by downloading the types file via the button on the right and importing it in your application within \`src/index.ts\`. +`, + js: undefined, + bash: undefined, + }, + graphql: { + key: 'graphql', + category: 'entities', + title: 'GraphQL vs PostgREST', + description: ` +If you have a GraphQL background, you might be wondering if you can fetch your data in a single round-trip. The answer is yes! The syntax is very similar. This example shows how you might achieve the same thing with Apollo GraphQL and Supabase. + +Still want GraphQL? +If you still want to use GraphQL, you can. Supabase provides you with a full Postgres database, so as long as your middleware can connect to the database then you can still use the tools you love. You can find the database connection details [in the settings](/project/[ref]/settings/database). +`, + js: (apikey?: string, endpoint?: string) => ` +// With Apollo GraphQL +const { loading, error, data } = useQuery(gql\` + query GetDogs { + dogs { + id + breed + owner { + id + name + } + } + } + \`) + +// With Supabase +const { data, error } = await supabase + .from('dogs') + .select(\` + id, breed, + owner (id, name) + \`) +`, + bash: (apikey?: string, endpoint?: string) => ` +// With Apollo GraphQL +const { loading, error, data } = useQuery(gql\` + query GetDogs { + dogs { + id + breed + owner { + id + name + } + } + } + \`) + +// With Supabase +const { data, error } = await supabase + .from('dogs') + .select(\` + id, breed, + owner (id, name) + \`) + `, + }, + // Stored Procedures + storedProceduresIntroduction: { + key: 'stored-procedures-introduction', + category: 'stored-procedures', + title: 'Introduction', + description: ` +All of your database stored procedures are available on your API. This means you can build your logic directly into the database (if you're brave enough)! + +The API endpoint supports POST (and in some cases GET) to execute the function. +`, + js: undefined, + bash: undefined, + }, + // Realtime + realtime: { + key: 'realtime-introduction', + category: 'realtime', + title: 'Introduction', + description: ` +Supabase provides a globally distributed cluster of Realtime servers that enable the following functionality: + +- [Broadcast](https://supabase.com/docs/guides/realtime/broadcast): Send ephemeral messages from client to clients with low latency. +- [Presence](https://supabase.com/docs/guides/realtime/presence): Track and synchronize shared state between clients. +- [Postgres Changes](https://supabase.com/docs/guides/realtime/postgres-changes): Listen to Postgres database changes and send them to authorized clients. +`, + js: undefined, + bash: undefined, + }, + subscribeChannel: { + key: 'subscribe-to-channel', + category: 'realtime', + title: 'Subscribe to channel', + description: ` +Creates an event handler that listens to changes. + +- By default, Broadcast and Presence are enabled for all projects. +- By default, listening to database changes is disabled for new projects due to database performance and security concerns. You can turn it on by managing Realtime's [replication](https://supabase.com/docs/guides/api#realtime-api-overview). +- You can receive the "previous" data for updates and deletes by setting the table's \`REPLICA IDENTITY\` to \`FULL\` (e.g., \`ALTER TABLE your_table REPLICA IDENTITY FULL;\`). +- Row level security is not applied to delete statements. When RLS is enabled and replica identity is set to full, only the primary key is sent to clients. +`, + js: () => ` +supabase + .channel('any') + .on('broadcast', { event: 'cursor-pos' }, payload => { + console.log('Cursor position received!', payload) + }) + .subscribe((status) => { + if (status === 'SUBSCRIBED') { + channel.send({ + type: 'broadcast', + event: 'cursor-pos', + payload: { x: Math.random(), y: Math.random() }, + }) + } + }) + `, + bash: () => `# Realtime streams are only supported by our client libraries`, + }, + unsubscribeChannel: { + key: 'unsubscribe-channel', + category: 'realtime', + title: 'Unsubscribe from a channel', + description: ` +Unsubscribes and removes Realtime channel from Realtime client. + +Removing a channel is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes. Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed. +`, + js: () => `supabase.removeChannel(myChannel)`, + bash: () => `# Realtime streams are only supported by our client libraries`, + }, + unsubscribeChannels: { + key: 'unsubscribe-channels', + category: 'realtime', + title: 'Unsubscribe from all channels', + description: ` +Unsubscribes and removes all Realtime channels from Realtime client. + +Removing a channel is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes. Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed. +`, + js: () => `supabase.removeChannels()`, + bash: () => `# Realtime streams are only supported by our client libraries`, + }, + retrieveAllChannels: { + key: 'unsubscribe-channel', + category: 'realtime', + title: 'Unsubscribe from a channel', + description: ` +Returns all Realtime channels. +`, + js: () => `const channels = supabase.getChannels()`, + bash: () => `# Realtime streams are only supported by our client libraries`, + }, +} + +export const DOCS_RESOURCE_CONTENT: { + [key: string]: { + key: string + title: string + category: string + description?: string + docsUrl: string + code: (props: any) => { key: string; title?: string; bash: string; js: string }[] + } +} = { + rpcSingle: { + key: 'invoke-function', + title: 'Invoke function', + category: 'stored-procedures', + description: undefined, + docsUrl: 'https://supabase.com/docs/reference/javascript/rpc', + code: ({ + rpcName, + rpcParams, + endpoint, + apiKey, + showBearer = true, + }: { + rpcName: string + rpcParams: any[] + endpoint: string + apiKey: string + showBearer: boolean + }) => { + let rpcList = rpcParams.map((x) => `"${x.name}": "value"`).join(', ') + let noParams = !rpcParams.length + let bashParams = noParams ? '' : `\n-d '{ ${rpcList} }' \\` + let jsParams = noParams + ? '' + : `, {${ + rpcParams.length + ? rpcParams + .map((x) => `\n ${x.name}`) + .join(`, `) + .concat('\n ') + : '' + }}` + return [ + { + key: 'rpc-single', + title: undefined, + bash: ` + curl -X POST '${endpoint}/rest/v1/rpc/${rpcName}' \\${bashParams} + -H "Content-Type: application/json" \\ + -H "apikey: ${apiKey}" ${ + showBearer + ? `\\ + -H "Authorization: Bearer ${apiKey}"` + : '' + } + `, + js: ` +let { data, error } = await supabase + .rpc('${rpcName}'${jsParams}) + +if (error) console.error(error) +else console.log(data) + `, + }, + ] + }, + }, + readRows: { + key: 'read-rows', + title: `Read rows`, + category: 'entities', + docsUrl: 'https://supabase.com/docs/reference/javascript/select', + description: `To read rows in this table, use the \`select\` method.`, + code: ({ + resourceId, + endpoint, + apikey, + }: { + resourceId: string + endpoint: string + apikey: string + }) => { + return [ + { + key: 'read-all-rows', + title: 'Read all rows', + bash: ` +curl '${endpoint}/rest/v1/${resourceId}?select=*' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer ${apikey}" + `, + js: ` +let { data: ${resourceId}, error } = await supabase + .from('${resourceId}') + .select('*') + `, + }, + { + key: 'read-specific-columns', + title: 'Read specific columns', + bash: ` +curl '${endpoint}/rest/v1/${resourceId}?select=some_column,other_column' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer ${apikey}" + `, + js: ` +let { data: ${resourceId}, error } = await supabase + .from('${resourceId}') + .select('some_column,other_column') + `, + }, + { + key: 'read-foreign-tables', + title: 'Read referenced tables', + bash: ` +curl '${endpoint}/rest/v1/${resourceId}?select=some_column,other_table(foreign_key)' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer ${apikey}" + `, + js: ` +let { data: ${resourceId}, error } = await supabase + .from('${resourceId}') + .select(\` + some_column, + other_table ( + foreign_key + ) + \`) + `, + }, + { + key: 'with-pagination', + title: 'With pagination', + bash: ` +curl '${endpoint}/rest/v1/${resourceId}?select=*' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer ${apikey}" \\ +-H "Range: 0-9" + `, + js: ` +let { data: ${resourceId}, error } = await supabase + .from('${resourceId}') + .select('*') + .range(0, 9) + `, + }, + ] + }, + }, + filtering: { + key: 'filter-rows', + category: 'entities', + title: 'Filtering', + description: `Supabase provides a wide range of filters`, + docsUrl: 'https://supabase.com/docs/reference/javascript/using-filters', + code: ({ + resourceId, + endpoint, + apikey, + }: { + resourceId: string + endpoint: string + apikey: string + }) => { + return [ + { + key: 'with-filtering', + title: 'With filtering', + bash: ` +curl '${endpoint}/rest/v1/${resourceId}?id=eq.1&select=*' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer ${apikey}" \\ +-H "Range: 0-9" + `, + js: ` +let { data: ${resourceId}, error } = await supabase + .from('${resourceId}') + .select("*") + + // Filters + .eq('column', 'Equal to') + .gt('column', 'Greater than') + .lt('column', 'Less than') + .gte('column', 'Greater than or equal to') + .lte('column', 'Less than or equal to') + .like('column', '%CaseSensitive%') + .ilike('column', '%CaseInsensitive%') + .is('column', null) + .in('column', ['Array', 'Values']) + .neq('column', 'Not equal to') + + // Arrays + .contains('array_column', ['array', 'contains']) + .containedBy('array_column', ['contained', 'by']) + `, + }, + ] + }, + }, + insertRows: { + key: 'insert-rows', + category: 'entities', + title: 'Insert rows', + description: ` +\`insert\` lets you insert into your tables. You can also insert in bulk and do UPSERT. + +\`insert\` will also return the replaced values for UPSERT. +`, + docsUrl: 'https://supabase.com/docs/reference/javascript/insert', + code: ({ + resourceId, + endpoint, + apikey, + }: { + resourceId: string + endpoint: string + apikey: string + }) => { + return [ + { + key: 'insert-a-row', + title: 'Insert a row', + bash: ` +curl -X POST '${endpoint}/rest/v1/${resourceId}' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer ${apikey}" \\ +-H "Content-Type: application/json" \\ +-H "Prefer: return=minimal" \\ +-d '{ "some_column": "someValue", "other_column": "otherValue" }' + `, + js: ` +const { data, error } = await supabase + .from('${resourceId}') + .insert([ + { some_column: 'someValue', other_column: 'otherValue' }, + ]) + .select() + `, + }, + { + key: 'insert-many-rows', + title: 'Insert many rows', + bash: ` +curl -X POST '${endpoint}/rest/v1/${resourceId}' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer ${apikey}" \\ +-H "Content-Type: application/json" \\ +-d '[{ "some_column": "someValue" }, { "other_column": "otherValue" }]' + `, + js: ` +const { data, error } = await supabase + .from('${resourceId}') + .insert([ + { some_column: 'someValue' }, + { some_column: 'otherValue' }, + ]) + .select() + `, + }, + { + key: 'upsert-matching-rows', + title: 'Upsert matching rows', + bash: ` +curl -X POST '${endpoint}/rest/v1/${resourceId}' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer ${apikey}" \\ +-H "Content-Type: application/json" \\ +-H "Prefer: resolution=merge-duplicates" \\ +-d '{ "some_column": "someValue", "other_column": "otherValue" }' + `, + js: ` +const { data, error } = await supabase + .from('${resourceId}') + .upsert({ some_column: 'someValue' }) + .select() + `, + }, + ] + }, + }, + updateRows: { + key: 'update-rows', + category: 'entities', + title: 'Update rows', + description: ` +\`update\` lets you update rows. \`update\` will match all rows by default. You can update specific rows using horizontal filters, e.g. \`eq\`, \`lt\`, and \`is\`. + +\`update\` will also return the replaced values for UPDATE. +`, + docsUrl: 'https://supabase.com/docs/reference/javascript/update', + code: ({ + resourceId, + endpoint, + apikey, + }: { + resourceId: string + endpoint: string + apikey: string + }) => { + return [ + { + key: 'update-matching-rows', + title: 'Update matching rows', + bash: ` +curl -X PATCH '${endpoint}/rest/v1/${resourceId}?some_column=eq.someValue' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer ${apikey}" \\ +-H "Content-Type: application/json" \\ +-H "Prefer: return=minimal" \\ +-d '{ "other_column": "otherValue" }' + `, + js: ` +const { data, error } = await supabase + .from('${resourceId}') + .update({ other_column: 'otherValue' }) + .eq('some_column', 'someValue') + .select() + `, + }, + ] + }, + }, + deleteRows: { + key: 'delete-rows', + category: 'entities', + title: 'Delete rows', + description: ` +\`delete\` lets you delete rows. \`delete\` will match all rows by default, so remember to specify your filters! +`, + docsUrl: 'https://supabase.com/docs/reference/javascript/delete', + code: ({ + resourceId, + endpoint, + apikey, + }: { + resourceId: string + endpoint: string + apikey: string + }) => { + return [ + { + key: 'delete-matching-rows', + title: 'Delete matching rows', + bash: ` +curl -X DELETE '${endpoint}/rest/v1/${resourceId}?some_column=eq.someValue' \\ +-H "apikey: ${apikey}" \\ +-H "Authorization: Bearer ${apikey}" + `, + js: ` +const { error } = await supabase + .from('${resourceId}') + .delete() + .eq('some_column', 'someValue') + `, + }, + ] + }, + }, + subscribeChanges: { + key: 'subscribe-changes', + category: 'entities', + title: 'Subscribe to changes', + description: ` +Supabase provides realtime functionality and broadcasts database changes to authorized users depending on Row Level Security (RLS) policies. +`, + docsUrl: 'https://supabase.com/docs/reference/javascript/subscribe', + code: ({ resourceId }: { resourceId: string }) => { + return [ + { + key: 'subscribe-all-events', + title: 'Subscribe to all events', + bash: `# Realtime streams are only supported by our client libraries`, + js: ` +const channels = supabase.channel('custom-all-channel') + .on( + 'postgres_changes', + { event: '*', schema: 'public', table: '${resourceId}' }, + (payload) => { + console.log('Change received!', payload) + } + ) + .subscribe()`, + }, + { + key: 'subscribe-to-inserts', + title: 'Subscribe to inserts', + bash: `# Realtime streams are only supported by our client libraries`, + js: ` +const channels = supabase.channel('custom-insert-channel') + .on( + 'postgres_changes', + { event: 'INSERT', schema: 'public', table: '${resourceId}' }, + (payload) => { + console.log('Change received!', payload) + } + ) + .subscribe()`, + }, + { + key: 'subscribe-to-updates', + title: 'Subscribe to updates', + bash: `# Realtime streams are only supported by our client libraries`, + js: ` +const channels = supabase.channel('custom-update-channel') + .on( + 'postgres_changes', + { event: 'UPDATE', schema: 'public', table: '${resourceId}' }, + (payload) => { + console.log('Change received!', payload) + } + ) + .subscribe()`, + }, + { + key: 'subscribe-to-deletes', + title: 'Subscribe to deletes', + bash: `# Realtime streams are only supported by our client libraries`, + js: ` +const channels = supabase.channel('custom-delete-channel') + .on( + 'postgres_changes', + { event: 'DELETE', schema: 'public', table: '${resourceId}' }, + (payload) => { + console.log('Change received!', payload) + } + ) + .subscribe()`, + }, + { + key: 'subscribe-to-specific-rows', + title: 'Subscribe to specific rows', + bash: `# Realtime streams are only supported by our client libraries`, + js: ` +const channels = supabase.channel('custom-filter-channel') + .on( + 'postgres_changes', + { event: '*', schema: 'public', table: '${resourceId}', filter: 'some_column=eq.some_value' }, + (payload) => { + console.log('Change received!', payload) + } + ) + .subscribe()`, + }, + ] + }, + }, + uploadFile: { + key: 'upload-file', + category: 'storage', + title: 'Upload a file', + docsUrl: 'https://supabase.com/docs/reference/javascript/storage-from-upload', + description: ` +Upload a file to an existing bucket. RLS policy permissions required: +- \`buckets\` table permissions: none +- \`objects\` table permissions: only \`insert\` when you are uploading new files and \`select\`, \`insert\`, and \`update\` when you are upserting files. +`, + code: ({ name, apikey, endpoint }: { name: string; apikey: string; endpoint: string }) => [ + { + key: 'storage-upload-file', + title: undefined, + bash: ` +curl -X POST '${endpoint}/storage/v1/object/${name}/folder/avatar1.png' \\ +-H 'Content-Type: image/png' \\ +-H "Authorization: Bearer ${apikey}" \\ +--data-binary @/path/to/your/file' +-H 'Content-Type: multipart/form-data' \\ +-H "Authorization: Bearer ${apikey}" \\ +--data-raw $'your_file_data' + `, + js: ` +const avatarFile = event.target.files[0] +const { data, error } = await supabase + .storage + .from('${name}') + .upload('folder/avatar1.png', avatarFile, { + cacheControl: '3600', + upsert: false + }) +`, + }, + ], + }, + deleteFiles: { + key: 'delete-files', + category: 'storage', + title: 'Delete files', + docsUrl: 'https://supabase.com/docs/reference/javascript/storage-from-remove', + description: ` +Delete files within the bucket. RLS policy permissions required: +- \`buckets\` table permissions: none +- \`objects\` table permissions: \`delete\` and \`select\` +`, + code: ({ name, apikey, endpoint }: { name: string; apikey: string; endpoint: string }) => [ + { + key: 'storage-delete-files', + title: undefined, + bash: ` +curl -X DELETE '${endpoint}/storage/v1/object/${name}' \\ +-H "Content-Type: application/json" \\ +-H "Authorization: Bearer ${apikey}" \\ +-d '{ "prefixes": ["file_name", "another_file_name"] }' +`, + js: ` +const { data, error } = await supabase + .storage + .from('${name}') + .remove(['folder/avatar1.png']) + `, + }, + ], + }, + listFiles: { + key: 'list-files', + category: 'storage', + title: 'List all files', + docsUrl: 'https://supabase.com/docs/reference/javascript/storage-from-list', + description: ` +List all files within the bucket. RLS policy permissions required: +- \`buckets\` table permissions: none +- \`objects\` table permissions: \`select\` +`, + code: ({ name, apikey, endpoint }: { name: string; apikey: string; endpoint: string }) => [ + { + key: 'storage-list-files', + title: undefined, + bash: ` +curl -X POST '${endpoint}/storage/v1/object/list/${name}' \\ +-H "Content-Type: application/json" \\ +-H "Authorization: Bearer ${apikey}" \\ +-d '{ "limit": 100, "offset": 0, "prefix": "", "sortBy": { "column": "name", "order": "asc" } }'`, + js: ` +const { data, error } = await supabase + .storage + .from('${name}') + .list('folder', { + limit: 100, + offset: 0, + sortBy: { column: 'name', order: 'asc' }, + }) + `, + }, + ], + }, + downloadFile: { + key: 'download-file', + category: 'storage', + title: 'Download a file', + docsUrl: 'https://supabase.com/docs/reference/javascript/storage-from-download', + description: ` +Downloads a file from a private bucket. For public buckets, make a request to the URL returned from getPublicUrl instead. RLS policy permissions required: +- \`buckets\` table permissions: none +- \`objects\` table permissions: \`select\` +`, + code: ({ name, apikey, endpoint }: { name: string; apikey: string; endpoint: string }) => [ + { + key: 'storage-download-file', + title: undefined, + bash: ` +curl -X GET '${endpoint}/storage/v1/object/${name}/folder/avatar1.png' \\ +-H "Content-Type: application/json" \\ +-H "Authorization: Bearer ${apikey}" \\ +--output avatar1.png +`, + js: ` +const { data, error } = await supabase + .storage + .from('${name}') + .download('folder/avatar1.png') + `, + }, + ], + }, + createSignedURL: { + key: 'create-signed-url', + category: 'storage', + title: 'Create a signed URL', + docsUrl: 'https://supabase.com/docs/reference/javascript/storage-from-createsignedurl', + description: ` +Create a signed URL which can be used to share a file for a fixed amount of time. RLS policy permissions required: +- \`buckets\` table permissions: none +- \`objects\` table permissions: \`select\` +`, + code: ({ name, apikey, endpoint }: { name: string; apikey: string; endpoint: string }) => [ + { + key: 'storage-create-signed-url', + title: undefined, + bash: ` +curl -X POST '${endpoint}/storage/v1/object/sign/${name}/folder/avatar1.png' \\ +-H "Content-Type: application/json" \\ +-H "Authorization: Bearer ${apikey}" \\ +-d '{ "expiresIn": 60 }' + `, + js: ` +const { data, error } = await supabase + .storage + .from('${name}') + .createSignedUrl('folder/avatar1.png', 60) + `, + }, + ], + }, + retrievePublicURL: { + key: 'retrieve-public-url', + category: 'storage', + title: 'Retrieve public URL', + docsUrl: 'https://supabase.com/docs/reference/javascript/storage-from-getpublicurl', + description: ` +A simple convenience function to get the URL for an asset in a public bucket. If you do not want to use this function, you can construct the public URL by concatenating the bucket URL with the path to the asset. + +This function does not verify if the bucket is public. If a public URL is created for a bucket which is not public, you will not be able to download the asset. + +The bucket needs to be set to public, either via \`updateBucket()\` or by going to Storage on supabase.com/dashboard, clicking the overflow menu on a bucket and choosing "Make public" + +RLS policy permissions required: +- \`buckets\` table permissions: none +- \`objects\` table permissions: none +`, + code: ({ name, apikey, endpoint }: { name: string; apikey: string; endpoint: string }) => [ + { + key: 'storage-retrieve-public-url', + title: undefined, + bash: ` +# No bash command available. +# You can construct the public URL by concatenating the bucket URL with the path to the asset +# e.g ${endpoint}/storage/v1/object/public/${name}/folder/avatar1.png`, + js: ` +const { data } = supabase + .storage + .from('${name}') + .getPublicUrl('folder/avatar1.png') + `, + }, + ], + }, + invokeEdgeFunction: { + key: 'invoke-edge-function', + category: 'edge-functions', + title: 'Invoke an edge function', + docsUrl: 'https://supabase.com/docs/reference/javascript/functions-invoke', + description: ` +Invokes a Supabase Edge Function. Requires an Authorization header, and invoke params generally match the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) spec. + +When you pass in a body to your function, we automatically attach the \`Content-Type\` header for \`Blob\`, \`ArrayBuffer\`, \`File\`, \`FormData\` and \`String\`. If it doesn't match any of these types we assume the payload is \`json\`, serialize it and attach the \`Content-Type\` header as \`application/json\`. You can override this behavior by passing in a \`Content-Type\` header of your own. + +Responses are automatically parsed as \`json\`, \`blob\` and \`form-data\` depending on the \`Content-Type\` header sent by your function. Responses are parsed as \`text\` by default. +`, + code: ({ name, endpoint, apikey }: { name: string; endpoint: string; apikey: string }) => [ + { + key: 'invoke-edge-function', + title: undefined, + bash: ` +curl --request POST '${endpoint}/functions/v1/${name}' \\ +--header 'Authorization: Bearer ${apikey}' \\ +--header 'Content-Type: application/json' \\ +--data '{ "name": "Functions" }' + `, + js: ` +const { data, error } = await supabase + .functions + .invoke('${name}', { + body: { foo: 'bar' } + })`, + }, + ], + }, +} diff --git a/apps/docs/components/APIDocs/ProjectAPIDocs.tsx b/apps/docs/components/APIDocs/ProjectAPIDocs.tsx new file mode 100644 index 00000000000..fd6a143b758 --- /dev/null +++ b/apps/docs/components/APIDocs/ProjectAPIDocs.tsx @@ -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 ( + snap.setShowProjectApiDocs(false)} + > +
+
+
+

API Docs

+
+ {!isEntityDocs && } + +
+
+ + {snap.activeDocsSection.length === 1 ? : } +
+ +
+ {snap.activeDocsSection[0] === 'introduction' && ( + + )} + + {snap.activeDocsSection[0] === 'user-management' && ( + + )} + + {snap.activeDocsSection[0] === 'realtime' && } + + {snap.activeDocsSection[0] === 'storage' && ( + <> + {snap.activeDocsSection[1] !== undefined ? ( + + ) : ( + + )} + + )} + + {snap.activeDocsSection[0] === 'edge-functions' && ( + <> + {snap.activeDocsSection[1] !== undefined ? ( + + ) : ( + + )} + + )} + + {snap.activeDocsSection[0] === 'entities' && ( + <> + {snap.activeDocsSection[1] !== undefined ? ( + + ) : ( + + )} + + )} + + {snap.activeDocsSection[0] === 'stored-procedures' && ( + <> + {snap.activeDocsSection[1] !== undefined ? ( + + ) : ( + + )} + + )} +
+
+
+ ) +} + +export default ProjectAPIDocs diff --git a/apps/docs/components/APIDocs/ResourceContent.tsx b/apps/docs/components/APIDocs/ResourceContent.tsx new file mode 100644 index 00000000000..3594a34e489 --- /dev/null +++ b/apps/docs/components/APIDocs/ResourceContent.tsx @@ -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 ( +
+
+
+

{snippet.title}

+ {snippet.docsUrl !== undefined && ( + + )} +
+ {snippet.description !== undefined && ( +
+
+ +
+
+ )} +
+ {codeSnippets.map((codeSnippet) => ( +
+

{codeSnippet.title}

+
+
+ + {codeSnippet[selectedLanguage]} + +
+
+
+ ))} +
+ ) +} +export default ResourceContent diff --git a/apps/docs/components/APIDocs/SecondLevelNav.tsx b/apps/docs/components/APIDocs/SecondLevelNav.tsx new file mode 100644 index 00000000000..0cd9c7f92ca --- /dev/null +++ b/apps/docs/components/APIDocs/SecondLevelNav.tsx @@ -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 ( +
+
+ {isNewAPIDocsEnabled && ( +
+ + + +
+ +
+
+ + + + + {content[section].options.map((option) => ( + updateSection(option.name)} + onClick={() => updateSection(option.name)} + > +

{option.name}

+
+ ))} +
+
+
+
+
+ +
+ {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 ( +

navigateToSection(item.key)} + > + {item.title} +

+ ) + })} +
+ +
+ + +

Unable to find what you're looking for?

+
+ +

+ The API methods shown here are only the commonly used ones to get you started building + quickly. +

+

+ Head over to our docs site for the full API documentation. +

+ +
+
+
+
+ ) +} + +export default SecondLevelNav diff --git a/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx index 3adebfe9d72..63d3fad7a86 100644 --- a/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx @@ -90,7 +90,7 @@ const GlobalNavigationMenu: FC = () => {
{section[0].menuItems?.map((menuItem, menuItemIndex) => ( - {menuItemIndex !== 0 && } + {menuItemIndex !== 0 && } {menuItem.map((item, itemIdx) => !item.href ? (
case 'contributing': return + case 'api-docs': + return default: return } diff --git a/apps/docs/components/Navigation/NavigationMenu/MenuIcons.tsx b/apps/docs/components/Navigation/NavigationMenu/MenuIcons.tsx index c9dfa878980..548931c2508 100644 --- a/apps/docs/components/Navigation/NavigationMenu/MenuIcons.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/MenuIcons.tsx @@ -635,3 +635,26 @@ export function IconTroubleshooting({ width = 16, height = 16, className }: Home ) } + +export function IconAPIDocs({ width = 16, height = 16, className }: HomeMenuIcon) { + return ( + + + + + + + + ) +} diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index b8abddbb635..7faa66477ad 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -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', }, ], [