From a909c0869d8bff32749eeebb4406f988aa80308b Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Fri, 20 Feb 2026 09:33:32 -0700 Subject: [PATCH 1/7] chore: enable new filter bar as a default opt in (#43028) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? This is the first step for the GA rollout --------- Co-authored-by: Joshen Lim --- .../App/FeaturePreview/Branching2Preview.tsx | 19 +- .../FeaturePreview.constants.tsx | 53 ----- .../FeaturePreview/FeaturePreviewContext.tsx | 116 ++++------- .../FeaturePreview/FeaturePreviewModal.tsx | 193 ++++++++++-------- .../FeaturePreview/QueueOperationsPreview.tsx | 12 +- .../FeaturePreview/TableFilterBarPreview.tsx | 13 +- .../App/FeaturePreview/UnifiedLogsPreview.tsx | 18 +- .../App/FeaturePreview/useFeaturePreviews.ts | 87 ++++++++ .../Auth/Policies/PolicyEditorModal/index.tsx | 10 +- .../components/interfaces/LocalDropdown.tsx | 8 +- .../components/interfaces/UserDropdown.tsx | 16 +- .../AdvisorsLayout/AdvisorRulesLayout.tsx | 2 +- .../components/layouts/DefaultLayout.tsx | 92 +++++---- .../ObservabilityLayout.tsx | 27 +-- .../TableEditorLayout/TableEditorLayout.tsx | 45 +++- .../Banners/BannerTableEditorFilter.tsx | 70 +++++++ apps/studio/pages/_app.tsx | 8 +- .../pages/project/[ref]/auth/policies.tsx | 17 +- .../[ref]/database/column-privileges.tsx | 15 +- apps/studio/tailwind.config.js | 6 + packages/common/constants/local-storage.ts | 2 + .../src/FilterBar/CommandListItem.tsx | 2 +- .../FilterBar/DefaultCommandList.helpers.tsx | 6 +- 23 files changed, 480 insertions(+), 357 deletions(-) delete mode 100644 apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx create mode 100644 apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts create mode 100644 apps/studio/components/ui/BannerStack/Banners/BannerTableEditorFilter.tsx diff --git a/apps/studio/components/interfaces/App/FeaturePreview/Branching2Preview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/Branching2Preview.tsx index 35f3c6c7f427f..b98518dfb9829 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/Branching2Preview.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/Branching2Preview.tsx @@ -1,18 +1,10 @@ -import Image from 'next/image' - import { InlineLink } from 'components/ui/InlineLink' import { BASE_PATH, DOCS_URL } from 'lib/constants' +import Image from 'next/image' export const Branching2Preview = () => { return (
- api-docs-side-panel-preview

Create branches, review changes, and merge back into production all through the dashboard. Read the below limitations and our{' '} @@ -21,6 +13,15 @@ export const Branching2Preview = () => { {' '} before opting in.

+ + api-docs-side-panel-preview +

Limitations:

    diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx deleted file mode 100644 index a18fe30d764f2..0000000000000 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreview.constants.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { LOCAL_STORAGE_KEYS } from 'common' - -export const FEATURE_PREVIEWS = [ - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, - name: 'New Logs interface', - discussionsUrl: 'https://github.com/orgs/supabase/discussions/37234', - isNew: true, - isPlatformOnly: true, - }, - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0, - name: 'Branching via dashboard', - discussionsUrl: 'https://github.com/orgs/supabase/discussions/branching-2-0', - isNew: true, - isPlatformOnly: true, - }, - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES, - name: 'Disable Advisor rules', - discussionsUrl: undefined, - isNew: true, - isPlatformOnly: true, - }, - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL, - name: 'Project API documentation', - discussionsUrl: 'https://github.com/orgs/supabase/discussions/18038', - isNew: false, - isPlatformOnly: false, - }, - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS, - name: 'Column-level privileges', - discussionsUrl: 'https://github.com/orgs/supabase/discussions/20295', - isNew: false, - isPlatformOnly: false, - }, - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_QUEUE_OPERATIONS, - name: 'Queue table operations', - discussionsUrl: 'https://github.com/orgs/supabase/discussions/42460', - isNew: true, - isPlatformOnly: false, - }, - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR, - name: 'New Table Filter Bar', - discussionsUrl: 'https://github.com/orgs/supabase/discussions/42461', - isNew: true, - isPlatformOnly: false, - }, -] as const diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx index 68b5367357aae..043ee9cd303fa 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx @@ -12,7 +12,8 @@ import { useState, } from 'react' -import { FEATURE_PREVIEWS } from './FeaturePreview.constants' +import { useFeaturePreviews } from './useFeaturePreviews' +import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent' type FeaturePreviewContextType = { flags: { [key: string]: boolean } @@ -28,35 +29,30 @@ export const useFeaturePreviewContext = () => useContext(FeaturePreviewContext) export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren<{}>) => { const { hasLoaded } = useContext(FeatureFlagContext) - - // [Joshen] Similar logic to feature flagging previews, we can use flags to default opt in previews - const isDefaultOptIn = (feature: (typeof FEATURE_PREVIEWS)[number]) => { - switch (feature.key) { - default: - return false - } - } + const featurePreviews = useFeaturePreviews() const [flags, setFlags] = useState(() => - FEATURE_PREVIEWS.reduce((a, b) => { - return { ...a, [b.key]: false } - }, {}) + featurePreviews.reduce((a, b) => ({ ...a, [b.key]: false }), {}) ) + const initializeFlags = useStaticEffectEvent(() => { + setFlags( + featurePreviews.reduce((a, b) => { + const defaultOptIn = b.isDefaultOptIn + const localStorageValue = localStorage.getItem(b.key) + return { + ...a, + [b.key]: !localStorageValue ? defaultOptIn : localStorageValue === 'true', + } + }, {}) + ) + }) + useEffect(() => { if (typeof window !== 'undefined') { - setFlags( - FEATURE_PREVIEWS.reduce((a, b) => { - const defaultOptIn = isDefaultOptIn(b) - const localStorageValue = localStorage.getItem(b.key) - return { - ...a, - [b.key]: !localStorageValue ? defaultOptIn : localStorageValue === 'true', - } - }, {}) - ) + initializeFlags() } - }, [hasLoaded]) + }, [hasLoaded, initializeFlags]) const value = { flags, @@ -97,12 +93,14 @@ export const useUnifiedLogsPreview = () => { export const useIsBranching2Enabled = () => { const { flags } = useFeaturePreviewContext() - return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0] + const gitlessBranchingEnabled = useFlag('gitlessBranching') + return gitlessBranchingEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0] } export const useIsAdvisorRulesEnabled = () => { const { flags } = useFeaturePreviewContext() - return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES] + const advisorRulesEnabled = useFlag('advisorRules') + return advisorRulesEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES] } export const useIsQueueOperationsEnabled = () => { @@ -116,79 +114,41 @@ export const useIsTableFilterBarEnabled = () => { } export const useFeaturePreviewModal = () => { + const featurePreviews = useFeaturePreviews() const [featurePreviewModal, setFeaturePreviewModal] = useQueryState('featurePreviewModal') - const gitlessBranchingEnabled = useFlag('gitlessBranching') - const advisorRulesEnabled = useFlag('advisorRules') - const isUnifiedLogsPreviewAvailable = useFlag('unifiedLogs') - const selectedFeatureKeyFromQuery = featurePreviewModal?.trim() ?? null const showFeaturePreviewModal = selectedFeatureKeyFromQuery !== null - // [Joshen] Use this if we want to feature flag previews - const isFeaturePreviewReleasedToPublic = useCallback( - (feature: (typeof FEATURE_PREVIEWS)[number]) => { - switch (feature.key) { - case 'supabase-ui-branching-2-0': - return gitlessBranchingEnabled - case 'supabase-ui-advisor-rules': - return advisorRulesEnabled - case 'supabase-ui-preview-unified-logs': - return isUnifiedLogsPreviewAvailable - default: - return true - } - }, - [gitlessBranchingEnabled, advisorRulesEnabled, isUnifiedLogsPreviewAvailable] - ) - const selectedFeatureKey = ( - !selectedFeatureKeyFromQuery - ? FEATURE_PREVIEWS.filter((feature) => isFeaturePreviewReleasedToPublic(feature))[0].key - : selectedFeatureKeyFromQuery - ) as (typeof FEATURE_PREVIEWS)[number]['key'] + !selectedFeatureKeyFromQuery ? featurePreviews[0].key : selectedFeatureKeyFromQuery + ) as (typeof featurePreviews)[number]['key'] const selectFeaturePreview = useCallback( - (featureKey: (typeof FEATURE_PREVIEWS)[number]['key']) => { + (featureKey: (typeof featurePreviews)[number]['key']) => { setFeaturePreviewModal(featureKey) }, [setFeaturePreviewModal] ) - const openFeaturePreviewModal = useCallback(() => { - selectFeaturePreview(selectedFeatureKey) - }, [selectFeaturePreview, selectedFeatureKey]) - - const closeFeaturePreviewModal = useCallback(() => { - setFeaturePreviewModal(null) - }, [setFeaturePreviewModal]) - - const toggleFeaturePreviewModal = useCallback(() => { - if (showFeaturePreviewModal) { - closeFeaturePreviewModal() - } else { - openFeaturePreviewModal() - } - }, [showFeaturePreviewModal, openFeaturePreviewModal, closeFeaturePreviewModal]) + const toggleFeaturePreviewModal = useCallback( + (value: boolean) => { + if (!value) { + setFeaturePreviewModal(null) + } else { + selectFeaturePreview(selectedFeatureKey) + } + }, + [selectFeaturePreview, setFeaturePreviewModal, selectedFeatureKey] + ) return useMemo( () => ({ showFeaturePreviewModal, selectedFeatureKey, selectFeaturePreview, - openFeaturePreviewModal, - closeFeaturePreviewModal, toggleFeaturePreviewModal, - isFeaturePreviewReleasedToPublic, }), - [ - showFeaturePreviewModal, - selectedFeatureKey, - selectFeaturePreview, - openFeaturePreviewModal, - closeFeaturePreviewModal, - toggleFeaturePreviewModal, - isFeaturePreviewReleasedToPublic, - ] + [showFeaturePreviewModal, selectedFeatureKey, selectFeaturePreview, toggleFeaturePreviewModal] ) } diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index d3d8396389434..cbf5eb934fe4c 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -5,17 +5,29 @@ import { IS_PLATFORM } from 'lib/constants' import { ExternalLink, Eye, EyeOff, FlaskConical } from 'lucide-react' import Link from 'next/link' import { ReactNode } from 'react' -import { Badge, Button, cn, Modal, ScrollArea } from 'ui' +import { + Badge, + Button, + cn, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + ScrollArea, +} from 'ui' import { AdvisorRulesPreview } from './AdvisorRulesPreview' import { APISidePanelPreview } from './APISidePanelPreview' import { Branching2Preview } from './Branching2Preview' import { CLSPreview } from './CLSPreview' -import { FEATURE_PREVIEWS } from './FeaturePreview.constants' import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext' import { QueueOperationsPreview } from './QueueOperationsPreview' import { TableFilterBarPreview } from './TableFilterBarPreview' import { UnifiedLogsPreview } from './UnifiedLogsPreview' +import { useFeaturePreviews } from './useFeaturePreviews' const FEATURE_PREVIEW_KEY_TO_CONTENT: { [key: string]: ReactNode @@ -29,14 +41,14 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: { [LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR]: , } -const FeaturePreviewModal = () => { +export const FeaturePreviewModal = () => { const { ref } = useParams() + const featurePreviews = useFeaturePreviews() const { showFeaturePreviewModal, selectedFeatureKey, selectFeaturePreview, - closeFeaturePreviewModal, - isFeaturePreviewReleasedToPublic, + toggleFeaturePreviewModal, } = useFeaturePreviewModal() const { data: org } = useSelectedOrganizationQuery() const featurePreviewContext = useFeaturePreviewContext() @@ -44,12 +56,12 @@ const FeaturePreviewModal = () => { const { flags, onUpdateFlag } = featurePreviewContext const selectedFeature = - FEATURE_PREVIEWS.find((preview) => preview.key === selectedFeatureKey) ?? FEATURE_PREVIEWS[0] + featurePreviews.find((preview) => preview.key === selectedFeatureKey) ?? featurePreviews[0] const isSelectedFeatureEnabled = flags[selectedFeatureKey] const allFeaturePreviews = IS_PLATFORM - ? FEATURE_PREVIEWS - : FEATURE_PREVIEWS.filter((x) => !x.isPlatformOnly) + ? featurePreviews + : featurePreviews.filter((x) => !x.isPlatformOnly) const toggleFeature = () => { onUpdateFlag(selectedFeature.key, !isSelectedFeatureEnabled) @@ -61,90 +73,93 @@ const FeaturePreviewModal = () => { } return ( - - {FEATURE_PREVIEWS.length > 0 ? ( -
    -
    - - {allFeaturePreviews - .filter((feature) => isFeaturePreviewReleasedToPublic(feature)) - .map((feature) => { - const isEnabled = flags[feature.key] ?? false + + + + Dashboard feature previews + Get early access to new features and give feedback + - return ( -
    selectFeaturePreview(feature.key)} - className={cn( - 'flex items-center space-x-3 p-4 border-b cursor-pointer bg transition', - selectedFeature.key === feature.key ? 'bg-surface-300' : 'bg-surface-100' - )} - > - {isEnabled ? ( - - ) : ( - - )} -

    - {feature.name} -

    -
    - ) - })} -
    -
    -
    -
    -
    -

    {selectedFeature?.name}

    - {selectedFeature?.isNew && New} + + + + {featurePreviews.length > 0 ? ( +
    +
    + + {allFeaturePreviews.map((feature) => { + const isEnabled = flags[feature.key] ?? false + + return ( +
    selectFeaturePreview(feature.key)} + className={cn( + 'flex items-center justify-between p-4 border-b cursor-pointer bg transition', + selectedFeature.key === feature.key ? 'bg-surface-300' : 'bg-surface-100' + )} + > +
    + {isEnabled ? ( + + ) : ( + + )} +

    + {feature.name} +

    +
    + {feature.isNew && New} +
    + ) + })} +
    -
    - {selectedFeature?.discussionsUrl !== undefined && ( - - )} - +
    +
    +

    {selectedFeature?.name}

    +
    + {selectedFeature?.discussionsUrl !== undefined && ( + + )} + +
    +
    + {FEATURE_PREVIEW_KEY_TO_CONTENT[selectedFeature.key]}
    - {FEATURE_PREVIEW_KEY_TO_CONTENT[selectedFeature.key]} -
    -
    - ) : ( -
    - -
    -

    No feature previews available

    -

    - Have an idea for the dashboard? Let us know via GitHub Discussions! -

    -
    - -
    - )} - + ) : ( +
    + +
    +

    No feature previews available

    +

    + Have an idea for the dashboard? Let us know via GitHub Discussions! +

    +
    + +
    + )} + + + ) } - -export default FeaturePreviewModal diff --git a/apps/studio/components/interfaces/App/FeaturePreview/QueueOperationsPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/QueueOperationsPreview.tsx index 12c1450e7a78e..022bd257e2bef 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/QueueOperationsPreview.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/QueueOperationsPreview.tsx @@ -1,13 +1,19 @@ +import { useParams } from 'common' import { BASE_PATH } from 'lib/constants' import Image from 'next/image' +import { InlineLink } from '@/components/ui/InlineLink' + export const QueueOperationsPreview = () => { + const { ref = '_' } = useParams() + return (

    - Queue your table edits and review all pending changes before saving them to your database. - This gives you more control over when changes are committed, allowing you to batch multiple - edits and review them together. + Queue your table edits in the{' '} + Table Editor and review all pending + changes before saving them to your database. This gives you more control over when changes + are committed, allowing you to batch multiple edits and review them together.

    { + const { ref = '_' } = useParams() + return (

    - An intuitive new way to filter your table data. Build complex filters visually with support - for multiple data types (strings, numbers, dates, booleans) and operators. The new interface - makes it easier to understand and modify your filters at a glance. + An intuitive new way to filter your table data in the{' '} + Table Editor. Build complex filters + visually with support for multiple data types (strings, numbers, dates, booleans) and + operators. The new interface makes it easier to understand and modify your filters at a + glance.

    { - const { ref } = useParams() + const { ref = '_' } = useParams() return (
    - new-logs-preview

    Experience our enhanced Logs interface with improved filtering, real-time updates, and a unified view across all your services. Built for better performance and easier debugging. @@ -22,6 +15,15 @@ export const UnifiedLogsPreview = () => {

    This interface will only be available for organizations on the Team plan or above.

    + + new-logs-preview +

    Enabling this preview will:

      diff --git a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts new file mode 100644 index 0000000000000..f16028a0ac4ab --- /dev/null +++ b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts @@ -0,0 +1,87 @@ +import { LOCAL_STORAGE_KEYS, useFlag } from 'common' + +type FeaturePreview = { + key: string + name: string + discussionsUrl?: string + isNew: boolean + /** If feature flag is only relevant for the hosted platform */ + isPlatformOnly: boolean + /** If feature flag should be enabled by default for users, if not yet toggled before */ + isDefaultOptIn: boolean + /** Visibility in the feature preview modal (For feature flagging a feature preview) */ + enabled: boolean +} + +export const useFeaturePreviews = (): FeaturePreview[] => { + const gitlessBranchingEnabled = useFlag('gitlessBranching') + const advisorRulesEnabled = useFlag('advisorRules') + const isUnifiedLogsPreviewAvailable = useFlag('unifiedLogs') + const tableEditorNewFilterBar = useFlag('tableEditorNewFilterBar') + + return [ + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, + name: 'New Logs interface', + discussionsUrl: 'https://github.com/orgs/supabase/discussions/37234', + enabled: isUnifiedLogsPreviewAvailable, + isNew: false, + isPlatformOnly: true, + isDefaultOptIn: false, + }, + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0, + name: 'Branching via dashboard', + discussionsUrl: 'https://github.com/orgs/supabase/discussions/branching-2-0', + enabled: gitlessBranchingEnabled, + isNew: false, + isPlatformOnly: true, + isDefaultOptIn: false, + }, + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES, + name: 'Disable Advisor rules', + discussionsUrl: undefined, + enabled: advisorRulesEnabled, + isNew: false, + isPlatformOnly: true, + isDefaultOptIn: false, + }, + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL, + name: 'Project API documentation', + discussionsUrl: 'https://github.com/orgs/supabase/discussions/18038', + enabled: true, + isNew: false, + isPlatformOnly: false, + isDefaultOptIn: false, + }, + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS, + name: 'Column-level privileges', + discussionsUrl: 'https://github.com/orgs/supabase/discussions/20295', + enabled: true, + isNew: false, + isPlatformOnly: false, + isDefaultOptIn: false, + }, + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_QUEUE_OPERATIONS, + name: 'Queue table operations', + discussionsUrl: 'https://github.com/orgs/supabase/discussions/42460', + enabled: true, + isNew: true, + isPlatformOnly: false, + isDefaultOptIn: false, + }, + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR, + name: 'New Table Filter Bar', + discussionsUrl: 'https://github.com/orgs/supabase/discussions/42461', + enabled: true, + isNew: true, + isPlatformOnly: false, + isDefaultOptIn: tableEditorNewFilterBar, + }, + ].sort((a, b) => Number(b.isNew) - Number(a.isNew)) +} diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx index 5efc952762226..96de1a741d866 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx @@ -1,12 +1,12 @@ -import { isEmpty, noop } from 'lodash' -import { useCallback, useEffect, useState } from 'react' -import { toast } from 'sonner' - import { useFeaturePreviewModal } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import useLatest from 'hooks/misc/useLatest' import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { isEmpty, noop } from 'lodash' +import { useCallback, useEffect, useState } from 'react' +import { toast } from 'sonner' import { Modal } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' + import { POLICY_MODAL_VIEWS } from '../Policies.constants' import { PolicyFormField, @@ -114,7 +114,7 @@ const PolicyEditorModal = ({ /* Methods that are for the UI */ const onToggleFeaturePreviewModal = () => { - toggleFeaturePreviewModal() + toggleFeaturePreviewModal(true) onSelectCancel() } diff --git a/apps/studio/components/interfaces/LocalDropdown.tsx b/apps/studio/components/interfaces/LocalDropdown.tsx index d227a9ff4df11..c4950b00cde51 100644 --- a/apps/studio/components/interfaces/LocalDropdown.tsx +++ b/apps/studio/components/interfaces/LocalDropdown.tsx @@ -1,7 +1,6 @@ import { ProfileImage } from 'components/ui/ProfileImage' import { Command, FlaskConical } from 'lucide-react' import { useTheme } from 'next-themes' - import { Button, DropdownMenu, @@ -17,12 +16,13 @@ import { Theme, } from 'ui' import { useSetCommandMenuOpen } from 'ui-patterns' + import { useFeaturePreviewModal } from './App/FeaturePreview/FeaturePreviewContext' export const LocalDropdown = () => { const { theme, setTheme } = useTheme() const setCommandMenuOpen = useSetCommandMenuOpen() - const { openFeaturePreviewModal } = useFeaturePreviewModal() + const { toggleFeaturePreviewModal } = useFeaturePreviewModal() return ( @@ -37,8 +37,8 @@ export const LocalDropdown = () => { toggleFeaturePreviewModal(true)} + onSelect={() => toggleFeaturePreviewModal(true)} > Feature previews diff --git a/apps/studio/components/interfaces/UserDropdown.tsx b/apps/studio/components/interfaces/UserDropdown.tsx index 2e1a940f1968d..2a32c009cb1e0 100644 --- a/apps/studio/components/interfaces/UserDropdown.tsx +++ b/apps/studio/components/interfaces/UserDropdown.tsx @@ -2,7 +2,7 @@ import { ProfileImage } from 'components/ui/ProfileImage' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { IS_PLATFORM } from 'lib/constants' import { useProfileNameAndPicture } from 'lib/profile' -import { Command, FlaskConical, Loader2, ScrollText, Settings } from 'lucide-react' +import { FlaskConical, Loader2, ScrollText, Settings } from 'lucide-react' import { useTheme } from 'next-themes' import Link from 'next/link' import { useRouter } from 'next/router' @@ -21,7 +21,6 @@ import { singleThemes, Theme, } from 'ui' -import { useCommandMenuOpenedTelemetry, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu' import { useFeaturePreviewModal } from './App/FeaturePreview/FeaturePreviewContext' @@ -32,14 +31,7 @@ export function UserDropdown() { const profileShowEmailEnabled = useIsFeatureEnabled('profile:show_email') const { username, avatarUrl, primaryEmail, isLoading } = useProfileNameAndPicture() - const setCommandMenuOpen = useSetCommandMenuOpen() - const sendTelemetry = useCommandMenuOpenedTelemetry() - const { openFeaturePreviewModal } = useFeaturePreviewModal() - - const handleCommandMenuOpen = () => { - setCommandMenuOpen(true) - sendTelemetry() - } + const { toggleFeaturePreviewModal } = useFeaturePreviewModal() return ( @@ -95,8 +87,8 @@ export function UserDropdown() { toggleFeaturePreviewModal(true)} + // onSelect={() => toggleFeaturePreviewModal(true)} > Feature previews diff --git a/apps/studio/components/layouts/AdvisorsLayout/AdvisorRulesLayout.tsx b/apps/studio/components/layouts/AdvisorsLayout/AdvisorRulesLayout.tsx index a26200f858e01..7978435a94a28 100644 --- a/apps/studio/components/layouts/AdvisorsLayout/AdvisorRulesLayout.tsx +++ b/apps/studio/components/layouts/AdvisorsLayout/AdvisorRulesLayout.tsx @@ -1,6 +1,6 @@ +import { useParams } from 'common' import { PropsWithChildren } from 'react' -import { useParams } from 'common' import DefaultLayout from '../DefaultLayout' import { PageLayout } from '../PageLayout/PageLayout' import AdvisorsLayout from './AdvisorsLayout' diff --git a/apps/studio/components/layouts/DefaultLayout.tsx b/apps/studio/components/layouts/DefaultLayout.tsx index 3b838766968c3..01fd1703ffc87 100644 --- a/apps/studio/components/layouts/DefaultLayout.tsx +++ b/apps/studio/components/layouts/DefaultLayout.tsx @@ -1,13 +1,15 @@ -import { useRouter } from 'next/router' -import { PropsWithChildren } from 'react' - import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { AppBannerWrapper } from 'components/interfaces/App/AppBannerWrapper' import { Sidebar } from 'components/interfaces/Sidebar' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useCheckLatestDeploy } from 'hooks/use-check-latest-deploy' +import { useRouter } from 'next/router' +import { PropsWithChildren } from 'react' import { useAppStateSnapshot } from 'state/app-state' import { ResizablePanel, ResizablePanelGroup, SidebarProvider } from 'ui' + +import { BannerStack } from '../ui/BannerStack/BannerStack' +import { BannerStackProvider } from '../ui/BannerStack/BannerStackProvider' import { LayoutHeader } from './ProjectLayout/LayoutHeader/LayoutHeader' import { LayoutSidebar } from './ProjectLayout/LayoutSidebar' import { LayoutSidebarProvider } from './ProjectLayout/LayoutSidebar/LayoutSidebarProvider' @@ -60,48 +62,52 @@ export const DefaultLayout = ({ -
      - {/* 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') + }} + > +
      +
      + + Preview + +
      + +
      +

      + {text} +

      +
      +
      +
      +
      +

      New Table Filter Bar

      +

      + Build and modify complex filters visually +

      +
      + +
      +
      + ) +} 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/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.
      -
      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/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() { From 3fcb6cbe2cd41693447c27879700bba83e96a16d Mon Sep 17 00:00:00 2001 From: Matt Rossman <22670878+mattrossman@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:36:06 -0500 Subject: [PATCH 2/7] fix(studio): pass providerOptions to all AI SDK calls (#43031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `reasoningEffort: 'minimal'` was [configured](https://github.com/supabase/supabase/blob/d5cc70560d/apps/studio/lib/ai/model.utils.ts#L55-L59) in the provider registry but `getModel()` returns it as a separate value that callers must destructure and forward — and 7 of 8 endpoints weren't doing so. This meant `gpt-5-mini` (a reasoning model) was running at default reasoning effort for every call. This PR destructures `providerOptions` from `getModel()` and passes it to `generateObject`/`generateText` in all affected endpoints. ## Benchmark (local, median of 5 runs) | Endpoint | Before (s) | After (s) | Speedup | |----------|-----------|----------|---------| | title-v2 | 7.0 | 1.9 | 3.7x | | cron-v2 | 2.3 | 0.9 | 2.6x | | filter-v1 | 5.8 | 2.2 | 2.6x | | feedback/classify | 3.5 | 0.9 | 3.9x | | feedback/rate | 2.9 | 0.9 | 3.2x | `code/complete` and `policy` also received the fix but aren't benchmarked here as they require a live DB connection and use multi-step tool calls (separate latency concern tracked in AI-419). To test the SQL naming, visit the SQL Editor in sidebar, add some SQL like: ```sql create table todos ( id serial primary key, task text not null, completed boolean default false ); ``` Right click on the snippet, "Rename" and "Rename with Supabase AI" Closes AI-443 --- apps/studio/pages/api/ai/code/complete.ts | 2 ++ apps/studio/pages/api/ai/feedback/classify.ts | 11 ++++++++--- apps/studio/pages/api/ai/feedback/rate.ts | 7 ++++++- apps/studio/pages/api/ai/sql/cron-v2.ts | 12 ++++++++---- apps/studio/pages/api/ai/sql/filter-v1.ts | 10 +++++++--- apps/studio/pages/api/ai/sql/policy.ts | 14 +++++++++----- apps/studio/pages/api/ai/sql/title-v2.ts | 12 ++++++++---- 7 files changed, 48 insertions(+), 20 deletions(-) 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: From ad88746d3f1aad6a8bf04346a0bf02a8640097cf Mon Sep 17 00:00:00 2001 From: Ignacio Dobronich Date: Fri, 20 Feb 2026 14:16:11 -0300 Subject: [PATCH 3/7] fix: remove entitlement checks from realtime settings (#43052) The Realtime configurable values are not hard limits, so we shouldn't model them as entitlements in the Realtime Settings. --- .../interfaces/Realtime/RealtimeSettings.tsx | 32 +++---------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx index a725664e6f352..f235598abe9ca 100644 --- a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx +++ b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx @@ -20,7 +20,6 @@ import { import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements' import { Button, Card, @@ -90,38 +89,15 @@ export const RealtimeSettings = () => { }, }) - const { getEntitlementMax: getEntitledMaxPayloadSize } = useCheckEntitlements( - 'realtime.max_payload_size_in_kb' - ) - const entitledMaxPayloadSize = getEntitledMaxPayloadSize() ?? 3000 - - const { getEntitlementMax: getEntitledMaxConcurrentUsers } = useCheckEntitlements( - 'realtime.max_concurrent_users' - ) - const entitledMaxConcurrentUsers = getEntitledMaxConcurrentUsers() ?? 50000 - - const { getEntitlementMax: getEntitledMaxEventsPerSecond } = useCheckEntitlements( - 'realtime.max_events_per_second' - ) - const entitledMaxEventsPerSecond = getEntitledMaxEventsPerSecond() ?? 10000 - - const { getEntitlementMax: getEntitledMaxPresenceEventsPerSecond } = useCheckEntitlements( - 'realtime.max_presence_events_per_second' - ) - const entitledMaxPresenceEventsPerSecond = getEntitledMaxPresenceEventsPerSecond() ?? 10000 - const FormSchema = z.object({ connection_pool: z.coerce .number() .min(1) .max(maxConn?.maxConnections ?? 100), - max_concurrent_users: z.coerce.number().min(1).max(entitledMaxConcurrentUsers), - max_events_per_second: z.coerce.number().min(1).max(entitledMaxEventsPerSecond), - max_presence_events_per_second: z.coerce - .number() - .min(1) - .max(entitledMaxPresenceEventsPerSecond), - max_payload_size_in_kb: z.coerce.number().min(1).max(entitledMaxPayloadSize), + max_concurrent_users: z.coerce.number().min(1).max(50000), + max_events_per_second: z.coerce.number().min(1).max(10000), + max_presence_events_per_second: z.coerce.number().min(1).max(10000), + max_payload_size_in_kb: z.coerce.number().min(1).max(3000), suspend: z.boolean(), // [Joshen] These fields are temporarily hidden from the UI // max_bytes_per_second: z.coerce.number().min(1).max(10000000), From a7e0a428feb224e29ed8132556d361ad30eb9e2a Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 20 Feb 2026 19:23:36 +0100 Subject: [PATCH 4/7] fix: improve error handling for upload cases in storage explorer (#43054) This pull request refactors and improves error handling in the `createStorageExplorerState` function within `apps/studio/state/storage-explorer.tsx`. The changes make the switch statement more robust and readable by introducing block scoping for each case and handling additional error scenarios with more specific messages. * Refactored the switch statement to use block scoping (`{}`) for each case, improving readability and preventing variable leakage between cases. * Enhanced the handling of HTTP 400 errors by checking the response body for specific error messages, and displaying more precise error toasts for "Invalid key" and "Invalid Compact JWS" cases. * Added a default case to the switch statement to catch and display any other error messages not explicitly handled, ensuring users receive feedback for unexpected errors. --- apps/studio/state/storage-explorer.tsx | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) 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}`) From 0835e49a34778275777799fe5100d6fa4ccb4750 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Fri, 20 Feb 2026 12:28:29 -0700 Subject: [PATCH 5/7] fix: updated project name truncation (#43060) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix: project name truncation if its to long --- .../interfaces/Home/ProjectList/ProjectCard.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx b/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx index 679f34995aa39..226379f7557c9 100644 --- a/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx +++ b/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx @@ -3,7 +3,7 @@ import CardButton from 'components/ui/CardButton' import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper' import type { IntegrationProjectConnection } from 'data/integrations/integrations.types' import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref' -import { OrgProject, getComputeSize } from 'data/projects/org-projects-infinite-query' +import { getComputeSize, OrgProject } from 'data/projects/org-projects-infinite-query' import type { ResourceWarning } from 'data/usage/resource-warnings-query' import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' @@ -15,11 +15,11 @@ import { toast } from 'sonner' import type { Organization } from 'types' import { Button, + copyToClipboard, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - copyToClipboard, } from 'ui' import { inferProjectStatus } from './ProjectCard.utils' @@ -116,7 +116,7 @@ export const ProjectCard = ({

      {desc}

    -
    +
    )} {isGithubIntegrated && ( -
    -
    +
    +

    {githubRepository}

    From cff64ea1a96daa9a1f74a20a20dca48fe8515ec2 Mon Sep 17 00:00:00 2001 From: Jeremias Menichelli Date: Fri, 20 Feb 2026 20:45:30 +0100 Subject: [PATCH 6/7] fix(ui): Make heading anchors focusable (#43058) --- packages/ui/src/components/CustomHTMLElements/Heading.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 && ( )} From 571e0603c09f5a8a941ce4334e34a5793588f368 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Fri, 20 Feb 2026 13:31:26 -0700 Subject: [PATCH 7/7] fix: E2E tests that are flaky due banner (#43062) --- e2e/studio/features/table-editor.spec.ts | 76 +++++++++++++++++------- e2e/studio/utils/test.ts | 10 +++- 2 files changed, 65 insertions(+), 21 deletions(-) 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) + }, })