diff --git a/.github/workflows/update-js-libs.yml b/.github/workflows/update-js-libs.yml
index aa41efd4db685..fb5e6f6705988 100644
--- a/.github/workflows/update-js-libs.yml
+++ b/.github/workflows/update-js-libs.yml
@@ -48,6 +48,9 @@ jobs:
# Update @supabase/realtime-js
sed -i "s/'@supabase\/realtime-js': .*/'@supabase\/realtime-js': ${{ github.event.inputs.version }}/" pnpm-workspace.yaml
+ # Update @supabase/postgrest-js
+ sed -i "s/'@supabase\/postgrest-js': .*/'@supabase\/postgrest-js': ${{ github.event.inputs.version }}/" pnpm-workspace.yaml
+
echo "Updated pnpm-workspace.yaml:"
cat pnpm-workspace.yaml
@@ -69,6 +72,7 @@ jobs:
- Updated @supabase/supabase-js to ${{ github.event.inputs.version }}
- Updated @supabase/auth-js to ${{ github.event.inputs.version }}
- Updated @supabase/realtime-js to ${{ github.event.inputs.version }}
+ - Updated @supabase/postgest-js to ${{ github.event.inputs.version }}
- Refreshed pnpm-lock.yaml
This PR was created automatically.
diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx
index b74d5b6c45a8c..c56f34d0fe9dd 100644
--- a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx
+++ b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx
@@ -9,7 +9,7 @@ import { memo, useState } from 'react'
import { useIsLoggedIn, useIsUserLoading, useUser } from 'common'
import { isFeatureEnabled } from 'common/enabled-features'
import { Button, buttonVariants, cn } from 'ui'
-import { AuthenticatedDropdownMenu, CommandMenuTrigger } from 'ui-patterns'
+import { AuthenticatedDropdownMenu, CommandMenuTriggerInput } from 'ui-patterns'
import { getCustomContent } from '../../../lib/custom-content/getCustomContent'
import GlobalNavigationMenu from './GlobalNavigationMenu'
import useDropdownMenu from './useDropdownMenu'
@@ -43,37 +43,14 @@ const TopNavBar: FC = () => {
-
-
-
+
+ Search
+ docs...
+ >
+ }
+ />
-
-
+
+
{x.argument_types || '-'}
- {x.return_type}
+ {x.return_type === 'trigger' ? (
+
+ {x.return_type}
+
+ ) : (
+
+ {x.return_type}
+
+ )}
- {x.security_definer ? 'Definer' : 'Invoker'}
+
+ {x.security_definer ? 'Definer' : 'Invoker'}
+
{!isLocked && (
diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx
index 5d8476f3998f8..7dbaaaae4bdc6 100644
--- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx
+++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx
@@ -3,6 +3,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop } from 'lodash'
import { Search } from 'lucide-react'
import { useRouter } from 'next/router'
+import { parseAsJson, useQueryState } from 'nuqs'
import { useParams } from 'common'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
@@ -27,6 +28,10 @@ import {
TableHeader,
TableRow,
} from 'ui'
+import {
+ ReportsSelectFilter,
+ selectFilterSchema,
+} from 'components/interfaces/Reports/v2/ReportsSelectFilter'
import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning'
import FunctionList from './FunctionList'
@@ -51,6 +56,16 @@ const FunctionsList = ({
const filterString = search ?? ''
+ // Filters
+ const [returnTypeFilter, setReturnTypeFilter] = useQueryState(
+ 'return_type',
+ parseAsJson(selectFilterSchema.parse)
+ )
+ const [securityFilter, setSecurityFilter] = useQueryState(
+ 'security',
+ parseAsJson(selectFilterSchema.parse)
+ )
+
const setFilterString = (str: string) => {
const url = new URL(document.URL)
if (str === '') {
@@ -84,6 +99,18 @@ const FunctionsList = ({
connectionString: project?.connectionString,
})
+ // Get unique return types from functions in the selected schema
+ const schemaFunctions = (functions ?? []).filter((fn) => fn.schema === selectedSchema)
+ const uniqueReturnTypes = Array.from(new Set(schemaFunctions.map((fn) => fn.return_type))).sort()
+
+ // Get security options based on what exists in the selected schema
+ const hasDefiner = schemaFunctions.some((fn) => fn.security_definer)
+ const hasInvoker = schemaFunctions.some((fn) => !fn.security_definer)
+ const securityOptions = [
+ ...(hasDefiner ? [{ label: 'Definer', value: 'definer' }] : []),
+ ...(hasInvoker ? [{ label: 'Invoker', value: 'invoker' }] : []),
+ ]
+
if (isLoading) return
if (isError) return
@@ -132,6 +159,22 @@ const FunctionsList = ({
className="w-full lg:w-52"
onChange={(e) => setFilterString(e.target.value)}
/>
+ ({
+ label: type,
+ value: type,
+ }))}
+ value={returnTypeFilter ?? []}
+ onChange={setReturnTypeFilter}
+ showSearch
+ />
+
@@ -201,6 +244,8 @@ const FunctionsList = ({
schema={selectedSchema}
filterString={filterString}
isLocked={isSchemaLocked}
+ returnTypeFilter={returnTypeFilter ?? []}
+ securityFilter={securityFilter ?? []}
duplicateFunction={duplicateFunction}
editFunction={editFunction}
deleteFunction={deleteFunction}
diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx
index bb2fede7e02d0..9e0862014fea8 100644
--- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx
+++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx
@@ -44,8 +44,10 @@ const TriggerList = ({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
- const filteredTriggers = (triggers ?? []).filter((x) =>
- includes(x.name.toLowerCase(), filterString.toLowerCase())
+ const filteredTriggers = (triggers ?? []).filter(
+ (x) =>
+ includes(x.name.toLowerCase(), filterString.toLowerCase()) ||
+ (x.function_name && includes(x.function_name.toLowerCase(), filterString.toLowerCase()))
)
const _triggers = sortBy(
diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx
index 9160a211ca5d4..30b651ba93887 100644
--- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx
+++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx
@@ -2,7 +2,7 @@ import { PostgresTrigger } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop } from 'lodash'
import { DatabaseZap, FunctionSquare, Plus, Search, Shield } from 'lucide-react'
-import { useState } from 'react'
+import { parseAsString, useQueryState } from 'nuqs'
import AlphaPreview from 'components/to-be-cleaned/AlphaPreview'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
@@ -48,7 +48,11 @@ const TriggersList = ({
const { data: project } = useSelectedProjectQuery()
const aiSnap = useAiAssistantStateSnapshot()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
- const [filterString, setFilterString] = useState
('')
+
+ const [filterString, setFilterString] = useQueryState(
+ 'search',
+ parseAsString.withDefault('').withOptions({ history: 'replace', clearOnDefault: true })
+ )
const { data: protectedSchemas } = useProtectedSchemas()
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts
index 1c2da89ce34b9..2db5478777f72 100644
--- a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts
+++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts
@@ -12,7 +12,14 @@ export function getAvailableRegions(cloudProvider: CloudProvider): Region {
case 'AWS_K8S':
return AWS_REGIONS
case 'AWS_NIMBUS':
- // Only allow US East for Nimbus
+ if (process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod') {
+ // Only allow Southeast Asia for Nimbus (local/staging)
+ return {
+ SOUTHEAST_ASIA: AWS_REGIONS.SOUTHEAST_ASIA,
+ }
+ }
+
+ // Only allow US East for Nimbus (prod)
return {
EAST_US: AWS_REGIONS.EAST_US,
}
diff --git a/apps/studio/components/interfaces/UserDropdown.tsx b/apps/studio/components/interfaces/UserDropdown.tsx
index 1fe53c346dc1e..02d3d4a046302 100644
--- a/apps/studio/components/interfaces/UserDropdown.tsx
+++ b/apps/studio/components/interfaces/UserDropdown.tsx
@@ -23,7 +23,7 @@ import {
Theme,
singleThemes,
} from 'ui'
-import { useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
+import { useCommandMenuOpenedTelemetry, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
import { useFeaturePreviewModal } from './App/FeaturePreview/FeaturePreviewContext'
export function UserDropdown() {
@@ -35,8 +35,14 @@ export function UserDropdown() {
const signOut = useSignOut()
const setCommandMenuOpen = useSetCommandMenuOpen()
+ const sendTelemetry = useCommandMenuOpenedTelemetry()
const { openFeaturePreviewModal } = useFeaturePreviewModal()
+ const handleCommandMenuOpen = () => {
+ setCommandMenuOpen(true)
+ sendTelemetry()
+ }
+
return (
@@ -97,7 +103,7 @@ export function UserDropdown() {
Feature previews
- setCommandMenuOpen(true)}>
+
Command menu
diff --git a/apps/studio/data/subscriptions/org-subscription-confirm-pending-change.ts b/apps/studio/data/subscriptions/org-subscription-confirm-pending-change.ts
index fc660901b76c1..7aad279c93cce 100644
--- a/apps/studio/data/subscriptions/org-subscription-confirm-pending-change.ts
+++ b/apps/studio/data/subscriptions/org-subscription-confirm-pending-change.ts
@@ -3,6 +3,7 @@ import { toast } from 'sonner'
import { handleError, post } from 'data/fetchers'
import type { ResponseError } from 'types'
+import type { components } from 'api-types'
import { organizationKeys } from 'data/organizations/keys'
import { subscriptionKeys } from './keys'
import { usageKeys } from 'data/usage/keys'
@@ -63,6 +64,17 @@ export const useConfirmPendingSubscriptionChangeMutation = ({
async onSuccess(data, variables, context) {
const { slug } = variables
+ // Handle 202 Accepted - show toast and skip query invalidation
+ // The 200 success response returns void, so if data exists it must be 202
+ if (data && 'message' in data) {
+ const pendingResponse = data as components['schemas']['PendingConfirmationResponse']
+ toast.success(pendingResponse.message, {
+ dismissible: true,
+ duration: 10_000,
+ })
+ return
+ }
+
// [Kevin] Backend can return stale data as it's waiting for the Stripe-sync to complete. Until that's solved in the backend
// we are going back to monkey here and delay the invalidation
await new Promise((resolve) => setTimeout(resolve, 2000))
diff --git a/apps/studio/data/subscriptions/org-subscription-confirm-pending-create.ts b/apps/studio/data/subscriptions/org-subscription-confirm-pending-create.ts
index 8d60276bb9447..dc41f110121ec 100644
--- a/apps/studio/data/subscriptions/org-subscription-confirm-pending-create.ts
+++ b/apps/studio/data/subscriptions/org-subscription-confirm-pending-create.ts
@@ -3,10 +3,10 @@ import { toast } from 'sonner'
import { handleError, post } from 'data/fetchers'
import type { ResponseError } from 'types'
+import type { components } from 'api-types'
import { organizationKeys } from 'data/organizations/keys'
import { permissionKeys } from 'data/permissions/keys'
import { castOrganizationResponseToOrganization } from 'data/organizations/organizations-query'
-import type { components } from 'api-types'
export type PendingSubscriptionCreateVariables = {
payment_intent_id: string
@@ -56,6 +56,16 @@ export const useConfirmPendingSubscriptionCreateMutation = ({
PendingSubscriptionCreateVariables
>((vars) => confirmPendingSubscriptionCreate(vars), {
async onSuccess(data, variables, context) {
+ // Handle 202 Accepted - show toast and skip query updates
+ if (data && 'message' in data && !('slug' in data)) {
+ const pendingResponse = data as components['schemas']['PendingConfirmationResponse']
+ toast.success(pendingResponse.message, {
+ dismissible: true,
+ duration: 10_000,
+ })
+ return
+ }
+
// [Joshen] We're manually updating the query client here as the org's subscription is
// created async, and the invalidation will happen too quick where the GET organizations
// endpoint will error out with a 500 since the subscription isn't created yet.
diff --git a/apps/studio/hooks/misc/useStudioCommandMenuTelemetry.ts b/apps/studio/hooks/misc/useStudioCommandMenuTelemetry.ts
new file mode 100644
index 0000000000000..925c9c600a861
--- /dev/null
+++ b/apps/studio/hooks/misc/useStudioCommandMenuTelemetry.ts
@@ -0,0 +1,40 @@
+import { useCallback } from 'react'
+
+import { useParams } from 'common'
+import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
+import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
+import type {
+ CommandMenuOpenedEvent,
+ CommandMenuCommandSelectedEvent,
+ CommandMenuSearchSubmittedEvent,
+} from 'common/telemetry-constants'
+
+export function useStudioCommandMenuTelemetry() {
+ const { ref: projectRef } = useParams()
+ const { data: organization } = useSelectedOrganizationQuery()
+ const { mutate: sendEvent } = useSendEventMutation()
+
+ const onTelemetry = useCallback(
+ (
+ event:
+ | CommandMenuOpenedEvent
+ | CommandMenuCommandSelectedEvent
+ | CommandMenuSearchSubmittedEvent
+ ) => {
+ // Add studio-specific groups (project and organization)
+ const eventWithGroups = {
+ ...event,
+ groups: {
+ ...event.groups,
+ ...(projectRef && { project: projectRef }),
+ ...(organization?.slug && { organization: organization.slug }),
+ },
+ }
+
+ sendEvent(eventWithGroups)
+ },
+ [projectRef, organization?.slug, sendEvent]
+ )
+
+ return { onTelemetry }
+}
diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx
index fb4ae48c14c00..f6816f5ed7968 100644
--- a/apps/studio/pages/_app.tsx
+++ b/apps/studio/pages/_app.tsx
@@ -30,13 +30,12 @@ import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import Head from 'next/head'
import { NuqsAdapter } from 'nuqs/adapters/next/pages'
-import { ErrorInfo, PropsWithChildren, useCallback } from 'react'
+import { ErrorInfo, useCallback } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import {
FeatureFlagProvider,
getFlags,
- LOCAL_STORAGE_KEYS,
TelemetryTagManager,
ThemeProvider,
useThemeSandbox,
@@ -52,14 +51,13 @@ import { GlobalErrorBoundaryState } from 'components/ui/ErrorBoundary/GlobalErro
import { useRootQueryClient } from 'data/query-client'
import { customFont, sourceCodePro } from 'fonts'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
-import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { AuthProvider } from 'lib/auth'
import { API_URL, BASE_PATH, IS_PLATFORM, useDefaultProvider } from 'lib/constants'
import { ProfileProvider } from 'lib/profile'
import { Telemetry } from 'lib/telemetry'
import { AppPropsWithLayout } from 'types'
import { SonnerToaster, TooltipProvider } from 'ui'
-import { CommandProvider } from 'ui-patterns/CommandMenu'
+import { StudioCommandProvider as CommandProvider } from 'components/interfaces/App/CommandMenu/StudioCommandProvider'
dayjs.extend(customParseFormat)
dayjs.extend(utc)
@@ -80,15 +78,6 @@ loader.config({
},
})
-const CommandProviderWithPreferences = ({ children }: PropsWithChildren) => {
- const [commandMenuHotkeyEnabled] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.HOTKEY_COMMAND_MENU,
- true
- )
-
- return {children}
-}
-
// [Joshen TODO] Once we settle on the new nav layout - we'll need a lot of clean up in terms of our layout components
// a lot of them are unnecessary and introduce way too many cluttered CSS especially with the height styles that make
// debugging way too difficult. Ideal scenario is we just have one AppLayout to control the height and scroll areas of
@@ -161,7 +150,7 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
disableTransitionOnChange
>
-
+
{getLayout()}
@@ -169,7 +158,7 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
-
+
diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json
index 05c09a053f4b6..50d4e479fb592 100644
--- a/apps/ui-library/package.json
+++ b/apps/ui-library/package.json
@@ -47,7 +47,7 @@
"@radix-ui/react-toggle-group": "*",
"@radix-ui/react-tooltip": "*",
"@react-router/fs-routes": "^7.4.0",
- "@supabase/postgrest-js": "*",
+ "@supabase/postgrest-js": "catalog:",
"@supabase/supa-mdx-lint": "0.2.6-alpha",
"@tanstack/react-query": "^5.83.0",
"@supabase/vue-blocks": "workspace:*",
diff --git a/apps/ui-library/registry/default/blocks/infinite-query-hook/hooks/use-infinite-query.ts b/apps/ui-library/registry/default/blocks/infinite-query-hook/hooks/use-infinite-query.ts
index 93a8fe00f8b72..8ca64107484a0 100644
--- a/apps/ui-library/registry/default/blocks/infinite-query-hook/hooks/use-infinite-query.ts
+++ b/apps/ui-library/registry/default/blocks/infinite-query-hook/hooks/use-infinite-query.ts
@@ -1,7 +1,7 @@
'use client'
import { createClient } from '@/registry/default/fixtures/lib/supabase/client'
-import { PostgrestQueryBuilder } from '@supabase/postgrest-js'
+import { PostgrestQueryBuilder, type PostgrestClientOptions } from '@supabase/postgrest-js'
import { type SupabaseClient } from '@supabase/supabase-js'
import { useEffect, useRef, useSyncExternalStore } from 'react'
@@ -44,8 +44,16 @@ type SupabaseTableName = keyof DatabaseSchema['Tables']
// Extracts the table definition from the database type
type SupabaseTableData = DatabaseSchema['Tables'][T]['Row']
+// Default client options for PostgrestQueryBuilder
+type DefaultClientOptions = PostgrestClientOptions
+
type SupabaseSelectBuilder = ReturnType<
- PostgrestQueryBuilder['select']
+ PostgrestQueryBuilder<
+ DefaultClientOptions,
+ DatabaseSchema,
+ DatabaseSchema['Tables'][T],
+ T
+ >['select']
>
// A function that modifies the query. Can be used to sort, filter, etc. If .range is used, it will be overwritten.
diff --git a/apps/www/hooks/useWwwCommandMenuTelemetry.ts b/apps/www/hooks/useWwwCommandMenuTelemetry.ts
new file mode 100644
index 0000000000000..cf7a6037efff7
--- /dev/null
+++ b/apps/www/hooks/useWwwCommandMenuTelemetry.ts
@@ -0,0 +1,26 @@
+import { useCallback } from 'react'
+
+import type {
+ CommandMenuOpenedEvent,
+ CommandMenuCommandSelectedEvent,
+ CommandMenuSearchSubmittedEvent,
+} from 'common/telemetry-constants'
+import { useSendTelemetryEvent } from 'lib/telemetry'
+
+export function useWwwCommandMenuTelemetry() {
+ const sendTelemetryEvent = useSendTelemetryEvent()
+
+ const onTelemetry = useCallback(
+ (
+ event:
+ | CommandMenuOpenedEvent
+ | CommandMenuCommandSelectedEvent
+ | CommandMenuSearchSubmittedEvent
+ ) => {
+ sendTelemetryEvent(event)
+ },
+ [sendTelemetryEvent]
+ )
+
+ return { onTelemetry }
+}
diff --git a/apps/www/lib/telemetry.ts b/apps/www/lib/telemetry.ts
index 0e781bd5eed0a..686ed8482fe92 100644
--- a/apps/www/lib/telemetry.ts
+++ b/apps/www/lib/telemetry.ts
@@ -4,16 +4,20 @@ import { sendTelemetryEvent } from 'common'
import type { TelemetryEvent } from 'common/telemetry-constants'
import { API_URL } from 'lib/constants'
import { usePathname, useSearchParams } from 'next/navigation'
+import { useCallback } from 'react'
export function useSendTelemetryEvent() {
const pathname = usePathname()
const searchParams = useSearchParams()
- return (event: TelemetryEvent) => {
- const url = new URL(API_URL ?? 'http://localhost:3000')
- url.pathname = pathname ?? ''
- url.search = searchParams?.toString() ?? ''
+ return useCallback(
+ (event: TelemetryEvent) => {
+ const url = new URL(API_URL ?? 'http://localhost:3000')
+ url.pathname = pathname ?? ''
+ url.search = searchParams?.toString() ?? ''
- return sendTelemetryEvent(API_URL, event, url.toString())
- }
+ return sendTelemetryEvent(API_URL, event, url.toString())
+ },
+ [pathname, searchParams]
+ )
}
diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx
index f839f5cbd4042..55a845f09b0bb 100644
--- a/apps/www/pages/_app.tsx
+++ b/apps/www/pages/_app.tsx
@@ -12,7 +12,7 @@ import {
useThemeSandbox,
} from 'common'
import { DefaultSeo } from 'next-seo'
-import { AppProps } from 'next/app'
+import type { AppProps } from 'next/app'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { SonnerToaster, themes, TooltipProvider } from 'ui'
@@ -26,10 +26,12 @@ import MetaFaviconsPagesRouter, {
import { WwwCommandMenu } from '~/components/CommandMenu'
import { API_URL, APP_NAME, DEFAULT_META_DESCRIPTION } from '~/lib/constants'
import useDarkLaunchWeeks from '../hooks/useDarkLaunchWeeks'
+import { useWwwCommandMenuTelemetry } from '../hooks/useWwwCommandMenuTelemetry'
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter()
const { hasAcceptedConsent } = useConsentToast()
+ const { onTelemetry } = useWwwCommandMenuTelemetry()
useThemeSandbox()
@@ -43,7 +45,7 @@ export default function App({ Component, pageProps }: AppProps) {
let faviconRoute = DEFAULT_FAVICON_ROUTE
let themeColor = DEFAULT_FAVICON_THEME_COLOR
- if (router.asPath && router.asPath.includes('/launch-week/x')) {
+ if (router.asPath?.includes('/launch-week/x')) {
applicationName = 'Supabase LWX'
faviconRoute = 'images/launchweek/lwx/favicon'
themeColor = 'FFFFFF'
@@ -95,7 +97,7 @@ export default function App({ Component, pageProps }: AppProps) {
forcedTheme={forceDarkMode ? 'dark' : undefined}
>
-
+
diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts
index 1dd23516f7797..d8a7dbb1c9009 100644
--- a/packages/api-types/types/platform.d.ts
+++ b/packages/api-types/types/platform.d.ts
@@ -7612,6 +7612,9 @@ export interface components {
}[]
defaultPaymentMethodId: string | null
}
+ PendingConfirmationResponse: {
+ message: string
+ }
PgbouncerConfigResponse: {
connection_string: string
db_dns_name: string
@@ -13831,6 +13834,14 @@ export interface operations {
}
content?: never
}
+ 202: {
+ headers: {
+ [name: string]: unknown
+ }
+ content: {
+ 'application/json': components['schemas']['PendingConfirmationResponse']
+ }
+ }
/** @description Unauthorized */
401: {
headers: {
@@ -16169,6 +16180,14 @@ export interface operations {
'application/json': components['schemas']['CreateOrganizationResponse']
}
}
+ 202: {
+ headers: {
+ [name: string]: unknown
+ }
+ content: {
+ 'application/json': components['schemas']['PendingConfirmationResponse']
+ }
+ }
/** @description Failed to confirm subscription changes */
500: {
headers: {
diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts
index 5d699a3d4c884..0d370b71ea45d 100644
--- a/packages/common/telemetry-constants.ts
+++ b/packages/common/telemetry-constants.ts
@@ -1949,6 +1949,78 @@ export interface AuthUsersSearchSubmittedEvent {
groups: TelemetryGroups
}
+/**
+ * User opened the command menu.
+ *
+ * @group Events
+ * @source ui-patterns
+ * @page any
+ */
+export interface CommandMenuOpenedEvent {
+ action: 'command_menu_opened'
+ properties: {
+ /**
+ * The trigger that opened the command menu
+ */
+ trigger_type: 'keyboard_shortcut' | 'search_input'
+ /**
+ * The location where the command menu was opened
+ */
+ trigger_location?: string
+ /**
+ * In which app the command input was typed
+ */
+ app: 'studio' | 'docs' | 'www'
+ }
+ groups: Partial
+}
+
+/**
+ * User typed a search term in the command menu input.
+ *
+ * @group Events
+ * @source ui-patterns
+ * @page any
+ */
+export interface CommandMenuSearchSubmittedEvent {
+ action: 'command_menu_search_submitted'
+ properties: {
+ /**
+ * Search term typed into the command menu input
+ */
+ value: string
+ /**
+ * In which app the command input was typed
+ */
+ app: 'studio' | 'docs' | 'www'
+ }
+ groups: Partial
+}
+
+/**
+ * User selected a command from the command menu.
+ *
+ * @group Events
+ * @source ui-patterns
+ * @page any
+ */
+export interface CommandMenuCommandSelectedEvent {
+ action: 'command_menu_command_selected'
+ properties: {
+ /**
+ * The selected command
+ */
+ command_name: string
+ command_value?: string
+ command_type: 'action' | 'route'
+ /**
+ * In which app the command input was typed
+ */
+ app: 'studio' | 'docs' | 'www'
+ }
+ groups: Partial
+}
+
/**
* @hidden
*/
@@ -2063,3 +2135,6 @@ export type TelemetryEvent =
| TableDataAddedEvent
| TableRLSEnabledEvent
| AuthUsersSearchSubmittedEvent
+ | CommandMenuOpenedEvent
+ | CommandMenuSearchSubmittedEvent
+ | CommandMenuCommandSelectedEvent
diff --git a/packages/ui-patterns/src/CommandMenu/api/CommandInput.tsx b/packages/ui-patterns/src/CommandMenu/api/CommandInput.tsx
index a2386f484af12..afa27b8361c5a 100644
--- a/packages/ui-patterns/src/CommandMenu/api/CommandInput.tsx
+++ b/packages/ui-patterns/src/CommandMenu/api/CommandInput.tsx
@@ -1,11 +1,15 @@
'use client'
-import React, { forwardRef, useEffect, useRef, useState } from 'react'
+import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
+import type React from 'react'
-import { useBreakpoint } from 'common'
+import { useBreakpoint, useDebounce } from 'common'
import { CommandInput_Shadcn_, cn } from 'ui'
import { useQuery, useSetQuery } from './hooks/queryHooks'
+import { useCommandMenuTelemetryContext } from './hooks/useCommandMenuTelemetryContext'
+
+const INPUT_TYPED_EVENT_DEBOUNCE_TIME = 2000 // 2s
function useFocusInputOnWiderScreens(ref: React.ForwardedRef) {
const isBelowSm = useBreakpoint('sm')
@@ -50,8 +54,56 @@ const CommandInput = forwardRef<
const [inputValue, setInputValue] = useState(query)
useEffect(() => {
setInputValue(query)
+ previousValueRef.current = query
}, [query])
+ // Get telemetry context
+ const telemetryContext = useCommandMenuTelemetryContext()
+ const previousValueRef = useRef(inputValue)
+
+ const inputTelemetryEvent = useCallback(
+ (value: string) => {
+ if (telemetryContext?.onTelemetry) {
+ const event = {
+ action: 'command_menu_search_submitted' as const,
+ properties: {
+ value: value,
+ app: telemetryContext.app,
+ },
+ groups: {},
+ }
+ telemetryContext.onTelemetry(event)
+ }
+ },
+ [telemetryContext]
+ )
+
+ const debouncedTelemetry = useDebounce(
+ useCallback(() => {
+ inputTelemetryEvent(inputValue)
+ previousValueRef.current = inputValue
+ }, [inputTelemetryEvent, inputValue]),
+ INPUT_TYPED_EVENT_DEBOUNCE_TIME
+ )
+
+ const handleValueChange = useCallback(
+ (value: string) => {
+ setInputValue(value)
+
+ // Only trigger telemetry if the user is adding characters (not removing with backspace)
+ const isAddingCharacters = value.length > previousValueRef.current.length
+
+ if (!isAddingCharacters) {
+ previousValueRef.current = value
+ return
+ }
+
+ // Trigger debounced telemetry
+ debouncedTelemetry()
+ },
+ [debouncedTelemetry]
+ )
+
// To handle CJK input
const [imeComposing, setImeComposing] = useState(false)
useEffect(() => {
@@ -67,8 +119,8 @@ const CommandInput = forwardRef<
autoFocus={false}
ref={inputRef}
value={inputValue}
- onValueChange={setInputValue}
- placeholder="Type a command or search..."
+ onValueChange={handleValueChange}
+ placeholder="Run a command or search..."
onCompositionStart={() => setImeComposing(true)}
onCompositionEnd={() => setImeComposing(false)}
className={cn(
diff --git a/packages/ui-patterns/src/CommandMenu/api/CommandMenu.tsx b/packages/ui-patterns/src/CommandMenu/api/CommandMenu.tsx
index 205d8f1f6f808..1c1238310ef2a 100644
--- a/packages/ui-patterns/src/CommandMenu/api/CommandMenu.tsx
+++ b/packages/ui-patterns/src/CommandMenu/api/CommandMenu.tsx
@@ -1,6 +1,6 @@
'use client'
-import { AlertTriangle, ArrowLeft } from 'lucide-react'
+import { AlertTriangle, ArrowLeft, Command, Search } from 'lucide-react'
import type { HTMLAttributes, MouseEvent, PropsWithChildren, ReactElement, ReactNode } from 'react'
import { Children, cloneElement, forwardRef, isValidElement, useEffect, useMemo } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
@@ -11,6 +11,7 @@ import { Button, Command_Shadcn_, Dialog, DialogContent, cn } from 'ui'
import { useCurrentPage, usePageComponent, usePopPage } from './hooks/pagesHooks'
import { useQuery, useSetQuery } from './hooks/queryHooks'
+import { useCommandMenuTelemetryContext } from './hooks/useCommandMenuTelemetryContext'
import {
useCommandMenuSize,
useCommandMenuOpen,
@@ -26,6 +27,7 @@ function Breadcrumb({ className }: { className?: string }) {
return (