mirror of
https://github.com/supabase/supabase.git
synced 2026-05-09 10:19:50 -04:00
3cb440e844
## Summary - [**SUPABASE-APP-E2R**](https://supabase.sentry.io/issues/SUPABASE-APP-E2R): Guard against undefined entries in notifications array in `AdvisorButton` (optional chaining on `.some()` callbacks) - [**SUPABASE-APP-EBA**](https://supabase.sentry.io/issues/SUPABASE-APP-EBA): Remove render-time `handleError()` throw in `useEdgeFunctionsDiff` — the hook already handles missing body data gracefully - [**SUPABASE-APP-BVN**](https://supabase.sentry.io/issues/SUPABASE-APP-BVN) / [**SUPABASE-APP-BTV**](https://supabase.sentry.io/issues/SUPABASE-APP-BTV): Guard `localStorage` access in `FeaturePreviewContext` with try-catch, matching the established pattern in `useLocalStorage.ts` (Safari private browsing) - [**SUPABASE-APP-AV3**](https://supabase.sentry.io/issues/SUPABASE-APP-AV3): Filter stale folder IDs before passing `expandedIds` to `react-accessible-treeview` in the SQL editor nav ## Test plan - [x] Verify AdvisorButton renders without errors when notifications data has sparse pages - [x] Verify branch merge page loads when edge function body fetch fails - [x] Verify feature previews initialize correctly in Safari private browsing - [x] Verify SQL editor folder expand/collapse works after deleting a folder <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Feature preview now falls back safely when browser storage is unavailable * Notifications display updated to tolerate missing entries without errors * Private snippets navigation no longer preserves expansion state for removed nodes * **Refactor** * Streamlined error aggregation in edge functions diff processing <!-- end of auto-generated comment: release notes by coderabbit.ai -->
285 lines
8.8 KiB
TypeScript
285 lines
8.8 KiB
TypeScript
import { basename } from 'path'
|
|
import { useQueries, useQueryClient } from '@tanstack/react-query'
|
|
import { useCallback, useMemo } from 'react'
|
|
|
|
import {
|
|
getEdgeFunctionBody,
|
|
type EdgeFunctionBodyData,
|
|
} from '@/data/edge-functions/edge-function-body-query'
|
|
import {
|
|
useEdgeFunctionsQuery,
|
|
type EdgeFunctionsData,
|
|
} from '@/data/edge-functions/edge-functions-query'
|
|
import { edgeFunctionsKeys } from '@/data/edge-functions/keys'
|
|
|
|
interface UseEdgeFunctionsDiffProps {
|
|
currentBranchRef?: string
|
|
mainBranchRef?: string
|
|
}
|
|
|
|
export type FileStatus = 'added' | 'removed' | 'modified' | 'unchanged'
|
|
|
|
export interface FileInfo {
|
|
key: string
|
|
status: FileStatus
|
|
}
|
|
|
|
export interface FunctionFileInfo {
|
|
[functionSlug: string]: FileInfo[]
|
|
}
|
|
|
|
export interface EdgeFunctionsDiffResult {
|
|
addedSlugs: string[]
|
|
removedSlugs: string[]
|
|
modifiedSlugs: string[]
|
|
addedBodiesMap: Record<string, EdgeFunctionBodyData | undefined>
|
|
removedBodiesMap: Record<string, EdgeFunctionBodyData | undefined>
|
|
currentBodiesMap: Record<string, EdgeFunctionBodyData | undefined>
|
|
mainBodiesMap: Record<string, EdgeFunctionBodyData | undefined>
|
|
functionFileInfo: FunctionFileInfo
|
|
isLoading: boolean
|
|
hasChanges: boolean
|
|
refetchCurrentBranchFunctions: () => void
|
|
refetchMainBranchFunctions: () => void
|
|
currentBranchFunctions?: EdgeFunctionsData
|
|
mainBranchFunctions?: EdgeFunctionsData
|
|
clearDiffsOptimistically: () => void
|
|
}
|
|
|
|
// Small helper around path.basename but avoids importing the full Node path lib for the browser bundle
|
|
const fileKey = (fullPath: string) => basename(fullPath)
|
|
|
|
export const useEdgeFunctionsDiff = ({
|
|
currentBranchRef,
|
|
mainBranchRef,
|
|
}: UseEdgeFunctionsDiffProps): EdgeFunctionsDiffResult => {
|
|
const queryClient = useQueryClient()
|
|
|
|
// Fetch edge functions for both branches
|
|
const {
|
|
data: currentBranchFunctions,
|
|
isPending: isCurrentFunctionsLoading,
|
|
refetch: refetchCurrentBranchFunctions,
|
|
} = useEdgeFunctionsQuery(
|
|
{ projectRef: currentBranchRef },
|
|
{
|
|
enabled: !!currentBranchRef,
|
|
refetchOnMount: 'always',
|
|
staleTime: 30000, // 30 seconds
|
|
}
|
|
)
|
|
|
|
const {
|
|
data: mainBranchFunctions,
|
|
isPending: isMainFunctionsLoading,
|
|
refetch: refetchMainBranchFunctions,
|
|
} = useEdgeFunctionsQuery(
|
|
{ projectRef: mainBranchRef },
|
|
{
|
|
enabled: !!mainBranchRef,
|
|
refetchOnMount: 'always',
|
|
staleTime: 30000, // 30 seconds
|
|
}
|
|
)
|
|
|
|
// Identify added / removed / overlapping functions
|
|
const {
|
|
added = [],
|
|
removed = [],
|
|
modified = [],
|
|
} = useMemo(() => {
|
|
if (!currentBranchFunctions || !mainBranchFunctions) {
|
|
return { added: [], removed: [], modified: [] }
|
|
}
|
|
|
|
const currentFuncs = currentBranchFunctions ?? []
|
|
const mainFuncs = mainBranchFunctions ?? []
|
|
|
|
const added = currentFuncs.filter((c) => !mainFuncs.find((m) => m.slug === c.slug))
|
|
const removed = mainFuncs.filter((m) => !currentFuncs.find((c) => c.slug === m.slug))
|
|
const modified = currentFuncs.filter((c) =>
|
|
mainFuncs.find(
|
|
(m) => m.slug === c.slug && (m.ezbr_sha256 === undefined || m.ezbr_sha256 !== c.ezbr_sha256)
|
|
)
|
|
)
|
|
|
|
return { added, removed, modified }
|
|
}, [currentBranchFunctions, mainBranchFunctions])
|
|
|
|
const addedSlugs = added.map((f) => f.slug)
|
|
const removedSlugs = removed.map((f) => f.slug)
|
|
const maybeModifiedSlugs = modified.map((f) => f.slug)
|
|
|
|
// Fetch function bodies ---------------------------------------------------
|
|
const currentBodiesQueries = useQueries({
|
|
queries: maybeModifiedSlugs.map((slug) => ({
|
|
queryKey: ['edge-function-body', currentBranchRef, slug],
|
|
queryFn: ({ signal }: { signal?: AbortSignal }) =>
|
|
getEdgeFunctionBody({ projectRef: currentBranchRef, slug }, signal),
|
|
enabled: !!currentBranchRef,
|
|
refetchOnMount: 'always' as const,
|
|
})),
|
|
})
|
|
|
|
const mainBodiesQueries = useQueries({
|
|
queries: maybeModifiedSlugs.map((slug) => ({
|
|
queryKey: ['edge-function-body', mainBranchRef, slug],
|
|
queryFn: ({ signal }: { signal?: AbortSignal }) =>
|
|
getEdgeFunctionBody({ projectRef: mainBranchRef, slug }, signal),
|
|
enabled: !!mainBranchRef,
|
|
refetchOnMount: 'always' as const,
|
|
})),
|
|
})
|
|
|
|
const addedBodiesQueries = useQueries({
|
|
queries: addedSlugs.map((slug) => ({
|
|
queryKey: ['edge-function-body', currentBranchRef, slug],
|
|
queryFn: ({ signal }: { signal?: AbortSignal }) =>
|
|
getEdgeFunctionBody({ projectRef: currentBranchRef, slug }, signal),
|
|
enabled: !!currentBranchRef,
|
|
refetchOnMount: 'always' as const,
|
|
})),
|
|
})
|
|
|
|
const removedBodiesQueries = useQueries({
|
|
queries: removedSlugs.map((slug) => ({
|
|
queryKey: ['edge-function-body', mainBranchRef, slug],
|
|
queryFn: ({ signal }: { signal?: AbortSignal }) =>
|
|
getEdgeFunctionBody({ projectRef: mainBranchRef, slug }, signal),
|
|
enabled: !!mainBranchRef,
|
|
refetchOnMount: 'always' as const,
|
|
})),
|
|
})
|
|
|
|
// Flatten loading flags ----------------------------------------------------
|
|
const isLoading =
|
|
[
|
|
...currentBodiesQueries,
|
|
...mainBodiesQueries,
|
|
...addedBodiesQueries,
|
|
...removedBodiesQueries,
|
|
].some((q) => q.isLoading) ||
|
|
isCurrentFunctionsLoading ||
|
|
isMainFunctionsLoading
|
|
|
|
// Build lookup maps --------------------------------------------------------
|
|
const currentBodiesMap: Record<string, EdgeFunctionBodyData | undefined> = {}
|
|
currentBodiesQueries.forEach((q, idx) => {
|
|
if (q.data) currentBodiesMap[maybeModifiedSlugs[idx]] = q.data
|
|
})
|
|
|
|
const mainBodiesMap: Record<string, EdgeFunctionBodyData | undefined> = {}
|
|
mainBodiesQueries.forEach((q, idx) => {
|
|
if (q.data) mainBodiesMap[maybeModifiedSlugs[idx]] = q.data
|
|
})
|
|
|
|
const addedBodiesMap: Record<string, EdgeFunctionBodyData | undefined> = {}
|
|
addedBodiesQueries.forEach((q, idx) => {
|
|
if (q.data) addedBodiesMap[addedSlugs[idx]] = q.data
|
|
})
|
|
|
|
const removedBodiesMap: Record<string, EdgeFunctionBodyData | undefined> = {}
|
|
removedBodiesQueries.forEach((q, idx) => {
|
|
if (q.data) removedBodiesMap[removedSlugs[idx]] = q.data
|
|
})
|
|
|
|
// Determine modified slugs and build file info -----------------------------
|
|
const modifiedSlugs: string[] = []
|
|
const functionFileInfo: FunctionFileInfo = {}
|
|
|
|
// Process overlapping functions to determine modifications and file info
|
|
maybeModifiedSlugs.forEach((slug) => {
|
|
const currentBody = currentBodiesMap[slug]
|
|
const mainBody = mainBodiesMap[slug]
|
|
if (!currentBody || !mainBody) return
|
|
|
|
const allFileKeys = new Set(
|
|
[...currentBody.files, ...mainBody.files].map((f) => fileKey(f.name))
|
|
)
|
|
const fileInfos: FileInfo[] = []
|
|
let hasModifications = false
|
|
|
|
for (const key of allFileKeys) {
|
|
const currentFile = currentBody.files.find((f) => fileKey(f.name) === key)
|
|
const mainFile = mainBody.files.find((f) => fileKey(f.name) === key)
|
|
|
|
let status: FileStatus = 'unchanged'
|
|
|
|
if (!currentFile && mainFile) {
|
|
status = 'removed'
|
|
hasModifications = true
|
|
} else if (currentFile && !mainFile) {
|
|
status = 'added'
|
|
hasModifications = true
|
|
} else if (currentFile && mainFile && currentFile.content !== mainFile.content) {
|
|
status = 'modified'
|
|
hasModifications = true
|
|
}
|
|
|
|
fileInfos.push({ key, status })
|
|
}
|
|
|
|
if (hasModifications) {
|
|
modifiedSlugs.push(slug)
|
|
functionFileInfo[slug] = fileInfos
|
|
}
|
|
})
|
|
|
|
// Add file info for added functions
|
|
addedSlugs.forEach((slug) => {
|
|
const body = addedBodiesMap[slug]
|
|
if (body) {
|
|
functionFileInfo[slug] = body.files.map((file) => ({
|
|
key: fileKey(file.name),
|
|
status: 'added' as FileStatus,
|
|
}))
|
|
}
|
|
})
|
|
|
|
// Add file info for removed functions
|
|
removedSlugs.forEach((slug) => {
|
|
const body = removedBodiesMap[slug]
|
|
if (body) {
|
|
functionFileInfo[slug] = body.files.map((file) => ({
|
|
key: fileKey(file.name),
|
|
status: 'removed' as FileStatus,
|
|
}))
|
|
}
|
|
})
|
|
|
|
const hasChanges = addedSlugs.length > 0 || removedSlugs.length > 0 || modifiedSlugs.length > 0
|
|
|
|
const clearDiffsOptimistically = useCallback(() => {
|
|
if (!currentBranchRef || !mainBranchFunctions) return
|
|
|
|
queryClient.setQueryData(edgeFunctionsKeys.list(currentBranchRef), mainBranchFunctions)
|
|
|
|
mainBranchFunctions.forEach((func) => {
|
|
const mainBody = mainBodiesMap[func.slug]
|
|
if (mainBody) {
|
|
queryClient.setQueryData(['edge-function-body', currentBranchRef, func.slug], mainBody)
|
|
}
|
|
})
|
|
}, [currentBranchRef, mainBranchFunctions, mainBodiesMap, queryClient])
|
|
|
|
return {
|
|
addedSlugs,
|
|
removedSlugs,
|
|
modifiedSlugs,
|
|
addedBodiesMap,
|
|
removedBodiesMap,
|
|
currentBodiesMap,
|
|
mainBodiesMap,
|
|
functionFileInfo,
|
|
isLoading,
|
|
hasChanges,
|
|
refetchCurrentBranchFunctions,
|
|
refetchMainBranchFunctions,
|
|
currentBranchFunctions,
|
|
mainBranchFunctions,
|
|
clearDiffsOptimistically,
|
|
}
|
|
}
|
|
|
|
export default useEdgeFunctionsDiff
|