Files
supabase__supabase/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx
Monica Khoury cb21b89899 feat: make feature previews easier to discover (#50086)
## What kind of change does this PR introduce?

Feature previews are only discoverable if you already know to look for
the "Feature previews" item under the account profile dropdown. There's
no way to find or toggle them from Cmd+K.

Fixes:
[FE-4305](https://linear.app/supabase/issue/FE-4305/make-feature-previews-easier-to-discover).

## What is the new behavior?

- Adds a "Feature previews..." command to Cmd+K that opens a drill-down
page listing all available previews, grouped by category (mirroring the
existing settings modal's grouping via a new shared
useVisibleFeaturePreviewsByCategory hook, so the two can't drift apart).
- Each preview can be toggled on/off directly from the list, with a
"New" badge for new previews and a "Default" badge + hint for previews
that can no longer be turned off.
- Each preview also has a hidden "View details" command (surfaces via
search) that opens the full settings modal to that preview's
description/discussion link.

## Heads up for reviewers/testing

Enabling a feature preview only navigates to its page when you're
already within a project route in the URL (`/project/<ref>/...`). In the
preview environment, there’s no project in scope, so enabling the
feature still works and updates the flag, but it won't navigate to the
feature's project-specific page.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - Added feature preview pages and actions to the command menu.
  - Feature previews are organized by category for easier browsing.
- Enabling a preview with a dedicated page takes you directly to that
page.
- Preview activation and deactivation now provide status notifications.

- **Bug Fixes**
- Preview toggles and project navigation no longer unexpectedly return
users to the root command menu.
- Only relevant, available previews are shown based on the current
environment.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-21 17:52:15 +03:00

202 lines
6.8 KiB
TypeScript

import { FeatureFlagContext, LOCAL_STORAGE_KEYS, safeLocalStorage, useFlag } from 'common'
import { noop } from 'lodash'
import { useQueryState } from 'nuqs'
import {
createContext,
useCallback,
useContext,
useEffect,
useEffectEvent,
useMemo,
useState,
type PropsWithChildren,
} from 'react'
import { useFeaturePreviews } from './useFeaturePreviews'
import { IS_PLATFORM } from '@/lib/constants'
import { EMPTY_OBJ } from '@/lib/void'
type FeaturePreviewContextType = {
flags: { [key: string]: boolean }
isInitialized: boolean
onUpdateFlag: (key: string, value: boolean) => void
}
const FeaturePreviewContext = createContext<FeaturePreviewContextType>({
flags: EMPTY_OBJ,
isInitialized: false,
onUpdateFlag: noop,
})
export const useFeaturePreviewContext = () => useContext(FeaturePreviewContext)
export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren) => {
const { hasLoaded } = useContext(FeatureFlagContext)
const featurePreviews = useFeaturePreviews()
const [flags, setFlags] = useState(() =>
featurePreviews.reduce((a, b) => ({ ...a, [b.key]: false }), {})
)
// Tracks whether `flags` reflects the loaded feature flags (vs. the pre-load
// defaults). Only set true once `initializeFlags` runs with `hasLoaded`.
const [isInitialized, setIsInitialized] = useState(false)
const initializeFlags = useEffectEvent(() => {
setFlags(
featurePreviews.reduce((a, b) => {
// Platform-only previews can never be enabled outside the hosted platform
if (!IS_PLATFORM && b.isPlatformOnly) {
return { ...a, [b.key]: false }
}
// A forced preview has become the default behavior, so it's on whatever
// the user stored previously — including an explicit opt-out. Applied
// here rather than in the individual `useIsXEnabled` helpers so that the
// feature preview modal reflects it too.
if (b.isForced) {
return { ...a, [b.key]: true }
}
const defaultOptIn = b.isDefaultOptIn
const localStorageValue = safeLocalStorage.getItem(b.key)
return {
...a,
[b.key]: !localStorageValue ? defaultOptIn : localStorageValue === 'true',
}
}, {})
)
})
useEffect(() => {
if (typeof window !== 'undefined') {
initializeFlags()
// Defer marking initialized until the underlying flags have loaded, so
// flag-derived defaults (e.g. default opt-in) are reflected in `flags`.
if (hasLoaded) setIsInitialized(true)
}
}, [hasLoaded])
const value = {
flags,
isInitialized,
onUpdateFlag: (key: string, value: boolean) => {
safeLocalStorage.setItem(key, value ? 'true' : 'false')
setFlags((prevFlags) => ({ ...prevFlags, [key]: value }))
},
}
return <FeaturePreviewContext.Provider value={value}>{children}</FeaturePreviewContext.Provider>
}
// Helpers
export const useIsColumnLevelPrivilegesEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]
}
export const useUnifiedLogsPreview = () => {
const { flags, isInitialized, onUpdateFlag } = useFeaturePreviewContext()
const isLoading = !isInitialized
const isEnabled = IS_PLATFORM && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]
const hasToggledPreview = !!safeLocalStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS)
const isDefaultOptIn = IS_PLATFORM && !hasToggledPreview
const enable = () => onUpdateFlag(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, true)
const disable = () => onUpdateFlag(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, false)
return { isEnabled, isLoading, isDefaultOptIn, enable, disable }
}
export const useIsPgDeltaDiffEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_PG_DELTA_DIFF]
}
export const useIsAdvisorRulesEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES]
}
export const useIsPlatformWebhooksEnabled = () => {
const { flags } = useFeaturePreviewContext()
const platformWebhooksEnabled = useFlag('platformWebhooks')
return platformWebhooksEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]
}
export const useIsJitDbAccessEnabled = () => {
const { flags } = useFeaturePreviewContext()
const jitDbAccessEnabled = useFlag('jitDbAccess')
return jitDbAccessEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]
}
/**
* Whether the SQL Editor saves snippets only on request. True when the user
* opted into the preview themselves *or* the `sqlEditorManualSaveForced` rollout
* has reached them — the preview's `isForced` flag folds the latter into `flags`.
*/
export const useIsSqlEditorManualSaveEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]
}
export const useIsMarketplaceEnabled = () => {
const { flags } = useFeaturePreviewContext()
const isMarketplaceEnabled = useFlag('marketplaceIntegrations')
return isMarketplaceEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]
}
export const useIsExplorerEnabled = () => {
const { flags } = useFeaturePreviewContext()
const isExplorerEnabled = useFlag('explorer')
return isExplorerEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_EXPLORER]
}
export const useIsStorageVersioningEnabled = () => {
const { flags } = useFeaturePreviewContext()
const isStorageVersioningEnabled = useFlag('storageVersioningPrivateAlpha')
return isStorageVersioningEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_STORAGE_VERSIONING]
}
export const useFeaturePreviewModal = () => {
const featurePreviews = useFeaturePreviews()
const [featurePreviewModal, setFeaturePreviewModal] = useQueryState('featurePreviewModal')
const selectedFeatureKeyFromQuery = featurePreviewModal?.trim() ?? null
const showFeaturePreviewModal = selectedFeatureKeyFromQuery !== null
const selectedFeatureKey = (
!selectedFeatureKeyFromQuery ? featurePreviews[0].key : selectedFeatureKeyFromQuery
) as (typeof featurePreviews)[number]['key']
const selectFeaturePreview = useCallback(
(featureKey: (typeof featurePreviews)[number]['key']) => {
setFeaturePreviewModal(featureKey)
},
[setFeaturePreviewModal]
)
const toggleFeaturePreviewModal = useCallback(
(value: boolean) => {
if (!value) {
setFeaturePreviewModal(null)
} else {
selectFeaturePreview(selectedFeatureKey)
}
},
[selectFeaturePreview, setFeaturePreviewModal, selectedFeatureKey]
)
return useMemo(
() => ({
showFeaturePreviewModal,
selectedFeatureKey,
selectFeaturePreview,
toggleFeaturePreviewModal,
}),
[showFeaturePreviewModal, selectedFeatureKey, selectFeaturePreview, toggleFeaturePreviewModal]
)
}