Files
supabase/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx
Danny White b3edaa02f0 feat(studio): remove unified logs banner and deprioritise tos banner (#49239)
## What kind of change does this PR introduce?

Studio UI cleanup for sidebar BannerStack items and Unified Logs preview
defaults.

## What is the current behavior?

The sidebar BannerStack shows both a Unified Logs promo banner and a
Terms of Service update notice. Unified Logs has been default opt-in for
a while, and the ToS banner currently shares priority with other
higher-value notices. The default opt-in behaviour is still gated behind
the `unifiedLogsDefaultOptIn` feature flag.

Closes
[DEPR-646](https://linear.app/supabase/issue/DEPR-646/remove-unified-logs-banner-and-deprioritise-tos-banner).

| Before |
| --- |
| <img width="1024" height="759" alt="5717"
src="https://github.com/user-attachments/assets/1a19e6f6-0c7f-49e8-8e7e-9f83196b2353"
/> |

## What is the new behavior?

- Removes the Unified Logs BannerStack item and its component
- Keeps the ToS update banner but lowers its priority so other banners
surface first
- Sets Unified Logs `isDefaultOptIn` to `true` and removes
`unifiedLogsDefaultOptIn` flag usage

## To test

- Open any project in Studio (e.g. `/project/<ref>`)
- Confirm the sidebar BannerStack no longer shows the "Unified Logs is
here" banner
- If you have not dismissed the ToS notice and it is still before the
expiry date, confirm it still appears but sits behind higher-priority
banners (e.g. free micro upgrade on eligible projects)
- Open `/project/<ref>/logs` and confirm Unified Logs loads by default
for users who have not previously toggled the preview off

## After merge

- [ ] Retire the `unifiedLogsDefaultOptIn` PostHog flag

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

- **New Features**
- Unified Logs preview is now enabled by default when available, while
preserving individual user choices.

- **Bug Fixes**
- Terms of Service update notifications now appear with higher priority.

- **Changes**
- Removed the Unified Logs promotional banner, including related
navigation, dismissal, and tracking behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-19 17:37:14 +10:00

207 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 { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
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')
const updatedFlags = { ...flags, [key]: value }
setFlags(updatedFlags)
},
}
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 useIsDatabaseConnectionsEnabled = () => {
const { flags, isInitialized } = useFeaturePreviewContext()
const [localStorageFlag] = useLocalStorageQuery<boolean | null>(
LOCAL_STORAGE_KEYS.UI_PREVIEW_DATABASE_CONNECTIONS,
null
)
const previouslyToggled = localStorageFlag !== null
return {
enabled: flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_DATABASE_CONNECTIONS],
isInitialized,
previouslyToggled,
}
}
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]
)
}