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