mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 11:55:05 +03:00
## Problem The redesigned pipeline creation flow (mega branch being worked on in #49243) needs an incremental rollout while the existing creation sheet remains the default. ## Solution - Register **Pipeline creation wizard** in Feature Previews, with visibility controlled by the `pipelineCreationWizard` ConfigCat flag. - Require the flag and an explicit user opt-in (via Feature previews dialog) before enabling the preview. This adds the preview registration, copy, storage key, and gate. It does not change creation routing or ship the wizard. The complete gated flow can be tested on #49243. The ConfigCat flag is configured as follows: - **Local/staging:** true for everyone. - **Production:** false by default, true for the **Supabase Team Email** segment. The flag makes the preview visible; it does not opt anyone in. Users enable it once in Feature Previews. No dev toolbar override is needed in local/staging. ## Review instructions 1. In local or staging hosted Studio, open **Feature previews**. Find **Pipeline creation wizard**. With no saved preference, it should be off by default. 2. Toggle it on and reload. The opt-in should persist. Toggle it off and reload to confirm opt-out persists. **Add pipeline** still opens the sheet in this foundation PR.
229 lines
7.6 KiB
TypeScript
229 lines
7.6 KiB
TypeScript
import {
|
|
FeatureFlagContext,
|
|
LOCAL_STORAGE_KEYS,
|
|
safeLocalStorage,
|
|
useFlag,
|
|
useParams,
|
|
} 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 { useProjectStorageConfigQuery } from '@/data/config/project-storage-config-query'
|
|
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 usePipelineCreationPreview = () => {
|
|
const { flags, isInitialized } = useFeaturePreviewContext()
|
|
const isAvailable = useFlag('pipelineCreationWizard')
|
|
const isEnabled =
|
|
IS_PLATFORM &&
|
|
isInitialized &&
|
|
isAvailable &&
|
|
flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_PIPELINE_CREATION] === true
|
|
|
|
return { isEnabled, isLoading: !isInitialized }
|
|
}
|
|
|
|
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]
|
|
}
|
|
|
|
/** Three gates: the ConfigCat kill switch, the project's capability, and the user's opt-in. */
|
|
export const useIsStorageVersioningEnabled = () => {
|
|
const { ref } = useParams()
|
|
const { flags } = useFeaturePreviewContext()
|
|
const isStorageVersioningEnabled = useFlag('storageVersioningPrivateAlpha')
|
|
const { data } = useProjectStorageConfigQuery({ projectRef: ref })
|
|
const isObjectVersioningAvailable = !!data?.capabilities?.object_versioning
|
|
return (
|
|
isStorageVersioningEnabled &&
|
|
isObjectVersioningAvailable &&
|
|
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]
|
|
)
|
|
}
|