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
62 changes: 27 additions & 35 deletions apps/docs/components/GuidesSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

import { Feedback } from '~/components/Feedback'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import { isFeatureEnabled } from 'common'
import { askAiUrls, isFeatureEnabled, useCopyMarkdownFromUrl } from 'common'
import { Chatgpt, Claude } from 'icons'
import { Check, Copy, Sparkles } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useState } from 'react'
import { cn } from 'ui'
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
import { Toc, TOCItems, TOCScrollArea } from 'ui-patterns/Toc'
Expand All @@ -22,38 +21,22 @@ interface TOCHeader {
}

function AiTools({ className }: { className?: string }) {
const [copied, setCopied] = useState(false)
const path = usePathname()
const sendTelemetryEvent = useSendTelemetryEvent()
const { copied, copyMarkdown } = useCopyMarkdownFromUrl()
const urls = askAiUrls(`https://supabase.com/docs${path}`)

function handleAgentSetupClick() {
sendTelemetryEvent({ action: 'agent_setup_clicked' })
}

async function copyMarkdown() {
const mdUrl = `/docs/${path}.md`

try {
const res = await fetch(mdUrl)
let text: string

if (res.ok) {
text = await res.text()
} else {
// Default to HTML content within the article when no .md file is available.
text = document.getElementById('sb-docs-guide-main-article')?.innerHTML ?? ''
}

await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (error) {
console.error('Failed to copy markdown', error)
}

sendTelemetryEvent({
action: 'copy_as_markdown_clicked',
async function handleCopy() {
const ok = await copyMarkdown(`/docs${path}.md`, {
fallback: () => document.getElementById('sb-docs-guide-main-article')?.innerHTML ?? '',
})
if (ok) {
sendTelemetryEvent({ action: 'copy_as_markdown_clicked', properties: { pageType: 'guide' } })
}
}

return (
Expand All @@ -75,38 +58,47 @@ function AiTools({ className }: { className?: string }) {
</Link>
<button
tabIndex={0}
onClick={copyMarkdown}
onClick={handleCopy}
className="flex cursor-pointer items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground text-left transition-colors"
>
{copied ? (
<Check size={14} strokeWidth={1.5} className="text-brand" />
<Check size={14} strokeWidth={1.5} className="text-brand" aria-hidden />
) : (
<Copy size={14} strokeWidth={1.5} />
<Copy size={14} strokeWidth={1.5} aria-hidden />
)}
{copied ? 'Copied!' : 'Copy as Markdown'}
</button>
<span className="sr-only" role="status">
{copied ? 'Copied to clipboard' : ''}
</span>
<a
href={`https://chatgpt.com/?hint=search&q=Read from https://supabase.com/docs${path} so I can ask questions about its contents`}
href={urls.chatgpt}
target="_blank"
onClick={() =>
sendTelemetryEvent({ action: 'ask_ai_clicked', properties: { agent: 'chatgpt' } })
sendTelemetryEvent({
action: 'ask_ai_clicked',
properties: { agent: 'chatgpt', pageType: 'guide' },
})
}
rel="noreferrer noopener"
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground transition-colors"
>
<Chatgpt size={14} />
<Chatgpt size={14} aria-hidden />
Ask ChatGPT
</a>
<a
href={`https://claude.ai/new?q=Read from https://supabase.com/docs${path} so I can ask questions about its contents`}
href={urls.claude}
target="_blank"
onClick={() =>
sendTelemetryEvent({ action: 'ask_ai_clicked', properties: { agent: 'claude' } })
sendTelemetryEvent({
action: 'ask_ai_clicked',
properties: { agent: 'claude', pageType: 'guide' },
})
}
rel="noreferrer noopener"
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground transition-colors"
>
<Claude size={14} />
<Claude size={14} aria-hidden />
Ask Claude
</a>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Projects must meet these requirements to use Read Replicas:

## Creating a Read Replica

To add a Read Replica, go to the [Database Replication page](/dashboard/project/_/database/replication) in your project dashboard.
To add a Read Replica, go to the [Infrastructure](/dashboard/project/_/settings/infrastructure) settings page in your project dashboard.

You can also manage Read Replicas using the Management API (beta functionality):

Expand Down
5 changes: 3 additions & 2 deletions apps/studio/TANSTACK_MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/project/$ref/database/publications/$id.tsx` ← `pages/project/[ref]/database/publications/[id].tsx`
- [x] A `routes/project/$ref/database/replication/index.tsx` ← `pages/project/[ref]/database/replication/index.tsx`
- [x] A `routes/project/$ref/database/replication/$pipelineId.tsx` ← `pages/project/[ref]/database/replication/[pipelineId].tsx`
- [x] A `routes/project/$ref/database/replication/replica/$replicaId.tsx` ← `pages/project/[ref]/database/replication/replica/[replicaId].tsx`
- [x] A `routes/project/$ref/database/replication/replica/$replicaId.tsx` ← `pages/project/[ref]/database/replication/replica/[replicaId].tsx` (redirects to Infrastructure)
- [x] A `routes/project/$ref/database/triggers/index.tsx` ← `pages/project/[ref]/database/triggers/index.tsx`
- [x] A `routes/project/$ref/database/triggers/data.tsx` ← `pages/project/[ref]/database/triggers/data.tsx` (sub-shell at `database/triggers.tsx` provides PageLayout + nav, parent shell provides DatabaseLayout)
- [x] A `routes/project/$ref/database/triggers/event.tsx` ← `pages/project/[ref]/database/triggers/event.tsx` (same as data)
Expand Down Expand Up @@ -296,7 +296,8 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/project/$ref/settings/addons.tsx` ← `pages/project/[ref]/settings/addons.tsx`
- [x] A `routes/project/$ref/settings/api.tsx` ← `pages/project/[ref]/settings/api.tsx` (sets `skipSettingsLayout: true` — page is a useEffect redirect)
- [x] A `routes/project/$ref/settings/dashboard.tsx` ← `pages/project/[ref]/settings/dashboard.tsx`
- [x] A `routes/project/$ref/settings/infrastructure.tsx` ← `pages/project/[ref]/settings/infrastructure.tsx`
- [x] A `routes/project/$ref/settings/infrastructure/index.tsx` ← `pages/project/[ref]/settings/infrastructure.tsx`
- [x] A `routes/project/$ref/settings/infrastructure/replica/$replicaId.tsx` ← `pages/project/[ref]/settings/infrastructure/replica/[replicaId].tsx`
- [x] A `routes/project/$ref/settings/integrations.tsx` ← `pages/project/[ref]/settings/integrations.tsx`
- [x] A `routes/project/$ref/settings/log-drains.tsx` ← `pages/project/[ref]/settings/log-drains.tsx`
- [x] A `routes/project/$ref/settings/api-keys/index.tsx` ← `pages/project/[ref]/settings/api-keys/index.tsx` (under `api-keys.tsx` sub-shell with ApiKeysLayout)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { DestinationIcon } from './DestinationIcon'
import type { DestinationType } from './DestinationPanel/DestinationPanel.types'

const DESTINATION_TYPES: DestinationType[] = [
'Read Replica',
'BigQuery',
'Analytics Bucket',
'DuckLake',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { DestinationType } from './DestinationPanel/DestinationPanel.types'
type DestinationIconComponent = ComponentType<SVGProps<SVGSVGElement> & { size?: string | number }>

const destinationIconByType: Record<DestinationType, DestinationIconComponent> = {
'Read Replica': Database,
BigQuery,
'Analytics Bucket': AnalyticsBucket,
DuckLake: Database,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,14 @@ import { useIsETLPrivateAlpha } from '../useIsETLPrivateAlpha'
import { DestinationForm } from './DestinationForm'
import { DestinationType } from './DestinationPanel.types'
import { DestinationTypeSelection } from './DestinationTypeSelection'
import { ReadReplicaForm } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaForm'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { DocsButton } from '@/components/ui/DocsButton'
import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query'
import { checkLocalETLNotSetUp } from '@/data/replication/utils'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { DOCS_URL } from '@/lib/constants'

interface DestinationPanelProps {
onSuccessCreateReadReplica?: () => void
}

export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPanelProps) => {
export const DestinationPanel = () => {
const { ref: projectRef } = useParams()
const enablePgReplicate = useIsETLPrivateAlpha()
const { error: destinationsError } = useReplicationDestinationsQuery({ projectRef })
Expand All @@ -45,7 +40,6 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane
const [urlDestinationType, setDestinationType] = useQueryState(
'destinationType',
parseAsStringEnum<DestinationType>([
'Read Replica',
'BigQuery',
'Analytics Bucket',
'DuckLake',
Expand Down Expand Up @@ -148,7 +142,7 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane
<SheetDescription>
{editMode
? 'Update the configuration for this destination.'
: 'Add a read replica or an external destination.'}
: 'Connect an external destination for analytics workloads.'}
</SheetDescription>
</div>
<DocsButton
Expand All @@ -157,15 +151,7 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane
/>
</SheetHeader>

{destinationType === 'Read Replica' ? (
<ReadReplicaForm
typeSelection={typeSelection}
checkIsDirtyRef={checkIsDirtyRef}
onClose={onClose}
onCancel={confirmOnClose}
onSuccess={() => onSuccessCreateReadReplica?.()}
/>
) : !enablePgReplicate ? (
{!enablePgReplicate ? (
<div className="grow overflow-auto min-h-0">
{pipelinesTypeSelection}
<SheetSection>
Expand All @@ -174,8 +160,7 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane
<h4>Request Pipelines access</h4>
<p className="text-sm text-foreground-light">
Pipelines is in <span className="text-foreground">public alpha</span> and
being rolled out gradually. Request access below to join the waitlist. Read
replicas are available now.
being rolled out gradually. Request access below to join the waitlist.
</p>
</div>
<div className="flex gap-x-2">
Expand Down Expand Up @@ -207,7 +192,7 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane
) : (
<DestinationForm
visible={visible}
selectedType={destinationType ?? 'Read Replica'}
selectedType={destinationType ?? 'BigQuery'}
existingDestination={existingDestination}
typeSelection={pipelinesTypeSelection}
checkIsDirtyRef={checkIsDirtyRef}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export type DestinationType =
| 'Read Replica'
| 'BigQuery'
| 'Analytics Bucket'
| 'DuckLake'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { fireEvent, screen } from '@testing-library/react'
import { platformComponents as components } from 'api-types'
import { mockAnimationsApi } from 'jsdom-testing-mocks'
import { HttpResponse } from 'msw'
import { describe, expect, test, vi } from 'vitest'
import { beforeEach, describe, expect, test, vi } from 'vitest'

import { DestinationTypeSelection } from './DestinationTypeSelection'
import { customRender } from '@/tests/lib/custom-render'
Expand Down Expand Up @@ -30,8 +30,12 @@ vi.mock('../useIsETLPrivateAlpha', () => ({
useIsETLClickHousePrivateAlpha: () => mockClickHouseEnabled(),
}))

const mockInfrastructureReadReplicas = vi.fn(() => true)

vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({
useIsFeatureEnabled: () => ({ infrastructureReadReplicas: true }),
useIsFeatureEnabled: () => ({
infrastructureReadReplicas: mockInfrastructureReadReplicas(),
}),
}))

// Background queries from useDestinationInformation (sources + pipelines fire
Expand All @@ -54,20 +58,12 @@ const addBackgroundMocks = () => {
}

describe('DestinationTypeSelection', () => {
test('shows placeholder when no type is selected', async () => {
mockBigQueryEnabled.mockReturnValue(false)
mockIcebergEnabled.mockReturnValue(false)
mockDucklakeEnabled.mockReturnValue(false)
mockSnowflakeEnabled.mockReturnValue(false)
mockClickHouseEnabled.mockReturnValue(false)
addBackgroundMocks()

customRender(<DestinationTypeSelection />)

expect(await screen.findByText('Select a destination type')).toBeInTheDocument()
beforeEach(() => {
mockInfrastructureReadReplicas.mockReturnValue(true)
window.localStorage.clear()
})

test('renders Read Replica in the Other group when dropdown is opened', async () => {
test('shows placeholder when no type is selected', async () => {
mockBigQueryEnabled.mockReturnValue(false)
mockIcebergEnabled.mockReturnValue(false)
mockDucklakeEnabled.mockReturnValue(false)
Expand All @@ -77,10 +73,7 @@ describe('DestinationTypeSelection', () => {

customRender(<DestinationTypeSelection />)

fireEvent.click(await screen.findByRole('combobox'))

expect(await screen.findByText('Other')).toBeInTheDocument()
expect(screen.getByText('Read Replica')).toBeInTheDocument()
expect(await screen.findByText('Select a destination type')).toBeInTheDocument()
})

test('renders the Pipelines group with BigQuery when the flag is enabled', async () => {
Expand Down Expand Up @@ -111,8 +104,7 @@ describe('DestinationTypeSelection', () => {

fireEvent.click(await screen.findByRole('combobox'))

expect(await screen.findByText('Other')).toBeInTheDocument()
expect(screen.getByText('Read Replica')).toBeInTheDocument()
expect(screen.queryByText('Read Replica')).not.toBeInTheDocument()
expect(screen.queryByText('BigQuery')).not.toBeInTheDocument()
expect(screen.queryByText('DuckLake')).not.toBeInTheDocument()
expect(screen.queryByText('Analytics Bucket')).not.toBeInTheDocument()
Expand Down Expand Up @@ -165,5 +157,39 @@ describe('DestinationTypeSelection', () => {
customRender(<DestinationTypeSelection />, { nuqs: { searchParams: { edit: '1' } } })

expect(await screen.findByRole('combobox')).toBeDisabled()
expect(screen.queryByText('Read replicas have moved')).not.toBeInTheDocument()
})

test('shows a callout pointing read replicas to Infrastructure in create mode', async () => {
mockBigQueryEnabled.mockReturnValue(false)
mockIcebergEnabled.mockReturnValue(false)
mockDucklakeEnabled.mockReturnValue(false)
mockSnowflakeEnabled.mockReturnValue(false)
mockClickHouseEnabled.mockReturnValue(false)
mockInfrastructureReadReplicas.mockReturnValue(true)
addBackgroundMocks()

customRender(<DestinationTypeSelection />)

expect(await screen.findByText('Read replicas have moved')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Go to Infrastructure' })).toHaveAttribute(
'href',
expect.stringContaining('/settings/infrastructure')
)
})

test('hides the read replicas callout when Infrastructure read replicas are disabled', async () => {
mockBigQueryEnabled.mockReturnValue(false)
mockIcebergEnabled.mockReturnValue(false)
mockDucklakeEnabled.mockReturnValue(false)
mockSnowflakeEnabled.mockReturnValue(false)
mockClickHouseEnabled.mockReturnValue(false)
mockInfrastructureReadReplicas.mockReturnValue(false)
addBackgroundMocks()

customRender(<DestinationTypeSelection />)

expect(await screen.findByText('Select a destination type')).toBeInTheDocument()
expect(screen.queryByText('Read replicas have moved')).not.toBeInTheDocument()
})
})
Loading
Loading