mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
## 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? - Noticing our code we have many patterns of calling localstorage and handling those errors - We should add those in a single well tested file - Handle those errors in the singleton which makes it easier for us to debug customer issues. Logger is outputing local storage warnings for feature we expose - Side effect of this is random crashes on studio when local storage isn't available or handled correctly <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Improved browser storage handling across the app for more reliable persistence and graceful behavior in restricted or non-browser environments (settings, previews, charts, tabs, sign-in/session flows, integrations, and UI state). * **New Features** * Introduced a safe storage layer to standardize and harden local/session persistence. * **Tests** * Added comprehensive tests covering the new safe storage behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
162 lines
4.4 KiB
TypeScript
162 lines
4.4 KiB
TypeScript
import { safeLocalStorage } from 'common'
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
|
|
interface ChartHoverState {
|
|
hoveredIndex: number | null
|
|
hoveredChart: string | null
|
|
syncHover: boolean
|
|
syncTooltip: boolean
|
|
}
|
|
|
|
const CHART_HOVER_SYNC_STORAGE_KEY = 'supabase-chart-hover-sync-enabled'
|
|
const CHART_TOOLTIP_SYNC_STORAGE_KEY = 'supabase-chart-tooltip-sync-enabled'
|
|
|
|
// Global state shared across all hook instances
|
|
let globalState: ChartHoverState = {
|
|
hoveredIndex: null,
|
|
hoveredChart: null,
|
|
syncHover: true,
|
|
syncTooltip: true,
|
|
}
|
|
|
|
// Subscribers for state changes
|
|
const subscribers = new Set<(state: ChartHoverState) => void>()
|
|
|
|
// Load initial sync settings from localStorage. safeLocalStorage handles SSR and
|
|
// unavailable storage; the try/catch only guards JSON.parse against bad values.
|
|
try {
|
|
const hoverSyncStored = safeLocalStorage.getItem(CHART_HOVER_SYNC_STORAGE_KEY)
|
|
const tooltipSyncStored = safeLocalStorage.getItem(CHART_TOOLTIP_SYNC_STORAGE_KEY)
|
|
|
|
if (hoverSyncStored !== null) {
|
|
globalState.syncHover = JSON.parse(hoverSyncStored)
|
|
}
|
|
if (tooltipSyncStored !== null) {
|
|
globalState.syncTooltip = JSON.parse(tooltipSyncStored)
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to parse chart sync settings from localStorage:', error)
|
|
}
|
|
|
|
function notifySubscribers() {
|
|
subscribers.forEach((callback) => callback(globalState))
|
|
}
|
|
|
|
function updateGlobalState(updates: Partial<ChartHoverState>) {
|
|
const prevState = globalState
|
|
globalState = { ...globalState, ...updates }
|
|
|
|
// Save sync settings to localStorage when they change
|
|
if (updates.syncHover !== undefined) {
|
|
safeLocalStorage.setItem(CHART_HOVER_SYNC_STORAGE_KEY, JSON.stringify(globalState.syncHover))
|
|
}
|
|
if (updates.syncTooltip !== undefined) {
|
|
safeLocalStorage.setItem(
|
|
CHART_TOOLTIP_SYNC_STORAGE_KEY,
|
|
JSON.stringify(globalState.syncTooltip)
|
|
)
|
|
}
|
|
|
|
// Only notify if state actually changed
|
|
if (JSON.stringify(prevState) !== JSON.stringify(globalState)) {
|
|
notifySubscribers()
|
|
}
|
|
}
|
|
|
|
export function useChartHoverState(chartId: string) {
|
|
const [state, setState] = useState<ChartHoverState>(globalState)
|
|
|
|
// Subscribe to global state changes
|
|
useEffect(() => {
|
|
const callback = (newState: ChartHoverState) => {
|
|
setState(newState)
|
|
}
|
|
|
|
subscribers.add(callback)
|
|
|
|
return () => {
|
|
subscribers.delete(callback)
|
|
}
|
|
}, [])
|
|
|
|
// Set hover state for this chart
|
|
const setHover = useCallback(
|
|
(index: number | null) => {
|
|
if (globalState.syncHover) {
|
|
// If sync is enabled, update global state
|
|
updateGlobalState({
|
|
hoveredIndex: index,
|
|
hoveredChart: index !== null ? chartId : null,
|
|
})
|
|
} else {
|
|
// If sync is disabled, only update local state
|
|
setState((prev) => ({
|
|
...prev,
|
|
hoveredIndex: index,
|
|
hoveredChart: index !== null ? chartId : null,
|
|
}))
|
|
}
|
|
},
|
|
[chartId]
|
|
)
|
|
|
|
// Clear hover state
|
|
const clearHover = useCallback(() => {
|
|
if (globalState.syncHover) {
|
|
updateGlobalState({
|
|
hoveredIndex: null,
|
|
hoveredChart: null,
|
|
})
|
|
} else {
|
|
setState((prev) => ({
|
|
...prev,
|
|
hoveredIndex: null,
|
|
hoveredChart: null,
|
|
}))
|
|
}
|
|
}, [])
|
|
|
|
// Set sync settings (for settings component)
|
|
const setSyncHover = useCallback((enabled: boolean) => {
|
|
updateGlobalState({
|
|
syncHover: enabled,
|
|
// If turning off hover sync, also turn off tooltip sync
|
|
...(enabled === false && { syncTooltip: false }),
|
|
})
|
|
}, [])
|
|
|
|
const setSyncTooltip = useCallback((enabled: boolean) => {
|
|
updateGlobalState({
|
|
syncTooltip: enabled,
|
|
// If turning on tooltip sync, also turn on hover sync
|
|
...(enabled === true && { syncHover: true }),
|
|
})
|
|
}, [])
|
|
|
|
// Determine if this chart should show synced state
|
|
const isCurrentChart = state.hoveredChart === chartId
|
|
const shouldShowSyncedState = state.syncHover && state.hoveredChart !== null && !isCurrentChart
|
|
|
|
return {
|
|
// Current state
|
|
hoveredIndex: shouldShowSyncedState
|
|
? state.hoveredIndex
|
|
: isCurrentChart
|
|
? state.hoveredIndex
|
|
: null,
|
|
syncHover: state.syncHover,
|
|
syncTooltip: state.syncTooltip,
|
|
hoveredChart: state.hoveredChart,
|
|
|
|
// Actions
|
|
setHover,
|
|
clearHover,
|
|
setSyncHover,
|
|
setSyncTooltip,
|
|
|
|
// Helpers
|
|
isHovered: state.hoveredIndex !== null && (isCurrentChart || shouldShowSyncedState),
|
|
isCurrentChart,
|
|
}
|
|
}
|