LogsSidebarMenuV2.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import { IS_PLATFORM, useFlag, useParams } from 'common'
  2. import { ChevronRight, CircleHelpIcon, Plus } from 'lucide-react'
  3. import Link from 'next/link'
  4. import { useRouter } from 'next/router'
  5. import React, { useState } from 'react'
  6. import {
  7. Badge,
  8. Button,
  9. cn,
  10. Collapsible,
  11. CollapsibleContent,
  12. CollapsibleTrigger,
  13. Separator,
  14. } from 'ui'
  15. import {
  16. InnerSideBarEmptyPanel,
  17. InnerSideBarFilters,
  18. InnerSideBarFilterSearchInput,
  19. InnerSideMenuItem,
  20. } from 'ui-patterns/InnerSideMenu'
  21. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  22. import { FeaturePreviewSidebarPanel } from '../../ui/FeaturePreviewSidebarPanel'
  23. import {
  24. useFeaturePreviewModal,
  25. useUnifiedLogsPreview,
  26. } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
  27. import { useIsETLPrivateAlpha } from '@/components/interfaces/Database/Replication/useIsETLPrivateAlpha'
  28. import { LOG_DRAIN_TYPES } from '@/components/interfaces/LogDrains/LogDrains.constants'
  29. import SavedQueriesItem from '@/components/interfaces/Settings/Logs/Logs.SavedQueriesItem'
  30. import { LogsSidebarItem } from '@/components/interfaces/Settings/Logs/SidebarV2/SidebarItem'
  31. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  32. import { useContentQuery } from '@/data/content/content-query'
  33. import { useReplicationSourcesQuery } from '@/data/replication/sources-query'
  34. import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
  35. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  36. export function SidebarCollapsible({
  37. children,
  38. title,
  39. defaultOpen,
  40. }: {
  41. children: React.ReactNode
  42. title: string
  43. defaultOpen?: boolean
  44. }) {
  45. return (
  46. <Collapsible defaultOpen={defaultOpen}>
  47. <CollapsibleTrigger className="flex items-center gap-x-2 px-4 [&[data-state=open]>svg]:rotate-90! pb-2">
  48. <ChevronRight
  49. size={16}
  50. className={'text-foreground-light transition-transform duration-200'}
  51. />
  52. <span className="text-foreground-light font-mono text-sm uppercase">{title}</span>
  53. </CollapsibleTrigger>
  54. <CollapsibleContent>{children}</CollapsibleContent>
  55. </Collapsible>
  56. )
  57. }
  58. export function LogsSidebarMenuV2() {
  59. const router = useRouter()
  60. const { ref } = useParams() as { ref: string }
  61. const unifiedLogsFlagEnabled = useFlag('unifiedLogs')
  62. const { selectFeaturePreview } = useFeaturePreviewModal()
  63. const { enable: enableUnifiedLogs, isEligible: isUnifiedLogsEligible } = useUnifiedLogsPreview()
  64. const [searchText, setSearchText] = useState('')
  65. const {
  66. projectAuthAll: authEnabled,
  67. projectStorageAll: storageEnabled,
  68. realtimeAll: realtimeEnabled,
  69. logsTemplates: templatesEnabled,
  70. logsCollections: collectionsEnabled,
  71. } = useIsFeatureEnabled([
  72. 'project_storage:all',
  73. 'project_auth:all',
  74. 'realtime:all',
  75. 'logs:templates',
  76. 'logs:collections',
  77. ])
  78. const enablePgReplicate = useIsETLPrivateAlpha()
  79. const { data: etlData, isPending: isETLLoading } = useReplicationSourcesQuery(
  80. {
  81. projectRef: ref,
  82. },
  83. {
  84. enabled: enablePgReplicate,
  85. retry: false,
  86. refetchOnMount: false,
  87. refetchOnWindowFocus: false,
  88. }
  89. )
  90. // [Jordi] We only want to show ETL logs if the user has the feature enabled AND they're using the feature aka they've created a source.
  91. const showETLLogs = enablePgReplicate && (etlData?.sources?.length ?? 0) > 0 && !isETLLoading
  92. const { hasAccess: hasDedicatedPooler } = useCheckEntitlements('dedicated_pooler')
  93. const { data: savedQueriesRes, isPending: savedQueriesLoading } = useContentQuery({
  94. projectRef: ref,
  95. type: 'log_sql',
  96. })
  97. const savedQueries = [...(savedQueriesRes?.content ?? [])]
  98. .filter((c) => c.type === 'log_sql')
  99. .sort((a, b) => a.name.localeCompare(b.name))
  100. function isActive(path: string) {
  101. return router.asPath.includes(path)
  102. }
  103. const BASE_COLLECTIONS = [
  104. {
  105. name: 'API Gateway',
  106. key: 'edge-logs',
  107. url: `/project/${ref}/logs/edge-logs`,
  108. items: [],
  109. },
  110. {
  111. name: 'Postgres',
  112. key: 'postgres-logs',
  113. url: `/project/${ref}/logs/postgres-logs`,
  114. items: [],
  115. },
  116. {
  117. name: 'PostgREST',
  118. key: 'postgrest-logs',
  119. url: `/project/${ref}/logs/postgrest-logs`,
  120. items: [],
  121. },
  122. IS_PLATFORM
  123. ? {
  124. name: hasDedicatedPooler ? 'Shared Pooler' : 'Pooler',
  125. key: 'pooler-logs',
  126. url: `/project/${ref}/logs/pooler-logs`,
  127. items: [],
  128. }
  129. : null,
  130. hasDedicatedPooler && IS_PLATFORM
  131. ? {
  132. name: 'Dedicated Pooler',
  133. key: 'dedicated-pooler-logs',
  134. url: `/project/${ref}/logs/dedicated-pooler-logs`,
  135. items: [],
  136. }
  137. : null,
  138. authEnabled
  139. ? {
  140. name: 'Auth',
  141. key: 'auth-logs',
  142. url: `/project/${ref}/logs/auth-logs`,
  143. items: [],
  144. }
  145. : null,
  146. storageEnabled
  147. ? {
  148. name: 'Storage',
  149. key: 'storage-logs',
  150. url: `/project/${ref}/logs/storage-logs`,
  151. items: [],
  152. }
  153. : null,
  154. realtimeEnabled
  155. ? {
  156. name: 'Realtime',
  157. key: 'realtime-logs',
  158. url: `/project/${ref}/logs/realtime-logs`,
  159. items: [],
  160. }
  161. : null,
  162. {
  163. name: 'Edge Functions',
  164. key: 'edge-functions-logs',
  165. url: `/project/${ref}/logs/edge-functions-logs`,
  166. items: [],
  167. },
  168. {
  169. name: 'Cron',
  170. key: 'pg_cron',
  171. url: `/project/${ref}/logs/pgcron-logs`,
  172. items: [],
  173. },
  174. showETLLogs
  175. ? {
  176. name: 'Replication',
  177. key: 'replication_logs',
  178. url: `/project/${ref}/logs/replication-logs`,
  179. items: [],
  180. }
  181. : null,
  182. ].filter((x) => x !== null)
  183. const OPERATIONAL_COLLECTIONS = IS_PLATFORM
  184. ? [
  185. {
  186. name: 'Postgres Version Upgrade',
  187. key: 'pg-upgrade-logs',
  188. url: `/project/${ref}/logs/pg-upgrade-logs`,
  189. items: [],
  190. },
  191. ]
  192. : []
  193. const filteredLogs = BASE_COLLECTIONS.filter((collection) => {
  194. return collection?.name.toLowerCase().includes(searchText.toLowerCase())
  195. })
  196. const filteredOperationalLogs = OPERATIONAL_COLLECTIONS.filter((collection) => {
  197. return collection?.name.toLowerCase().includes(searchText.toLowerCase())
  198. })
  199. return (
  200. <div className="pb-4 relative">
  201. {IS_PLATFORM && !unifiedLogsFlagEnabled && (
  202. <FeaturePreviewSidebarPanel
  203. className="mx-4 mt-4"
  204. illustration={<Badge variant="default">Coming soon</Badge>}
  205. title="New logs"
  206. description="Get early access"
  207. actions={
  208. <Link href="https://forms.supabase.com/unified-logs-signup" target="_blank">
  209. <Button type="default" size="tiny">
  210. Early access
  211. </Button>
  212. </Link>
  213. }
  214. />
  215. )}
  216. {isUnifiedLogsEligible && (
  217. <FeaturePreviewSidebarPanel
  218. className="mx-4 mt-4"
  219. title="Introducing unified logs"
  220. description="A unified view across all services with improved filtering and real-time updates."
  221. illustration={<Badge variant="success">New</Badge>}
  222. actions={
  223. <>
  224. <Button
  225. size="tiny"
  226. type="default"
  227. onClick={() => {
  228. enableUnifiedLogs()
  229. router.push(`/project/${ref}/logs`)
  230. }}
  231. >
  232. Enable preview
  233. </Button>
  234. <ButtonTooltip
  235. type="default"
  236. className="px-1.5"
  237. icon={<CircleHelpIcon />}
  238. onClick={() => selectFeaturePreview('briven-ui-preview-unified-logs')}
  239. tooltip={{ content: { side: 'bottom', text: 'More information' } }}
  240. />
  241. </>
  242. }
  243. />
  244. )}
  245. <div
  246. className={cn(
  247. 'flex gap-x-2 items-center sticky top-0 bg-background-200 z-1 px-4',
  248. !templatesEnabled ? 'pt-4' : 'py-4'
  249. )}
  250. >
  251. <InnerSideBarFilters className="w-full p-0 gap-0">
  252. <InnerSideBarFilterSearchInput
  253. name="search-collections"
  254. placeholder="Search collections..."
  255. aria-labelledby="Search collections"
  256. value={searchText}
  257. onChange={(e) => setSearchText(e.target.value)}
  258. ></InnerSideBarFilterSearchInput>
  259. </InnerSideBarFilters>
  260. <Button
  261. type="default"
  262. icon={<Plus className="text-foreground" />}
  263. className="w-[26px]"
  264. onClick={() => router.push(`/project/${ref}/logs/explorer`)}
  265. />
  266. </div>
  267. {templatesEnabled && (
  268. <div className="px-2">
  269. <InnerSideMenuItem
  270. title="Templates"
  271. isActive={isActive(`/project/${ref}/logs/explorer/templates`)}
  272. href={`/project/${ref}/logs/explorer/templates`}
  273. >
  274. Templates
  275. </InnerSideMenuItem>
  276. </div>
  277. )}
  278. <Separator className="my-4" />
  279. {collectionsEnabled && (
  280. <>
  281. <SidebarCollapsible title="Collections" defaultOpen={true}>
  282. {filteredLogs.map((collection) => {
  283. const isItemActive = isActive(collection?.url ?? '')
  284. return (
  285. <LogsSidebarItem
  286. key={collection?.key ?? ''}
  287. isActive={isItemActive}
  288. href={collection?.url ?? ''}
  289. label={collection?.name ?? ''}
  290. />
  291. )
  292. })}
  293. </SidebarCollapsible>
  294. {OPERATIONAL_COLLECTIONS.length > 0 && (
  295. <>
  296. <Separator className="my-4" />
  297. <SidebarCollapsible title="Database operations" defaultOpen={true}>
  298. {filteredOperationalLogs.map((collection) => (
  299. <LogsSidebarItem
  300. key={collection.key}
  301. isActive={isActive(collection.url)}
  302. href={collection.url}
  303. label={collection.name}
  304. />
  305. ))}
  306. </SidebarCollapsible>
  307. </>
  308. )}
  309. <Separator className="my-4" />
  310. </>
  311. )}
  312. <SidebarCollapsible title="Queries" defaultOpen={true}>
  313. {savedQueriesLoading && (
  314. <div className="p-4">
  315. <GenericSkeletonLoader />
  316. </div>
  317. )}
  318. {savedQueries.length === 0 && (
  319. <InnerSideBarEmptyPanel
  320. className="mx-4"
  321. title="No queries created yet"
  322. description={
  323. IS_PLATFORM ? 'Create and save your queries to use them in the explorer' : undefined
  324. }
  325. actions={
  326. <Button asChild type="default">
  327. <Link href={`/project/${ref}/logs/explorer`}>Create query</Link>
  328. </Button>
  329. }
  330. />
  331. )}
  332. {savedQueries.map((query) => (
  333. <SavedQueriesItem item={query} key={query.id} />
  334. ))}
  335. </SidebarCollapsible>
  336. <Separator className="my-4" />
  337. <FeaturePreviewSidebarPanel
  338. className="mx-4 mt-4"
  339. title="Capture your logs"
  340. description="Send logs to your preferred observability or storage platform."
  341. illustration={
  342. <div className="flex items-center gap-4">
  343. {LOG_DRAIN_TYPES.filter((t) =>
  344. ['datadog', 'sentry', 'webhook', 'loki'].includes(t.value)
  345. ).map((type) =>
  346. React.cloneElement(type.icon, { key: type.name, height: 20, width: 20 })
  347. )}
  348. </div>
  349. }
  350. actions={
  351. <Button asChild type="default">
  352. <Link href={`/project/${ref}/settings/log-drains`}>Go to Log Drains</Link>
  353. </Button>
  354. }
  355. />
  356. </div>
  357. )
  358. }