AdvisorPanel.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import { useMemo, useRef } from 'react'
  2. import { AdvisorDetail } from './AdvisorDetail'
  3. import { AdvisorFilters } from './AdvisorFilters'
  4. import type { AdvisorItem } from './AdvisorPanel.types'
  5. import {
  6. createAdvisorLintItems,
  7. createAdvisorNotificationItems,
  8. sortAdvisorItems,
  9. } from './AdvisorPanel.utils'
  10. import { AdvisorPanelBody } from './AdvisorPanelBody'
  11. import { AdvisorPanelHeader } from './AdvisorPanelHeader'
  12. import { useAdvisorSignals } from './useAdvisorSignals'
  13. import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
  14. import { useProjectLintsQuery } from '@/data/lint/lint-query'
  15. import { Notification, useNotificationsV2Query } from '@/data/notifications/notifications-v2-query'
  16. import { useNotificationsV2UpdateMutation } from '@/data/notifications/notifications-v2-update-mutation'
  17. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  18. import { IS_PLATFORM } from '@/lib/constants'
  19. import { useTrack } from '@/lib/telemetry/track'
  20. import { AdvisorTab, useAdvisorStateSnapshot } from '@/state/advisor-state'
  21. import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
  22. export const AdvisorPanel = () => {
  23. const track = useTrack()
  24. const {
  25. activeTab,
  26. severityFilters,
  27. selectedItemId,
  28. selectedItemSource,
  29. setActiveTab,
  30. setSeverityFilters,
  31. clearSeverityFilters,
  32. setSelectedItem,
  33. notificationFilterStatuses,
  34. notificationFilterPriorities,
  35. setNotificationFilters,
  36. resetNotificationFilters,
  37. } = useAdvisorStateSnapshot()
  38. const { data: project } = useSelectedProjectQuery()
  39. const { activeSidebar, closeSidebar } = useSidebarManagerSnapshot()
  40. const isSidebarOpen = activeSidebar?.id === SIDEBAR_KEYS.ADVISOR_PANEL
  41. const markedRead = useRef<string[]>([])
  42. const hasProjectRef = !!project?.ref
  43. const shouldLoadProjectAdvisorData = isSidebarOpen && hasProjectRef && activeTab !== 'messages'
  44. const {
  45. data: lintData,
  46. isPending: isLintsLoading,
  47. isError: isLintsError,
  48. } = useProjectLintsQuery({ projectRef: project?.ref }, { enabled: shouldLoadProjectAdvisorData })
  49. const { data: signalItems } = useAdvisorSignals({
  50. projectRef: project?.ref,
  51. enabled: shouldLoadProjectAdvisorData,
  52. })
  53. // Notifications should always load when sidebar is open (shown in both 'all' and 'messages' tabs)
  54. const shouldLoadNotifications = isSidebarOpen && IS_PLATFORM
  55. const notificationStatus = useMemo(() => {
  56. if (notificationFilterStatuses.includes('archived')) {
  57. return 'archived'
  58. }
  59. if (notificationFilterStatuses.includes('unread')) {
  60. return 'new'
  61. }
  62. return undefined
  63. }, [notificationFilterStatuses])
  64. // Memoize filters to prevent query key changes on every render
  65. // Use selected organization and project if they exist
  66. const notificationFilters = useMemo(
  67. () => ({ priority: notificationFilterPriorities }),
  68. [notificationFilterPriorities]
  69. )
  70. const {
  71. data: notificationsData,
  72. isPending: isNotificationsLoading,
  73. isError: isNotificationsError,
  74. } = useNotificationsV2Query(
  75. {
  76. status: notificationStatus,
  77. filters: notificationFilters,
  78. limit: 20,
  79. },
  80. { enabled: shouldLoadNotifications }
  81. )
  82. const { mutate: updateNotifications } = useNotificationsV2UpdateMutation()
  83. const notifications = useMemo(() => {
  84. return notificationsData?.pages.flatMap((page) => page) ?? []
  85. }, [notificationsData?.pages])
  86. const markNotificationsRead = () => {
  87. if (markedRead.current.length > 0) {
  88. updateNotifications({ ids: markedRead.current, status: 'seen' })
  89. }
  90. }
  91. const lintItems = useMemo<AdvisorItem[]>(() => {
  92. return createAdvisorLintItems(lintData ?? [])
  93. }, [lintData])
  94. const notificationItems = useMemo<AdvisorItem[]>(() => {
  95. if (!IS_PLATFORM) return []
  96. return createAdvisorNotificationItems(notifications)
  97. }, [notifications])
  98. const combinedItems = useMemo<AdvisorItem[]>(() => {
  99. return sortAdvisorItems([...lintItems, ...signalItems, ...notificationItems])
  100. }, [lintItems, signalItems, notificationItems])
  101. const filteredItems = useMemo<AdvisorItem[]>(() => {
  102. return combinedItems.filter((item) => {
  103. // Filter by severity
  104. if (severityFilters.length > 0 && !severityFilters.includes(item.severity)) {
  105. return false
  106. }
  107. // Filter by tab
  108. if (activeTab === 'all') {
  109. // When no projectRef, only show notifications in 'all' tab
  110. if (!hasProjectRef && item.source !== 'notification') {
  111. return false
  112. }
  113. return true
  114. }
  115. return item.tab === activeTab
  116. })
  117. }, [combinedItems, severityFilters, activeTab, hasProjectRef])
  118. const itemsFilteredByTabOnly = useMemo<AdvisorItem[]>(() => {
  119. return combinedItems.filter((item) => {
  120. if (activeTab === 'all') {
  121. // When no projectRef, only show notifications in 'all' tab
  122. if (!hasProjectRef && item.source !== 'notification') {
  123. return false
  124. }
  125. return true
  126. }
  127. return item.tab === activeTab
  128. })
  129. }, [combinedItems, activeTab, hasProjectRef])
  130. const hiddenItemsCount = itemsFilteredByTabOnly.length - filteredItems.length
  131. const selectedItem = combinedItems.find(
  132. (item) => item.id === selectedItemId && item.source === selectedItemSource
  133. )
  134. const isDetailView = !!selectedItem
  135. // Only show loading state if the query is actually enabled
  136. const isLintsActuallyLoading = shouldLoadProjectAdvisorData && isLintsLoading
  137. const isNotificationsActuallyLoading = shouldLoadNotifications && isNotificationsLoading
  138. // [Joshen] Opting to ignore loading and error state of advisor signals - render lints irregardless of banned ips
  139. const isLoading = isLintsActuallyLoading || isNotificationsActuallyLoading
  140. const isError = isLintsError || isNotificationsError
  141. const handleTabChange = (tab: string) => {
  142. setActiveTab(tab as AdvisorTab)
  143. setSelectedItem(undefined)
  144. }
  145. const handleBackToList = () => {
  146. setSelectedItem(undefined)
  147. markNotificationsRead()
  148. }
  149. const handleClose = () => {
  150. markNotificationsRead()
  151. closeSidebar(SIDEBAR_KEYS.ADVISOR_PANEL)
  152. }
  153. const handleItemClick = (item: AdvisorItem) => {
  154. setSelectedItem(item.id, item.source)
  155. if (item.source === 'notification') {
  156. const notification = item.original as Notification
  157. if (notification.status === 'new' && !markedRead.current.includes(notification.id)) {
  158. markedRead.current.push(notification.id)
  159. }
  160. }
  161. const advisorCategory =
  162. item.source === 'lint'
  163. ? item.original.categories.includes('SECURITY')
  164. ? 'SECURITY'
  165. : item.original.categories.includes('PERFORMANCE')
  166. ? 'PERFORMANCE'
  167. : undefined
  168. : item.source === 'signal'
  169. ? 'SECURITY'
  170. : undefined
  171. const advisorType =
  172. item.source === 'signal'
  173. ? item.type
  174. : item.source === 'lint'
  175. ? item.original.name
  176. : item.title
  177. const advisorLevel = item.source === 'lint' ? item.original.level : undefined
  178. track('advisor_detail_opened', {
  179. origin: 'advisor_panel',
  180. advisorCategory,
  181. advisorSource: item.source,
  182. advisorType,
  183. advisorLevel,
  184. })
  185. }
  186. const handleUpdateNotificationStatus = (id: string, status: 'archived' | 'seen') => {
  187. updateNotifications({ ids: [id], status })
  188. }
  189. const handleClearAllFilters = () => {
  190. clearSeverityFilters()
  191. resetNotificationFilters()
  192. }
  193. const hasAnyFilters = severityFilters.length > 0 || notificationFilterStatuses.length > 0
  194. return (
  195. <div className="flex h-full flex-col bg-background">
  196. {isDetailView ? (
  197. <>
  198. <AdvisorPanelHeader
  199. selectedItem={selectedItem}
  200. onBack={handleBackToList}
  201. onClose={handleClose}
  202. />
  203. <div className="flex-1 overflow-y-auto">
  204. {selectedItem ? (
  205. <AdvisorDetail
  206. item={selectedItem}
  207. projectRef={project?.ref ?? ''}
  208. onUpdateNotificationStatus={handleUpdateNotificationStatus}
  209. onAfterLintAction={handleBackToList}
  210. />
  211. ) : (
  212. <div className="px-6 py-8">
  213. <p className="text-sm text-foreground-light">
  214. Select an advisor item to view more details.
  215. </p>
  216. </div>
  217. )}
  218. </div>
  219. </>
  220. ) : (
  221. <>
  222. <AdvisorFilters
  223. activeTab={activeTab}
  224. onTabChange={handleTabChange}
  225. severityFilters={[...severityFilters]}
  226. onSeverityFiltersChange={setSeverityFilters}
  227. statusFilters={[...notificationFilterStatuses]}
  228. onStatusFiltersChange={(values) => {
  229. notificationFilterStatuses
  230. .filter((status) => !values.includes(status))
  231. .forEach((status) => setNotificationFilters(status, 'status'))
  232. values
  233. .filter((status) => !notificationFilterStatuses.includes(status))
  234. .forEach((status) => setNotificationFilters(status, 'status'))
  235. }}
  236. onClose={handleClose}
  237. isPlatform={IS_PLATFORM}
  238. />
  239. <div className="flex-1 overflow-y-auto">
  240. <AdvisorPanelBody
  241. isLoading={isLoading}
  242. isError={isError}
  243. filteredItems={filteredItems}
  244. activeTab={activeTab}
  245. severityFilters={[...severityFilters]}
  246. onItemClick={handleItemClick}
  247. onClearFilters={handleClearAllFilters}
  248. hiddenItemsCount={hiddenItemsCount}
  249. hasAnyFilters={hasAnyFilters}
  250. hasProjectRef={hasProjectRef}
  251. />
  252. </div>
  253. </>
  254. )}
  255. </div>
  256. )
  257. }