From 765f95ce9fe83abe853296e4c39cec77753f706d Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Thu, 12 Feb 2026 09:21:54 -0700 Subject: [PATCH 1/5] chore: update filters with new animations based on feedback (#42704) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? - Updated placeholder logic to be more context aware on tables - Added AI loader - more tests for utils ## Demo https://github.com/user-attachments/assets/0ea75037-3cd2-4394-9f99-ee5b3bff87c8 ## Summary by CodeRabbit * **New Features** * Enhanced loading indicator with animated AI icon during AI-powered operations. * Dynamic filter placeholder text that displays available filter options. * **Style** * Simplified filter item selection UI by removing visual selection checkmarks. * Improved label display formatting for filter actions. --- .../header/filter/FilterPopoverNew.tsx | 8 +- .../src/FilterBar/CommandListItem.tsx | 3 +- .../src/FilterBar/FilterBar.test.tsx | 26 +--- .../ui-patterns/src/FilterBar/FilterGroup.tsx | 14 +- .../ui-patterns/src/FilterBar/utils.test.ts | 120 ++++++++++++++++++ packages/ui-patterns/src/FilterBar/utils.ts | 33 +++++ 6 files changed, 176 insertions(+), 28 deletions(-) diff --git a/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx b/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx index b4323dde9d5c3..9a731fc4edf2f 100644 --- a/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx +++ b/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx @@ -5,15 +5,15 @@ import { format } from 'date-fns' import { Loader2 } from 'lucide-react' import { useCallback, useMemo, useState } from 'react' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' -import { Button, Calendar } from 'ui' +import { AiIconAnimation, Button, Calendar } from 'ui' import { CustomOptionProps, FilterBar, FilterGroup, FilterOption, FilterProperty, - SerializableFilterProperty, isGroup, + SerializableFilterProperty, updateGroupAtPath, } from 'ui-patterns' @@ -190,7 +190,9 @@ export const FilterPopoverNew = ({ isRefetching = false }: FilterPopoverProps) = [generateFilters, serializableFilterProperties, handleFilterChange, setFreeformText] ) - const icon = isRefetching ? ( + const icon = isGenerating ? ( + + ) : isRefetching ? ( ) : null diff --git a/packages/ui-patterns/src/FilterBar/CommandListItem.tsx b/packages/ui-patterns/src/FilterBar/CommandListItem.tsx index 8c05ebb985b85..d5f42c4a5bd08 100644 --- a/packages/ui-patterns/src/FilterBar/CommandListItem.tsx +++ b/packages/ui-patterns/src/FilterBar/CommandListItem.tsx @@ -2,6 +2,7 @@ import { cn } from 'ui' import { OperatorSymbolBadge } from './OperatorSymbolBadge' import { MenuItem } from './types' +import { getActionItemLabel } from './utils' export type CommandListItemProps = { item: MenuItem @@ -31,7 +32,7 @@ export function CommandListItem({ > {includeIcon && item.icon} - {item.label} + {getActionItemLabel(item)} {item.operatorSymbol && } diff --git a/packages/ui-patterns/src/FilterBar/FilterBar.test.tsx b/packages/ui-patterns/src/FilterBar/FilterBar.test.tsx index 0c1841e919efc..5504c209f5285 100644 --- a/packages/ui-patterns/src/FilterBar/FilterBar.test.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterBar.test.tsx @@ -51,11 +51,7 @@ describe('FilterBar', () => { /> ) - expect( - screen.getByPlaceholderText( - 'Ask AI for help (e.g. Find all users with name John) or filter...' - ) - ).toBeInTheDocument() + expect(screen.getByPlaceholderText('Filter by Name, Status, Count')).toBeInTheDocument() }) it('renders with search input', () => { @@ -69,9 +65,7 @@ describe('FilterBar', () => { /> ) - const input = screen.getByPlaceholderText( - 'Ask AI for help (e.g. Find all users with name John) or filter...' - ) + const input = screen.getByPlaceholderText('Filter by Name, Status, Count') expect(input).toBeInTheDocument() }) @@ -92,9 +86,7 @@ describe('FilterBar', () => { /> ) - const freeform = screen.getByPlaceholderText( - 'Ask AI for help (e.g. Find all users with name John) or filter...' - ) + const freeform = screen.getByPlaceholderText('Filter by Name, Status, Count') freeform.focus() await user.click(freeform) @@ -144,9 +136,7 @@ describe('FilterBar', () => { /> ) - const freeform = screen.getByPlaceholderText( - 'Ask AI for help (e.g. Find all users with name John) or filter...' - ) + const freeform = screen.getByPlaceholderText('Filter by Name, Status, Count') await user.click(freeform) await user.click(screen.getByText('Status')) @@ -236,9 +226,7 @@ describe('FilterBar', () => { /> ) - const freeform = screen.getByPlaceholderText( - 'Ask AI for help (e.g. Find all users with name John) or filter...' - ) + const freeform = screen.getByPlaceholderText('Filter by Name, Status, Count...') await user.click(freeform) await user.click(screen.getByText('Tag')) @@ -300,9 +288,7 @@ describe('FilterBar', () => { /> ) - const freeform = screen.getByPlaceholderText( - 'Ask AI for help (e.g. Find all users with name John) or filter...' - ) + const freeform = screen.getByPlaceholderText('Filter by Name, Status, Count') await user.click(freeform) expect(await screen.findByText('Name')).toBeInTheDocument() diff --git a/packages/ui-patterns/src/FilterBar/FilterGroup.tsx b/packages/ui-patterns/src/FilterBar/FilterGroup.tsx index dca2232a6fdd1..310708e9627c0 100644 --- a/packages/ui-patterns/src/FilterBar/FilterGroup.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterGroup.tsx @@ -15,7 +15,7 @@ import { FilterCondition } from './FilterCondition' import { useDeferredBlur, useHighlightNavigation } from './hooks' import { buildPropertyItems } from './menuItems' import { FilterGroup as FilterGroupType } from './types' -import { pathsEqual } from './utils' +import { buildFilterPlaceholder, pathsEqual } from './utils' export type FilterGroupProps = { group: FilterGroupType @@ -100,6 +100,14 @@ export function FilterGroup({ group, path }: FilterGroupProps) { [filterProperties, isActive, freeformText, localFreeformValue, actions, supportsOperators] ) + const emptyPlaceholder = useMemo( + () => + buildFilterPlaceholder(filterProperties, { + hasActions: actions && actions.length > 0, + }), + [filterProperties, actions] + ) + // Only the root group should expand to fill available space const isRootGroup = path.length === 0 @@ -184,9 +192,7 @@ export function FilterGroup({ group, path }: FilterGroupProps) { onKeyDown={handleFreeformKeyDown} className="border-none bg-transparent text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full flex-1 h-auto min-w-0 px-2 py-0" placeholder={ - group.conditions.length === 0 - ? 'Ask AI for help (e.g. Find all users with name John) or filter...' - : 'Add more filters...' + group.conditions.length === 0 ? emptyPlaceholder : 'Add more filters...' } disabled={isLoading} /> diff --git a/packages/ui-patterns/src/FilterBar/utils.test.ts b/packages/ui-patterns/src/FilterBar/utils.test.ts index 92395eb563839..c3a2d8f725190 100644 --- a/packages/ui-patterns/src/FilterBar/utils.test.ts +++ b/packages/ui-patterns/src/FilterBar/utils.test.ts @@ -5,14 +5,17 @@ import { FilterGroup, FilterProperty, MenuItem } from './types' import { addFilterToGroup, addGroupToGroup, + buildFilterPlaceholder, findConditionByPath, findGroupByPath, + getActionItemLabel, groupMenuItemsByOperator, isAsyncOptionsFunction, isCustomOptionObject, isFilterOptionObject, isSyncOptionsFunction, removeFromGroup, + truncateText, updateNestedLogicalOperator, updateNestedOperator, updateNestedValue, @@ -335,4 +338,121 @@ describe('FilterBar Utils', () => { expect(result[0].items).toHaveLength(2) }) }) + + describe('truncateText', () => { + it('returns text unchanged if under max length', () => { + expect(truncateText('hello', 10)).toBe('hello') + }) + + it('returns text unchanged if exactly max length', () => { + expect(truncateText('hello', 5)).toBe('hello') + }) + + it('truncates text and adds ellipsis if over max length', () => { + expect(truncateText('hello world', 5)).toBe('hello...') + }) + + it('handles empty string', () => { + expect(truncateText('', 10)).toBe('') + }) + }) + + describe('getActionItemLabel', () => { + it('returns original label for non-action items', () => { + const item: MenuItem = { value: 'test', label: 'Test Label' } + expect(getActionItemLabel(item)).toBe('Test Label') + }) + + it('returns original label for action items without input value', () => { + const item: MenuItem = { value: 'ai', label: 'Filter by AI', isAction: true } + expect(getActionItemLabel(item)).toBe('Filter by AI') + }) + + it('returns formatted label for action items with input value', () => { + const item: MenuItem = { + value: 'ai', + label: 'Filter by AI', + isAction: true, + actionInputValue: 'Find users', + } + expect(getActionItemLabel(item)).toBe('Ask AI: "Find users"') + }) + + it('truncates long input values at 30 characters', () => { + const item: MenuItem = { + value: 'ai', + label: 'Filter by AI', + isAction: true, + actionInputValue: 'Find all users who registered in the last 30 days', + } + expect(getActionItemLabel(item)).toBe('Ask AI: "Find all users who registered ..."') + }) + }) + + describe('buildFilterPlaceholder', () => { + it('returns default message for empty properties without actions', () => { + expect(buildFilterPlaceholder([])).toBe('Add filters...') + }) + + it('returns default message with AI mention when actions exist', () => { + expect(buildFilterPlaceholder([], { hasActions: true })).toBe('Add filters or ask AI...') + }) + + it('shows single property name without actions', () => { + const props = [{ label: 'Name', name: 'name', type: 'string' as const }] + expect(buildFilterPlaceholder(props)).toBe('Filter by Name') + }) + + it('shows single property name with AI suffix when actions exist', () => { + const props = [{ label: 'Name', name: 'name', type: 'string' as const }] + expect(buildFilterPlaceholder(props, { hasActions: true })).toBe('Filter by Name or ask AI') + }) + + it('shows multiple property names up to max', () => { + const props = [ + { label: 'Name', name: 'name', type: 'string' as const }, + { label: 'Status', name: 'status', type: 'string' as const }, + { label: 'Created At', name: 'created_at', type: 'date' as const }, + ] + expect(buildFilterPlaceholder(props)).toBe('Filter by Name, Status, Created At') + }) + + it('truncates with ellipsis when more than max properties', () => { + const props = [ + { label: 'Name', name: 'name', type: 'string' as const }, + { label: 'Status', name: 'status', type: 'string' as const }, + { label: 'Created At', name: 'created_at', type: 'date' as const }, + { label: 'Updated At', name: 'updated_at', type: 'date' as const }, + ] + expect(buildFilterPlaceholder(props)).toBe('Filter by Name, Status, Created At...') + }) + + it('truncates with ellipsis and AI suffix when actions exist', () => { + const props = [ + { label: 'Name', name: 'name', type: 'string' as const }, + { label: 'Status', name: 'status', type: 'string' as const }, + { label: 'Created At', name: 'created_at', type: 'date' as const }, + { label: 'Updated At', name: 'updated_at', type: 'date' as const }, + ] + expect(buildFilterPlaceholder(props, { hasActions: true })).toBe( + 'Filter by Name, Status, Created At... or ask AI' + ) + }) + + it('respects custom maxProperties parameter', () => { + const props = [ + { label: 'Name', name: 'name', type: 'string' as const }, + { label: 'Status', name: 'status', type: 'string' as const }, + ] + expect(buildFilterPlaceholder(props, { maxProperties: 1 })).toBe('Filter by Name...') + }) + + it('does not add ellipsis when properties equal maxProperties', () => { + const props = [ + { label: 'Name', name: 'name', type: 'string' as const }, + { label: 'Status', name: 'status', type: 'string' as const }, + ] + expect(buildFilterPlaceholder(props, { maxProperties: 2 })).toBe('Filter by Name, Status') + }) + }) }) diff --git a/packages/ui-patterns/src/FilterBar/utils.ts b/packages/ui-patterns/src/FilterBar/utils.ts index 3fc143c58f397..719ececa866dc 100644 --- a/packages/ui-patterns/src/FilterBar/utils.ts +++ b/packages/ui-patterns/src/FilterBar/utils.ts @@ -276,3 +276,36 @@ export function groupMenuItemsByOperator(items: MenuItem[]): MenuItemGroup[] { items: grouped.get(groupKey) ?? [], })) } + +export function truncateText(text: string, maxLength: number): string { + if (text.length <= maxLength) return text + return text.slice(0, maxLength) + '...' +} + +export function getActionItemLabel(item: MenuItem): string { + if (item.isAction && item.actionInputValue) { + return `Ask AI: "${truncateText(item.actionInputValue, 30)}"` + } + return item.label +} + +export function buildFilterPlaceholder( + filterProperties: FilterProperty[], + options: { maxProperties?: number; hasActions?: boolean } = {} +): string { + const { maxProperties = 3, hasActions = false } = options + + if (filterProperties.length === 0) { + return hasActions ? 'Add filters or ask AI...' : 'Add filters...' + } + + const propertyNames = filterProperties + .slice(0, maxProperties) + .map((prop) => prop.label) + .join(', ') + + const suffix = filterProperties.length > maxProperties ? '...' : '' + const aiSuffix = hasActions ? ' or ask AI' : '' + + return `Filter by ${propertyNames}${suffix}${aiSuffix}` +} From 2cb5befaa377a42b6d6ca152b98105b59054f2f4 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Thu, 12 Feb 2026 09:22:04 -0700 Subject: [PATCH 2/5] fix: updated text jumping for value (#42743) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix: when editing the value on the filter, going to the middle and changing something forces you back and the end. Very annoying ## Summary by CodeRabbit * **Refactor** * Improved filter condition state management to ensure filter values remain consistent and responsive during user interactions. --- .../src/FilterBar/FilterCondition.tsx | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/ui-patterns/src/FilterBar/FilterCondition.tsx b/packages/ui-patterns/src/FilterBar/FilterCondition.tsx index 1c10369aae184..7c685c437fd6c 100644 --- a/packages/ui-patterns/src/FilterBar/FilterCondition.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterCondition.tsx @@ -58,19 +58,25 @@ export function FilterCondition({ const [showValueCustom, setShowValueCustom] = useState(false) const [hasTypedOperator, setHasTypedOperator] = useState(false) const [hasTypedValue, setHasTypedValue] = useState(false) + const [localValue, setLocalValue] = useState((condition.value ?? '').toString()) + + const conditionValue = (condition.value ?? '').toString() // Reset "has typed" state when focus changes useEffect(() => { - if (!isOperatorActive) { - setHasTypedOperator(false) - } - }, [isOperatorActive, setHasTypedOperator]) + if (!isOperatorActive) setHasTypedOperator(false) + }, [isOperatorActive]) useEffect(() => { - if (!isActive) { - setHasTypedValue(false) + if (!isActive) setHasTypedValue(false) + }, [isActive]) + + // Sync local value with condition.value when it changes externally (e.g., dropdown selection) + useEffect(() => { + if (localValue !== conditionValue) { + setLocalValue(conditionValue) } - }, [isActive, setHasTypedValue]) + }, [conditionValue]) useEffect(() => { if (isActive && valueRef.current) { @@ -108,7 +114,7 @@ export function FilterCondition({ filterProperties, propertyOptionsCache, loadingOptions, - (condition.value ?? '').toString(), + conditionValue, hasTypedValue ), [ @@ -117,7 +123,7 @@ export function FilterCondition({ filterProperties, propertyOptionsCache, loadingOptions, - condition.value, + conditionValue, hasTypedValue, ] ) @@ -196,6 +202,7 @@ export function FilterCondition({ const onValueChange = useCallback( (e: React.ChangeEvent) => { setHasTypedValue(true) + setLocalValue(e.target.value) handleInputChange(path, e.target.value) }, [handleInputChange, path] @@ -270,7 +277,7 @@ export function FilterCondition({ handleInputFocus(path)} onBlur={handleValueBlur} @@ -279,9 +286,7 @@ export function FilterCondition({ disabled={isLoading} aria-label={`Value for ${property.label}`} /> - - {(condition.value ?? '').toString() || ' '} - + {localValue || ' '} Date: Thu, 12 Feb 2026 20:01:53 +0100 Subject: [PATCH 3/5] chore: upgrade next-mdx-remote to v6 in apps/docs (#42748) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the CONTRIBUTING.md file. YES ## What kind of change does this PR introduce? Dependency upgrade (next-mdx-remote v4 → v6) ## What is the current behavior? The docs app uses next-mdx-remote v4.4.1 with MDX v2. ## What is the new behavior? - Upgraded to next-mdx-remote v6.0.0 (uses MDX v3) - Updated @mdx-js/loader and @mdx-js/react to v3 - Upgraded remark-gfm to v4 for MDX v3 compatibility - Removed deprecated `useDynamicImport` option (now default) - Added `blockJS: false` to preserve JS expressions in MDX content Build compiles successfully. Testing shows the same pre-existing prerender error on /guides/troubleshooting as on master (supabaseUrl is required). ## Summary by CodeRabbit * **Chores** * Upgraded MDX and markdown tooling to major releases (MDX v3, next-mdx-remote v6, remark-gfm v4). * Adjusted MDX serialization to disable embedded JS handling and remove legacy dynamic-import behavior for more consistent rendering of docs, guides, and code examples. --------- Co-authored-by: Claude Haiku 4.5 --- apps/docs/features/docs/MdxBase.tsx | 2 +- apps/docs/lib/docs.ts | 2 +- apps/docs/lib/mdx/generateRefMarkdown.tsx | 3 +- apps/docs/package.json | 8 +- apps/www/lib/mdx/mdxSerialize.ts | 1 + apps/www/package.json | 6 +- .../pages/partners/integrations/[slug].tsx | 5 +- pnpm-lock.yaml | 225 ++++++------------ pnpm-workspace.yaml | 1 + 9 files changed, 90 insertions(+), 163 deletions(-) diff --git a/apps/docs/features/docs/MdxBase.tsx b/apps/docs/features/docs/MdxBase.tsx index 672a49b04ab06..e0f29ecd057f2 100644 --- a/apps/docs/features/docs/MdxBase.tsx +++ b/apps/docs/features/docs/MdxBase.tsx @@ -10,8 +10,8 @@ import { components } from '~/features/docs/MdxBase.shared' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' const mdxOptions: SerializeOptions = { + blockJS: false, mdxOptions: { - useDynamicImport: true, remarkPlugins: [[remarkMath, { singleDollarTextMath: false }], remarkGfm], rehypePlugins: [rehypeKatex as any], }, diff --git a/apps/docs/lib/docs.ts b/apps/docs/lib/docs.ts index 6e6d44b55e4ff..3e8ea84a746be 100644 --- a/apps/docs/lib/docs.ts +++ b/apps/docs/lib/docs.ts @@ -126,8 +126,8 @@ export async function getGuidesStaticProps( } const mdxOptions: SerializeOptions = { + blockJS: false, mdxOptions: { - useDynamicImport: true, remarkPlugins: [[remarkMath, { singleDollarTextMath: false }], remarkGfm], rehypePlugins: [rehypeKatex as any], }, diff --git a/apps/docs/lib/mdx/generateRefMarkdown.tsx b/apps/docs/lib/mdx/generateRefMarkdown.tsx index 75ac7a3d06acb..bd3b03fb4c2aa 100644 --- a/apps/docs/lib/mdx/generateRefMarkdown.tsx +++ b/apps/docs/lib/mdx/generateRefMarkdown.tsx @@ -40,13 +40,12 @@ async function generateRefMarkdown(sections: ICommonMarkdown[], slug: string) { // introPage: introPages.includes(x), content: content ? await serialize(content ?? '', { + blockJS: false, // MDX's available options, see the MDX docs for more info. // https://mdxjs.com/packages/mdx/#compilefile-options mdxOptions: { - useDynamicImport: true, remarkPlugins: [remarkGfm], }, - // Indicates whether or not to parse the frontmatter from the mdx source }) : null, }) diff --git a/apps/docs/package.json b/apps/docs/package.json index 17908a4ff2cc0..298f9686230a7 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -43,8 +43,8 @@ }, "dependencies": { "@har-sdk/openapi-sampler": "^2.2.0", - "@mdx-js/loader": "^2.1.5", - "@mdx-js/react": "^2.3.0", + "@mdx-js/loader": "^3.0.0", + "@mdx-js/react": "^3.0.0", "@next/bundle-analyzer": "15.3.1", "@next/mdx": "15.3.1", "@octokit/auth-app": "^7.0.0", @@ -91,7 +91,7 @@ "micromark-extension-gfm": "^2.0.3", "micromark-extension-mdxjs": "^1.0.0", "next": "catalog:", - "next-mdx-remote": "^4.4.1", + "next-mdx-remote": "^6.0.0", "next-plugin-yaml": "^1.0.1", "next-themes": "^0.3.0", "nuqs": "^1.19.1", @@ -108,7 +108,7 @@ "rehype-slug": "^5.1.0", "remark": "^14.0.2", "remark-emoji": "^3.1.2", - "remark-gfm": "^3.0.1", + "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "server-only": "^0.0.1", "shared-data": "workspace:*", diff --git a/apps/www/lib/mdx/mdxSerialize.ts b/apps/www/lib/mdx/mdxSerialize.ts index 61d8949eaf359..f281ed6a6adfc 100644 --- a/apps/www/lib/mdx/mdxSerialize.ts +++ b/apps/www/lib/mdx/mdxSerialize.ts @@ -89,6 +89,7 @@ export async function mdxSerialize(source: string, options?: { tocDepth?: number let collectedToc: TocItem[] = [] const mdxSource = await serialize(preprocessedSource, { + blockJS: false, scope: { chCodeConfig: codeHikeOptions, }, diff --git a/apps/www/package.json b/apps/www/package.json index d9488aff7a47b..eb0a3c7b41c28 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -22,7 +22,7 @@ "@codesandbox/sandpack-react": "^2.20.0", "@hcaptcha/react-hcaptcha": "^1.12.0", "@heroicons/react": "^1.0.6", - "@mdx-js/react": "^2.3.0", + "@mdx-js/react": "^3.0.0", "@next/bundle-analyzer": "15.3.1", "@next/mdx": "15.3.1", "@octokit/auth-app": "^7.0.0", @@ -61,7 +61,7 @@ "micromark-extension-gfm": "^2.0.3", "micromark-extension-mdxjs": "^1.0.1", "next": "catalog:", - "next-mdx-remote": "^4.4.1", + "next-mdx-remote": "^6.0.0", "next-seo": "^6.5.0", "next-themes": "^0.3.0", "nuqs": "^2.8.1", @@ -78,7 +78,7 @@ "recharts": "catalog:", "rehype-slug": "^5.1.0", "remark": "^15.0.1", - "remark-gfm": "^3.0.1", + "remark-gfm": "^4.0.0", "shared-data": "workspace:*", "swiper": "^11.0.7", "typed.js": "^2.0.16", diff --git a/apps/www/pages/partners/integrations/[slug].tsx b/apps/www/pages/partners/integrations/[slug].tsx index 38fe738a2fb05..da731314ccd73 100644 --- a/apps/www/pages/partners/integrations/[slug].tsx +++ b/apps/www/pages/partners/integrations/[slug].tsx @@ -340,8 +340,11 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { // Parse markdown const overview = await serialize(partner.overview, { + blockJS: false, + scope: { + chCodeConfig: codeHikeOptions, + }, mdxOptions: { - useDynamicImport: true, remarkPlugins: [remarkGfm, [remarkCodeHike, codeHikeOptions]], }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8c8c8139b7be..1ea1b670f7efb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,17 +283,17 @@ importers: specifier: ^2.2.0 version: 2.2.0 '@mdx-js/loader': - specifier: ^2.1.5 - version: 2.3.0(supports-color@8.1.1)(webpack@5.94.0) + specifier: ^3.0.0 + version: 3.1.1(supports-color@8.1.1)(webpack@5.94.0) '@mdx-js/react': - specifier: ^2.3.0 - version: 2.3.0(react@18.3.1) + specifier: ^3.0.0 + version: 3.1.1(@types/react@18.3.3)(react@18.3.1) '@next/bundle-analyzer': specifier: 15.3.1 version: 15.3.1 '@next/mdx': specifier: 15.3.1 - version: 15.3.1(@mdx-js/loader@2.3.0(supports-color@8.1.1)(webpack@5.94.0))(@mdx-js/react@2.3.0(react@18.3.1)) + version: 15.3.1(@mdx-js/loader@3.1.1(supports-color@8.1.1)(webpack@5.94.0))(@mdx-js/react@3.1.1(@types/react@18.3.3)(react@18.3.1)) '@octokit/auth-app': specifier: ^7.0.0 version: 7.1.5 @@ -427,8 +427,8 @@ importers: specifier: 'catalog:' version: 15.5.10(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) next-mdx-remote: - specifier: ^4.4.1 - version: 4.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) + specifier: ^6.0.0 + version: 6.0.0(@types/react@18.3.3)(react@18.3.1)(supports-color@8.1.1) next-plugin-yaml: specifier: ^1.0.1 version: 1.0.1 @@ -478,8 +478,8 @@ importers: specifier: ^3.1.2 version: 3.1.2 remark-gfm: - specifier: ^3.0.1 - version: 3.0.1(supports-color@8.1.1) + specifier: ^4.0.0 + version: 4.0.1(supports-color@8.1.1) remark-math: specifier: ^6.0.0 version: 6.0.0(supports-color@8.1.1) @@ -1568,14 +1568,14 @@ importers: specifier: ^1.0.6 version: 1.0.6(react@18.3.1) '@mdx-js/react': - specifier: ^2.3.0 - version: 2.3.0(react@18.3.1) + specifier: ^3.0.0 + version: 3.1.1(@types/react@18.3.3)(react@18.3.1) '@next/bundle-analyzer': specifier: 15.3.1 version: 15.3.1 '@next/mdx': specifier: 15.3.1 - version: 15.3.1(@mdx-js/loader@2.3.0(supports-color@8.1.1)(webpack@5.94.0))(@mdx-js/react@2.3.0(react@18.3.1)) + version: 15.3.1(@mdx-js/loader@3.1.1(supports-color@8.1.1)(webpack@5.94.0))(@mdx-js/react@3.1.1(@types/react@18.3.3)(react@18.3.1)) '@octokit/auth-app': specifier: ^7.0.0 version: 7.1.5 @@ -1685,8 +1685,8 @@ importers: specifier: 'catalog:' version: 15.5.10(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) next-mdx-remote: - specifier: ^4.4.1 - version: 4.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) + specifier: ^6.0.0 + version: 6.0.0(@types/react@18.3.3)(react@18.3.1)(supports-color@8.1.1) next-seo: specifier: ^6.5.0 version: 6.5.0(next@15.5.10(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1736,8 +1736,8 @@ importers: specifier: ^15.0.1 version: 15.0.1(supports-color@8.1.1) remark-gfm: - specifier: ^3.0.1 - version: 3.0.1(supports-color@8.1.1) + specifier: ^4.0.0 + version: 4.0.1(supports-color@8.1.1) shared-data: specifier: workspace:* version: link:../../packages/shared-data @@ -4536,20 +4536,21 @@ packages: peerDependencies: esbuild: ^0.25.2 - '@mdx-js/loader@2.3.0': - resolution: {integrity: sha512-IqsscXh7Q3Rzb+f5DXYk0HU71PK+WuFsEhf+mSV3fOhpLcEpgsHvTQ2h0T6TlZ5gHOaBeFjkXwB52by7ypMyNg==} + '@mdx-js/loader@3.1.1': + resolution: {integrity: sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ==} peerDependencies: - webpack: '>=4' - - '@mdx-js/mdx@2.3.0': - resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==} + webpack: '>=5' + peerDependenciesMeta: + webpack: + optional: true '@mdx-js/mdx@3.0.1': resolution: {integrity: sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==} - '@mdx-js/react@2.3.0': - resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==} + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} peerDependencies: + '@types/react': '>=16' react: '>=16' '@mermaid-js/parser@0.6.3': @@ -11660,15 +11661,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} - estree-util-attach-comments@2.1.1: - resolution: {integrity: sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==} - estree-util-attach-comments@3.0.0: resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} - estree-util-build-jsx@2.2.2: - resolution: {integrity: sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==} - estree-util-build-jsx@3.0.1: resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} @@ -11678,9 +11673,6 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - estree-util-to-js@1.2.0: - resolution: {integrity: sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==} - estree-util-to-js@2.0.0: resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} @@ -12515,9 +12507,6 @@ packages: hast-util-raw@9.0.1: resolution: {integrity: sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA==} - hast-util-to-estree@2.3.3: - resolution: {integrity: sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==} - hast-util-to-estree@3.1.3: resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} @@ -12787,9 +12776,6 @@ packages: inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} - inline-style-parser@0.2.3: - resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==} - inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} @@ -13756,10 +13742,6 @@ packages: mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} - markdown-extensions@1.1.1: - resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==} - engines: {node: '>=0.10.0'} - markdown-extensions@2.0.0: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} @@ -14516,12 +14498,11 @@ packages: react: '*' react-dom: '*' - next-mdx-remote@4.4.1: - resolution: {integrity: sha512-1BvyXaIou6xy3XoNF4yaMZUCb6vD2GTAa5ciOa6WoO+gAUTYsb1K4rI/HSC2ogAWLrb/7VSV52skz07vOzmqIQ==} + next-mdx-remote@6.0.0: + resolution: {integrity: sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ==} engines: {node: '>=14', npm: '>=7'} peerDependencies: - react: '>=16.x <=18.x' - react-dom: '>=16.x <=18.x' + react: '>=16' next-plugin-yaml@1.0.1: resolution: {integrity: sha512-k13wbpN33wTgrgWUBiXPtObXO94JOEiwvYKJ3X0JrBz2yaW24EfNaVqHwA0dwhsn9PyPIKhPhgsu3bhJsNs+cQ==} @@ -16382,9 +16363,6 @@ packages: remark-mdx-frontmatter@4.0.0: resolution: {integrity: sha512-PZzAiDGOEfv1Ua7exQ8S5kKxkD8CDaSb4nM+1Mprs6u8dyvQifakh+kCj6NovfGXW+bTvrhjaR3srzjS2qJHKg==} - remark-mdx@2.3.0: - resolution: {integrity: sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==} - remark-mdx@3.0.1: resolution: {integrity: sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA==} @@ -17244,9 +17222,6 @@ packages: style-to-object@0.4.2: resolution: {integrity: sha512-1JGpfPB3lo42ZX8cuPrheZbfQ6kqPPnPHlKMyeRYtfKD+0jG+QsXgXN57O/dvJlzlB2elI6dGmrPnl5VPQFPaA==} - style-to-object@1.0.6: - resolution: {integrity: sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==} - style-to-object@1.0.8: resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} @@ -17906,6 +17881,9 @@ packages: unist-util-remove-position@5.0.0: resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + unist-util-remove@4.0.0: + resolution: {integrity: sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==} + unist-util-stringify-position@2.0.3: resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} @@ -17927,6 +17905,9 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universal-github-app-jwt@2.2.0: resolution: {integrity: sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==} @@ -18210,8 +18191,8 @@ packages: vfile-location@5.0.2: resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==} - vfile-matter@3.0.1: - resolution: {integrity: sha512-CAAIDwnh6ZdtrqAuxdElUqQRQDQgbbIrYtDYI8gCjXS1qQ+1XdLoK8FIZWxJwn0/I+BkSSZpar3SOgjemQz4fg==} + vfile-matter@5.0.1: + resolution: {integrity: sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw==} vfile-message@2.0.4: resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} @@ -21496,36 +21477,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/loader@2.3.0(supports-color@8.1.1)(webpack@5.94.0)': + '@mdx-js/loader@3.1.1(supports-color@8.1.1)(webpack@5.94.0)': dependencies: - '@mdx-js/mdx': 2.3.0(supports-color@8.1.1) - source-map: 0.7.4 + '@mdx-js/mdx': 3.0.1(supports-color@8.1.1) + source-map: 0.7.6 + optionalDependencies: webpack: 5.94.0 transitivePeerDependencies: - supports-color - '@mdx-js/mdx@2.3.0(supports-color@8.1.1)': - dependencies: - '@types/estree-jsx': 1.0.1 - '@types/mdx': 2.0.10 - estree-util-build-jsx: 2.2.2 - estree-util-is-identifier-name: 2.1.0 - estree-util-to-js: 1.2.0 - estree-walker: 3.0.3 - hast-util-to-estree: 2.3.3(supports-color@8.1.1) - markdown-extensions: 1.1.1 - periscopic: 3.1.0 - remark-mdx: 2.3.0(supports-color@8.1.1) - remark-parse: 10.0.2(supports-color@8.1.1) - remark-rehype: 10.1.0 - unified: 10.1.2 - unist-util-position-from-estree: 1.1.2 - unist-util-stringify-position: 3.0.3 - unist-util-visit: 4.1.2 - vfile: 5.3.7 - transitivePeerDependencies: - - supports-color - '@mdx-js/mdx@3.0.1(supports-color@8.1.1)': dependencies: '@types/estree': 1.0.5 @@ -21549,12 +21509,12 @@ snapshots: unified: 11.0.5 unist-util-position-from-estree: 2.0.0 unist-util-stringify-position: 4.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 transitivePeerDependencies: - supports-color - '@mdx-js/react@2.3.0(react@18.3.1)': + '@mdx-js/react@3.1.1(@types/react@18.3.3)(react@18.3.1)': dependencies: '@types/mdx': 2.0.10 '@types/react': 18.3.3 @@ -21700,12 +21660,12 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@next/mdx@15.3.1(@mdx-js/loader@2.3.0(supports-color@8.1.1)(webpack@5.94.0))(@mdx-js/react@2.3.0(react@18.3.1))': + '@next/mdx@15.3.1(@mdx-js/loader@3.1.1(supports-color@8.1.1)(webpack@5.94.0))(@mdx-js/react@3.1.1(@types/react@18.3.3)(react@18.3.1))': dependencies: source-map: 0.7.4 optionalDependencies: - '@mdx-js/loader': 2.3.0(supports-color@8.1.1)(webpack@5.94.0) - '@mdx-js/react': 2.3.0(react@18.3.1) + '@mdx-js/loader': 3.1.1(supports-color@8.1.1)(webpack@5.94.0) + '@mdx-js/react': 3.1.1(@types/react@18.3.3)(react@18.3.1) '@next/swc-darwin-arm64@15.5.7': optional: true @@ -29671,20 +29631,10 @@ snapshots: estraverse@5.3.0: {} - estree-util-attach-comments@2.1.1: - dependencies: - '@types/estree': 1.0.5 - estree-util-attach-comments@3.0.0: dependencies: '@types/estree': 1.0.5 - estree-util-build-jsx@2.2.2: - dependencies: - '@types/estree-jsx': 1.0.1 - estree-util-is-identifier-name: 2.1.0 - estree-walker: 3.0.3 - estree-util-build-jsx@3.0.1: dependencies: '@types/estree-jsx': 1.0.1 @@ -29696,12 +29646,6 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} - estree-util-to-js@1.2.0: - dependencies: - '@types/estree-jsx': 1.0.1 - astring: 1.8.6 - source-map: 0.7.6 - estree-util-to-js@2.0.0: dependencies: '@types/estree-jsx': 1.0.1 @@ -30707,31 +30651,11 @@ snapshots: mdast-util-to-hast: 13.2.1 parse5: 7.2.1 unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 web-namespaces: 2.0.1 zwitch: 2.0.4 - hast-util-to-estree@2.3.3(supports-color@8.1.1): - dependencies: - '@types/estree': 1.0.5 - '@types/estree-jsx': 1.0.1 - '@types/hast': 2.3.6 - '@types/unist': 2.0.8 - comma-separated-tokens: 2.0.3 - estree-util-attach-comments: 2.1.1 - estree-util-is-identifier-name: 2.1.0 - hast-util-whitespace: 2.0.1 - mdast-util-mdx-expression: 1.3.2(supports-color@8.1.1) - mdast-util-mdxjs-esm: 1.3.1(supports-color@8.1.1) - property-information: 6.3.0 - space-separated-tokens: 2.0.2 - style-to-object: 0.4.2 - unist-util-position: 4.0.4 - zwitch: 2.0.4 - transitivePeerDependencies: - - supports-color - hast-util-to-estree@3.1.3(supports-color@8.1.1): dependencies: '@types/estree': 1.0.5 @@ -30781,7 +30705,7 @@ snapshots: mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) property-information: 6.3.0 space-separated-tokens: 2.0.2 - style-to-object: 1.0.6 + style-to-object: 1.0.8 unist-util-position: 5.0.0 vfile-message: 4.0.2 transitivePeerDependencies: @@ -31079,8 +31003,6 @@ snapshots: inline-style-parser@0.1.1: {} - inline-style-parser@0.2.3: {} - inline-style-parser@0.2.4: {} inline-style-prefixer@7.0.0: @@ -32078,8 +32000,6 @@ snapshots: mark.js@8.11.1: {} - markdown-extensions@1.1.1: {} - markdown-extensions@2.0.0: {} markdown-it@12.3.2: @@ -32449,7 +32369,7 @@ snapshots: micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 mdast-util-to-markdown@1.5.0: @@ -32471,7 +32391,7 @@ snapshots: mdast-util-phrasing: 4.1.0 mdast-util-to-string: 4.0.0 micromark-util-decode-string: 2.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 zwitch: 2.0.4 mdast-util-to-string@3.2.0: @@ -33412,15 +33332,18 @@ snapshots: - markdown-wasm - supports-color - next-mdx-remote@4.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1): + next-mdx-remote@6.0.0(@types/react@18.3.3)(react@18.3.1)(supports-color@8.1.1): dependencies: - '@mdx-js/mdx': 2.3.0(supports-color@8.1.1) - '@mdx-js/react': 2.3.0(react@18.3.1) + '@babel/code-frame': 7.29.0 + '@mdx-js/mdx': 3.0.1(supports-color@8.1.1) + '@mdx-js/react': 3.1.1(@types/react@18.3.3)(react@18.3.1) react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - vfile: 5.3.7 - vfile-matter: 3.0.1 + unist-util-remove: 4.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + vfile-matter: 5.0.1 transitivePeerDependencies: + - '@types/react' - supports-color next-plugin-yaml@1.0.1: @@ -35781,13 +35704,6 @@ snapshots: unified: 11.0.5 yaml: 2.8.1 - remark-mdx@2.3.0(supports-color@8.1.1): - dependencies: - mdast-util-mdx: 2.0.1(supports-color@8.1.1) - micromark-extension-mdxjs: 1.0.1 - transitivePeerDependencies: - - supports-color - remark-mdx@3.0.1(supports-color@8.1.1): dependencies: mdast-util-mdx: 3.0.0(supports-color@8.1.1) @@ -36883,10 +36799,6 @@ snapshots: dependencies: inline-style-parser: 0.1.1 - style-to-object@1.0.6: - dependencies: - inline-style-parser: 0.2.3 - style-to-object@1.0.8: dependencies: inline-style-parser: 0.2.4 @@ -37611,7 +37523,13 @@ snapshots: unist-util-remove-position@5.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 + + unist-util-remove@4.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 unist-util-stringify-position@2.0.3: dependencies: @@ -37647,6 +37565,12 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + universal-github-app-jwt@2.2.0: {} universal-user-agent@7.0.2: {} @@ -37889,11 +37813,10 @@ snapshots: '@types/unist': 3.0.3 vfile: 6.0.3 - vfile-matter@3.0.1: + vfile-matter@5.0.1: dependencies: - '@types/js-yaml': 4.0.6 - is-buffer: 2.0.5 - js-yaml: 4.1.1 + vfile: 6.0.3 + yaml: 2.8.1 vfile-message@2.0.4: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9249b99a53ab2..0ebdcd1344250 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -58,6 +58,7 @@ minimumReleaseAgeExclude: - diff - lodash-es - lodash + - next-mdx-remote onlyBuiltDependencies: - supabase From e99ea31e0a6ac7a8c80fe6f01c631609ac3063ff Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Thu, 12 Feb 2026 12:45:25 -0700 Subject: [PATCH 4/5] fix: updated guides data out of mdx into separate tsx file (#42751) --- apps/docs/content/guides/ai.mdx | 70 +-------------- apps/docs/content/guides/self-hosting.mdx | 15 +--- apps/docs/content/guides/storage.mdx | 11 +-- apps/docs/features/docs/MdxBase.tsx | 12 +-- apps/docs/lib/guidesData.ts | 102 ++++++++++++++++++++++ 5 files changed, 112 insertions(+), 98 deletions(-) create mode 100644 apps/docs/lib/guidesData.ts diff --git a/apps/docs/content/guides/ai.mdx b/apps/docs/content/guides/ai.mdx index c2cfe4743d73f..1c4cc86b92309 100644 --- a/apps/docs/content/guides/ai.mdx +++ b/apps/docs/content/guides/ai.mdx @@ -31,7 +31,7 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup {/* */}
- {examples.map((x) => ( + {aiExamples.map((x) => (
@@ -42,40 +42,6 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup ))}
-export const examples = [ - { - name: 'Headless Vector Search', - description: 'A toolkit to perform vector similarity search on your knowledge base embeddings.', - href: '/guides/ai/examples/headless-vector-search', - }, - { - name: 'Image Search with OpenAI CLIP', - description: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.', - href: '/guides/ai/examples/image-search-openai-clip', - }, - { - name: 'Hugging Face inference', - description: 'Generate image captions using Hugging Face.', - href: '/guides/ai/examples/huggingface-image-captioning', - }, - { - name: 'OpenAI completions', - description: 'Generate GPT text completions using OpenAI in Edge Functions.', - href: '/guides/ai/examples/openai', - }, - { - name: 'Building ChatGPT Plugins', - description: 'Use Supabase as a Retrieval Store for your ChatGPT plugin.', - href: '/guides/ai/examples/building-chatgpt-plugins', - }, - { - name: 'Vector search with Next.js and OpenAI', - description: - 'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.', - href: '/guides/ai/examples/nextjs-vector-search', - }, -] - {/* */} ## Integrations @@ -83,7 +49,7 @@ export const examples = [ {/* */}
- {integrations.map((x) => ( + {aiIntegrations.map((x) => (
{x.description} @@ -92,38 +58,6 @@ export const examples = [ ))}
-export const integrations = [ - { - name: 'OpenAI', - description: - 'OpenAI is an AI research and deployment company. Supabase provides a simple way to use OpenAI in your applications.', - href: '/guides/ai/examples/building-chatgpt-plugins', - }, - { - name: 'Amazon Bedrock', - description: - 'A fully managed service that offers a choice of high-performing foundation models from leading AI companies.', - href: '/guides/ai/integrations/amazon-bedrock', - }, - { - name: 'Hugging Face', - description: - "Hugging Face is an open-source provider of NLP technologies. Supabase provides a simple way to use Hugging Face's models in your applications.", - href: '/guides/ai/hugging-face', - }, - { - name: 'LangChain', - description: - 'LangChain is a language-agnostic, open-source, and self-hosted API for text translation, summarization, and sentiment analysis.', - href: '/guides/ai/langchain', - }, - { - name: 'LlamaIndex', - description: 'LlamaIndex is a data framework for your LLM applications.', - href: '/guides/ai/integrations/llamaindex', - }, -] - {/* */} ## Case studies diff --git a/apps/docs/content/guides/self-hosting.mdx b/apps/docs/content/guides/self-hosting.mdx index 60d129837edeb..37ac3e9b57190 100644 --- a/apps/docs/content/guides/self-hosting.mdx +++ b/apps/docs/content/guides/self-hosting.mdx @@ -34,7 +34,7 @@ The fastest and recommended way to self-host Supabase is using Docker. There are several other ways to deploy Supabase with the help of community-driven projects. These projects may be outdated and are seeking active maintainers. If you're interested in maintaining one of these projects, [contact the Community team](/open-source/contributing/supasquad).
- {community.map((x) => ( + {selfHostingCommunity.map((x) => (
-export const community = [ - { - name: 'Kubernetes', - description: 'Helm charts to deploy a Supabase on Kubernetes.', - href: 'https://github.com/supabase-community/supabase-kubernetes', - }, - { - name: 'Traefik', - description: 'A self-hosted Supabase setup with Traefik as a reverse proxy.', - href: 'https://github.com/supabase-community/supabase-traefik', - }, -] - ## About self-hosting Self-hosting is a good fit if you need full control over your data, have compliance requirements that prevent using managed services, or want to run Supabase in an isolated environment. diff --git a/apps/docs/content/guides/storage.mdx b/apps/docs/content/guides/storage.mdx index 5139bd5873e32..51b8a466b64b0 100644 --- a/apps/docs/content/guides/storage.mdx +++ b/apps/docs/content/guides/storage.mdx @@ -71,7 +71,7 @@ Specialized storage for vector embeddings and similarity search operations. Desi Check out all of the Storage [templates and examples](https://github.com/supabase/supabase/tree/master/examples/storage) in our GitHub repository.
- {examples.map((x) => ( + {storageExamples.map((x) => (
@@ -82,15 +82,6 @@ Check out all of the Storage [templates and examples](https://github.com/supabas ))}
-export const examples = [ - { - name: 'Resumable Uploads with Uppy', - description: - 'Use Uppy to upload files to Supabase Storage using the TUS protocol (resumable uploads).', - href: 'https://github.com/supabase/supabase/tree/master/examples/storage/resumable-upload-uppy', - }, -] - ## Resources Find the source code and documentation in the Supabase GitHub repository. diff --git a/apps/docs/features/docs/MdxBase.tsx b/apps/docs/features/docs/MdxBase.tsx index e0f29ecd057f2..ba2d15b3a540a 100644 --- a/apps/docs/features/docs/MdxBase.tsx +++ b/apps/docs/features/docs/MdxBase.tsx @@ -1,14 +1,14 @@ +import { preprocessMdxWithDefaults } from '~/features/directives/utils' +import { components } from '~/features/docs/MdxBase.shared' +import { guidesData } from '~/lib/guidesData' +import { SerializeOptions } from '~/types/next-mdx-remote-serialize' +import { isFeatureEnabled } from 'common' import { MDXRemote } from 'next-mdx-remote/rsc' import { type ComponentProps } from 'react' import rehypeKatex from 'rehype-katex' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' -import { isFeatureEnabled } from 'common' -import { preprocessMdxWithDefaults } from '~/features/directives/utils' -import { components } from '~/features/docs/MdxBase.shared' -import { SerializeOptions } from '~/types/next-mdx-remote-serialize' - const mdxOptions: SerializeOptions = { blockJS: false, mdxOptions: { @@ -40,7 +40,7 @@ const MDXRemoteBase = async ({ } = mdxOptions const finalOptions = { - scope: { isFeatureEnabled }, + scope: { isFeatureEnabled, ...guidesData }, ...mdxOptions, ...otherOptions, mdxOptions: { diff --git a/apps/docs/lib/guidesData.ts b/apps/docs/lib/guidesData.ts new file mode 100644 index 0000000000000..2bf5c7ccc37db --- /dev/null +++ b/apps/docs/lib/guidesData.ts @@ -0,0 +1,102 @@ +/** + * Data used in guide MDX files. + * + * This data is passed to MDX files via scope to avoid using `export const` + * within MDX content, which is not supported by next-mdx-remote. + * + * @see https://github.com/hashicorp/next-mdx-remote#import--export + */ + +export const guidesData = { + // apps/docs/content/guides/self-hosting.mdx + selfHostingCommunity: [ + { + name: 'Kubernetes', + description: 'Helm charts to deploy a Supabase on Kubernetes.', + href: 'https://github.com/supabase-community/supabase-kubernetes', + }, + { + name: 'Traefik', + description: 'A self-hosted Supabase setup with Traefik as a reverse proxy.', + href: 'https://github.com/supabase-community/supabase-traefik', + }, + ], + + // apps/docs/content/guides/ai.mdx + aiExamples: [ + { + name: 'Headless Vector Search', + description: + 'A toolkit to perform vector similarity search on your knowledge base embeddings.', + href: '/guides/ai/examples/headless-vector-search', + }, + { + name: 'Image Search with OpenAI CLIP', + description: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.', + href: '/guides/ai/examples/image-search-openai-clip', + }, + { + name: 'Hugging Face inference', + description: 'Generate image captions using Hugging Face.', + href: '/guides/ai/examples/huggingface-image-captioning', + }, + { + name: 'OpenAI completions', + description: 'Generate GPT text completions using OpenAI in Edge Functions.', + href: '/guides/ai/examples/openai', + }, + { + name: 'Building ChatGPT Plugins', + description: 'Use Supabase as a Retrieval Store for your ChatGPT plugin.', + href: '/guides/ai/examples/building-chatgpt-plugins', + }, + { + name: 'Vector search with Next.js and OpenAI', + description: + 'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.', + href: '/guides/ai/examples/nextjs-vector-search', + }, + ], + + aiIntegrations: [ + { + name: 'OpenAI', + description: + 'OpenAI is an AI research and deployment company. Supabase provides a simple way to use OpenAI in your applications.', + href: '/guides/ai/examples/building-chatgpt-plugins', + }, + { + name: 'Amazon Bedrock', + description: + 'A fully managed service that offers a choice of high-performing foundation models from leading AI companies.', + href: '/guides/ai/integrations/amazon-bedrock', + }, + { + name: 'Hugging Face', + description: + "Hugging Face is an open-source provider of NLP technologies. Supabase provides a simple way to use Hugging Face's models in your applications.", + href: '/guides/ai/hugging-face', + }, + { + name: 'LangChain', + description: + 'LangChain is a language-agnostic, open-source, and self-hosted API for text translation, summarization, and sentiment analysis.', + href: '/guides/ai/langchain', + }, + { + name: 'LlamaIndex', + description: 'LlamaIndex is a data framework for your LLM applications.', + href: '/guides/ai/integrations/llamaindex', + }, + ], + + // apps/docs/content/guides/storage.mdx + storageExamples: [ + { + name: 'Resumable Uploads with Uppy', + description: + 'Use Uppy to upload files to Supabase Storage using the TUS protocol (resumable uploads).', + href: 'https://github.com/supabase/supabase/tree/master/examples/storage/resumable-upload-uppy', + }, + ], +} From f7bf7d7ce459912eff3c296dbcc0c82b828490b9 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:57:44 -0500 Subject: [PATCH 5/5] feat(studio): move data api docs to integrations section (#42749) Feature / Refactor ## What is the current behavior? Data API docs live at the `/api` route as a standalone page. Old links point to the previous location. ## What is the new behavior? Data API docs are moved to the integrations section with a dedicated docs tab and settings tab. Old links are cleaned up, a mobile menu is added for data API docs navigation, and minor code review fixes are applied. ## Additional context Resolves FE-2517 ## Summary by CodeRabbit * **New Features** * Revamped API docs UI with reusable section layout, language toggle (JS/Bash), API key selection, and improved code snippets * Added Data API docs tab, mobile navigation, and dedicated loading/error/disabled states * **Navigation Updates** * Moved API docs and related links into the Integrations/Data API area and added redirects to new routes * Updated various internal links to the new Data API settings and overview locations * **Tests** * Added comprehensive unit tests for Data API utilities --- .../Auth/Policies/PolicyTableRow/index.tsx | 17 +- .../interfaces/Docs/Authentication.tsx | 180 ++++---- .../interfaces/Docs/CodeSnippet.tsx | 17 +- .../components/interfaces/Docs/DocSection.tsx | 24 ++ .../components/interfaces/Docs/Docs.types.ts | 2 +- .../interfaces/Docs/GeneralContent.tsx | 19 +- .../interfaces/Docs/GeneratingTypes.tsx | 85 ++-- .../interfaces/Docs/Introduction.tsx | 34 +- .../interfaces/Docs/LangSelector.tsx | 213 +++++----- .../Docs/Pages/Rpc/Introduction.tsx | 16 +- .../Docs/Pages/Tables/Introduction.tsx | 100 ++--- .../interfaces/Docs/Pages/UserManagement.tsx | 391 +++++++++--------- .../components/interfaces/Docs/Param.tsx | 75 +--- .../interfaces/Docs/ResourceContent.tsx | 258 ++++++------ .../components/interfaces/Docs/RpcContent.tsx | 59 +-- .../Home/NewProjectPanel/NewProjectPanel.tsx | 20 +- .../DataApi/DataApi.utils.test.ts | 248 +++++++++++ .../Integrations/DataApi/DataApi.utils.ts | 93 +++++ .../DataApi/DataApiDisabledState.tsx | 31 ++ .../Integrations/DataApi/DocView.tsx | 76 ++++ .../Integrations/DataApi/DocViewError.tsx | 14 + .../Integrations/DataApi/DocViewLoading.tsx | 12 + .../Integrations/DataApi/DocsMenu.tsx | 83 ++++ .../Integrations/DataApi/DocsMobileNav.tsx | 60 +++ .../Integrations/DataApi/DocsTab.tsx | 91 ++++ .../Integrations/DataApi/SettingsTab.tsx | 23 +- .../Landing/Integrations.constants.tsx | 30 +- .../interfaces/Linter/Linter.utils.tsx | 10 +- .../ProjectAPIDocs/FirstLevelNav.tsx | 2 +- .../ProjectAPIDocs.Commands.tsx | 8 +- .../Settings/API/DataApiProjectUrlCard.tsx | 2 +- .../API/DataApiProjectUrlCard.utils.test.ts | 120 ------ .../API/DataApiProjectUrlCard.utils.ts | 34 -- .../InstanceNode.tsx | 4 +- apps/studio/components/interfaces/Sidebar.tsx | 1 + .../TableEditor/ApiAccessToggle.tsx | 2 +- .../layouts/DocsLayout/DocsLayout.tsx | 9 +- .../layouts/DocsLayout/DocsLayout.utils.tsx | 40 +- .../NavigationBar/NavigationBar.utils.tsx | 23 +- .../ProjectSettings.Commands.tsx | 9 +- apps/studio/next.config.js | 5 + apps/studio/pages/project/[ref]/api/index.tsx | 163 +------- 42 files changed, 1587 insertions(+), 1116 deletions(-) create mode 100644 apps/studio/components/interfaces/Docs/DocSection.tsx create mode 100644 apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts create mode 100644 apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts create mode 100644 apps/studio/components/interfaces/Integrations/DataApi/DataApiDisabledState.tsx create mode 100644 apps/studio/components/interfaces/Integrations/DataApi/DocView.tsx create mode 100644 apps/studio/components/interfaces/Integrations/DataApi/DocViewError.tsx create mode 100644 apps/studio/components/interfaces/Integrations/DataApi/DocViewLoading.tsx create mode 100644 apps/studio/components/interfaces/Integrations/DataApi/DocsMenu.tsx create mode 100644 apps/studio/components/interfaces/Integrations/DataApi/DocsMobileNav.tsx create mode 100644 apps/studio/components/interfaces/Integrations/DataApi/DocsTab.tsx delete mode 100644 apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.utils.test.ts delete mode 100644 apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.utils.ts diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx index 19c0031b0966b..a2ef83a4e49e8 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx @@ -1,12 +1,7 @@ import type { PostgresPolicy } from '@supabase/postgres-meta' +import { useParams } from 'common' import { noop } from 'lodash' import { memo, useMemo } from 'react' - -import { useParams } from 'common' -import AlertError from 'components/ui/AlertError' -import { InlineLink } from 'components/ui/InlineLink' -import { useTablesRolesAccessQuery } from 'data/tables/tables-roles-access-query' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Card, CardContent, @@ -20,10 +15,15 @@ import { } from 'ui' import { Admonition } from 'ui-patterns' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' + import { usePoliciesData } from '../PoliciesDataContext' import { PolicyRow } from './PolicyRow' import type { PolicyTable } from './PolicyTableRow.types' import { PolicyTableRowHeader } from './PolicyTableRowHeader' +import AlertError from '@/components/ui/AlertError' +import { InlineLink } from '@/components/ui/InlineLink' +import { useTablesRolesAccessQuery } from '@/data/tables/tables-roles-access-query' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' export interface PolicyTableRowProps { table: PolicyTable @@ -120,7 +120,10 @@ const PolicyTableRowComponent = ({

No data will be selectable via Supabase APIs as this schema is not exposed. You may configure this in your project’s{' '} - API settings. + + API settings + + .

)} diff --git a/apps/studio/components/interfaces/Docs/Authentication.tsx b/apps/studio/components/interfaces/Docs/Authentication.tsx index 88a063b844da8..aa4d72d166874 100644 --- a/apps/studio/components/interfaces/Docs/Authentication.tsx +++ b/apps/studio/components/interfaces/Docs/Authentication.tsx @@ -1,12 +1,13 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import Link from 'next/link' - import { useParams } from 'common' -import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' -import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' + import CodeSnippet from './CodeSnippet' +import { DocSection } from './DocSection' import Snippets from './Snippets' +import { InlineLink } from '@/components/ui/InlineLink' +import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' +import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' interface AuthenticationProps { selectedLang: 'bash' | 'js' @@ -35,83 +36,102 @@ const Authentication = ({ selectedLang, showApiKey }: AuthenticationProps) => { : 'SUPABASE_SERVICE_KEY' return ( - <> -

Authentication

-
-
-

Supabase works through a mixture of JWT and Key auth.

-

- If no Authorization header is included, the API will assume that you are - making a request with an anonymous user. -

-

- If an Authorization header is included, the API will "switch" to the role - of the user making the request. See the User Management section for more details. -

-

We recommend setting your keys as Environment Variables.

-
-
+
+ +

Supabase works through a mixture of JWT and Key auth.

+

+ If no Authorization header is included, the API will assume that you are + making a request with an anonymous user. +

+

+ If an Authorization header is included, the API will "switch" to the role + of the user making the request. See the User Management section for more details. +

+

We recommend setting your keys as Environment Variables.

+ + } + /> -

Client API Keys

-
-
-

- 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 - . -

-

- We have provided you a Client Key to get started. You will soon be able to add as many - keys as you like. You can find the anon key in the{' '} - API Settings page. -

-
-
- - -
-
+ +

+ 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. +

+

+ We have provided you a Client Key to get started. You will soon be able to add as many + keys as you like. You can find the anon key in the{' '} + + API Keys Settings + {' '} + page. +

+ + } + snippets={ + <> + + + + } + /> -

Service Keys

-
-
-

- 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 will refer to the key using the name SERVICE_KEY. -

-

- We have provided you with a Service Key to get started. Soon you will be able to add as - many keys as you like. You can find the service_role in the{' '} - API Settings page. -

-
-
- - -
-
- + +

+ 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 will refer to the key using the name{' '} + SERVICE_KEY. +

+

+ We have provided you with a Service Key to get started. Soon you will be able to add + as many keys as you like. You can find the service_role in the{' '} + + API Keys Settings + {' '} + page. +

+ + } + snippets={ + <> + + + + } + /> +
) } diff --git a/apps/studio/components/interfaces/Docs/CodeSnippet.tsx b/apps/studio/components/interfaces/Docs/CodeSnippet.tsx index fe2db105f64c4..daa780fc0269f 100644 --- a/apps/studio/components/interfaces/Docs/CodeSnippet.tsx +++ b/apps/studio/components/interfaces/Docs/CodeSnippet.tsx @@ -1,8 +1,9 @@ import { useParams } from 'common' -import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { SimpleCodeBlock } from 'ui' +import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' +import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' + interface CodeSnippetProps { selectedLang: 'bash' | 'js' snippet: { @@ -33,11 +34,13 @@ const CodeSnippet = ({ selectedLang, snippet }: CodeSnippetProps) => { if (!snippet[selectedLang]) return null return ( -
-

{snippet.title}

- - {snippet[selectedLang]?.code} - +
+

{snippet.title}

+
+ + {snippet[selectedLang]?.code} + +
) } diff --git a/apps/studio/components/interfaces/Docs/DocSection.tsx b/apps/studio/components/interfaces/Docs/DocSection.tsx new file mode 100644 index 0000000000000..00da8e397567b --- /dev/null +++ b/apps/studio/components/interfaces/Docs/DocSection.tsx @@ -0,0 +1,24 @@ +import { ReactNode } from 'react' +import { cn } from 'ui' + +interface DocSectionProps { + title: ReactNode + id?: string + content: ReactNode + snippets?: ReactNode + className?: string +} + +export const DocSection = ({ title, id, content, snippets, className }: DocSectionProps) => { + return ( +
+
+ {title &&

{title}

} + {content} +
+
+ {snippets} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Docs/Docs.types.ts b/apps/studio/components/interfaces/Docs/Docs.types.ts index 7ea36375c9086..200dcb6f7c103 100644 --- a/apps/studio/components/interfaces/Docs/Docs.types.ts +++ b/apps/studio/components/interfaces/Docs/Docs.types.ts @@ -1,4 +1,4 @@ -export type showApiKey = { +export type ShowApiKey = { key: string name: string } diff --git a/apps/studio/components/interfaces/Docs/GeneralContent.tsx b/apps/studio/components/interfaces/Docs/GeneralContent.tsx index 72be21d3d769c..bb428100c3539 100644 --- a/apps/studio/components/interfaces/Docs/GeneralContent.tsx +++ b/apps/studio/components/interfaces/Docs/GeneralContent.tsx @@ -1,8 +1,9 @@ -import Authentication from 'components/interfaces/Docs/Authentication' -import Introduction from 'components/interfaces/Docs/Introduction' -import RpcIntroduction from 'components/interfaces/Docs/Pages/Rpc/Introduction' -import TablesIntroduction from 'components/interfaces/Docs/Pages/Tables/Introduction' -import { UserManagement } from 'components/interfaces/Docs/Pages/UserManagement' +import { DocSection } from './DocSection' +import Authentication from '@/components/interfaces/Docs/Authentication' +import Introduction from '@/components/interfaces/Docs/Introduction' +import RpcIntroduction from '@/components/interfaces/Docs/Pages/Rpc/Introduction' +import TablesIntroduction from '@/components/interfaces/Docs/Pages/Tables/Introduction' +import { UserManagement } from '@/components/interfaces/Docs/Pages/UserManagement' interface GeneralContentProps { page?: string @@ -21,9 +22,9 @@ export const GeneralContent = ({ selectedLang, page, showApiKey }: GeneralConten if (selected == 'rpc-intro') return else return ( -
-

Not found

-

Looks like you went somewhere that nobody knows.

-
+ Looks like you went somewhere that nobody knows.

} + /> ) } diff --git a/apps/studio/components/interfaces/Docs/GeneratingTypes.tsx b/apps/studio/components/interfaces/Docs/GeneratingTypes.tsx index 6605631c78e18..9460c6d2e448e 100644 --- a/apps/studio/components/interfaces/Docs/GeneratingTypes.tsx +++ b/apps/studio/components/interfaces/Docs/GeneratingTypes.tsx @@ -1,21 +1,22 @@ +import { useParams } from 'common' import { Download } from 'lucide-react' -import Link from 'next/link' import { useState } from 'react' import { toast } from 'sonner' - -import { useParams } from 'common' -import CodeSnippet from 'components/interfaces/Docs/CodeSnippet' -import { DocsButton } from 'components/ui/DocsButton' -import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' -import { generateTypes } from 'data/projects/project-type-generation-query' -import { DOCS_URL } from 'lib/constants' import { Button } from 'ui' +import { DocSection } from './DocSection' +import CodeSnippet from '@/components/interfaces/Docs/CodeSnippet' +import { DocsButton } from '@/components/ui/DocsButton' +import { InlineLink } from '@/components/ui/InlineLink' +import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query' +import { generateTypes } from '@/data/projects/project-type-generation-query' +import { DOCS_URL } from '@/lib/constants' + interface Props { selectedLang: 'bash' | 'js' } -export default function GeneratingTypes({ selectedLang }: Props) { +export function GeneratingTypes({ selectedLang }: Props) { const { ref } = useParams() const [isGeneratingTypes, setIsGeneratingTypes] = useState(false) @@ -41,42 +42,48 @@ export default function GeneratingTypes({ selectedLang }: Props) { } return ( - <> -

- Generating types - -

-
-
+ + Generating types + + + } + content={ + <>

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, or - by downloading the types file via the button on the right and importing it in your + + Supabase CLI + + , or by downloading the types file via the button on the right and importing it in your application within src/index.ts.

-
-
+ } + snippets={ +
-
-

- {selectedLang === 'js' && ( - - )} -

-

+

+ {selectedLang === 'js' && ( + + )} +

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

@@ -85,9 +92,9 @@ export default function GeneratingTypes({ selectedLang }: Props) { selectedLang={selectedLang} snippet={localSnippets.generateTypes(ref ?? '')} /> -
-
- +
+ } + /> ) } diff --git a/apps/studio/components/interfaces/Docs/Introduction.tsx b/apps/studio/components/interfaces/Docs/Introduction.tsx index 18ee355957d3b..49bc91da599ad 100644 --- a/apps/studio/components/interfaces/Docs/Introduction.tsx +++ b/apps/studio/components/interfaces/Docs/Introduction.tsx @@ -1,11 +1,12 @@ import { useParams } from 'common' -import Snippets from 'components/interfaces/Docs/Snippets' -import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' -import { InlineLink } from 'components/ui/InlineLink' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import CodeSnippet from './CodeSnippet' +import { DocSection } from './DocSection' import PublicSchemaNotEnabledAlert from './PublicSchemaNotEnabledAlert' +import Snippets from '@/components/interfaces/Docs/Snippets' +import { InlineLink } from '@/components/ui/InlineLink' +import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query' +import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' interface Props { selectedLang: 'bash' | 'js' @@ -27,26 +28,31 @@ export default function Introduction({ selectedLang }: Props) { .includes('public') return ( - <> -

Connect to your project

-
-
+

All projects have a RESTful endpoint that you can use with your project's API key to query and manage your database. These can be obtained from the{' '} - API settings. + + API settings + + .

You can initialize a new Supabase client using the createClient() method. The Supabase client is your entrypoint to the rest of the Supabase functionality and is the easiest way to interact with everything we offer within the Supabase ecosystem.

-
-
+ + } + snippets={ + <> {isSuccess && !isPublicSchemaEnabled && } -
-
- + + } + /> ) } diff --git a/apps/studio/components/interfaces/Docs/LangSelector.tsx b/apps/studio/components/interfaces/Docs/LangSelector.tsx index 18558668c9394..dc34caa2ef322 100644 --- a/apps/studio/components/interfaces/Docs/LangSelector.tsx +++ b/apps/studio/components/interfaces/Docs/LangSelector.tsx @@ -1,11 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Key } from 'lucide-react' -import { useMemo } from 'react' - import { useParams } from 'common' -import type { showApiKey } from 'components/interfaces/Docs/Docs.types' -import { useAPIKeysQuery } from 'data/api-keys/api-keys-query' -import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { EyeOff, Key } from 'lucide-react' +import { useMemo } from 'react' import { Button, DropdownMenu, @@ -16,15 +12,21 @@ import { DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, + ToggleGroup, + ToggleGroupItem, } from 'ui' +import type { ShowApiKey } from '@/components/interfaces/Docs/Docs.types' +import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query' +import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' + const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' } interface LangSelectorProps { - selectedLang: string - selectedApiKey: showApiKey - setSelectedLang: (selectedLang: string) => void - setSelectedApiKey: (showApiKey: showApiKey) => void + selectedLang: 'js' | 'bash' + selectedApiKey: ShowApiKey + setSelectedLang: (selectedLang: 'js' | 'bash') => void + setSelectedApiKey: (key: ShowApiKey) => void } export const LangSelector = ({ @@ -52,103 +54,97 @@ export const LangSelector = ({ const secretKeys = useMemo(() => apiKeys.filter(({ type }) => type === 'secret'), [apiKeys]) return ( -
-
- - - {selectedLang == 'bash' && - canReadAPIKeys && - !isLoadingAPIKeys && - apiKeys && - apiKeys.length > 0 && ( -
-
- - Project API key: -
- - - - - - - setSelectedApiKey(DEFAULT_KEY)} - > - Hide keys - + + + {selectedLang == 'bash' ? ( + canReadAPIKeys && + !isLoadingAPIKeys && + apiKeys && + apiKeys.length > 0 && ( + + + + + + + setSelectedApiKey(DEFAULT_KEY)} + > + Hide keys + - {publishableKeys.length > 0 && ( - <> - - Publishable keys - {publishableKeys.map((key) => { - const value = key.api_key - return ( - - setSelectedApiKey({ - name: `Publishable key: ${key.name}`, - key: value, - }) - } - > - {key.name} - - ) - })} - - )} - - {secretKeys.length > 0 && ( - <> - - Secret keys - {secretKeys.map((key) => { - const value = key.prefix + '...' - return ( - - setSelectedApiKey({ name: `Secret key: ${key.name}`, key: value }) - } - > - {key.name} - - ) - })} - - )} + {publishableKeys.length > 0 && ( + <> + + Publishable keys + {publishableKeys.map((key) => { + const value = key.api_key + return ( + + setSelectedApiKey({ + name: `Publishable key: ${key.name}`, + key: value, + }) + } + > + {key.name} + + ) + })} + + )} + {secretKeys.length > 0 && ( + <> + Secret keys + {secretKeys.map((key) => { + const value = key.prefix + '...' + return ( + + setSelectedApiKey({ name: `Secret key: ${key.name}`, key: value }) + } + > + {key.name} + + ) + })} + + )} + {legacyKeys.length > 0 && ( + <> + JWT-based legacy keys {legacyKeys.map((key) => { @@ -166,12 +162,15 @@ export const LangSelector = ({ ) })} - - - -
- )} -
+ + )} + + + + ) + ) : ( +
+ )}
) } diff --git a/apps/studio/components/interfaces/Docs/Pages/Rpc/Introduction.tsx b/apps/studio/components/interfaces/Docs/Pages/Rpc/Introduction.tsx index bc78b7ef7b6c7..48e4a85380313 100644 --- a/apps/studio/components/interfaces/Docs/Pages/Rpc/Introduction.tsx +++ b/apps/studio/components/interfaces/Docs/Pages/Rpc/Introduction.tsx @@ -1,17 +1,19 @@ +import { DocSection } from '../../DocSection' + const Introduction = () => { return ( - <> -

Introduction

-
-
+

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.

-
-
- + + } + /> ) } diff --git a/apps/studio/components/interfaces/Docs/Pages/Tables/Introduction.tsx b/apps/studio/components/interfaces/Docs/Pages/Tables/Introduction.tsx index b8c130e216ede..b5a2e9e019f26 100644 --- a/apps/studio/components/interfaces/Docs/Pages/Tables/Introduction.tsx +++ b/apps/studio/components/interfaces/Docs/Pages/Tables/Introduction.tsx @@ -1,10 +1,11 @@ import { useParams } from 'common' -import Link from 'next/link' -import CodeSnippet from 'components/interfaces/Docs/CodeSnippet' -import GeneratingTypes from 'components/interfaces/Docs/GeneratingTypes' -import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' +import { DocSection } from '../../DocSection' import PublicSchemaNotEnabledAlert from '../../PublicSchemaNotEnabledAlert' +import CodeSnippet from '@/components/interfaces/Docs/CodeSnippet' +import { GeneratingTypes } from '@/components/interfaces/Docs/GeneratingTypes' +import { InlineLink } from '@/components/ui/InlineLink' +import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query' interface IntroductionProps { selectedLang: 'bash' | 'js' @@ -21,62 +22,65 @@ const Introduction = ({ selectedLang }: IntroductionProps) => { .includes('public') return ( - <> -

Introduction

-
-
+
+ All views and tables in the public schema and accessible by the active database role for a request are available for querying.

-
-
- {isSuccess && !isPublicSchemaEnabled && } -
-
+ } + snippets={isSuccess && !isPublicSchemaEnabled && } + /> -

Non-exposed tables

-
-
+ If you don't want to expose tables in your API, simply add them to a different schema (not the public schema).

-
-
-
+ } + /> -

- GraphQL vs Supabase -

-
-
-

- 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. -

-
-
- - -
-
- + + GraphQL vs Supabase + + } + content={ + <> +

+ 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. + +

+ + } + snippets={ + <> + + + + } + /> +
) } diff --git a/apps/studio/components/interfaces/Docs/Pages/UserManagement.tsx b/apps/studio/components/interfaces/Docs/Pages/UserManagement.tsx index 35a056b4a0226..776c904d21a04 100644 --- a/apps/studio/components/interfaces/Docs/Pages/UserManagement.tsx +++ b/apps/studio/components/interfaces/Docs/Pages/UserManagement.tsx @@ -1,13 +1,14 @@ -import Link from 'next/link' +import { useParams } from 'common' import { useRouter } from 'next/router' -import { useParams } from 'common' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' -import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import { DOCS_URL } from 'lib/constants' -import { makeRandomString } from 'lib/helpers' import CodeSnippet from '../CodeSnippet' +import { DocSection } from '../DocSection' import Snippets from '../Snippets' +import { InlineLink } from '@/components/ui/InlineLink' +import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' +import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' +import { DOCS_URL } from '@/lib/constants' +import { makeRandomString } from '@/lib/helpers' const randomPassword = makeRandomString(20) @@ -31,158 +32,172 @@ export const UserManagement = ({ selectedLang, showApiKey }: UserManagementProps const endpoint = `${protocol}://${hostEndpoint ?? ''}` return ( - <> -

User Management

-
-
-

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. -

-
-
+
+ +

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 that 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. +

+ + } + /> -

Sign up

-
-
-

Allow your users to sign up and create a new account.

-

- After they have signed up, all interactions using the Supabase JS client will be - performed as "that user". -

-
-
+ +

Allow your users to sign up and create a new account.

+

+ After they have signed up, all interactions using the Supabase JS client will be + performed as "that user". +

+ + } + snippets={ -
-
+ } + /> -

Log in with Email/Password

-
-
-

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". -

-
-
+ +

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". +

+ + } + snippets={ -
-
+ } + /> -

Log in with Magic Link via Email

-
-
-

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". -

-
-
+ +

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". +

+ + } + snippets={ -
-
+ } + /> -

Sign Up with Phone/Password

-
-
-

- 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. -

-
-
+ +

+ 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. +

+ + } + snippets={ -
-
+ } + /> -

Login via SMS OTP

-
-
-

- 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. -

-
-
+ +

+ 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. +

+ + } + snippets={ -
-
+ } + /> -

Verify an SMS OTP

-
-
-

- 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. -

-
-
+ +

+ 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. +

+ + } + snippets={ -
-
+ } + /> {authenticationSignInProviders && ( - <> -

Log in with Third Party OAuth

-
-
+

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 - + {' '} .

View all the available{' '} - + Third Party OAuth providers - +

After they have logged in, all interactions using the Supabase JS client will be @@ -190,131 +205,111 @@ export const UserManagement = ({ selectedLang, showApiKey }: UserManagementProps

Generate your Client ID and secret from:{` `} - + Google - - ,{` `} - - GitHub - - ,{` `} - - GitLab - - ,{` `} - - Facebook - + ,{` `} - + GitHub, + {` `} + GitLab,{` `} + Facebook,{` `} + Bitbucket - + .

-
-
- -
-
- + + } + snippets={ + + } + /> )} -

User

-
-
-

Get the JSON object for the logged in user.

-
-
+ Get the JSON object for the logged in user.

} + snippets={ -
-
+ } + /> -

Forgotten Password Email

-
-
+ 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.

-
-
+ } + snippets={ -
-
+ } + /> -

Update User

-
-
+ Update the user with a new email or password. Each key (email, password, and data) is optional

-
-
+ } + snippets={ -
-
+ } + /> -

Log out

-
-
+ After calling log out, all interactions using the Supabase JS client will be "anonymous".

-
-
+ } + snippets={ -
-
+ } + /> -

Send a User an Invite over Email

-
-
-

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. -

-
-
+ +

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. +

+ + } + snippets={ -
-
- + } + /> +
) } diff --git a/apps/studio/components/interfaces/Docs/Param.tsx b/apps/studio/components/interfaces/Docs/Param.tsx index 88ebeda3f46b3..41ef4b7d5bfcd 100644 --- a/apps/studio/components/interfaces/Docs/Param.tsx +++ b/apps/studio/components/interfaces/Docs/Param.tsx @@ -1,8 +1,7 @@ import { noop } from 'lodash' import { Badge } from 'ui' -import Description from 'components/interfaces/Docs/Description' -import { Code, Database } from 'lucide-react' +import Description from '@/components/interfaces/Docs/Description' function getColumnType(type: string, format: string) { // json and jsonb both have type=undefined, so check format instead @@ -43,65 +42,35 @@ const Param = ({ onDesciptionUpdated = noop, }: ParamProps) => { return ( - <> -
-
-
- - -
- {name} -
-
-
+
+
+

{name}

{required ? 'Required' : 'Optional'}
{format && ( -
-
- -
- - - - {getColumnType(type, format)} - - -
-
-
- -
- - - - {format} - - -
-
-
- )} - {description !== false && ( -
- - +
+ +
{getColumnType(type, format)}
+ +
{format}
+ {description !== false && ( + <> + +
+ +
+ + )}
)} - +
) } export default Param diff --git a/apps/studio/components/interfaces/Docs/ResourceContent.tsx b/apps/studio/components/interfaces/Docs/ResourceContent.tsx index e055021341248..f5f58076ab591 100644 --- a/apps/studio/components/interfaces/Docs/ResourceContent.tsx +++ b/apps/studio/components/interfaces/Docs/ResourceContent.tsx @@ -1,14 +1,16 @@ +import { useParams } from 'common' import { Table2 } from 'lucide-react' -import { useParams } from 'common' -import CodeSnippet from 'components/interfaces/Docs/CodeSnippet' -import Description from 'components/interfaces/Docs/Description' -import Param from 'components/interfaces/Docs/Param' -import Snippets from 'components/interfaces/Docs/Snippets' -import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' -import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query' -import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import { DOCS_URL } from 'lib/constants' +import { DocSection } from './DocSection' +import CodeSnippet from '@/components/interfaces/Docs/CodeSnippet' +import Description from '@/components/interfaces/Docs/Description' +import Param from '@/components/interfaces/Docs/Param' +import Snippets from '@/components/interfaces/Docs/Snippets' +import { InlineLink } from '@/components/ui/InlineLink' +import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query' +import { useProjectJsonSchemaQuery } from '@/data/docs/project-json-schema-query' +import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' +import { DOCS_URL } from '@/lib/constants' interface ResourceContentProps { apiEndpoint: string @@ -55,32 +57,35 @@ export const ResourceContent = ({ if (!paths || !definitions) return null return ( - <> -

- - - - {resourceId} -

+
+ + + {resourceId} + + } + content={ + <> + + + + } + /> -
-
- - -
-
-
{properties.length > 0 && ( -
+
{properties.map((x) => ( -
-
+ -
-
+ } + snippets={ -
-
+ } + /> ))}
)} + {methods.includes('GET') && ( - <> -

Read rows

-
- -
+ + } + snippets={ + <> -
-
-
-
-

Filtering

-

Supabase provides a wide range of filters.

-

- - Learn more - -

-
-
-
-
- + + } + /> )} + {methods.includes('POST') && ( - <> -

Insert rows

-
-
+

insert lets you insert into your tables. You can also insert in bulk and do UPSERT. @@ -188,16 +181,12 @@ export const ResourceContent = ({ insert will also return the replaced values for UPSERT.

- - Learn more - + Learn more

-
-
+ + } + snippets={ + <> -
-
- + + } + /> )} + {methods.includes('PATCH') && ( - <> -

Update rows

-
-
+

update lets you update rows. update will match all rows by default. You can update specific rows using horizontal filters, e.g. eq @@ -228,73 +218,61 @@ export const ResourceContent = ({ update will also return the replaced values for UPDATE.

- - Learn more - + Learn more

-
-
- -
-
- + + } + snippets={ + + } + /> )} + {methods.includes('DELETE') && ( - <> -

Delete rows

-
-
+

delete lets you delete rows. delete will match all rows by default, so remember to specify your filters!

- - Learn more - + Learn more

-
-
- -
-
- + + } + snippets={ + + } + /> )} + {realtimeEnabled && (methods.includes('DELETE') || methods.includes('POST') || methods.includes('PATCH')) && ( - <> -

Subscribe to changes

-
-
+

Supabase provides realtime functionality and broadcasts database changes to authorized users depending on Row Level Security (RLS) policies.

- + Learn more - +

-
-
+ + } + snippets={ + <> -
-
- + + } + /> )} - +
) } diff --git a/apps/studio/components/interfaces/Docs/RpcContent.tsx b/apps/studio/components/interfaces/Docs/RpcContent.tsx index 8f02ea9ee4823..5fb8d1ac6f69a 100644 --- a/apps/studio/components/interfaces/Docs/RpcContent.tsx +++ b/apps/studio/components/interfaces/Docs/RpcContent.tsx @@ -1,10 +1,12 @@ import { useParams } from 'common' -import CodeSnippet from 'components/interfaces/Docs/CodeSnippet' -import Description from 'components/interfaces/Docs/Description' -import Param from 'components/interfaces/Docs/Param' -import Snippets from 'components/interfaces/Docs/Snippets' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' -import { ProjectJsonSchemaPaths } from 'data/docs/project-json-schema-query' + +import { DocSection } from './DocSection' +import CodeSnippet from '@/components/interfaces/Docs/CodeSnippet' +import Description from '@/components/interfaces/Docs/Description' +import Param from '@/components/interfaces/Docs/Param' +import Snippets from '@/components/interfaces/Docs/Snippets' +import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' +import { ProjectJsonSchemaPaths } from '@/data/docs/project-json-schema-query' /** * TODO: need to support rpc with the same name and different params type @@ -50,17 +52,18 @@ export const RpcContent = ({ if (!path) return null return ( - <> -

- {meta.id} -

- -
-
- - -
-
+
+ + + + + } + snippets={ -
-
+ } + /> + {rpcParams.length > 0 && ( -
-

Function Arguments

+
+ {rpcParams.map((x, i) => { return ( -
-
+ -
-
-
+ } + /> ) })}
)} - +
) } diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx index 55b77743b2155..44db9c1c7dc16 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx @@ -1,17 +1,17 @@ -import { ExternalLink, Settings } from 'lucide-react' -import Link from 'next/link' - import { useParams } from 'common' -import { DocsButton } from 'components/ui/DocsButton' -import { InlineLink } from 'components/ui/InlineLink' -import Panel from 'components/ui/Panel' -import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor' -import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { Auth, EdgeFunctions, Realtime, SqlEditor, Storage, TableEditor } from 'icons' -import { DOCS_URL } from 'lib/constants' +import { ExternalLink, Settings } from 'lucide-react' +import Link from 'next/link' import { Button } from 'ui' + import { APIKeys } from './APIKeys' import { GetStartedHero } from './GetStartedHero' +import { DocsButton } from '@/components/ui/DocsButton' +import { InlineLink } from '@/components/ui/InlineLink' +import Panel from '@/components/ui/Panel' +import { EditorIndexPageLink } from '@/data/prefetchers/project.$ref.editor' +import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' +import { DOCS_URL } from '@/lib/constants' export const NewProjectPanel = () => { const { ref } = useParams() @@ -222,7 +222,7 @@ export const NewProjectPanel = () => {
diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts new file mode 100644 index 0000000000000..838bc0f72330c --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts @@ -0,0 +1,248 @@ +import { describe, expect, it } from 'vitest' + +import { buildEntityMaps, getApiEndpoint, getProjectApiEndpoint } from './DataApi.utils' +import type { ProjectSettings } from '@/data/config/project-settings-v2-query' +import type { + CustomDomainResponse, + CustomDomainsData, +} from '@/data/custom-domains/custom-domains-query' +import type { ProjectJsonSchemaPaths } from '@/data/docs/project-json-schema-query' +import type { LoadBalancer } from '@/data/read-replicas/load-balancers-query' +import type { Database } from '@/data/read-replicas/replicas-query' + +const makeCustomDomainData = (hostname: string): CustomDomainsData => ({ + customDomain: { + id: '', + ssl: {} as CustomDomainResponse['ssl'], + hostname, + status: 'active', + created_at: '', + custom_metadata: null, + custom_origin_server: '', + }, + status: '5_services_reconfigured', +}) + +const makeDatabase = ( + identifier: string, + restUrl: string +): Pick => ({ identifier, restUrl }) + +const makeLoadBalancer = (endpoint: string): Pick => ({ endpoint }) + +describe('getProjectApiEndpoint', () => { + it('returns custom domain URL when custom domain is active', () => { + expect( + getProjectApiEndpoint({ + settings: undefined, + customDomainData: makeCustomDomainData('api.example.com'), + }) + ).toBe('https://api.example.com') + }) + + it('returns settings-based URL when no custom domain', () => { + expect( + getProjectApiEndpoint({ + settings: { + app_config: { protocol: 'https', endpoint: 'abc.supabase.co' }, + } as ProjectSettings, + customDomainData: undefined, + }) + ).toBe('https://abc.supabase.co') + }) + + it('respects protocol from settings', () => { + expect( + getProjectApiEndpoint({ + settings: { + app_config: { protocol: 'http', endpoint: 'localhost:54321' }, + } as ProjectSettings, + customDomainData: undefined, + }) + ).toBe('http://localhost:54321') + }) + + it('returns placeholder when settings are undefined', () => { + expect( + getProjectApiEndpoint({ + settings: undefined, + customDomainData: undefined, + }) + ).toBe('https://-') + }) + + it('ignores inactive custom domain', () => { + const inactiveCustomDomain: CustomDomainsData = { + customDomain: null, + status: '0_no_hostname_configured', + } + + expect( + getProjectApiEndpoint({ + settings: { + app_config: { protocol: 'https', endpoint: 'abc.supabase.co' }, + } as ProjectSettings, + customDomainData: inactiveCustomDomain, + }) + ).toBe('https://abc.supabase.co') + }) +}) + +describe('getApiEndpoint', () => { + it('returns custom domain URL when custom domain is active and primary database is selected', () => { + expect( + getApiEndpoint({ + selectedDatabaseId: 'project-ref', + projectRef: 'project-ref', + customDomainData: makeCustomDomainData('api.example.com'), + loadBalancers: undefined, + selectedDatabase: makeDatabase( + 'project-ref', + 'https://project-ref.supabase.co/rest/v1' + ) as Database, + }) + ).toBe('https://api.example.com') + }) + + it('returns database restUrl when custom domain is active but a replica is selected', () => { + expect( + getApiEndpoint({ + selectedDatabaseId: 'replica-1', + projectRef: 'project-ref', + customDomainData: makeCustomDomainData('api.example.com'), + loadBalancers: undefined, + selectedDatabase: makeDatabase( + 'replica-1', + 'https://replica-1.supabase.co/rest/v1' + ) as Database, + }) + ).toBe('https://replica-1.supabase.co/rest/v1') + }) + + it('returns load balancer endpoint when load balancer is selected', () => { + expect( + getApiEndpoint({ + selectedDatabaseId: 'load-balancer', + projectRef: 'project-ref', + customDomainData: undefined, + loadBalancers: [makeLoadBalancer('https://lb.supabase.co') as LoadBalancer], + selectedDatabase: undefined, + }) + ).toBe('https://lb.supabase.co') + }) + + it('returns empty string when load balancer is selected but none exist', () => { + expect( + getApiEndpoint({ + selectedDatabaseId: 'load-balancer', + projectRef: 'project-ref', + customDomainData: undefined, + loadBalancers: undefined, + selectedDatabase: undefined, + }) + ).toBe('') + }) + + it('returns database restUrl for a normal database selection', () => { + expect( + getApiEndpoint({ + selectedDatabaseId: 'project-ref', + projectRef: 'project-ref', + customDomainData: undefined, + loadBalancers: undefined, + selectedDatabase: makeDatabase( + 'project-ref', + 'https://project-ref.supabase.co/rest/v1' + ) as Database, + }) + ).toBe('https://project-ref.supabase.co/rest/v1') + }) + + it('ignores custom domain when it is not active', () => { + const inactiveCustomDomain: CustomDomainsData = { + customDomain: null, + status: '0_no_hostname_configured', + } + + expect( + getApiEndpoint({ + selectedDatabaseId: 'project-ref', + projectRef: 'project-ref', + customDomainData: inactiveCustomDomain, + loadBalancers: undefined, + selectedDatabase: makeDatabase( + 'project-ref', + 'https://project-ref.supabase.co/rest/v1' + ) as Database, + }) + ).toBe('https://project-ref.supabase.co/rest/v1') + }) +}) + +describe('buildEntityMaps', () => { + it('returns empty maps for undefined paths', () => { + expect(buildEntityMaps(undefined)).toEqual({ resources: {}, rpcs: {} }) + }) + + it('returns empty maps for empty paths', () => { + expect(buildEntityMaps({})).toEqual({ resources: {}, rpcs: {} }) + }) + + it('skips the root path', () => { + const paths: ProjectJsonSchemaPaths = { + '/': { get: {} as ProjectJsonSchemaPaths[string]['get'] }, + } + expect(buildEntityMaps(paths)).toEqual({ resources: {}, rpcs: {} }) + }) + + it('classifies non-rpc paths as resources', () => { + const paths: ProjectJsonSchemaPaths = { + '/users': { get: {} as ProjectJsonSchemaPaths[string]['get'] }, + '/posts': { get: {} as ProjectJsonSchemaPaths[string]['get'] }, + } + const result = buildEntityMaps(paths) + expect(Object.keys(result.resources)).toEqual(['users', 'posts']) + expect(result.rpcs).toEqual({}) + }) + + it('classifies rpc/ paths as rpcs', () => { + const paths: ProjectJsonSchemaPaths = { + '/rpc/my_function': { post: {} as ProjectJsonSchemaPaths[string]['post'] }, + } + const result = buildEntityMaps(paths) + expect(result.resources).toEqual({}) + expect(Object.keys(result.rpcs)).toEqual(['my_function']) + }) + + it('enriches entities with displayName and camelCase', () => { + const paths: ProjectJsonSchemaPaths = { + '/user_profiles': { get: {} as ProjectJsonSchemaPaths[string]['get'] }, + '/rpc/get_user_count': { post: {} as ProjectJsonSchemaPaths[string]['post'] }, + } + const result = buildEntityMaps(paths) + + expect(result.resources['user_profiles']).toEqual({ + id: 'user_profiles', + displayName: 'user profiles', + camelCase: 'userProfiles', + }) + expect(result.rpcs['get_user_count']).toEqual({ + id: 'get_user_count', + displayName: 'get user count', + camelCase: 'getUserCount', + }) + }) + + it('handles mixed resources and rpcs', () => { + const paths: ProjectJsonSchemaPaths = { + '/': { get: {} as ProjectJsonSchemaPaths[string]['get'] }, + '/users': { get: {} as ProjectJsonSchemaPaths[string]['get'] }, + '/rpc/hello': { post: {} as ProjectJsonSchemaPaths[string]['post'] }, + '/posts': { get: {} as ProjectJsonSchemaPaths[string]['get'] }, + '/rpc/goodbye': { post: {} as ProjectJsonSchemaPaths[string]['post'] }, + } + const result = buildEntityMaps(paths) + expect(Object.keys(result.resources)).toEqual(['users', 'posts']) + expect(Object.keys(result.rpcs)).toEqual(['hello', 'goodbye']) + }) +}) diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts new file mode 100644 index 0000000000000..61990329ea510 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts @@ -0,0 +1,93 @@ +import type { ProjectSettings } from '@/data/config/project-settings-v2-query' +import type { CustomDomainsData } from '@/data/custom-domains/custom-domains-query' +import type { ProjectJsonSchemaPaths } from '@/data/docs/project-json-schema-query' +import type { LoadBalancer } from '@/data/read-replicas/load-balancers-query' +import type { Database } from '@/data/read-replicas/replicas-query' +import { snakeToCamel } from '@/lib/helpers' + +/** + * Resolves the primary project API endpoint, respecting custom domains. + */ +export function getProjectApiEndpoint({ + settings, + customDomainData, +}: { + settings: ProjectSettings | undefined + customDomainData: CustomDomainsData | undefined +}): string { + if (customDomainData?.customDomain?.status === 'active') { + return `https://${customDomainData.customDomain.hostname}` + } + + const protocol = settings?.app_config?.protocol ?? 'https' + const endpoint = settings?.app_config?.endpoint + return `${protocol}://${endpoint ?? '-'}` +} + +/** + * Resolves the API endpoint URL based on the selected database, custom domain + * status, and load balancer configuration. + */ +export function getApiEndpoint({ + selectedDatabaseId, + projectRef, + customDomainData, + loadBalancers, + selectedDatabase, +}: { + selectedDatabaseId: string | undefined + projectRef: string | undefined + customDomainData: CustomDomainsData | undefined + loadBalancers: Array | undefined + selectedDatabase: Database | undefined +}): string { + const isCustomDomainActive = customDomainData?.customDomain?.status === 'active' + const loadBalancerSelected = selectedDatabaseId === 'load-balancer' + + if (isCustomDomainActive && selectedDatabaseId === projectRef) { + return `https://${customDomainData.customDomain.hostname}` + } + + if (loadBalancerSelected) { + return loadBalancers?.[0]?.endpoint ?? '' + } + + return selectedDatabase?.restUrl ?? '' +} + +export type EnrichedEntity = { id: string; displayName: string; camelCase: string } +export type EntityMap = Record + +/** + * Partitions JSON schema paths into resource and RPC entity maps. + */ +export function buildEntityMaps(paths: ProjectJsonSchemaPaths | undefined): { + resources: EntityMap + rpcs: EntityMap +} { + const RPC_PREFIX = 'rpc/' + + return Object.keys(paths ?? {}).reduce<{ resources: EntityMap; rpcs: EntityMap }>( + (acc, name) => { + const trimmedName = name.slice(1) + if (!trimmedName.length) return acc + + const isRpc = trimmedName.startsWith(RPC_PREFIX) + const id = isRpc ? trimmedName.slice(RPC_PREFIX.length) : trimmedName + const enriched: EnrichedEntity = { + id, + displayName: id.replace(/_/g, ' '), + camelCase: snakeToCamel(id), + } + + if (isRpc) { + acc.rpcs[id] = enriched + } else { + acc.resources[id] = enriched + } + + return acc + }, + { resources: {}, rpcs: {} } + ) +} diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DataApiDisabledState.tsx b/apps/studio/components/interfaces/Integrations/DataApi/DataApiDisabledState.tsx new file mode 100644 index 0000000000000..ba316e61e4e9b --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/DataApi/DataApiDisabledState.tsx @@ -0,0 +1,31 @@ +import { useParams } from 'common' +import { AlertCircle } from 'lucide-react' +import Link from 'next/link' +import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_ } from 'ui' + +interface DataApiDisabledStateProps { + description: string +} + +export const DataApiDisabledState = ({ description }: DataApiDisabledStateProps) => { + const { ref: projectRef } = useParams() + + return ( +
+ + + Data API is disabled + + Enable the Data API in the{' '} + + Overview + {' '} + tab to {description}. + + +
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DocView.tsx b/apps/studio/components/interfaces/Integrations/DataApi/DocView.tsx new file mode 100644 index 0000000000000..7316245f00e49 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/DataApi/DocView.tsx @@ -0,0 +1,76 @@ +import { useParams } from 'common' + +import type { ShowApiKey } from '../../Docs/Docs.types' +import { GeneralContent } from '@/components/interfaces/Docs/GeneralContent' +import { ResourceContent } from '@/components/interfaces/Docs/ResourceContent' +import { RpcContent } from '@/components/interfaces/Docs/RpcContent' +import { + buildEntityMaps, + getProjectApiEndpoint, +} from '@/components/interfaces/Integrations/DataApi/DataApi.utils' +import { DocViewError } from '@/components/interfaces/Integrations/DataApi/DocViewError' +import { DocViewLoading } from '@/components/interfaces/Integrations/DataApi/DocViewLoading' +import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' +import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query' +import { useProjectJsonSchemaQuery } from '@/data/docs/project-json-schema-query' + +interface DocViewProps { + selectedLang: 'js' | 'bash' + selectedApiKey: ShowApiKey +} + +export const DocView = ({ selectedLang, selectedApiKey }: DocViewProps) => { + const { ref: projectRef, page, resource, rpc } = useParams() + + const { data: settings, error: settingsError } = useProjectSettingsV2Query({ projectRef }) + const { + data: jsonSchema, + error: jsonSchemaError, + isPending: isLoading, + refetch, + } = useProjectJsonSchemaQuery({ projectRef }) + const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) + + const endpoint = getProjectApiEndpoint({ settings, customDomainData }) + + const { paths } = jsonSchema || {} + const PAGE_KEY = resource || rpc || page || 'index' + + const { resources, rpcs } = buildEntityMaps(paths) + + if (settingsError || jsonSchemaError) { + return + } + + if (isLoading || !settings || !jsonSchema) { + return + } + + return ( +
+
+ {resource ? ( + + ) : rpc ? ( + + ) : ( + + )} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DocViewError.tsx b/apps/studio/components/interfaces/Integrations/DataApi/DocViewError.tsx new file mode 100644 index 0000000000000..02bdf11d74a1e --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/DataApi/DocViewError.tsx @@ -0,0 +1,14 @@ +interface DocViewErrorProps { + error: Error | null +} + +export const DocViewError = ({ error }: DocViewErrorProps) => { + return ( +
+
+

Error connecting to API

+

{error?.message ?? 'An unexpected error occurred'}

+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DocViewLoading.tsx b/apps/studio/components/interfaces/Integrations/DataApi/DocViewLoading.tsx new file mode 100644 index 0000000000000..87d27d4d4fa8a --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/DataApi/DocViewLoading.tsx @@ -0,0 +1,12 @@ +import { ShimmeringLoader } from 'ui-patterns' + +export const DocViewLoading = () => { + return ( +
+ + + + +
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DocsMenu.tsx b/apps/studio/components/interfaces/Integrations/DataApi/DocsMenu.tsx new file mode 100644 index 0000000000000..e514c77766076 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/DataApi/DocsMenu.tsx @@ -0,0 +1,83 @@ +import Link from 'next/link' +import { cn } from 'ui' + +import { ProductMenuGroup } from '@/components/ui/ProductMenu/ProductMenu.types' + +export const DocsMenu = ({ + menu, + activePage, +}: { + menu: Array + activePage?: string +}) => { + return ( + + ) +} diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DocsMobileNav.tsx b/apps/studio/components/interfaces/Integrations/DataApi/DocsMobileNav.tsx new file mode 100644 index 0000000000000..379f6703ef20a --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/DataApi/DocsMobileNav.tsx @@ -0,0 +1,60 @@ +import { Menu } from 'lucide-react' +import { useEffect, useState } from 'react' +import { Button, Sheet, SheetContent, SheetHeader, SheetTitle } from 'ui' + +import type { ShowApiKey } from '../../Docs/Docs.types' +import { LangSelector } from '../../Docs/LangSelector' +import { DocsMenu } from '@/components/interfaces/Integrations/DataApi/DocsMenu' +import { ProductMenuGroup } from '@/components/ui/ProductMenu/ProductMenu.types' + +interface DocsMobileNavProps { + activePage: string + menu: Array + selectedLang: 'js' | 'bash' + selectedApiKey: ShowApiKey + setSelectedLang: (lang: 'js' | 'bash') => void + setSelectedApiKey: (key: ShowApiKey) => void +} + +export const DocsMobileNav = ({ + activePage, + menu, + selectedLang, + selectedApiKey, + setSelectedLang, + setSelectedApiKey, +}: DocsMobileNavProps) => { + const [open, setOpen] = useState(false) + + // Close mobile nav when the active page changes + useEffect(() => { + setOpen(false) + }, [activePage]) + + return ( + <> +
+ +
+ + + + + Data API Docs + +
+ + +
+
+
+ + ) +} diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DocsTab.tsx b/apps/studio/components/interfaces/Integrations/DataApi/DocsTab.tsx new file mode 100644 index 0000000000000..859d733578b71 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/DataApi/DocsTab.tsx @@ -0,0 +1,91 @@ +import { useParams } from 'common' +import { useMemo, useState } from 'react' +import { ShimmeringLoader } from 'ui-patterns' + +import type { ShowApiKey } from '../../Docs/Docs.types' +import { LangSelector } from '../../Docs/LangSelector' +import { DataApiDisabledState } from '@/components/interfaces/Integrations/DataApi/DataApiDisabledState' +import { DocsMenu } from '@/components/interfaces/Integrations/DataApi/DocsMenu' +import { DocsMobileNav } from '@/components/interfaces/Integrations/DataApi/DocsMobileNav' +import { DocView } from '@/components/interfaces/Integrations/DataApi/DocView' +import { generateDocsMenu, getActivePage } from '@/components/layouts/DocsLayout/DocsLayout.utils' +import { useOpenAPISpecQuery } from '@/data/open-api/api-spec-query' +import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled' +import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { PROJECT_STATUS } from '@/lib/constants' + +export const DataApiDocsTab = () => { + const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' } + const { ref: projectRef, page, resource, rpc } = useParams() + const { data: project } = useSelectedProjectQuery() + const { projectAuthAll: authEnabled } = useIsFeatureEnabled(['project_auth:all']) + const isPaused = project?.status === PROJECT_STATUS.INACTIVE + + const [selectedLang, setSelectedLang] = useState<'js' | 'bash'>('js') + const [selectedApiKey, setSelectedApiKey] = useState(DEFAULT_KEY) + + const { isEnabled, isPending: isConfigLoading } = useIsDataApiEnabled({ projectRef }) + + const { data: openApiSpec } = useOpenAPISpecQuery( + { projectRef }, + { + enabled: !!projectRef && !isPaused && isEnabled, + } + ) + + const tableNames = useMemo( + () => (openApiSpec?.tables ?? []).map((table) => table.name), + [openApiSpec] + ) + const functionNames = useMemo( + () => (openApiSpec?.functions ?? []).map((fn) => fn.name), + [openApiSpec] + ) + + const activePage = useMemo(() => getActivePage({ page, resource, rpc }), [page, resource, rpc]) + + const docsBasePath = projectRef ? `/project/${projectRef}/integrations/data_api/docs` : undefined + + const menu = useMemo(() => { + if (!projectRef) return [] + return generateDocsMenu(projectRef, tableNames, functionNames, { authEnabled }, docsBasePath) + }, [projectRef, tableNames, functionNames, authEnabled, docsBasePath]) + + if (isConfigLoading) { + return ( +
+ +
+ ) + } + + if (!isEnabled) { + return + } + + return ( +
+ +
+ + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/DataApi/SettingsTab.tsx b/apps/studio/components/interfaces/Integrations/DataApi/SettingsTab.tsx index 819323890c765..fba36e35630af 100644 --- a/apps/studio/components/interfaces/Integrations/DataApi/SettingsTab.tsx +++ b/apps/studio/components/interfaces/Integrations/DataApi/SettingsTab.tsx @@ -1,9 +1,7 @@ import { useParams } from 'common' -import { AlertCircle } from 'lucide-react' -import Link from 'next/link' -import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_ } from 'ui' import { PageContainer } from 'ui-patterns' +import { DataApiDisabledState } from '@/components/interfaces/Integrations/DataApi/DataApiDisabledState' import { ServiceList } from '@/components/interfaces/Settings/API/ServiceList' import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled' @@ -12,24 +10,7 @@ export const DataApiSettingsTab = () => { const { isEnabled, isPending } = useIsDataApiEnabled({ projectRef }) if (!isPending && !isEnabled) { - return ( -
- - - Data API is disabled - - Enable the Data API in the{' '} - - Overview - {' '} - tab to configure settings. - - -
- ) + return } return ( diff --git a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx index c9ad5e65f21a9..56df1d9e3ebd9 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx @@ -2,13 +2,13 @@ import { Clock5, Code2, Layers, Timer, Vault, Webhook } from 'lucide-react' import dynamic from 'next/dynamic' import Image from 'next/image' import { ComponentType, ReactNode } from 'react' - -import { BASE_PATH, DOCS_URL } from 'lib/constants' import { cn } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' + import { UpgradeDatabaseAlert } from '../Queues/UpgradeDatabaseAlert' import { WRAPPERS } from '../Wrappers/Wrappers.constants' import { WrapperMeta } from '../Wrappers/Wrappers.types' +import { BASE_PATH, DOCS_URL } from '@/lib/constants' export type Navigation = { route: string @@ -35,10 +35,10 @@ export type IntegrationDefinition = { name: string websiteUrl: string } - requiredExtensions: string[] + requiredExtensions: Array /** Optional component to render if the integration requires extensions that are not available on the current database image */ missingExtensionsAlert?: ReactNode - navigation?: Navigation[] + navigation?: Array navigate: ( id: string, pageId: string | undefined, @@ -51,7 +51,7 @@ const authorSupabase = { websiteUrl: 'https://supabase.com', } -const SUPABASE_INTEGRATIONS: IntegrationDefinition[] = [ +const SUPABASE_INTEGRATIONS: Array = [ { id: 'queues', type: 'postgres_extension' as const, @@ -280,6 +280,10 @@ const SUPABASE_INTEGRATIONS: IntegrationDefinition[] = [ route: 'settings', label: 'Settings', }, + { + route: 'docs', + label: 'Docs', + }, ], navigate: (_id: string, pageId: string = 'overview', _childId: string | undefined) => { switch (pageId) { @@ -303,6 +307,16 @@ const SUPABASE_INTEGRATIONS: IntegrationDefinition[] = [ loading: Loading, } ) + case 'docs': + return dynamic( + () => + import('components/interfaces/Integrations/DataApi/DocsTab').then( + (mod) => mod.DataApiDocsTab + ), + { + loading: Loading, + } + ) } return null }, @@ -362,7 +376,7 @@ const SUPABASE_INTEGRATIONS: IntegrationDefinition[] = [ }, ] as const -const WRAPPER_INTEGRATIONS: IntegrationDefinition[] = WRAPPERS.map((w) => { +const WRAPPER_INTEGRATIONS: Array = WRAPPERS.map((w) => { return { id: w.name, type: 'wrapper' as const, @@ -413,7 +427,7 @@ const WRAPPER_INTEGRATIONS: IntegrationDefinition[] = WRAPPERS.map((w) => { } }) -const TEMPLATE_INTEGRATIONS: IntegrationDefinition[] = [ +const TEMPLATE_INTEGRATIONS: Array = [ { id: 'stripe_sync_engine', type: 'custom' as const, @@ -471,7 +485,7 @@ const TEMPLATE_INTEGRATIONS: IntegrationDefinition[] = [ }, ] -export const INTEGRATIONS: IntegrationDefinition[] = [ +export const INTEGRATIONS: Array = [ ...WRAPPER_INTEGRATIONS, ...SUPABASE_INTEGRATIONS, ...TEMPLATE_INTEGRATIONS, diff --git a/apps/studio/components/interfaces/Linter/Linter.utils.tsx b/apps/studio/components/interfaces/Linter/Linter.utils.tsx index 40f689d731ac0..6cf1078d4f3d9 100644 --- a/apps/studio/components/interfaces/Linter/Linter.utils.tsx +++ b/apps/studio/components/interfaces/Linter/Linter.utils.tsx @@ -12,12 +12,12 @@ import { User, } from 'lucide-react' import Link from 'next/link' - -import { LINTER_LEVELS, LintInfo } from 'components/interfaces/Linter/Linter.constants' -import { LINT_TYPES, Lint } from 'data/lint/lint-query' -import { DOCS_URL } from 'lib/constants' import { Badge, Button } from 'ui' +import { LINTER_LEVELS, LintInfo } from '@/components/interfaces/Linter/Linter.constants' +import { Lint, LINT_TYPES } from '@/data/lint/lint-query' +import { DOCS_URL } from '@/lib/constants' + export const lintInfoMap: LintInfo[] = [ { name: 'unindexed_foreign_keys', @@ -277,7 +277,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'leaked_service_key', title: 'Leaked Service Key Detected', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/api`, + link: ({ projectRef }) => `/project/${projectRef}/settings/api-keys`, linkText: 'View settings', docsLink: `${DOCS_URL}/guides/api/api-keys#the-servicerole-key`, category: 'security', diff --git a/apps/studio/components/interfaces/ProjectAPIDocs/FirstLevelNav.tsx b/apps/studio/components/interfaces/ProjectAPIDocs/FirstLevelNav.tsx index ce2206a198b18..d6b81fb57c345 100644 --- a/apps/studio/components/interfaces/ProjectAPIDocs/FirstLevelNav.tsx +++ b/apps/studio/components/interfaces/ProjectAPIDocs/FirstLevelNav.tsx @@ -118,7 +118,7 @@ export const FirstLevelNav = (): ReactNode => { } onClick={() => snap.setShowProjectApiDocs(false)} > - + GraphiQL diff --git a/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.Commands.tsx b/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.Commands.tsx index ab6ecb6037240..7a0bcf3ba2e1e 100644 --- a/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.Commands.tsx +++ b/apps/studio/components/interfaces/ProjectAPIDocs/ProjectAPIDocs.Commands.tsx @@ -13,25 +13,25 @@ export function useApiDocsGotoCommands(options?: CommandOptions) { { id: 'nav-api', name: 'Project API Docs', - route: `/project/${ref}/api`, + route: `/project/${ref}/integrations/data_api/docs`, defaultHidden: true, }, { id: 'nav-api-auth', name: 'Auth Docs', - route: `/project/${ref}/api?page=auth`, + route: `/project/${ref}/integrations/data_api/docs?page=auth`, defaultHidden: true, }, { id: 'nav-api-user-management', name: 'User Management Docs', - route: `/project/${ref}/api?page=users-management`, + route: `/project/${ref}/integrations/data_api/docs?page=users-management`, defaultHidden: true, }, { id: 'nav-api-graphql', name: 'GraphQL Docs', - route: `/project/${ref}/api/graphiql`, + route: `/project/${ref}/integrations/graphiql`, defaultHidden: true, }, ], diff --git a/apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.tsx b/apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.tsx index bbee03d3ba896..698832d1afe93 100644 --- a/apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.tsx +++ b/apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.tsx @@ -22,7 +22,7 @@ import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent' import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' -import { getApiEndpoint } from './DataApiProjectUrlCard.utils' +import { getApiEndpoint } from '@/components/interfaces/Integrations/DataApi/DataApi.utils' export const DataApiProjectUrlCard = () => { const { isPending: isLoading } = useSelectedProjectQuery() diff --git a/apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.utils.test.ts b/apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.utils.test.ts deleted file mode 100644 index 56bdea65cc6f2..0000000000000 --- a/apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.utils.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { getApiEndpoint } from './DataApiProjectUrlCard.utils' -import type { - CustomDomainResponse, - CustomDomainsData, -} from '@/data/custom-domains/custom-domains-query' -import type { LoadBalancer } from '@/data/read-replicas/load-balancers-query' -import type { Database } from '@/data/read-replicas/replicas-query' - -const makeCustomDomainData = (hostname: string): CustomDomainsData => ({ - customDomain: { - id: '', - ssl: {} as CustomDomainResponse['ssl'], - hostname, - status: 'active', - created_at: '', - custom_metadata: null, - custom_origin_server: '', - }, - status: '5_services_reconfigured', -}) - -const makeDatabase = ( - identifier: string, - restUrl: string -): Pick => ({ identifier, restUrl }) - -const makeLoadBalancer = (endpoint: string): Pick => ({ endpoint }) - -describe('getApiEndpoint', () => { - it('returns custom domain URL when custom domain is active and primary database is selected', () => { - expect( - getApiEndpoint({ - selectedDatabaseId: 'project-ref', - projectRef: 'project-ref', - customDomainData: makeCustomDomainData('api.example.com'), - loadBalancers: undefined, - selectedDatabase: makeDatabase( - 'project-ref', - 'https://project-ref.supabase.co/rest/v1' - ) as Database, - }) - ).toBe('https://api.example.com') - }) - - it('returns database restUrl when custom domain is active but a replica is selected', () => { - expect( - getApiEndpoint({ - selectedDatabaseId: 'replica-1', - projectRef: 'project-ref', - customDomainData: makeCustomDomainData('api.example.com'), - loadBalancers: undefined, - selectedDatabase: makeDatabase( - 'replica-1', - 'https://replica-1.supabase.co/rest/v1' - ) as Database, - }) - ).toBe('https://replica-1.supabase.co/rest/v1') - }) - - it('returns load balancer endpoint when load balancer is selected', () => { - expect( - getApiEndpoint({ - selectedDatabaseId: 'load-balancer', - projectRef: 'project-ref', - customDomainData: undefined, - loadBalancers: [makeLoadBalancer('https://lb.supabase.co') as LoadBalancer], - selectedDatabase: undefined, - }) - ).toBe('https://lb.supabase.co') - }) - - it('returns empty string when load balancer is selected but none exist', () => { - expect( - getApiEndpoint({ - selectedDatabaseId: 'load-balancer', - projectRef: 'project-ref', - customDomainData: undefined, - loadBalancers: undefined, - selectedDatabase: undefined, - }) - ).toBe('') - }) - - it('returns database restUrl for a normal database selection', () => { - expect( - getApiEndpoint({ - selectedDatabaseId: 'project-ref', - projectRef: 'project-ref', - customDomainData: undefined, - loadBalancers: undefined, - selectedDatabase: makeDatabase( - 'project-ref', - 'https://project-ref.supabase.co/rest/v1' - ) as Database, - }) - ).toBe('https://project-ref.supabase.co/rest/v1') - }) - - it('ignores custom domain when it is not active', () => { - const inactiveCustomDomain: CustomDomainsData = { - customDomain: null, - status: '0_no_hostname_configured', - } - - expect( - getApiEndpoint({ - selectedDatabaseId: 'project-ref', - projectRef: 'project-ref', - customDomainData: inactiveCustomDomain, - loadBalancers: undefined, - selectedDatabase: makeDatabase( - 'project-ref', - 'https://project-ref.supabase.co/rest/v1' - ) as Database, - }) - ).toBe('https://project-ref.supabase.co/rest/v1') - }) -}) diff --git a/apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.utils.ts b/apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.utils.ts deleted file mode 100644 index 880dbbb6c4fef..0000000000000 --- a/apps/studio/components/interfaces/Settings/API/DataApiProjectUrlCard.utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { CustomDomainsData } from '@/data/custom-domains/custom-domains-query' -import type { LoadBalancer } from '@/data/read-replicas/load-balancers-query' -import type { Database } from '@/data/read-replicas/replicas-query' - -/** - * Resolves the API endpoint URL based on the selected database, custom domain - * status, and load balancer configuration. - */ -export function getApiEndpoint({ - selectedDatabaseId, - projectRef, - customDomainData, - loadBalancers, - selectedDatabase, -}: { - selectedDatabaseId: string | undefined - projectRef: string | undefined - customDomainData: CustomDomainsData | undefined - loadBalancers: Array | undefined - selectedDatabase: Database | undefined -}): string { - const isCustomDomainActive = customDomainData?.customDomain?.status === 'active' - const loadBalancerSelected = selectedDatabaseId === 'load-balancer' - - if (isCustomDomainActive && selectedDatabaseId === projectRef) { - return `https://${customDomainData.customDomain.hostname}` - } - - if (loadBalancerSelected) { - return loadBalancers?.[0]?.endpoint ?? '' - } - - return selectedDatabase?.restUrl ?? '' -} diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx index 46a5afafd9432..30c9b1ab01475 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx @@ -96,7 +96,9 @@ export const LoadBalancerNode = ({ data }: NodeProps) => { - View API URL + + View API URL + diff --git a/apps/studio/components/interfaces/Sidebar.tsx b/apps/studio/components/interfaces/Sidebar.tsx index 321c961d1588a..dc65c2d2a1e06 100644 --- a/apps/studio/components/interfaces/Sidebar.tsx +++ b/apps/studio/components/interfaces/Sidebar.tsx @@ -262,6 +262,7 @@ const ProjectLinks = () => { const otherRoutes = generateOtherRoutes(ref, project, { unifiedLogs: isUnifiedLogsEnabled, showReports, + apiDocsSidePanel: isNewAPIDocsEnabled, }) const settingsRoutes = generateSettingsRoutes(ref, project) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx index 5b0613c0ee8d7..5083475f8328d 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx @@ -483,7 +483,7 @@ const SchemaExposureOptions = ({ To enable API access for this table, you need to first expose the{' '} {schemaName} schema in your{' '} API settings diff --git a/apps/studio/components/layouts/DocsLayout/DocsLayout.tsx b/apps/studio/components/layouts/DocsLayout/DocsLayout.tsx index c8679c24aa646..1046fbc4a6de0 100644 --- a/apps/studio/components/layouts/DocsLayout/DocsLayout.tsx +++ b/apps/studio/components/layouts/DocsLayout/DocsLayout.tsx @@ -11,7 +11,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { withAuth } from 'hooks/misc/withAuth' import { PROJECT_STATUS } from 'lib/constants' import { ProjectLayout } from '../ProjectLayout' -import { generateDocsMenu } from './DocsLayout.utils' +import { generateDocsMenu, getActivePage } from './DocsLayout.utils' function DocsLayout({ title, children }: { title: string; children: ReactElement }) { const router = useRouter() @@ -34,8 +34,11 @@ function DocsLayout({ title, children }: { title: string; children: ReactElement if (router.pathname.endsWith('graphiql')) return 'graphiql' const { page, rpc, resource } = router.query - if (!page && !resource && !rpc) return 'introduction' - return (page || rpc || resource || '') as string + return getActivePage({ + page: page as string | undefined, + resource: resource as string | undefined, + rpc: rpc as string | undefined, + }) } if (error) { diff --git a/apps/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx b/apps/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx index 2356d309900a6..d1586daaa85b5 100644 --- a/apps/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx +++ b/apps/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx @@ -1,23 +1,37 @@ -import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types' -import { BASE_PATH, DOCS_URL } from 'lib/constants' import { ArrowUpRight, Book, BookOpen } from 'lucide-react' import SVG from 'react-inlinesvg' +import type { ProductMenuGroup } from '@/components/ui/ProductMenu/ProductMenu.types' +import { BASE_PATH, DOCS_URL } from '@/lib/constants' + +export const getActivePage = (params: { + page?: string + resource?: string + rpc?: string +}): string => { + const { page, resource, rpc } = params + if (!page && !resource && !rpc) return 'introduction' + return (page || rpc || resource) as string +} + export const generateDocsMenu = ( ref: string, - tables: string[], - functions: string[], - flags?: { authEnabled: boolean } -): ProductMenuGroup[] => { + tables: Array, + functions: Array, + flags?: { authEnabled: boolean }, + basePath?: string +): Array => { + const docsBasePath = basePath ?? `/project/${ref}/integrations/data_api/docs` + return [ { title: 'Getting Started', items: [ - { name: 'Introduction', key: 'introduction', url: `/project/${ref}/api`, items: [] }, + { name: 'Introduction', key: 'introduction', url: docsBasePath, items: [] }, { name: 'Authentication', key: 'auth', - url: `/project/${ref}/api?page=auth`, + url: `${docsBasePath}?page=auth`, items: [], }, ...(flags?.authEnabled @@ -25,7 +39,7 @@ export const generateDocsMenu = ( { name: 'User Management', key: 'users-management', - url: `/project/${ref}/api?page=users-management`, + url: `${docsBasePath}?page=users-management`, items: [], }, ] @@ -38,14 +52,14 @@ export const generateDocsMenu = ( { name: 'Introduction', key: 'tables-intro', - url: `/project/${ref}/api?page=tables-intro`, + url: `${docsBasePath}?page=tables-intro`, items: [], }, ...tables.sort().map((table) => { return { name: table, key: table, - url: `/project/${ref}/api?resource=${table}`, + url: `${docsBasePath}?resource=${table}`, items: [], } }), @@ -57,11 +71,11 @@ export const generateDocsMenu = ( { name: 'Introduction', key: 'rpc-intro', - url: `/project/${ref}/api?page=rpc-intro`, + url: `${docsBasePath}?page=rpc-intro`, items: [], }, ...functions.map((fn) => { - return { name: fn, key: fn, url: `/project/${ref}/api?rpc=${fn}`, items: [] } + return { name: fn, key: fn, url: `${docsBasePath}?rpc=${fn}`, items: [] } }), ], }, diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 5914bd6282145..4a06b218dfe43 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -121,14 +121,15 @@ export const generateProductRoutes = ( export const generateOtherRoutes = ( ref?: string, project?: Project, - features?: { unifiedLogs?: boolean; showReports?: boolean } + features?: { unifiedLogs?: boolean; showReports?: boolean; apiDocsSidePanel?: boolean } ): Route[] => { const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP const buildingUrl = `/project/${ref}` - const { unifiedLogs, showReports } = features ?? {} + const { unifiedLogs, showReports, apiDocsSidePanel } = features ?? {} const unifiedLogsEnabled = unifiedLogs ?? false const reportsEnabled = showReports ?? true + const apiDocsSidePanelEnabled = apiDocsSidePanel ?? false return [ { @@ -159,12 +160,18 @@ export const generateOtherRoutes = ( ? `/project/${ref}/logs` : `/project/${ref}/logs/explorer`), }, - { - key: 'api', - label: 'API Docs', - icon: , - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/api`), - }, + ...(apiDocsSidePanelEnabled + ? [ + { + key: 'api', + label: 'API Docs', + icon: , + link: + ref && + (isProjectBuilding ? buildingUrl : `/project/${ref}/integrations/data_api/docs`), + }, + ] + : []), { key: 'integrations', label: 'Integrations', diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx index b5c5e893a020c..0caaf7a425b54 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx @@ -1,11 +1,12 @@ -import { useRouter } from 'next/router' import { useParams } from 'common' -import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' -import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { useRouter } from 'next/router' import type { CommandOptions, ICommand } from 'ui-patterns/CommandMenu' import { useRegisterCommands, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu' import { IRouteCommand } from 'ui-patterns/CommandMenu/internal/types' +import { COMMAND_MENU_SECTIONS } from '@/components/interfaces/App/CommandMenu/CommandMenu.utils' +import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' + export function useProjectSettingsGotoCommands(options?: CommandOptions) { const router = useRouter() const setIsOpen = useSetCommandMenuOpen() @@ -45,7 +46,7 @@ export function useProjectSettingsGotoCommands(options?: CommandOptions) { { id: 'nav-project-settings-api', name: 'API Settings', - route: `/project/${ref}/settings/api`, + route: `/project/${ref}/integrations/data_api/settings`, defaultHidden: true, }, { diff --git a/apps/studio/next.config.js b/apps/studio/next.config.js index b440009d0d93c..c615c69b2b361 100644 --- a/apps/studio/next.config.js +++ b/apps/studio/next.config.js @@ -444,6 +444,11 @@ const nextConfig = { destination: '/project/:ref/integrations/data_api/overview', permanent: false, }, + { + source: '/project/:ref/api', + destination: '/project/:ref/integrations/data_api/docs', + permanent: false, + }, ...(process.env.NEXT_PUBLIC_BASE_PATH?.length ? [ diff --git a/apps/studio/pages/project/[ref]/api/index.tsx b/apps/studio/pages/project/[ref]/api/index.tsx index 045bd19fe923d..d03be4be4fe4c 100644 --- a/apps/studio/pages/project/[ref]/api/index.tsx +++ b/apps/studio/pages/project/[ref]/api/index.tsx @@ -1,152 +1,25 @@ import { useParams } from 'common' -import { useState } from 'react' +import { useRouter } from 'next/router' +import { useEffect } from 'react' -import { GeneralContent } from 'components/interfaces/Docs/GeneralContent' -import { LangSelector } from 'components/interfaces/Docs/LangSelector' -import { ResourceContent } from 'components/interfaces/Docs/ResourceContent' -import { RpcContent } from 'components/interfaces/Docs/RpcContent' -import DefaultLayout from 'components/layouts/DefaultLayout' -import DocsLayout from 'components/layouts/DocsLayout/DocsLayout' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' -import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' -import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query' -import { snakeToCamel } from 'lib/helpers' -import type { NextPageWithLayout } from 'types' +import type { NextPageWithLayout } from '@/types' -const PageConfig: NextPageWithLayout = () => { - return -} - -PageConfig.getLayout = (page) => ( - - {page} - -) - -export default PageConfig - -const DocView = () => { - const functionPath = 'rpc/' - const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' } - - const { ref: projectRef, page, resource, rpc } = useParams() - const [selectedLang, setSelectedLang] = useState('js') - const [selectedApikey, setSelectedApiKey] = useState(DEFAULT_KEY) - - const { data: settings, error: settingsError } = useProjectSettingsV2Query({ projectRef }) - const { - data: jsonSchema, - error: jsonSchemaError, - isPending: isLoading, - refetch, - } = useProjectJsonSchemaQuery({ projectRef }) - const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) - - const refreshDocs = async () => await refetch() +const ApiDocsRedirect: NextPageWithLayout = () => { + const router = useRouter() + const { ref } = useParams() - const protocol = settings?.app_config?.protocol ?? 'https' - const hostEndpoint = settings?.app_config?.endpoint - const endpoint = - customDomainData?.customDomain?.status === 'active' - ? `https://${customDomainData.customDomain?.hostname}` - : `${protocol}://${hostEndpoint ?? '-'}` + useEffect(() => { + if (!router.isReady || !ref) return + const { ref: _ref, ...query } = router.query + router.replace({ + pathname: `/project/${ref}/integrations/data_api/docs`, + query, + }) + }, [router, ref]) - const { paths } = jsonSchema || {} - const PAGE_KEY: any = resource || rpc || page || 'index' - - const { resources, rpcs } = Object.entries(paths || {}).reduce( - (a, [name]) => { - const trimmedName = name.slice(1) - const id = trimmedName.replace(functionPath, '') - - const displayName = id.replace(/_/g, ' ') - const camelCase = snakeToCamel(id) - const enriched = { id, displayName, camelCase } - - if (!trimmedName.length) { - return a - } - - return { - resources: { - ...a.resources, - ...(!trimmedName.includes(functionPath) - ? { - [id]: enriched, - } - : {}), - }, - rpcs: { - ...a.rpcs, - ...(trimmedName.includes(functionPath) - ? { - [id]: enriched, - } - : {}), - }, - } - }, - { resources: {}, rpcs: {} } - ) - - if (settingsError || jsonSchemaError) { - return ( -
-

-

Error connecting to API

-

{`${settingsError || jsonSchemaError}`}

-

-
- ) - } + return null +} - if (isLoading || !settings || !jsonSchema) { - return ( -
-

Building docs ...

-
- ) - } +ApiDocsRedirect.getLayout = (page) => <>{page} - return ( -
-
-
- -
-
- {resource ? ( - - ) : rpc ? ( - - ) : ( - - )} -
-
-
- ) -} +export default ApiDocsRedirect