- {/* Top Banner */}
-
-
-
-
-
- {/* Main Content Area */}
-
- {/* Sidebar - Only show for project pages, not account pages */}
- {!router.pathname.startsWith('/account') &&
}
- {/* Main Content with Layout Sidebar */}
-
-
- {children}
-
-
+
+ {/* Top Banner */}
+
+
+
+
-
+
+ {/* Main Content Area */}
+
+ {/* Sidebar - Only show for project pages, not account pages */}
+ {!router.pathname.startsWith('/account') &&
}
+ {/* Main Content with Layout Sidebar */}
+
+
+ {children}
+
+
+
+
-
+
+
+
diff --git a/apps/studio/components/layouts/ObservabilityLayout/ObservabilityLayout.tsx b/apps/studio/components/layouts/ObservabilityLayout/ObservabilityLayout.tsx
index 6207f84d4f355..f8d3692ca5f6d 100644
--- a/apps/studio/components/layouts/ObservabilityLayout/ObservabilityLayout.tsx
+++ b/apps/studio/components/layouts/ObservabilityLayout/ObservabilityLayout.tsx
@@ -1,19 +1,17 @@
-import { PropsWithChildren, useEffect } from 'react'
-import { useParams } from 'common'
-import { LOCAL_STORAGE_KEYS, IS_PLATFORM } from 'common'
+import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useParams } from 'common'
+import { useIndexAdvisorStatus } from 'components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus'
+import { BannerIndexAdvisor } from 'components/ui/BannerStack/Banners/BannerIndexAdvisor'
+import { BannerMetricsAPI } from 'components/ui/BannerStack/Banners/BannerMetricsAPI'
+import { useBannerStack } from 'components/ui/BannerStack/BannerStackProvider'
import { UnknownInterface } from 'components/ui/UnknownInterface'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
-import { withAuth } from 'hooks/misc/withAuth'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
-import { BannerMetricsAPI } from 'components/ui/BannerStack/Banners/BannerMetricsAPI'
+import { withAuth } from 'hooks/misc/withAuth'
+import { usePathname } from 'next/navigation'
+import { PropsWithChildren, useEffect, useRef } from 'react'
+
import { ProjectLayout } from '../ProjectLayout'
import ObservabilityMenu from './ObservabilityMenu'
-import { BannerStackProvider, useBannerStack } from 'components/ui/BannerStack/BannerStackProvider'
-import { BannerStack } from 'components/ui/BannerStack/BannerStack'
-import { usePathname } from 'next/navigation'
-import { useIndexAdvisorStatus } from 'components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus'
-import { BannerIndexAdvisor } from 'components/ui/BannerStack/Banners/BannerIndexAdvisor'
-import { useRef } from 'react'
interface ObservabilityLayoutProps {
title?: string
@@ -105,12 +103,7 @@ const ObservabilityLayout = (props: PropsWithChildren
)
const { reportsAll } = useIsFeatureEnabled(['reports:all'])
if (reportsAll) {
- return (
-
-
-
-
- )
+ return
} else {
return
}
diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx
index 123146bffb19f..d2b622044a4fe 100644
--- a/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx
+++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx
@@ -1,17 +1,56 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { PropsWithChildren } from 'react'
-
-import { SaveQueueActionBar } from '@/components/grid/components/footer/operations/SaveQueueActionBar'
+import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import NoPermission from 'components/ui/NoPermission'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
+import { PropsWithChildren, useEffect } from 'react'
+
import { ProjectLayoutWithAuth } from '../ProjectLayout'
+import { SaveQueueActionBar } from '@/components/grid/components/footer/operations/SaveQueueActionBar'
+import { BannerTableEditorFilter } from '@/components/ui/BannerStack/Banners/BannerTableEditorFilter'
+import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
+import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
+
+const TABLE_EDITOR_NEW_FILTER_BANNER_ID = 'table-editor-new-filter-banner'
export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => {
+ const { ref } = useParams()
+ const { addBanner, dismissBanner } = useBannerStack()
+
+ const [isTableEditorNewFilterBannerDismissed] = useLocalStorageQuery(
+ LOCAL_STORAGE_KEYS.TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED(ref ?? ''),
+ false
+ )
+
const { can: canReadTables, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_READ,
'tables'
)
+ useEffect(() => {
+ if (!isPermissionsLoaded) return
+
+ if (canReadTables && !isTableEditorNewFilterBannerDismissed) {
+ addBanner({
+ id: TABLE_EDITOR_NEW_FILTER_BANNER_ID,
+ priority: 2,
+ isDismissed: false,
+ content: ,
+ })
+ } else {
+ dismissBanner(TABLE_EDITOR_NEW_FILTER_BANNER_ID)
+ }
+
+ return () => {
+ dismissBanner(TABLE_EDITOR_NEW_FILTER_BANNER_ID)
+ }
+ }, [
+ addBanner,
+ dismissBanner,
+ canReadTables,
+ isPermissionsLoaded,
+ isTableEditorNewFilterBannerDismissed,
+ ])
+
if (isPermissionsLoaded && !canReadTables) {
return (
diff --git a/apps/studio/components/ui/BannerStack/Banners/BannerTableEditorFilter.tsx b/apps/studio/components/ui/BannerStack/Banners/BannerTableEditorFilter.tsx
new file mode 100644
index 0000000000000..72be817f3e8ae
--- /dev/null
+++ b/apps/studio/components/ui/BannerStack/Banners/BannerTableEditorFilter.tsx
@@ -0,0 +1,70 @@
+import { LOCAL_STORAGE_KEYS } from 'common'
+import { useParams } from 'common/hooks'
+import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
+import { Search } from 'lucide-react'
+import { Badge, Button } from 'ui'
+
+import { BannerCard } from '../BannerCard'
+import { useBannerStack } from '../BannerStackProvider'
+import {
+ useFeaturePreviewModal,
+ useIsTableFilterBarEnabled,
+} from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
+
+export const BannerTableEditorFilter = () => {
+ const { ref } = useParams()
+ const { selectFeaturePreview } = useFeaturePreviewModal()
+ const isTableFilterBarEnabled = useIsTableFilterBarEnabled()
+
+ const { dismissBanner } = useBannerStack()
+ const [, setIsDismissed] = useLocalStorageQuery(
+ LOCAL_STORAGE_KEYS.TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED(ref ?? ''),
+ false
+ )
+
+ const text = "name = 'John Doe'"
+
+ return (
+ {
+ setIsDismissed(true)
+ dismissBanner('table-editor-new-filter-banner')
+ }}
+ >
+
+
+
+
New Table Filter Bar
+
+ Build and modify complex filters visually
+
+
+
selectFeaturePreview(LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR)}
+ >
+ {isTableFilterBarEnabled ? 'View' : 'Enable'} feature preview
+
+
+
+ )
+}
diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx
index 5875b41e480ff..cc7287109700e 100644
--- a/apps/studio/pages/_app.tsx
+++ b/apps/studio/pages/_app.tsx
@@ -25,20 +25,19 @@ import { HydrationBoundary, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import {
FeatureFlagProvider,
+ getFlags,
TelemetryTagManager,
ThemeProvider,
- getFlags,
useThemeSandbox,
} from 'common'
import MetaFaviconsPagesRouter from 'common/MetaFavicons/pages-router'
import { StudioCommandMenu } from 'components/interfaces/App/CommandMenu'
import { StudioCommandProvider as CommandProvider } from 'components/interfaces/App/CommandMenu/StudioCommandProvider'
import { FeaturePreviewContextProvider } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
-import FeaturePreviewModal from 'components/interfaces/App/FeaturePreview/FeaturePreviewModal'
+import { FeaturePreviewModal } from 'components/interfaces/App/FeaturePreview/FeaturePreviewModal'
import { MonacoThemeProvider } from 'components/interfaces/App/MonacoThemeProvider'
import { RouteValidationWrapper } from 'components/interfaces/App/RouteValidationWrapper'
import { MainScrollContainerProvider } from 'components/layouts/MainScrollContainerContext'
-import { DevToolbar, DevToolbarProvider } from 'dev-tools'
import { GlobalErrorBoundaryState } from 'components/ui/ErrorBoundary/GlobalErrorBoundaryState'
import { useRootQueryClient } from 'data/query-client'
import dayjs from 'dayjs'
@@ -47,6 +46,7 @@ import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
+import { DevToolbar, DevToolbarProvider } from 'dev-tools'
import { customFont, sourceCodePro } from 'fonts'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
@@ -56,7 +56,7 @@ import { ProfileProvider } from 'lib/profile'
import { Telemetry } from 'lib/telemetry'
import Head from 'next/head'
import { NuqsAdapter } from 'nuqs/adapters/next/pages'
-import { type ComponentProps, ErrorInfo, useCallback } from 'react'
+import { ErrorInfo, useCallback, type ComponentProps } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { AiAssistantStateContextProvider } from 'state/ai-assistant-state'
import type { AppPropsWithLayout } from 'types'
diff --git a/apps/studio/pages/api/ai/code/complete.ts b/apps/studio/pages/api/ai/code/complete.ts
index f23145983ec5a..300db283fec90 100644
--- a/apps/studio/pages/api/ai/code/complete.ts
+++ b/apps/studio/pages/api/ai/code/complete.ts
@@ -68,6 +68,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
model,
error: modelError,
promptProviderOptions,
+ providerOptions,
} = await getModel({
provider: 'openai',
routingKey: projectRef,
@@ -155,6 +156,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const { text } = await generateText({
model,
+ providerOptions,
stopWhen: stepCountIs(5),
messages: coreMessages,
tools,
diff --git a/apps/studio/pages/api/ai/feedback/classify.ts b/apps/studio/pages/api/ai/feedback/classify.ts
index ccebe72a43b0b..bc673a9aa25d1 100644
--- a/apps/studio/pages/api/ai/feedback/classify.ts
+++ b/apps/studio/pages/api/ai/feedback/classify.ts
@@ -1,8 +1,8 @@
import { generateObject } from 'ai'
-import { NextApiRequest, NextApiResponse } from 'next'
-import { z } from 'zod'
import { getModel } from 'lib/ai/model'
import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+import { z } from 'zod'
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req
@@ -28,7 +28,11 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
}
try {
- const { model, error: modelError } = await getModel({
+ const {
+ model,
+ error: modelError,
+ providerOptions,
+ } = await getModel({
provider: 'openai',
routingKey: 'feedback',
})
@@ -39,6 +43,7 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const { object } = await generateObject({
model,
+ providerOptions,
schema: z.object({
feedback_category: z.enum(['support', 'feedback', 'unknown']),
}),
diff --git a/apps/studio/pages/api/ai/feedback/rate.ts b/apps/studio/pages/api/ai/feedback/rate.ts
index c1cc9e8a1bf4f..641f11d469e5b 100644
--- a/apps/studio/pages/api/ai/feedback/rate.ts
+++ b/apps/studio/pages/api/ai/feedback/rate.ts
@@ -96,7 +96,11 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
})
try {
- const { model, error: modelError } = await getModel({
+ const {
+ model,
+ error: modelError,
+ providerOptions,
+ } = await getModel({
provider: 'openai',
isLimited: true,
routingKey: 'feedback',
@@ -108,6 +112,7 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const { object } = await generateObject({
model,
+ providerOptions,
schema: rateMessageResponseSchema,
prompt: `
Your job is to look at a Supabase Assistant conversation, which the user has given feedback on, and classify it.
diff --git a/apps/studio/pages/api/ai/sql/cron-v2.ts b/apps/studio/pages/api/ai/sql/cron-v2.ts
index 000106e276c0c..d9344c9a43fa2 100644
--- a/apps/studio/pages/api/ai/sql/cron-v2.ts
+++ b/apps/studio/pages/api/ai/sql/cron-v2.ts
@@ -1,10 +1,9 @@
import { generateObject } from 'ai'
import { source } from 'common-tags'
-import { NextApiRequest, NextApiResponse } from 'next'
-import { z } from 'zod'
-
import { getModel } from 'lib/ai/model'
import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+import { z } from 'zod'
const cronSchema = z.object({
cron_expression: z.string().describe('The generated cron expression.'),
@@ -34,7 +33,11 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
}
try {
- const { model, error: modelError } = await getModel({
+ const {
+ model,
+ error: modelError,
+ providerOptions,
+ } = await getModel({
provider: 'openai',
routingKey: 'cron',
})
@@ -45,6 +48,7 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const result = await generateObject({
model,
+ providerOptions,
schema: cronSchema,
prompt: source`
You are a cron syntax expert. Your purpose is to convert natural language time descriptions into valid cron expressions for pg_cron.
diff --git a/apps/studio/pages/api/ai/sql/filter-v1.ts b/apps/studio/pages/api/ai/sql/filter-v1.ts
index 64ddbaca735f2..9dd6cd2b2ad85 100644
--- a/apps/studio/pages/api/ai/sql/filter-v1.ts
+++ b/apps/studio/pages/api/ai/sql/filter-v1.ts
@@ -1,7 +1,5 @@
import { generateObject } from 'ai'
import { source } from 'common-tags'
-import { NextApiRequest, NextApiResponse } from 'next'
-
import { getModel } from 'lib/ai/model'
import apiWrapper from 'lib/api/apiWrapper'
import {
@@ -12,6 +10,7 @@ import {
serializeOptions,
validateFilterGroup,
} from 'lib/api/filterHelpers'
+import { NextApiRequest, NextApiResponse } from 'next'
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req
@@ -36,7 +35,11 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const { prompt, filterProperties } = parseResult.data
try {
- const { model, error: modelError } = await getModel({
+ const {
+ model,
+ error: modelError,
+ providerOptions,
+ } = await getModel({
provider: 'openai',
routingKey: 'sql',
})
@@ -61,6 +64,7 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const result = await generateObject({
model,
+ providerOptions,
schema: filterGroupSchema,
prompt: source`
You are an expert Postgres filter builder. Convert the user's request into structured filters.
diff --git a/apps/studio/pages/api/ai/sql/policy.ts b/apps/studio/pages/api/ai/sql/policy.ts
index d403e9b6b3551..8bf086cd2d863 100644
--- a/apps/studio/pages/api/ai/sql/policy.ts
+++ b/apps/studio/pages/api/ai/sql/policy.ts
@@ -1,15 +1,14 @@
-import { Output, generateText, stepCountIs } from 'ai'
+import { generateText, Output, stepCountIs } from 'ai'
import { IS_PLATFORM } from 'common'
import { source } from 'common-tags'
-import { NextApiRequest, NextApiResponse } from 'next'
-import { z } from 'zod'
-
import type { AiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi'
import { getModel } from 'lib/ai/model'
import { getOrgAIDetails } from 'lib/ai/org-ai-details'
import { RLS_PROMPT } from 'lib/ai/prompts'
import { getTools } from 'lib/ai/tools'
import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+import { z } from 'zod'
const policySchema = z.object({
sql: z.string().describe('The generated Postgres CREATE POLICY statement.'),
@@ -91,7 +90,11 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
}
try {
- const { model, error: modelError } = await getModel({
+ const {
+ model,
+ error: modelError,
+ providerOptions,
+ } = await getModel({
provider: 'openai',
routingKey: 'sql-policy',
})
@@ -110,6 +113,7 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const { experimental_output } = await generateText({
model,
+ providerOptions,
stopWhen: stepCountIs(5),
prompt: source`
You are a Postgres RLS (Row Level Security) expert.
diff --git a/apps/studio/pages/api/ai/sql/title-v2.ts b/apps/studio/pages/api/ai/sql/title-v2.ts
index ed552d1030e32..3c3a3cd6848c7 100644
--- a/apps/studio/pages/api/ai/sql/title-v2.ts
+++ b/apps/studio/pages/api/ai/sql/title-v2.ts
@@ -1,10 +1,9 @@
import { generateObject } from 'ai'
import { source } from 'common-tags'
-import { NextApiRequest, NextApiResponse } from 'next'
-import { z } from 'zod'
-
import { getModel } from 'lib/ai/model'
import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+import { z } from 'zod'
const titleSchema = z.object({
title: z
@@ -39,7 +38,11 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
}
try {
- const { model, error: modelError } = await getModel({
+ const {
+ model,
+ error: modelError,
+ providerOptions,
+ } = await getModel({
provider: 'openai',
routingKey: 'sql',
})
@@ -50,6 +53,7 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const result = await generateObject({
model,
+ providerOptions,
schema: titleSchema,
prompt: source`
Generate a short title and summarized description for this Postgres SQL snippet:
diff --git a/apps/studio/pages/project/[ref]/auth/policies.tsx b/apps/studio/pages/project/[ref]/auth/policies.tsx
index 5a5b148adb15d..f6ce2a7f3f1b6 100644
--- a/apps/studio/pages/project/[ref]/auth/policies.tsx
+++ b/apps/studio/pages/project/[ref]/auth/policies.tsx
@@ -1,10 +1,5 @@
import type { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { Search, X } from 'lucide-react'
-import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
-import { useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react'
-import { toast } from 'sonner'
-
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useIsInlineEditorEnabled } from 'components/interfaces/Account/Preferences/InlineEditorSettings'
import { Policies } from 'components/interfaces/Auth/Policies/Policies'
@@ -17,8 +12,7 @@ import { DefaultLayout } from 'components/layouts/DefaultLayout'
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import AlertError from 'components/ui/AlertError'
import { BannerRlsEventTrigger } from 'components/ui/BannerStack/Banners/BannerRlsEventTrigger'
-import { BannerStack } from 'components/ui/BannerStack/BannerStack'
-import { BannerStackProvider, useBannerStack } from 'components/ui/BannerStack/BannerStackProvider'
+import { useBannerStack } from 'components/ui/BannerStack/BannerStackProvider'
import { DocsButton } from 'components/ui/DocsButton'
import NoPermission from 'components/ui/NoPermission'
import { SchemaSelector } from 'components/ui/SchemaSelector'
@@ -30,6 +24,10 @@ import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { DOCS_URL } from 'lib/constants'
+import { Search, X } from 'lucide-react'
+import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
+import { useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react'
+import { toast } from 'sonner'
import { useEditorPanelStateSnapshot } from 'state/editor-panel-state'
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
import type { NextPageWithLayout } from 'types'
@@ -369,10 +367,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
AuthPoliciesPage.getLayout = (page) => (
-
- {page}
-
-
+ {page}
)
diff --git a/apps/studio/pages/project/[ref]/database/column-privileges.tsx b/apps/studio/pages/project/[ref]/database/column-privileges.tsx
index f3f6422babb15..ef1a7c32d04ee 100644
--- a/apps/studio/pages/project/[ref]/database/column-privileges.tsx
+++ b/apps/studio/pages/project/[ref]/database/column-privileges.tsx
@@ -1,9 +1,4 @@
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
-import { AlertCircle, XIcon } from 'lucide-react'
-import Link from 'next/link'
-import { useCallback, useEffect, useMemo, useState } from 'react'
-import { toast } from 'sonner'
-
import {
useFeaturePreviewModal,
useIsColumnLevelPrivilegesEnabled,
@@ -31,8 +26,12 @@ import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { DOCS_URL } from 'lib/constants'
+import { AlertCircle, XIcon } from 'lucide-react'
+import Link from 'next/link'
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { toast } from 'sonner'
import type { NextPageWithLayout } from 'types'
-import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
+import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
const EDITABLE_ROLES = ['authenticated', 'anon', 'service_role']
@@ -40,7 +39,7 @@ const EDITABLE_ROLES = ['authenticated', 'anon', 'service_role']
const PrivilegesPage: NextPageWithLayout = () => {
const { ref, table: paramTable } = useParams()
const { data: project } = useSelectedProjectQuery()
- const { openFeaturePreviewModal } = useFeaturePreviewModal()
+ const { toggleFeaturePreviewModal } = useFeaturePreviewModal()
const isEnabled = useIsColumnLevelPrivilegesEnabled()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
@@ -351,7 +350,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
You may access this feature by enabling it under dashboard feature previews.
-
+ toggleFeaturePreviewModal(true)}>
View feature previews
diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx
index 85e4eb938e792..4e98579275de5 100644
--- a/apps/studio/state/storage-explorer.tsx
+++ b/apps/studio/state/storage-explorer.tsx
@@ -1215,7 +1215,7 @@ function createStorageExplorerState({
const status = error.originalResponse?.getStatus()
switch (status) {
- case 415:
+ case 415: {
// Unsupported mime type
toast.error(
capitalize(
@@ -1227,20 +1227,38 @@ function createStorageExplorerState({
}
)
break
- case 413:
+ }
+ case 413: {
// Payload too large
toast.error(
`Failed to upload ${file.name}: File size exceeds the bucket file size limit.`
)
break
- case 409:
+ }
+ case 409: {
// Resource already exists
toast.error(`Failed to upload ${file.name}: File name already exists.`)
break
- case 400:
- // Invalid key
- toast.error(`Failed to upload ${file.name}: File name is invalid`)
+ }
+ case 400: {
+ const responseBody = error.originalResponse?.getBody()
+ if (typeof responseBody === 'string') {
+ if (responseBody.includes('Invalid key:')) {
+ toast.error(`Failed to upload ${file.name}: File name is invalid.`)
+ break
+ }
+
+ if (responseBody.includes('Invalid Compact JWS')) {
+ toast.error(`Failed to upload ${file.name}: Invalid Compact JWS.`)
+ break
+ }
+ }
+ // if it's not handled by the two ifs, fallthrough to the default case which shows the generic error message
+ }
+ default: {
+ toast.error(`Failed to upload ${file.name}: ${error.message}`)
break
+ }
}
} else {
toast.error(`Failed to upload ${file.name}: ${error.message}`)
diff --git a/apps/studio/tailwind.config.js b/apps/studio/tailwind.config.js
index b5fdcf67abc2a..ace71e440be9c 100644
--- a/apps/studio/tailwind.config.js
+++ b/apps/studio/tailwind.config.js
@@ -99,6 +99,12 @@ module.exports = config({
transform: 'rotate(10deg) scale(1.5) translateY(2rem)',
},
},
+ typewriter: {
+ from: { width: '0' },
+ },
+ 'blink-caret': {
+ '50%': { borderColor: 'transparent' },
+ },
},
},
},
diff --git a/e2e/studio/features/table-editor.spec.ts b/e2e/studio/features/table-editor.spec.ts
index e3a4429f71b54..6cc06f681ba3b 100644
--- a/e2e/studio/features/table-editor.spec.ts
+++ b/e2e/studio/features/table-editor.spec.ts
@@ -94,7 +94,10 @@ const deleteTable = async (page: Page, ref: string, tableName: string) => {
}
const deleteEnumIfExist = async (page: Page, ref: string, enumName: string) => {
- await waitForApiResponse(page, 'pg-meta', ref, 'types')
+ // Wait for the types page to fully load by checking for a known enum that always exists
+ await expect(page.getByRole('cell', { name: 'feedback_vote', exact: true })).toBeVisible({
+ timeout: 30_000,
+ })
// if enum (test) exists, delete it.
const exists = (await page.getByRole('cell', { name: enumName, exact: true }).count()) > 0
@@ -106,8 +109,11 @@ const deleteEnumIfExist = async (page: Page, ref: string, enumName: string) => {
.click()
await page.getByRole('menuitem', { name: 'Delete type' }).click()
await page.getByRole('heading', { name: 'Confirm to delete enumerated' }).click()
+ const deleteEnumPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
+ method: 'POST',
+ })
await page.getByRole('button', { name: 'Confirm delete' }).click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
+ await deleteEnumPromise
}
// Due to rate API rate limits run this test in serial mode on platform.
@@ -179,8 +185,9 @@ testRunner('table editor', () => {
.getByRole('button')
.nth(2)
.click()
+ const schemaPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-definition-')
await page.getByRole('menuitem', { name: 'Copy table schema' }).click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-definition-') // wait for endpoint to generate schema
+ await schemaPromise // wait for endpoint to generate schema
await page.waitForTimeout(500)
const copiedSchemaResult = await page.evaluate(() => navigator.clipboard.readText())
expect(copiedSchemaResult).toBe(`create table public.pw_table_actions (
@@ -197,8 +204,11 @@ testRunner('table editor', () => {
.nth(2)
.click()
await page.getByRole('menuitem', { name: 'Duplicate table' }).click()
+ const duplicatePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
+ method: 'POST',
+ })
await page.getByRole('button', { name: 'Save' }).click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // create duplicate table
+ await duplicatePromise // create duplicate table
await waitForTableToLoad(page, ref) // load tables
await expect(
page.getByLabel(`View ${tableNameActionsDuplicate}`, { exact: true })
@@ -279,8 +289,9 @@ testRunner('table editor', () => {
await page.locator('input[name="values.0.value"]').fill('value1')
await page.getByRole('button', { name: 'Add value' }).click()
await page.locator('input[name="values.1.value"]').fill('value2')
+ const createTypePromise = waitForApiResponse(page, 'pg-meta', ref, 'types')
await page.getByRole('button', { name: 'Create type' }).click()
- await waitForApiResponse(page, 'pg-meta', ref, 'types')
+ await createTypePromise
// verify enum is created
await expect(page.getByRole('cell', { name: enum_name, exact: true })).toBeVisible()
@@ -355,8 +366,11 @@ testRunner('table editor', () => {
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId('pw_column-input').fill(value)
+ const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
+ method: 'POST',
+ })
await page.getByTestId('action-bar-save-row').click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // insert rows
+ await insertPromise // insert rows
}
// verify row content
@@ -373,8 +387,11 @@ testRunner('table editor', () => {
await page.getByRole('menuitem', { name: 'Edit table' }).click()
await page.getByTestId('table-name-input').fill(tableNameUpdated)
await page.getByRole('textbox', { name: 'pw_column' }).fill(columnNameUpdated)
+ const updateTablePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=column-update', {
+ method: 'POST',
+ })
await page.getByRole('button', { name: 'Save' }).click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=column-update', { method: 'POST' }) // update table
+ await updateTablePromise // update table
await waitForTableToLoad(page, ref) // load tables
await expect(page.getByLabel(`View ${tableNameUpdated}`, { exact: true })).toBeVisible()
await expect(page.getByLabel(`View ${tableNameGridEditor}`, { exact: true })).not.toBeVisible()
@@ -557,8 +574,11 @@ testRunner('table editor', () => {
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${colName}-input`).fill(value)
+ const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
+ method: 'POST',
+ })
await page.getByTestId('action-bar-save-row').click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
+ await insertPromise
}
// Apply sorting
@@ -773,15 +793,21 @@ testRunner('table editor', () => {
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${colName}-input`).fill('first_row_value')
+ const insertFirstPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
+ method: 'POST',
+ })
await page.getByTestId('action-bar-save-row').click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
+ await insertFirstPromise
// Insert second row with value 'second_row_value'
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByTestId(`${colName}-input`).fill('second_row_value')
+ const insertSecondPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
+ method: 'POST',
+ })
await page.getByTestId('action-bar-save-row').click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
+ await insertSecondPromise
// Wait for grid to be visible
await expect(page.getByRole('grid')).toBeVisible()
@@ -861,8 +887,11 @@ testRunner('table editor', () => {
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'TRUE' }).click()
+ const insertTruePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
+ method: 'POST',
+ })
await page.getByTestId('action-bar-save-row').click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
+ await insertTruePromise
await expect(
page.getByRole('gridcell', { name: 'TRUE' }),
@@ -874,8 +903,11 @@ testRunner('table editor', () => {
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'FALSE' }).click()
+ const insertFalsePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
+ method: 'POST',
+ })
await page.getByTestId('action-bar-save-row').click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
+ await insertFalsePromise
// Verify FALSE value is preserved
await expect(
@@ -892,10 +924,10 @@ testRunner('table editor', () => {
await expect(booleanEditor, 'Boolean editor should be visible').toBeVisible()
// Change from false to true
- await booleanEditor.selectOption('true')
const updateTrueResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
+ await booleanEditor.selectOption('true')
await page.getByRole('columnheader', { name: 'id' }).click()
await updateTrueResponse
@@ -911,10 +943,10 @@ testRunner('table editor', () => {
await trueCell.dblclick()
await expect(booleanEditor, 'Boolean editor should be visible for second edit').toBeVisible()
- await booleanEditor.selectOption('false')
const updateFalseResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
+ await booleanEditor.selectOption('false')
await page.getByRole('columnheader', { name: 'id' }).click()
await updateFalseResponse
@@ -974,8 +1006,11 @@ testRunner('table editor', () => {
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'TRUE' }).click()
+ const insertTruePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
+ method: 'POST',
+ })
await page.getByTestId('action-bar-save-row').click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
+ await insertTruePromise
await expect(
page.getByRole('gridcell', { name: 'TRUE' }),
@@ -987,8 +1022,11 @@ testRunner('table editor', () => {
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'FALSE' }).click()
+ const insertFalsePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
+ method: 'POST',
+ })
await page.getByTestId('action-bar-save-row').click()
- await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
+ await insertFalsePromise
await expect(
page.getByRole('gridcell', { name: 'FALSE' }),
@@ -1002,11 +1040,10 @@ testRunner('table editor', () => {
const booleanEditor = page.locator('#boolean-editor')
await expect(booleanEditor, 'Boolean editor should be visible').toBeVisible()
- await booleanEditor.selectOption('null')
-
const updateNullResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
+ await booleanEditor.selectOption('null')
await page.getByRole('columnheader', { name: 'id' }).click()
await updateNullResponse
@@ -1018,11 +1055,10 @@ testRunner('table editor', () => {
const nullCellToFalse = page.getByRole('gridcell', { name: 'NULL' })
await nullCellToFalse.dblclick()
- await booleanEditor.selectOption('false')
-
const updateFalseResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
+ await booleanEditor.selectOption('false')
await page.getByRole('columnheader', { name: 'id' }).click()
await updateFalseResponse
diff --git a/e2e/studio/utils/test.ts b/e2e/studio/utils/test.ts
index 2970f39860792..8e134c064d4b7 100644
--- a/e2e/studio/utils/test.ts
+++ b/e2e/studio/utils/test.ts
@@ -1,6 +1,7 @@
+import path from 'path'
import { test as base } from '@playwright/test'
import dotenv from 'dotenv'
-import path from 'path'
+
import { env } from '../env.config.js'
dotenv.config({
@@ -18,4 +19,11 @@ export const test = base.extend({
env: env.STUDIO_URL,
ref: env.PROJECT_REF ?? 'default',
apiUrl: env.API_URL,
+ page: async ({ page }, use) => {
+ const ref = env.PROJECT_REF ?? 'default'
+ await page.addInitScript((ref) => {
+ localStorage.setItem(`table-editor-new-filter-banner-dismissed-${ref}`, JSON.stringify(true))
+ }, ref)
+ await use(page)
+ },
})
diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts
index d35915541a0f6..55d7e1ecc113a 100644
--- a/packages/common/constants/local-storage.ts
+++ b/packages/common/constants/local-storage.ts
@@ -100,6 +100,8 @@ export const LOCAL_STORAGE_KEYS = {
// Observability banner dismissed
OBSERVABILITY_BANNER_DISMISSED: (ref: string) => `observability-banner-dismissed-${ref}`,
+ TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED: (ref: string) =>
+ `table-editor-new-filter-banner-dismissed-${ref}`,
/**
* COMMON
diff --git a/packages/ui-patterns/src/FilterBar/CommandListItem.tsx b/packages/ui-patterns/src/FilterBar/CommandListItem.tsx
index 38b2e9e88bbad..f52cd04be3bc4 100644
--- a/packages/ui-patterns/src/FilterBar/CommandListItem.tsx
+++ b/packages/ui-patterns/src/FilterBar/CommandListItem.tsx
@@ -25,7 +25,7 @@ export function CommandListItem({
role="option"
onClick={() => onSelect(item)}
className={cn(
- 'relative flex items-center justify-between gap-2 px-2 py-1.5 text-xs cursor-pointer select-none outline-none text-foreground-light',
+ 'relative flex items-center justify-between gap-2 px-2 py-1.5 text-xs cursor-pointer select-none outline-none text-foreground',
isHighlighted && 'bg-surface-300',
!isHighlighted && 'hover:bg-surface-200'
)}
diff --git a/packages/ui-patterns/src/FilterBar/DefaultCommandList.helpers.tsx b/packages/ui-patterns/src/FilterBar/DefaultCommandList.helpers.tsx
index 7a507adc69fff..dce4ee08b85b7 100644
--- a/packages/ui-patterns/src/FilterBar/DefaultCommandList.helpers.tsx
+++ b/packages/ui-patterns/src/FilterBar/DefaultCommandList.helpers.tsx
@@ -3,11 +3,7 @@ export function EmptyState() {
}
export function GroupHeader({ label }: { label: string }) {
- return (
-
- {label}
-
- )
+ return {label}
}
export function GroupSeparator() {
diff --git a/packages/ui/src/components/CustomHTMLElements/Heading.tsx b/packages/ui/src/components/CustomHTMLElements/Heading.tsx
index 1b6a85586a0e0..5594c9e27338f 100644
--- a/packages/ui/src/components/CustomHTMLElements/Heading.tsx
+++ b/packages/ui/src/components/CustomHTMLElements/Heading.tsx
@@ -58,10 +58,9 @@ const Heading = forwardRef(
{anchor && (
- #
+ #
)}