Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/content/guides/integrations/partner-catalog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: 'Integrations and Partners'

The [Partner Catalog](/partners/catalog) is Supabase's public directory of third-party integrations. It lists tools that extend your Supabase project. These tools cover Auth, Caching, Hosting, and Low-code categories.

The Partner Catalog is different from [Dashboard Integrations](/guides/integrations#dashboard-integrations). You install Dashboard Integrations directly from a project in the Supabase Dashboard.
The Partner Catalog is different from [Dashboard Integrations](/docs/guides/integrations#dashboard-integrations). You install Dashboard Integrations directly from a project in the Supabase Dashboard.

## Build an integration

Expand Down
67 changes: 67 additions & 0 deletions apps/docs/features/ui/AgentPluginsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ const PLUGIN_CLIENTS: PluginClient[] = [
docsUrl:
'https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-installing',
},
{
key: 'kimi',
label: 'Kimi Code',
icon: 'kimi',
hasDistinctDarkIcon: true,
repoUrl: 'https://github.com/supabase-community/supabase-plugin',
docsUrl: 'https://www.kimi.com/code/docs/en/kimi-code-cli/customization/plugins.html',
},
{
key: 'vscode',
label: 'VS Code',
icon: 'vscode',
repoUrl: 'https://github.com/supabase-community/supabase-plugin',
docsUrl: 'https://code.visualstudio.com/docs/agent-customization/agent-plugins',
},
]

function PluginInstructions({ client }: { client: PluginClient }) {
Expand Down Expand Up @@ -172,6 +187,58 @@ function PluginInstructions({ client }: { client: PluginClient }) {
)
}

if (client.key === 'kimi') {
return (
<div className="space-y-3">
<p className="text-sm text-foreground-light">Open Kimi Code by running</p>
<CodeBlock value="kimi" language="bash" focusable={false} className="block" />
<p className="text-sm text-foreground-light">
Then install the Supabase plugin from within the session:
</p>
<CodeBlock
value="/plugins install https://github.com/supabase-community/supabase-plugin"
language="bash"
focusable={false}
className="block"
/>
<p className="text-xs text-foreground-lighter">
Confirm the trust prompt to install. Kimi adds the plugin to its native plugin store. Run{' '}
<code>/plugins</code> at any time to view or reload installed plugins.
</p>
</div>
)
}

if (client.key === 'vscode') {
return (
<div className="space-y-3">
<p className="text-sm text-foreground-light">
Open the Command Palette (<code>Cmd/Ctrl+Shift+P</code>) and run{' '}
<strong>Chat: Install Plugin From Source</strong>, then paste the Supabase plugin
repository URL:
</p>
<CodeBlock
value="https://github.com/supabase-community/supabase-plugin"
language="bash"
focusable={false}
className="block"
/>
<p className="text-xs text-foreground-lighter">
VS Code auto-detects the vendor-neutral{' '}
<a
href="https://github.com/vercel-labs/open-plugin-spec"
target="_blank"
rel="noopener noreferrer"
className="text-brand-link hover:underline"
>
Open Plugin
</a>{' '}
manifest in the repo. Review the trust prompt to finish installing.
</p>
</div>
)
}

if (client.key === 'github-copilot') {
return (
<div className="space-y-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type ResponseData = {
status: number
headers: Record<string, string>
headers: Record<string, string | string[]>
body: string
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { getAnchor } from '@ui/components/CustomHTMLElements/CustomHTMLElements.utils'

import { EdgeFunction } from '@/data/edge-functions/edge-function-query'
import { DOCS_URL } from '@/lib/constants'

export const generateCLICommands = ({
selectedFunction,
Expand Down Expand Up @@ -99,3 +102,18 @@ export const generateCLICommands = ({

return { managementCommands, secretCommands, invokeCommands }
}

export const getEdgeFunctionErrorDocs = (headers: Record<string, string | string[]>) => {
const header = Object.entries(headers).find(
([name]) => name.toLowerCase() === 'sb-error-code'
)?.[1]
const code = (Array.isArray(header) ? header[0] : header)?.trim()
const anchor = code ? getAnchor(code) : undefined

if (!code || !anchor) return undefined

return {
code,
href: `${DOCS_URL}/guides/functions/error-codes#${anchor}`,
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { Loader2, Plus, Send, X } from 'lucide-react'
import { BookOpen, Loader2, Plus, Send, X } from 'lucide-react'
import { useState } from 'react'
import { useFieldArray, useForm } from 'react-hook-form'
import {
Expand Down Expand Up @@ -37,6 +37,7 @@ import * as z from 'zod'

import { HTTP_METHODS } from './EdgeFunctionDetails.constants'
import { ErrorWithStatus, ResponseData } from './EdgeFunctionDetails.types'
import { getEdgeFunctionErrorDocs } from './EdgeFunctionDetails.utils'
import { RoleImpersonationPopover } from '@/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover'
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
import { useAPIKeys } from '@/data/api-keys/api-keys-query'
Expand Down Expand Up @@ -100,6 +101,7 @@ const EdgeFunctionTesterSheetContent = ({ visible, onClose }: EdgeFunctionTester

const [response, setResponse] = useState<ResponseData | null>(null)
const [error, setError] = useState<string | null>(null)
const errorDocs = response ? getEdgeFunctionErrorDocs(response.headers) : undefined

const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
const { data: apiKeysData } = useAPIKeys({ projectRef }, { enabled: canReadAPIKeys })
Expand Down Expand Up @@ -423,12 +425,28 @@ const EdgeFunctionTesterSheetContent = ({ visible, onClose }: EdgeFunctionTester
Headers
</TabsTrigger>
</div>
<Badge
variant={response.status >= 400 ? 'destructive' : 'success'}
className="-translate-y-1"
>
{response.status}
</Badge>
<div className="-translate-y-1 flex items-center gap-2">
{errorDocs !== undefined && (
<Button
asChild
variant="text"
size="tiny"
icon={<BookOpen size={14} />}
>
<a
href={errorDocs.href}
target="_blank"
rel="noreferrer"
aria-label={`View documentation for ${errorDocs.code} (opens in new tab)`}
>
Error docs
</a>
</Button>
)}
<Badge variant={response.status >= 400 ? 'destructive' : 'success'}>
{response.status}
</Badge>
</div>
</TabsList>
<TabsContent value="body" className="mt-0 flex-1 overflow-auto p-0">
<CodeBlock
Expand Down
172 changes: 100 additions & 72 deletions apps/studio/components/layouts/Tabs/SortableTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { AnimatePresence, motion } from 'framer-motion'
import { X } from 'lucide-react'
import { useMemo } from 'react'
import { useMemo, type KeyboardEvent } from 'react'
import { cn, TabsTrigger } from 'ui'

import { useEditorType } from '../editors/EditorsLayout.hooks'
Expand All @@ -17,6 +17,14 @@ import { useTabsStateSnapshot, type Tab } from '@/state/tabs'
* - Dynamic schema name display
* - Tab label animations
* - Close button interactions
*
* Markup: sortable shell (plain div) → TabsTrigger + close as siblings.
* dnd-kit `attributes` are intentionally not spread on the shell — they inject
* `role="button"` / `tabIndex={0}`, which nested a second button around the tab.
* Only PointerSensor is used for reorder, so those attributes are not required.
*
* Keyboard: ←/→ move between tabs (Radix). Delete/Backspace on a focused tab
* closes it. The active tab's close button is in the tab order.
*/
export const SortableTab = ({
tab,
Expand All @@ -37,7 +45,7 @@ export const SortableTab = ({
void tabs.handlerRegistrationVersion
const StatusIndicator = tabs.getTabStatusIndicator(tab.type)
const { selectedSchema: currentSchema } = useQuerySchemaState()
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
const { setNodeRef, listeners, transform, transition, isDragging } = useSortable({
id: tab.id,
})

Expand All @@ -54,89 +62,109 @@ export const SortableTab = ({
return openTabs.some((t) => editor === 'table' && t.metadata?.schema !== currentSchema)
}, [openTabs, currentSchema, editor])

// Create a motion version of TabsTrigger while preserving all functionality
// const MotionTabsTrigger = motion(TabsTrigger)
const isActive = tabs.activeTab === tab.id

const closeTabFromKeyboard = (event: KeyboardEvent) => {
if (event.key !== 'Delete' && event.key !== 'Backspace') return
event.preventDefault()
event.stopPropagation()
onClose(tab.id)
}

return (
<motion.div
ref={setNodeRef}
style={style}
{...attributes}
layoutId={tab.id}
transition={{ duration: 0.045 }}
animate={{ opacity: isDragging ? 0 : 1 }}
className={cn('flex items-center h-(--header-height) first-of-type:border-l')}
>
<TabsTrigger
value={tab.id}
onAuxClick={(e) => {
// Middle click closes tab
if (e.button === 1) {
e.preventDefault()
onClose(tab.id)
}
}}
onDoubleClick={() => tabs.makeTabPermanent(tab.id)}
className={cn(
'flex items-center gap-2 pl-3 pr-2.5 text-xs',
'bg-dash-sidebar/50 dark:bg-surface-100/50',
'data-[state=active]:bg-dash-sidebar dark:data-[state=active]:bg-surface-100',
'border-b border-default',
'data-[state=active]:border-b-background-dash-sidebar dark:data-[state=active]:border-b-background-surface-100',
'relative group h-full',
'hover:bg-surface-300 dark:hover:bg-surface-100',
tab.isPreview && 'italic font-light' // Optional: style preview tabs differently
)}
{...listeners}
>
<EntityTypeIcon type={tab.type} />
<div className="flex items-center gap-0">
<AnimatePresence mode="popLayout" initial>
{shouldShowSchema && (
<motion.span
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: 'auto' }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.15 }}
className="text-foreground-muted group-data-[state=active]:text-foreground-lighter"
>
{tab?.metadata?.schema}.
</motion.span>
)}
</AnimatePresence>
<span>{tab.label || 'Untitled'}</span>
</div>
{/* VS Code-style slot: the type's status indicator (e.g. an unsaved dot)
shows at rest and swaps to the close button on hover. */}
<div className="relative ml-1 flex size-5 items-center justify-center">
{StatusIndicator && (
<span className="absolute inset-0 flex items-center justify-center group-hover:opacity-0">
<StatusIndicator tab={tab} />
</span>
)}
<span
role="button"
aria-label="Close tab"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
}}
className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 hover:bg-200 rounded-xs cursor-pointer"
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onPointerDown={(e) => {
<div className="group/tab relative flex h-full min-w-0 items-center">
<TabsTrigger
value={tab.id}
onAuxClick={(e) => {
// Middle click closes tab
if (e.button === 1) {
e.preventDefault()
e.stopPropagation()
onClose(tab.id)
}}
}
}}
onDoubleClick={() => tabs.makeTabPermanent(tab.id)}
onKeyDown={closeTabFromKeyboard}
className={cn(
'flex items-center gap-2 pl-3 pr-2.5 text-xs',
'bg-dash-sidebar/50 dark:bg-surface-100/50',
'data-[state=active]:bg-dash-sidebar dark:data-[state=active]:bg-surface-100',
'border-b border-default',
'data-[state=active]:border-b-background-dash-sidebar dark:data-[state=active]:border-b-background-surface-100',
'relative group h-full',
'hover:bg-surface-300 dark:hover:bg-surface-100',
tab.isPreview && 'italic font-light' // Optional: style preview tabs differently
)}
{...listeners}
>
<EntityTypeIcon type={tab.type} />
<div className="flex items-center gap-0">
<AnimatePresence mode="popLayout" initial>
{shouldShowSchema && (
<motion.span
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: 'auto' }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.15 }}
className="text-foreground-muted group-data-[state=active]:text-foreground-lighter"
>
{tab?.metadata?.schema}.
</motion.span>
)}
</AnimatePresence>
<span>{tab.label || 'Untitled'}</span>
</div>
{/* Reserve status/close slot width; close is a sibling overlay, not nested. */}
<div
className="relative ml-1 flex size-5 shrink-0 items-center justify-center"
aria-hidden
>
<X size={12} className="text-foreground-light" />
</span>
</div>
<div className="absolute w-full top-0 left-0 right-0 h-px bg-foreground opacity-0 group-data-[state=active]:opacity-100" />
</TabsTrigger>
{StatusIndicator && (
<span className="absolute inset-0 flex items-center justify-center group-hover/tab:opacity-0 group-focus-within/tab:opacity-0">
<StatusIndicator tab={tab} />
</span>
)}
</div>
<div className="absolute w-full top-0 left-0 right-0 h-px bg-foreground opacity-0 group-data-[state=active]:opacity-100" />
</TabsTrigger>
{/* Sibling of TabsTrigger — not nested inside the tab button.
Only the active tab's close is in the tab order (roving tabs). Delete/Backspace
on the focused tab also closes. */}
<button
type="button"
tabIndex={isActive ? 0 : -1}
aria-label="Close tab"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onClose(tab.id)
}}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
className={cn(
'absolute top-1/2 right-2.5 z-10 -translate-y-1/2',
'flex size-5 items-center justify-center rounded-xs',
'opacity-0 group-hover/tab:opacity-100 group-focus-within/tab:opacity-100 focus-visible:opacity-100',
'hover:bg-200 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
'cursor-pointer'
)}
>
<X size={12} className="text-foreground-light" />
</button>
</div>
{index < openTabs.length && (
<div role="separator" className="h-full w-px bg-border" key={`separator-${tab.id}`} />
)}
Expand Down
Loading
Loading