Files
supabase/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.tsx
T
Joshen Lim 4f26af6259 Remove org slug and project ref filter for GET notifications request (#43167)
## Context

Since moving notifications to the Advisors Panel, we've been sending
`org_slug` and `project_ref` to the GET notifications endpoint, which
resulted in certain notifications not being returned such as those that
are user specific (no org slug nor project ref)

Am opting to remove both slug and ref filters for the notifications as
the notifications should be on a user level (irregardless if you're
within a project or not) - the Advisor's Panel's button in the layout
header would also suggest that notifications in there are not tied to an
org or project

## To test

This one's a bit tricky to test unless you have notifications on
staging, but i've double checked on prod with a curl command that
removing the org slug and project ref filters returns the correct
notifications
2026-02-25 16:09:21 +08:00

347 lines
11 KiB
TypeScript

import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
import {
Notification,
NotificationData,
useNotificationsV2Query,
} from 'data/notifications/notifications-v2-query'
import { useNotificationsV2UpdateMutation } from 'data/notifications/notifications-v2-update-mutation'
import dayjs from 'dayjs'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { IS_PLATFORM } from 'lib/constants'
import { useTrack } from 'lib/telemetry/track'
import { useMemo, useRef } from 'react'
import { AdvisorSeverity, AdvisorTab, useAdvisorStateSnapshot } from 'state/advisor-state'
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
import { AdvisorDetail } from './AdvisorDetail'
import { AdvisorFilters } from './AdvisorFilters'
import type { AdvisorItem } from './AdvisorPanel.types'
import { AdvisorPanelBody } from './AdvisorPanelBody'
import { AdvisorPanelHeader } from './AdvisorPanelHeader'
const severityOrder: Record<AdvisorSeverity, number> = {
critical: 0,
warning: 1,
info: 2,
}
const lintLevelToSeverity = (level: Lint['level']): AdvisorSeverity => {
switch (level) {
case 'ERROR':
return 'critical'
case 'WARN':
return 'warning'
default:
return 'info'
}
}
const notificationPriorityToSeverity = (priority: string | null | undefined): AdvisorSeverity => {
switch (priority) {
case 'Critical':
return 'critical'
case 'Warning':
return 'warning'
default:
return 'info'
}
}
export const AdvisorPanel = () => {
const track = useTrack()
const {
activeTab,
severityFilters,
selectedItemId,
selectedItemSource,
setActiveTab,
setSeverityFilters,
clearSeverityFilters,
setSelectedItem,
notificationFilterStatuses,
notificationFilterPriorities,
setNotificationFilters,
resetNotificationFilters,
} = useAdvisorStateSnapshot()
const { data: project } = useSelectedProjectQuery()
const { activeSidebar, closeSidebar } = useSidebarManagerSnapshot()
const isSidebarOpen = activeSidebar?.id === SIDEBAR_KEYS.ADVISOR_PANEL
const markedRead = useRef<string[]>([])
const hasProjectRef = !!project?.ref
const {
data: lintData,
isPending: isLintsLoading,
isError: isLintsError,
} = useProjectLintsQuery(
{ projectRef: project?.ref },
{ enabled: isSidebarOpen && hasProjectRef && activeTab !== 'messages' }
)
// Notifications should always load when sidebar is open (shown in both 'all' and 'messages' tabs)
const shouldLoadNotifications = isSidebarOpen && IS_PLATFORM
const notificationStatus = useMemo(() => {
if (notificationFilterStatuses.includes('archived')) {
return 'archived'
}
if (notificationFilterStatuses.includes('unread')) {
return 'new'
}
return undefined
}, [notificationFilterStatuses])
// Memoize filters to prevent query key changes on every render
// Use selected organization and project if they exist
const notificationFilters = useMemo(
() => ({ priority: notificationFilterPriorities }),
[notificationFilterPriorities]
)
const {
data: notificationsData,
isPending: isNotificationsLoading,
isError: isNotificationsError,
} = useNotificationsV2Query(
{
status: notificationStatus,
filters: notificationFilters,
limit: 20,
},
{ enabled: shouldLoadNotifications }
)
const { mutate: updateNotifications } = useNotificationsV2UpdateMutation()
const notifications = useMemo(() => {
return notificationsData?.pages.flatMap((page) => page) ?? []
}, [notificationsData?.pages])
const markNotificationsRead = () => {
if (markedRead.current.length > 0) {
updateNotifications({ ids: markedRead.current, status: 'seen' })
}
}
const lintItems = useMemo<AdvisorItem[]>(() => {
if (!lintData) return []
return lintData
.map((lint): AdvisorItem | null => {
const categories = lint.categories || []
const tab = categories.includes('SECURITY')
? ('security' as const)
: categories.includes('PERFORMANCE')
? ('performance' as const)
: undefined
if (!tab) return null
return {
id: lint.cache_key,
title: lint.detail,
severity: lintLevelToSeverity(lint.level),
createdAt: undefined,
tab,
source: 'lint' as const,
original: lint,
}
})
.filter((item): item is AdvisorItem => item !== null)
}, [lintData])
const notificationItems = useMemo<AdvisorItem[]>(() => {
if (!IS_PLATFORM) return []
return notifications?.map((notification): AdvisorItem => {
const data = notification.data as NotificationData
return {
id: notification.id,
title: data.title,
severity: notificationPriorityToSeverity(notification.priority),
createdAt: dayjs(notification.inserted_at).valueOf(),
tab: 'messages' as const,
source: 'notification' as const,
original: notification,
}
})
}, [notifications])
const combinedItems = useMemo<AdvisorItem[]>(() => {
const all = [...lintItems, ...notificationItems]
return all.sort((a, b) => {
const severityDiff = severityOrder[a.severity] - severityOrder[b.severity]
if (severityDiff !== 0) return severityDiff
const createdDiff = (b.createdAt ?? 0) - (a.createdAt ?? 0)
if (createdDiff !== 0) return createdDiff
return a.title.localeCompare(b.title)
})
}, [lintItems, notificationItems])
const filteredItems = useMemo<AdvisorItem[]>(() => {
return combinedItems.filter((item) => {
// Filter by severity
if (severityFilters.length > 0 && !severityFilters.includes(item.severity)) {
return false
}
// Filter by tab
if (activeTab === 'all') {
// When no projectRef, only show notifications in 'all' tab
if (!hasProjectRef && item.source !== 'notification') {
return false
}
return true
}
return item.tab === activeTab
})
}, [combinedItems, severityFilters, activeTab, hasProjectRef])
const itemsFilteredByTabOnly = useMemo<AdvisorItem[]>(() => {
return combinedItems.filter((item) => {
if (activeTab === 'all') {
// When no projectRef, only show notifications in 'all' tab
if (!hasProjectRef && item.source !== 'notification') {
return false
}
return true
}
return item.tab === activeTab
})
}, [combinedItems, activeTab, hasProjectRef])
const hiddenItemsCount = itemsFilteredByTabOnly.length - filteredItems.length
const selectedItem = combinedItems.find(
(item) => item.id === selectedItemId && item.source === selectedItemSource
)
const isDetailView = !!selectedItem
// Only show loading state if the query is actually enabled
const isLintsActuallyLoading =
isSidebarOpen && hasProjectRef && activeTab !== 'messages' && isLintsLoading
const isNotificationsActuallyLoading = shouldLoadNotifications && isNotificationsLoading
const isLoading = isLintsActuallyLoading || isNotificationsActuallyLoading
const isError = isLintsError || isNotificationsError
const handleTabChange = (tab: string) => {
setActiveTab(tab as AdvisorTab)
setSelectedItem(undefined)
}
const handleBackToList = () => {
setSelectedItem(undefined)
markNotificationsRead()
}
const handleClose = () => {
markNotificationsRead()
closeSidebar(SIDEBAR_KEYS.ADVISOR_PANEL)
}
const handleItemClick = (item: AdvisorItem) => {
setSelectedItem(item.id, item.source)
if (item.source === 'notification') {
const notification = item.original as Notification
if (notification.status === 'new' && !markedRead.current.includes(notification.id)) {
markedRead.current.push(notification.id)
}
}
const advisorCategory =
item.source === 'lint' && 'categories' in item.original
? item.original.categories[0]
: undefined
const advisorLevel =
item.source === 'lint' && 'level' in item.original ? item.original.level : undefined
track('advisor_detail_opened', {
origin: 'advisor_panel',
advisorCategory,
advisorSource: item.source,
advisorType: item.original.name,
advisorLevel,
})
}
const handleUpdateNotificationStatus = (id: string, status: 'archived' | 'seen') => {
updateNotifications({ ids: [id], status })
}
const handleClearAllFilters = () => {
clearSeverityFilters()
resetNotificationFilters()
}
const hasAnyFilters = severityFilters.length > 0 || notificationFilterStatuses.length > 0
return (
<div className="flex h-full flex-col bg-background">
{isDetailView ? (
<>
<AdvisorPanelHeader
selectedItem={selectedItem}
onBack={handleBackToList}
onClose={handleClose}
/>
<div className="flex-1 overflow-y-auto">
{selectedItem ? (
<AdvisorDetail
item={selectedItem}
projectRef={project?.ref ?? ''}
onUpdateNotificationStatus={handleUpdateNotificationStatus}
/>
) : (
<div className="px-6 py-8">
<p className="text-sm text-foreground-light">
Select an advisor item to view more details.
</p>
</div>
)}
</div>
</>
) : (
<>
<AdvisorFilters
activeTab={activeTab}
onTabChange={handleTabChange}
severityFilters={[...severityFilters]}
onSeverityFiltersChange={setSeverityFilters}
statusFilters={[...notificationFilterStatuses]}
onStatusFiltersChange={(values) => {
notificationFilterStatuses
.filter((status) => !values.includes(status))
.forEach((status) => setNotificationFilters(status, 'status'))
values
.filter((status) => !notificationFilterStatuses.includes(status))
.forEach((status) => setNotificationFilters(status, 'status'))
}}
onClose={handleClose}
isPlatform={IS_PLATFORM}
/>
<div className="flex-1 overflow-y-auto">
<AdvisorPanelBody
isLoading={isLoading}
isError={isError}
filteredItems={filteredItems}
activeTab={activeTab}
severityFilters={[...severityFilters]}
onItemClick={handleItemClick}
onClearFilters={handleClearAllFilters}
hiddenItemsCount={hiddenItemsCount}
hasAnyFilters={hasAnyFilters}
hasProjectRef={hasProjectRef}
/>
</div>
</>
)}
</div>
)
}