LogsSidebarMenuV2.tsx380 lines · main
| 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 | |
| 23 | import { FeaturePreviewSidebarPanel } from '../../ui/FeaturePreviewSidebarPanel' |
| 24 | import { |
| 25 | useFeaturePreviewModal, |
| 26 | useUnifiedLogsPreview, |
| 27 | } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' |
| 28 | import { useIsETLPrivateAlpha } from '@/components/interfaces/Database/Replication/useIsETLPrivateAlpha' |
| 29 | import { LOG_DRAIN_TYPES } from '@/components/interfaces/LogDrains/LogDrains.constants' |
| 30 | import SavedQueriesItem from '@/components/interfaces/Settings/Logs/Logs.SavedQueriesItem' |
| 31 | import { LogsSidebarItem } from '@/components/interfaces/Settings/Logs/SidebarV2/SidebarItem' |
| 32 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 33 | import { useContentQuery } from '@/data/content/content-query' |
| 34 | import { useReplicationSourcesQuery } from '@/data/replication/sources-query' |
| 35 | import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' |
| 36 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 37 | |
| 38 | export 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 | |
| 62 | export 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 | } |