diff --git a/apps/docs/features/ui/McpConfigPanel.tsx b/apps/docs/features/ui/McpConfigPanel.tsx index 629a15537b05f..fa954c0c8a081 100644 --- a/apps/docs/features/ui/McpConfigPanel.tsx +++ b/apps/docs/features/ui/McpConfigPanel.tsx @@ -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'] @@ -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(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 ( <>
@@ -288,6 +335,9 @@ export function McpConfigPanel() { projectRef={project?.ref} theme={theme as 'light' | 'dark'} isPlatform={isPlatform} + onCopyCallback={handleCopy} + onInstallCallback={handleInstall} + onClientSelect={setSelectedClient} />
{isPlatform && ( diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 4db31f999f8ab..dce66077a6be6 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -135,6 +135,7 @@ Rodrigo Mansueli Ronan Lehane Rory Wilding Ryan Goulet +Ruan Maia Sam Meech-Ward Sam Rome Sam Rose diff --git a/apps/studio/components/interfaces/App/CommandMenu/CommandMenu.tsx b/apps/studio/components/interfaces/App/CommandMenu/CommandMenu.tsx index a52b21a92f6b2..057d3b7d70dfb 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/CommandMenu.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/CommandMenu.tsx @@ -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, @@ -20,6 +21,7 @@ import { orderCommandSectionsByPriority } from './ordering' export default function StudioCommandMenu() { useApiKeysCommands() useApiUrlCommand() + useConnectCommands() useProjectLevelTableEditorCommands() useProjectSwitchCommand() useConfigureOrganizationCommand() diff --git a/apps/studio/components/interfaces/Billing/InvoiceStatusBadge.tsx b/apps/studio/components/interfaces/Billing/InvoiceStatusBadge.tsx index 2fe2dd3c9ee76..1360bca87e6ce 100644 --- a/apps/studio/components/interfaces/Billing/InvoiceStatusBadge.tsx +++ b/apps/studio/components/interfaces/Billing/InvoiceStatusBadge.tsx @@ -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['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 @@ -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['variant'], + } + : invoiceStatusMapping[status] return ( @@ -53,9 +60,25 @@ const InvoiceStatusBadge = ({ status, paymentAttempted }: InvoiceStatusBadgeProp {statusMapping?.label || status} - + {[InvoiceStatus.OPEN, InvoiceStatus.ISSUED, InvoiceStatus.UNCOLLECTIBLE].includes(status) && - (paymentAttempted ? ( + (paymentProcessing ? ( +
+

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

+ +

+ If you run into this, we recommend{' '} + + topping up your credits + {' '} + in advance to avoid running into this in the future. +

+
+ ) : paymentAttempted ? (

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 @@ -69,12 +92,6 @@ const InvoiceStatusBadge = ({ status, paymentAttempted }: InvoiceStatusBadgeProp

))} - {status === InvoiceStatus.DRAFT && ( -

- The invoice will soon be finalized and charged for. -

- )} - {status === InvoiceStatus.PAID && (

The invoice has been paid successfully. No action is required on your side. diff --git a/apps/studio/components/interfaces/Billing/Invoices.types.ts b/apps/studio/components/interfaces/Billing/Invoices.types.ts index 64e1d3555d2dc..b3734b0307809 100644 --- a/apps/studio/components/interfaces/Billing/Invoices.types.ts +++ b/apps/studio/components/interfaces/Billing/Invoices.types.ts @@ -1,5 +1,4 @@ export enum InvoiceStatus { - DRAFT = 'draft', PAID = 'paid', VOID = 'void', UNCOLLECTIBLE = 'uncollectible', diff --git a/apps/studio/components/interfaces/Connect/Connect.Commands.tsx b/apps/studio/components/interfaces/Connect/Connect.Commands.tsx new file mode 100644 index 0000000000000..6a370317080cf --- /dev/null +++ b/apps/studio/components/interfaces/Connect/Connect.Commands.tsx @@ -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: () => , + }, + { + 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: () => , + }, + ] as ICommand[], + { + enabled: !!selectedProject && isActiveHealthy, + orderSection: orderCommandSectionsByPriority, + sectionMeta: { priority: 2 }, + } + ) +} diff --git a/apps/studio/components/interfaces/Connect/Connect.tsx b/apps/studio/components/interfaces/Connect/Connect.tsx index 86c648b974607..3a3eb2ae7cb7d 100644 --- a/apps/studio/components/interfaces/Connect/Connect.tsx +++ b/apps/studio/components/interfaces/Connect/Connect.tsx @@ -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 ( { void } diff --git a/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx b/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx index 3667d93dd4838..59dce1062d612 100644 --- a/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx +++ b/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx @@ -9,7 +9,8 @@ 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' @@ -17,6 +18,8 @@ import { getConnectionStrings } from './DatabaseSettings.utils' interface ConnectContentTabProps extends HTMLAttributes { projectKeys: projectKeys filePath: string + connectionTab: 'App Frameworks' | 'Mobile Frameworks' | 'ORMs' + selectedFrameworkOrTool: string connectionStringPooler?: { transactionShared: string sessionShared: string @@ -28,11 +31,32 @@ interface ConnectContentTabProps extends HTMLAttributes { } export const ConnectTabContent = forwardRef( - ({ 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 }) @@ -84,18 +108,23 @@ export const ConnectTabContent = forwardRef - + + + ) } diff --git a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx index 72ad757d9a824..31b71135184a6 100644 --- a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx +++ b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx @@ -209,7 +209,12 @@ export const DatabaseConnectionString = () => { const lang = connectionInfo?.lang ?? 'Unknown' sendEvent({ action: 'connection_string_copied', - properties: { connectionType, lang, connectionMethod: connectionStringMethod }, + properties: { + connectionType, + lang, + connectionMethod: connectionStringMethod, + connectionTab: 'Connection String', + }, groups: { project: projectRef ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) } diff --git a/apps/studio/components/interfaces/Connect/McpTabContent.tsx b/apps/studio/components/interfaces/Connect/McpTabContent.tsx index bbb9beeb64024..941e2e3e8c9cf 100644 --- a/apps/studio/components/interfaces/Connect/McpTabContent.tsx +++ b/apps/studio/components/interfaces/Connect/McpTabContent.tsx @@ -2,8 +2,10 @@ import { IS_PLATFORM, useParams } from 'common' import Panel from 'components/ui/Panel' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { BASE_PATH } from 'lib/constants' +import { useTrack } from 'lib/telemetry/track' import { useTheme } from 'next-themes' -import { McpConfigPanel } from 'ui-patterns/McpUrlBuilder' +import { useState } from 'react' +import { McpConfigPanel, type McpClient } from 'ui-patterns/McpUrlBuilder' import type { projectKeys } from './Connect.types' export const McpTabContent = ({ projectKeys }: { projectKeys: projectKeys }) => { @@ -37,6 +39,40 @@ const McpTabContentInnerLoaded = ({ projectKeys: projectKeys }) => { const { resolvedTheme } = useTheme() + const track = useTrack() + const [selectedClient, setSelectedClient] = useState(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 + } + + track('connection_string_copied', { + connectionTab: 'MCP', + selectedItem: selectedClient?.label, + connectionType, + source: 'studio', + }) + } + + const handleInstall = () => { + if (selectedClient?.label) { + track('mcp_install_button_clicked', { + client: selectedClient.label, + source: 'studio', + }) + } + } return ( ) } diff --git a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx index 0eaea6050a5ea..ca876d5639e3e 100644 --- a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx +++ b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx @@ -147,11 +147,13 @@ export const InvoicesSettings = () => {

{x.amount_due > 0 && + !x.payment_is_processing && [ InvoiceStatus.UNCOLLECTIBLE, InvoiceStatus.OPEN, diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index 81755a605d59e..e4413322a9876 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -6882,6 +6882,7 @@ export interface components { invoice_pdf: string number: string payment_attempted: boolean + payment_is_processing: boolean period_end: number status: string subscription: string | null diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 335e300108d96..767ce07df87bd 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -69,17 +69,53 @@ export interface ConnectionStringCopiedEvent { action: 'connection_string_copied' properties: { /** - * Method selected by user, e.g. URI, PSQL, SQLAlchemy, etc. + * Method selected by user, e.g. URI, PSQL, SQLAlchemy, MCP URL, Framework snippet, Command Line, JSON, etc. + * Required for Connection String, App Frameworks, and Mobile Frameworks tabs */ - connectionType: string + connectionType?: string /** - * Language of the code block if selected, e.g. bash, go + * Language of the code block if selected, e.g. bash, go, http, typescript + * Required for Connection String, App Frameworks, and Mobile Frameworks tabs */ - lang: string + lang?: string /** * Connection Method, e.g. direct, transaction_pooler, session_pooler + * Only used for Connection String tab */ - connectionMethod: 'direct' | 'transaction_pooler' | 'session_pooler' + connectionMethod?: 'direct' | 'transaction_pooler' | 'session_pooler' + /** + * Tab from which the connection string was copied + */ + connectionTab: 'Connection String' | 'App Frameworks' | 'Mobile Frameworks' | 'ORMs' | 'MCP' + /** + * Selected framework, tool, or client (e.g., 'Next.js', 'Prisma', 'Cursor') + */ + selectedItem?: string + /** + * Source of the event, either 'studio' or 'docs' + */ + source?: 'studio' | 'docs' + } + groups: TelemetryGroups +} + +/** + * User clicked the MCP install button (one-click installation for Cursor or VS Code). + * + * @group Events + * @source studio, docs + */ +export interface McpInstallButtonClickedEvent { + action: 'mcp_install_button_clicked' + properties: { + /** + * The MCP client that was selected (e.g., 'Cursor', 'VS Code') + */ + client: string + /** + * Source of the event, either 'studio' or 'docs' + */ + source?: 'studio' | 'docs' } groups: TelemetryGroups } @@ -2320,6 +2356,7 @@ export type TelemetryEvent = | SignUpEvent | SignInEvent | ConnectionStringCopiedEvent + | McpInstallButtonClickedEvent | ApiDocsOpenedEvent | ApiDocsCodeCopyButtonClickedEvent | CronJobCreatedEvent diff --git a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx index c6463094a940e..11ab2f94d1373 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/McpConfigPanel.tsx @@ -17,6 +17,8 @@ export interface McpConfigPanelProps { projectRef?: string initialSelectedClient?: McpClient onClientSelect?: (client: McpClient) => void + onCopyCallback?: (type?: 'url' | 'json' | 'command') => void + onInstallCallback?: () => void theme?: 'light' | 'dark' className?: string isPlatform: boolean // For docs this is controlled by state, for studio by environment variable @@ -28,6 +30,8 @@ export function McpConfigPanel({ projectRef, initialSelectedClient, onClientSelect, + onCopyCallback, + onInstallCallback, className, theme = 'dark', isPlatform, @@ -93,6 +97,7 @@ export function McpConfigPanel({ hideLineNumbers language="http" className="max-h-64 overflow-y-auto" + onCopyCallback={() => onCopyCallback?.('url')} > {mcpUrl} @@ -122,6 +127,8 @@ export function McpConfigPanel({ basePath={basePath} selectedClient={selectedClient} clientConfig={clientConfig} + onCopyCallback={onCopyCallback} + onInstallCallback={onInstallCallback} />
diff --git a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx index da996a49ad115..81a39fba1b408 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationDisplay.tsx @@ -13,6 +13,8 @@ interface McpConfigurationDisplayProps { className?: string theme?: 'light' | 'dark' basePath: string + onCopyCallback?: (type?: 'url' | 'json' | 'command') => void + onInstallCallback?: () => void } export function McpConfigurationDisplay({ @@ -21,6 +23,8 @@ export function McpConfigurationDisplay({ className, theme = 'dark', basePath, + onCopyCallback, + onInstallCallback, }: McpConfigurationDisplayProps) { const mcpButtonData = getMcpButtonData({ basePath, @@ -40,6 +44,7 @@ export function McpConfigurationDisplay({ target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 [&>span]:flex [&>span]:items-center [&>span]:gap-2" + onClick={onInstallCallback} > )} - {selectedClient.primaryInstructions && selectedClient.primaryInstructions(clientConfig)} + {selectedClient.primaryInstructions && + selectedClient.primaryInstructions(clientConfig, onCopyCallback)} {selectedClient.configFile && (
@@ -73,9 +79,11 @@ export function McpConfigurationDisplay({ language="json" className="max-h-64 overflow-y-auto" focusable={false} + onCopyCallback={() => onCopyCallback?.('json')} /> - {selectedClient.alternateInstructions && selectedClient.alternateInstructions(clientConfig)} + {selectedClient.alternateInstructions && + selectedClient.alternateInstructions(clientConfig, onCopyCallback)} {(selectedClient.docsUrl || selectedClient.externalDocsUrl) && (
diff --git a/packages/ui-patterns/src/McpUrlBuilder/constants.tsx b/packages/ui-patterns/src/McpUrlBuilder/constants.tsx index e507b6266c6b0..1dae9d1bc0668 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/constants.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/constants.tsx @@ -108,7 +108,7 @@ export const MCP_CLIENTS: McpClient[] = [ }, } }, - alternateInstructions: () => ( + alternateInstructions: (_config, _onCopy) => (

Windsurf does not currently support remote MCP servers over HTTP transport. You need to use the mcp-remote package as a proxy. @@ -131,7 +131,7 @@ export const MCP_CLIENTS: McpClient[] = [ }, } }, - primaryInstructions: (_config) => { + primaryInstructions: (_config, onCopy) => { const config = _config as ClaudeCodeMcpConfig const command = `claude mcp add --scope project --transport http supabase "${config.mcpServers.supabase.url}"` return ( @@ -146,17 +146,24 @@ export const MCP_CLIENTS: McpClient[] = [ // This is a no-op but the CodeBlock component is designed to output // inline code if no className is given className="block" + onCopyCallback={() => onCopy?.('command')} />

) }, - alternateInstructions: () => ( + alternateInstructions: (_config, onCopy) => (

After configuring the MCP server, you need to authenticate. In a regular terminal (not the IDE extension) run:

- + onCopy?.('command')} + />

Select the "supabase" server, then "Authenticate" to begin the authentication flow.

diff --git a/packages/ui-patterns/src/McpUrlBuilder/types.ts b/packages/ui-patterns/src/McpUrlBuilder/types.ts index bf24a7ddc8c86..02f867a600294 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/types.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/types.ts @@ -13,8 +13,14 @@ export interface McpClient { configFile?: string generateDeepLink?: (config: McpClientConfig) => string | null transformConfig?: (config: McpClientBaseConfig) => McpClientConfig - primaryInstructions?: (config: McpClientConfig) => React.ReactNode - alternateInstructions?: (config: McpClientConfig) => React.ReactNode + primaryInstructions?: ( + config: McpClientConfig, + onCopy?: (type?: 'url' | 'json' | 'command') => void + ) => React.ReactNode + alternateInstructions?: ( + config: McpClientConfig, + onCopy?: (type?: 'url' | 'json' | 'command') => void + ) => React.ReactNode } export interface McpUrlBuilderConfig { diff --git a/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx b/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx index 109a9590dbfa7..154715866c10e 100644 --- a/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx +++ b/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx @@ -8,12 +8,15 @@ import { useTheme } from 'next-themes' import { Highlight, Language, Prism, themes } from 'prism-react-renderer' -import { PropsWithChildren, useEffect, useRef, useState } from 'react' +import { PropsWithChildren, createContext, useContext, useEffect, useRef, useState } from 'react' import { copyToClipboard } from '../../lib/utils' import { cn } from './../../lib/utils/cn' import { Button } from './../Button' import { dart } from './prism' +// Context for copy callback - can be provided by parent components +export const CopyCallbackContext = createContext<(() => void) | undefined>(undefined) + dart(Prism) const prism = { @@ -41,6 +44,7 @@ export const SimpleCodeBlock = ({ const { resolvedTheme } = useTheme() const [showCopied, setShowCopied] = useState(false) const target = useRef(null) + const contextOnCopy = useContext(CopyCallbackContext) let highlightLines: any = [] useEffect(() => { @@ -62,7 +66,9 @@ export const SimpleCodeBlock = ({ copyToClipboard(code) } setShowCopied(true) - onCopy?.() + // Use prop onCopy if provided, otherwise fall back to context + const copyCallback = onCopy || contextOnCopy + copyCallback?.() } return (