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
52 changes: 51 additions & 1 deletion apps/docs/features/ui/McpConfigPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import {
ScrollArea,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { McpConfigPanel as McpConfigPanelBase } from 'ui-patterns/McpUrlBuilder'
import { McpConfigPanel as McpConfigPanelBase, type McpClient } from 'ui-patterns/McpUrlBuilder'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { useDebounce } from '~/hooks/useDebounce'
import { useIntersectionObserver } from '~/hooks/useIntersectionObserver'
import { useProjectsInfiniteQuery } from '~/lib/fetch/projects-infinite'
import { useSendTelemetryEvent } from '~/lib/telemetry'

type PlatformType = (typeof PLATFORMS)[number]['value']

Expand Down Expand Up @@ -260,11 +261,57 @@ function PlatformSelector({
export function McpConfigPanel() {
const [selectedProject, setSelectedProject] = useState<{ ref: string; name: string } | null>(null)
const [selectedPlatform, setSelectedPlatform] = useState<'hosted' | 'local'>('hosted')
const [selectedClient, setSelectedClient] = useState<McpClient | null>(null)
const { theme } = useTheme()
const sendTelemetryEvent = useSendTelemetryEvent()

const isPlatform = selectedPlatform === 'hosted'
const project = isPlatform ? selectedProject : null

const handleCopy = (type?: 'url' | 'json' | 'command') => {
let connectionType: string
switch (type) {
case 'command':
connectionType = 'Command Line'
break
case 'json':
connectionType = 'JSON'
break
case 'url':
default:
connectionType = 'MCP URL'
break
}

sendTelemetryEvent({
action: 'connection_string_copied',
properties: {
connectionTab: 'MCP',
selectedItem: selectedClient?.label,
connectionType,
source: 'docs',
},
groups: {
...(project?.ref && { project: project.ref }),
} as any,
})
}

const handleInstall = () => {
if (selectedClient?.label) {
sendTelemetryEvent({
action: 'mcp_install_button_clicked',
properties: {
client: selectedClient.label,
source: 'docs',
},
groups: {
...(project?.ref && { project: project.ref }),
} as any,
})
}
}

return (
<>
<div className="not-prose">
Expand All @@ -288,6 +335,9 @@ export function McpConfigPanel() {
projectRef={project?.ref}
theme={theme as 'light' | 'dark'}
isPlatform={isPlatform}
onCopyCallback={handleCopy}
onInstallCallback={handleInstall}
onClientSelect={setSelectedClient}
/>
</div>
{isPlatform && (
Expand Down
1 change: 1 addition & 0 deletions apps/docs/public/humans.txt
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Rodrigo Mansueli
Ronan Lehane
Rory Wilding
Ryan Goulet
Ruan Maia
Sam Meech-Ward
Sam Rome
Sam Rose
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IS_PLATFORM } from 'common'
import { useBranchCommands } from 'components/interfaces/BranchManagement/Branch.Commands'
import { useConnectCommands } from 'components/interfaces/Connect/Connect.Commands'
import {
useQueryTableCommands,
useSnippetCommands,
Expand All @@ -20,6 +21,7 @@ import { orderCommandSectionsByPriority } from './ordering'
export default function StudioCommandMenu() {
useApiKeysCommands()
useApiUrlCommand()
useConnectCommands()
useProjectLevelTableEditorCommands()
useProjectSwitchCommand()
useConfigureOrganizationCommand()
Expand Down
47 changes: 32 additions & 15 deletions apps/studio/components/interfaces/Billing/InvoiceStatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import { InlineLink } from 'components/ui/InlineLink'
import { Badge, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { InvoiceStatus } from './Invoices.types'

interface InvoiceStatusBadgeProps {
status: InvoiceStatus
paymentAttempted: boolean
paymentProcessing: boolean
}

const invoiceStatusMapping: Record<
InvoiceStatus,
{ label: string; badgeVariant: React.ComponentProps<typeof Badge>['variant'] }
> = {
[InvoiceStatus.DRAFT]: {
label: 'Upcoming',
badgeVariant: 'warning',
},
[InvoiceStatus.PAID]: {
label: 'Paid',
badgeVariant: 'brand',
},
[InvoiceStatus.VOID]: {
label: 'Forgiven',
badgeVariant: 'brand',
badgeVariant: 'warning',
},

// We do not want to overcomplicate it for the user, so we'll treat uncollectible/open/issued the same from a user perspective
Expand All @@ -39,8 +37,17 @@ const invoiceStatusMapping: Record<
},
}

const InvoiceStatusBadge = ({ status, paymentAttempted }: InvoiceStatusBadgeProps) => {
const statusMapping = invoiceStatusMapping[status]
const InvoiceStatusBadge = ({
status,
paymentAttempted,
paymentProcessing,
}: InvoiceStatusBadgeProps) => {
const statusMapping = paymentProcessing
? {
label: 'Processing',
badgeVariant: 'warning' as React.ComponentProps<typeof Badge>['variant'],
}
: invoiceStatusMapping[status]

return (
<Tooltip>
Expand All @@ -53,9 +60,25 @@ const InvoiceStatusBadge = ({ status, paymentAttempted }: InvoiceStatusBadgeProp
{statusMapping?.label || status}
</Badge>
</TooltipTrigger>
<TooltipContent side="bottom">
<TooltipContent side="bottom" className="max-w-sm">
{[InvoiceStatus.OPEN, InvoiceStatus.ISSUED, InvoiceStatus.UNCOLLECTIBLE].includes(status) &&
(paymentAttempted ? (
(paymentProcessing ? (
<div className="space-y-1">
<p className="text-xs text-foreground">
While most credit card payments get processed instantly, some Indian credit card
providers may take up to 72 hours. Your card issuer has neither confirmed nor denied
the payment and we have to wait until the card issuer processed the payment.
</p>

<p className="text-xs text-foreground">
If you run into this, we recommend{' '}
<InlineLink href="https://supabase.com/docs/guides/platform/credits#credit-top-ups">
topping up your credits
</InlineLink>{' '}
in advance to avoid running into this in the future.
</p>
</div>
) : paymentAttempted ? (
<p className="text-xs text-foreground">
We were not able to collect the payment. Make sure you have a valid payment method and
enough funds. Outstanding invoices may cause restrictions. You can manually pay the
Expand All @@ -69,12 +92,6 @@ const InvoiceStatusBadge = ({ status, paymentAttempted }: InvoiceStatusBadgeProp
</p>
))}

{status === InvoiceStatus.DRAFT && (
<p className="text-xs text-foreground">
The invoice will soon be finalized and charged for.
</p>
)}

{status === InvoiceStatus.PAID && (
<p className="text-xs text-foreground">
The invoice has been paid successfully. No action is required on your side.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export enum InvoiceStatus {
DRAFT = 'draft',
PAID = 'paid',
VOID = 'void',
UNCOLLECTIBLE = 'uncollectible',
Expand Down
60 changes: 60 additions & 0 deletions apps/studio/components/interfaces/Connect/Connect.Commands.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Plug } from 'lucide-react'
import { useRouter } from 'next/router'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from 'lib/constants'
import type { ICommand } from 'ui-patterns/CommandMenu'
import { useRegisterCommands, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils'
import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering'

export function useConnectCommands() {
const router = useRouter()
const setIsOpen = useSetCommandMenuOpen()
const { data: selectedProject } = useSelectedProjectQuery()
const isActiveHealthy = selectedProject?.status === PROJECT_STATUS.ACTIVE_HEALTHY

useRegisterCommands(
COMMAND_MENU_SECTIONS.ACTIONS,
[
{
id: 'connect-to-project',
name: 'Connect to your project',
action: () => {
// Open the Connect dialog by setting the showConnect query param
router.push(
{
pathname: router.pathname,
query: { ...router.query, showConnect: 'true' },
},
undefined,
{ shallow: true }
)
setIsOpen(false)
},
icon: () => <Plug className="rotate-90" />,
},
{
id: 'connect-mcp',
name: 'Connect via MCP',
action: () => {
// Open the Connect dialog with MCP tab
router.push(
{
pathname: router.pathname,
query: { ...router.query, showConnect: 'true', connectTab: 'mcp' },
},
undefined,
{ shallow: true }
)
setIsOpen(false)
},
icon: () => <Plug className="rotate-90" />,
},
] as ICommand[],
{
enabled: !!selectedProject && isActiveHealthy,
orderSection: orderCommandSectionsByPriority,
sectionMeta: { priority: 2 },
}
)
}
14 changes: 14 additions & 0 deletions apps/studio/components/interfaces/Connect/Connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,18 @@ export const Connect = () => {
)
}

const connectionTabMap: Record<
string,
'App Frameworks' | 'Mobile Frameworks' | 'ORMs'
> = {
frameworks: 'App Frameworks',
mobiles: 'Mobile Frameworks',
orms: 'ORMs',
}
const connectionTab = connectionTabMap[type.key] || 'App Frameworks'
const selectedFrameworkOrTool =
connectionObject.find((item) => item.key === selectedParent)?.label || ''

return (
<TabsContent_Shadcn_
key={`content-${type.key}`}
Expand Down Expand Up @@ -467,6 +479,8 @@ export const Connect = () => {
<ConnectTabContent
projectKeys={projectKeys}
filePath={filePath}
connectionTab={connectionTab}
selectedFrameworkOrTool={selectedFrameworkOrTool}
className="rounded-b-none"
/>
<Panel.Notice
Expand Down
2 changes: 2 additions & 0 deletions apps/studio/components/interfaces/Connect/Connect.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ export interface ContentFileProps {
ipv4SupportedForDedicatedPooler: boolean
direct?: string
}
connectionTab: 'App Frameworks' | 'Mobile Frameworks' | 'ORMs'
onCopy?: () => void
}
57 changes: 43 additions & 14 deletions apps/studio/components/interfaces/Connect/ConnectTabContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configur
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { pluckObjectFields } from 'lib/helpers'
import { cn } from 'ui'
import { useTrack } from 'lib/telemetry/track'
import { cn, CopyCallbackContext } from 'ui'
import { getAddons } from '../Billing/Subscription/Subscription.utils'
import type { projectKeys } from './Connect.types'
import { getConnectionStrings } from './DatabaseSettings.utils'

interface ConnectContentTabProps extends HTMLAttributes<HTMLDivElement> {
projectKeys: projectKeys
filePath: string
connectionTab: 'App Frameworks' | 'Mobile Frameworks' | 'ORMs'
selectedFrameworkOrTool: string
connectionStringPooler?: {
transactionShared: string
sessionShared: string
Expand All @@ -28,11 +31,32 @@ interface ConnectContentTabProps extends HTMLAttributes<HTMLDivElement> {
}

export const ConnectTabContent = forwardRef<HTMLDivElement, ConnectContentTabProps>(
({ projectKeys, filePath, ...props }, ref) => {
({ projectKeys, filePath, connectionTab, selectedFrameworkOrTool, ...props }, ref) => {
const { ref: projectRef } = useParams()
const track = useTrack()
const { data: selectedOrg } = useSelectedOrganizationQuery()
const allowPgBouncerSelection = useMemo(() => selectedOrg?.plan.id !== 'free', [selectedOrg])

const handleCopy = () => {
const trackingProperties: {
connectionTab: 'App Frameworks' | 'Mobile Frameworks' | 'ORMs'
selectedItem: string
connectionType?: string
lang?: string
} = {
connectionTab,
selectedItem: selectedFrameworkOrTool,
}

// Only include connectionType and lang for App Frameworks and Mobile Frameworks
if (connectionTab !== 'ORMs') {
trackingProperties.connectionType = 'Framework snippet'
trackingProperties.lang = filePath.split('/').pop() ?? 'unknown'
}

track('connection_string_copied', trackingProperties)
}

const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef })
const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef })
Expand Down Expand Up @@ -84,18 +108,23 @@ export const ConnectTabContent = forwardRef<HTMLDivElement, ConnectContentTabPro

return (
<div ref={ref} {...props} className={cn('border rounded-lg', props.className)}>
<ContentFile
projectKeys={projectKeys}
filePath={filePath}
connectionStringPooler={{
transactionShared: connectionStringsShared.pooler.uri,
sessionShared: connectionStringsShared.pooler.uri.replace('6543', '5432'),
transactionDedicated: connectionStringsDedicated?.pooler.uri,
sessionDedicated: connectionStringsDedicated?.pooler.uri.replace('6543', '5432'),
ipv4SupportedForDedicatedPooler: !!ipv4Addon,
direct: connectionStringsShared.direct.uri,
}}
/>
<CopyCallbackContext.Provider value={handleCopy}>
<ContentFile
projectKeys={projectKeys}
filePath={filePath}
connectionTab={connectionTab}
selectedFrameworkOrTool={selectedFrameworkOrTool}
connectionStringPooler={{
transactionShared: connectionStringsShared.pooler.uri,
sessionShared: connectionStringsShared.pooler.uri.replace('6543', '5432'),
transactionDedicated: connectionStringsDedicated?.pooler.uri,
sessionDedicated: connectionStringsDedicated?.pooler.uri.replace('6543', '5432'),
ipv4SupportedForDedicatedPooler: !!ipv4Addon,
direct: connectionStringsShared.direct.uri,
}}
onCopy={handleCopy}
/>
</CopyCallbackContext.Provider>
</div>
)
}
Expand Down
Loading
Loading