mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 11:25:06 +03:00
## 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? Introduced a new library called Superbase/Server. To help developers using third party API frameworks, we want to make it easier than ever to install and use as needed. - One click copy - Custom prompt to get started - Validated API key permissions to ensure we don't leak secrets to other users in your org/project <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## New Features - Added a **Server** connection mode end-to-end, including mode-specific prompting and steps for installing **`@supabase/server`** and setting required variables. - Added a server **.env** panel with per-variable copy, **“Copy all variables”**, and permission-aware secret reveal/copy. ## Improvements - Updated connection UI layouts (mode selector grid and conditional config section). - Improved prompt copying to use mode-specific prompt text when available. ## Tests - Added UI tests for server env rendering, secret reveal/copy, **copy-all** behavior, and permission-restricted scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
167 lines
5.5 KiB
TypeScript
167 lines
5.5 KiB
TypeScript
import { type RefObject } from 'react'
|
|
import { Admonition } from 'ui-patterns/admonition'
|
|
|
|
import CopyButton from '@/components/ui/CopyButton'
|
|
import { BASE_PATH } from '@/lib/constants'
|
|
|
|
interface CopyPromptAdmonitionProps {
|
|
stepsContainerRef: RefObject<HTMLDivElement | null>
|
|
/** When set, the Copy prompt button uses this verbatim instead of scraping the steps. */
|
|
customPrompt?: string
|
|
}
|
|
|
|
const normalizeTextLines = (value: string) => {
|
|
return value
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
.join('\n')
|
|
}
|
|
|
|
const getStepTextContent = (contentElement: HTMLElement) => {
|
|
const clone = contentElement.cloneNode(true) as HTMLElement
|
|
clone
|
|
.querySelectorAll('pre, button, svg, input, textarea, select, [aria-hidden="true"]')
|
|
.forEach((element) => {
|
|
element.remove()
|
|
})
|
|
|
|
clone.querySelectorAll('p, div').forEach((el) => {
|
|
el.appendChild(document.createTextNode('\n'))
|
|
})
|
|
|
|
const text = clone.textContent ?? ''
|
|
return normalizeTextLines(text)
|
|
}
|
|
|
|
const getStepCodeSnippets = (contentElement: HTMLElement) => {
|
|
const snippets: Array<{ label: string; snippet: string }> = []
|
|
const seen = new Set<string>()
|
|
|
|
const addSnippet = (label: string, snippet: string) => {
|
|
if (!snippet || seen.has(snippet)) return
|
|
seen.add(snippet)
|
|
snippets.push({ label, snippet })
|
|
}
|
|
|
|
const getSnippet = (element: Element) => {
|
|
const copyValueElement = element.closest('[data-connect-copy-value]') as HTMLElement | null
|
|
return copyValueElement?.dataset.connectCopyValue?.trim() || element.textContent?.trim()
|
|
}
|
|
|
|
const tabContents = Array.from(
|
|
contentElement.querySelectorAll('[data-connect-tab-content]')
|
|
) as HTMLElement[]
|
|
|
|
tabContents.forEach((tabContent) => {
|
|
const label = tabContent.getAttribute('data-tab-label') || 'Code'
|
|
const tabSnippets = Array.from(tabContent.querySelectorAll('pre'))
|
|
.map(getSnippet)
|
|
.filter((snippet): snippet is string => Boolean(snippet))
|
|
|
|
if (tabSnippets.length === 0) {
|
|
const inlineSnippets = Array.from(tabContent.querySelectorAll('code'))
|
|
.filter((code) => !code.closest('pre') && code.closest('.font-mono'))
|
|
.map((code) => code.textContent?.trim())
|
|
.filter((snippet): snippet is string => Boolean(snippet))
|
|
inlineSnippets.forEach((snippet, index) => {
|
|
const inlineLabel = inlineSnippets.length > 1 ? `${label} (part ${index + 1})` : label
|
|
addSnippet(inlineLabel, snippet)
|
|
})
|
|
return
|
|
}
|
|
|
|
tabSnippets.forEach((snippet, index) => {
|
|
const tabLabel = tabSnippets.length > 1 ? `${label} (part ${index + 1})` : label
|
|
addSnippet(tabLabel, snippet)
|
|
})
|
|
})
|
|
|
|
contentElement.querySelectorAll('pre').forEach((pre) => {
|
|
if (pre.closest('[data-connect-tab-content]')) return
|
|
const snippet = getSnippet(pre)
|
|
if (snippet) addSnippet('Code', snippet)
|
|
})
|
|
|
|
contentElement.querySelectorAll('code').forEach((code) => {
|
|
if (code.closest('pre')) return
|
|
if (code.closest('[data-connect-tab-content]')) return
|
|
if (!code.closest('.font-mono')) return
|
|
const snippet = code.textContent?.trim()
|
|
if (snippet) addSnippet('Code', snippet)
|
|
})
|
|
|
|
return snippets
|
|
}
|
|
|
|
export const buildConnectPrompt = (stepsContainer: HTMLElement | null) => {
|
|
const stepElements = stepsContainer?.querySelectorAll('[data-connect-step]')
|
|
if (!stepElements?.length) return ''
|
|
|
|
const promptContent = Array.from(stepElements)
|
|
.map((stepElement, index) => {
|
|
const title = stepElement.getAttribute('data-step-title') ?? `Step ${index + 1}`
|
|
const description = stepElement.getAttribute('data-step-description') ?? ''
|
|
const contentElement = stepElement.querySelector('[data-step-content]') as HTMLElement | null
|
|
|
|
const details = contentElement ? getStepTextContent(contentElement) : ''
|
|
const codeSnippets = contentElement ? getStepCodeSnippets(contentElement) : []
|
|
|
|
const sections = [
|
|
`${index + 1}. ${title}`,
|
|
description,
|
|
details ? `Details:\n${details}` : null,
|
|
codeSnippets.length
|
|
? `Code:\n${codeSnippets
|
|
.map(({ label, snippet }) => `File: ${label}\n\`\`\`\n${snippet}\n\`\`\``)
|
|
.join('\n\n')}`
|
|
: null,
|
|
].filter(Boolean)
|
|
|
|
return sections.join('\n')
|
|
})
|
|
.join('\n\n')
|
|
|
|
return promptContent
|
|
}
|
|
|
|
export function CopyPromptAdmonition({
|
|
stepsContainerRef,
|
|
customPrompt,
|
|
}: CopyPromptAdmonitionProps) {
|
|
const handleCopyPrompt = () => {
|
|
return customPrompt ?? buildConnectPrompt(stepsContainerRef.current)
|
|
}
|
|
|
|
return (
|
|
<Admonition
|
|
type="tip"
|
|
showIcon={false}
|
|
layout="horizontal"
|
|
actions={
|
|
<CopyButton variant="default" copyLabel="Copy prompt" asyncText={handleCopyPrompt} />
|
|
}
|
|
>
|
|
<div className="absolute -inset-16 z-0 opacity-50">
|
|
<img
|
|
src={`${BASE_PATH}/img/reports/bg-grafana-dark.svg`}
|
|
alt="Supabase Grafana"
|
|
className="w-full h-full object-cover object-right hidden dark:block"
|
|
/>
|
|
<img
|
|
src={`${BASE_PATH}/img/reports/bg-grafana-light.svg`}
|
|
alt="Supabase Grafana"
|
|
className="w-full h-full object-cover object-right dark:hidden"
|
|
/>
|
|
<div className="absolute inset-0 bg-linear-to-r from-background-alternative to-transparent" />
|
|
</div>
|
|
|
|
<div className="relative flex flex-col md:flex-row md:items-center gap-y-2 md:gap-x-8 justify-between">
|
|
<div className="flex flex-col gap-y-0.5">
|
|
<p className="heading-default">Give your agent everything it needs</p>
|
|
</div>
|
|
</div>
|
|
</Admonition>
|
|
)
|
|
}
|