Files
supabase/apps/studio/components/interfaces/Observability/ObservabilityOverview.tsx
Jordi Enric 91d70a8c38 feat(studio): chart bar links open unified logs when enabled (#47502)
## What

When the unified logs preview is enabled, clicking a chart bar that
links to a logs view now opens **unified logs** (scoped to the service
and time bucket) instead of the legacy logs explorer.

Surfaces updated:
- Homepage project usage charts (`ProjectUsageSectionDeltas`,
`ProjectUsageSection`)
- Observability overview service health table (`ObservabilityOverview`)
— also fixes the API Gateway row and passes the time range via the
`date` param unified logs actually reads

Adds a small `buildUnifiedLogsUrl` helper so the deep-link format
(`filter=log_type:eq:<type>` + `date` epoch-ms range) lives in one
place.

When the preview is off, behavior is unchanged (legacy logs explorer).

Resolves O11Y-2133.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added unified logs navigation for project usage and observability
charts.
* Chart bar clicks now open the unified logs view with service-specific
filtering and a computed time window.
* **Bug Fixes**
* Updated observability and usage charts to generate the correct unified
logs URLs (including `log_type` filtering and optional date ranges).
* Preserved legacy log navigation behavior when unified logs are
disabled.
* **Tests**
* Added unit tests covering unified logs URL generation, query
parameters, and date handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:02:46 +02:00

249 lines
9.2 KiB
TypeScript

import { useQueryClient } from '@tanstack/react-query'
import { useParams } from 'common'
import dayjs from 'dayjs'
import { RefreshCw } from 'lucide-react'
import { useRouter } from 'next/router'
import { useCallback, useMemo, useState } from 'react'
import { Badge, Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { useUnifiedLogsPreview } from '../App/FeaturePreview/FeaturePreviewContext'
import { DatabaseInfrastructureSection } from './DatabaseInfrastructureSection'
import { OBSERVABILITY_DOCS_HREFS } from './Observability.constants'
import { useObservabilityOverviewData } from './ObservabilityOverview.utils'
import { ObservabilityOverviewFooter } from './ObservabilityOverviewFooter'
import { ServiceHealthTable } from './ServiceHealthTable'
import { useSlowQueriesCount } from './useSlowQueriesCount'
import ReportHeader from '@/components/interfaces/Reports/ReportHeader'
import ReportPadding from '@/components/interfaces/Reports/ReportPadding'
import {
buildUnifiedLogsUrl,
type UnifiedLogType,
} from '@/components/interfaces/UnifiedLogs/UnifiedLogs.utils'
import { DocsButton } from '@/components/ui/DocsButton'
import { ChartIntervalDropdown } from '@/components/ui/Logs/ChartIntervalDropdown'
import { CHART_INTERVALS } from '@/components/ui/Logs/logs.utils'
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
const REPORT_TITLE = 'Overview'
type ChartIntervalKey = '1hr' | '1day' | '7day'
export const ObservabilityOverview = () => {
const router = useRouter()
const { ref: projectRef } = useParams()
const { data: organization } = useSelectedOrganizationQuery()
const queryClient = useQueryClient()
const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview()
const { projectStorageAll: storageSupported } = useIsFeatureEnabled(['project_storage:all'])
const { isEnabled: isDataApiEnabled } = useIsDataApiEnabled({ projectRef })
const DEFAULT_INTERVAL: ChartIntervalKey = '1day'
const [interval, setInterval] = useState<ChartIntervalKey>(DEFAULT_INTERVAL)
const [refreshKey, setRefreshKey] = useState(0)
const [showIntervalDropdown, setShowIntervalDropdown] = useState(false)
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
const { datetimeFormat } = useMemo(() => {
const format = selectedInterval.format || 'MMM D, ha'
return { datetimeFormat: format }
}, [selectedInterval])
const overviewData = useObservabilityOverviewData(projectRef!, interval, refreshKey)
const { slowQueriesCount, isLoading: slowQueriesLoading } = useSlowQueriesCount(
projectRef,
refreshKey
)
const handleRefresh = useCallback(() => {
setRefreshKey((prev) => prev + 1)
queryClient.invalidateQueries({ queryKey: ['projects', projectRef, 'service-health'] })
queryClient.invalidateQueries({ queryKey: ['project-metrics'] })
queryClient.invalidateQueries({ queryKey: ['infra-monitoring'] })
queryClient.invalidateQueries({ queryKey: ['max-connections'] })
}, [queryClient, projectRef])
useShortcut(SHORTCUT_IDS.OBSERVABILITY_REFRESH, handleRefresh)
useShortcut(SHORTCUT_IDS.OBSERVABILITY_TOGGLE_DATE_PICKER, () => {
setShowIntervalDropdown((open) => !open)
})
const getLogsUrl = useCallback(
(logType: UnifiedLogType, legacyLogsUrl: string) =>
isUnifiedLogsEnabled
? buildUnifiedLogsUrl({ projectRef: projectRef!, logType })
: `/project/${projectRef}${legacyLogsUrl}`,
[projectRef, isUnifiedLogsEnabled]
)
const serviceBase = useMemo(
() => [
{
key: 'data_api' as const,
name: 'API Gateway',
reportUrl: undefined,
logType: 'edge' as const,
logsUrl: getLogsUrl('edge', '/logs/edge-logs'),
enabled: isDataApiEnabled,
hasReport: false,
},
{
key: 'db' as const,
name: 'Database',
reportUrl: `/project/${projectRef}/observability/database`,
logType: 'postgres' as const,
logsUrl: getLogsUrl('postgres', '/logs/postgres-logs'),
enabled: true,
hasReport: true,
},
{
key: 'postgrest' as const,
name: 'PostgREST',
reportUrl: `/project/${projectRef}/observability/postgrest`,
logType: 'postgrest' as const,
logsUrl: getLogsUrl('postgrest', '/logs/postgrest-logs'),
enabled: true,
hasReport: true,
},
{
key: 'auth' as const,
name: 'Auth',
reportUrl: `/project/${projectRef}/observability/auth`,
logType: 'auth' as const,
logsUrl: getLogsUrl('auth', '/logs/auth-logs'),
enabled: true,
hasReport: true,
},
{
key: 'functions' as const,
name: 'Edge Functions',
reportUrl: `/project/${projectRef}/observability/edge-functions`,
logType: 'edge function' as const,
logsUrl: getLogsUrl('edge function', '/logs/edge-functions-logs'),
enabled: true,
hasReport: true,
},
{
key: 'storage' as const,
name: 'Storage',
reportUrl: `/project/${projectRef}/observability/storage`,
logType: 'storage' as const,
logsUrl: getLogsUrl('storage', '/logs/storage-logs'),
enabled: storageSupported,
hasReport: true,
},
{
key: 'realtime' as const,
name: 'Realtime',
reportUrl: `/project/${projectRef}/observability/realtime`,
logType: 'realtime' as const,
logsUrl: getLogsUrl('realtime', '/logs/realtime-logs'),
enabled: true,
hasReport: true,
},
],
[projectRef, storageSupported, isDataApiEnabled, getLogsUrl]
)
const enabledServices = serviceBase.filter((s) => s.enabled)
const dbServiceData = overviewData.services.db
// Navigate to the log view scoped to the clicked bar's bucket window
const handleBarClick = useCallback(
(service: { logType: UnifiedLogType; logsUrl: string }) => (datum: any) => {
if (!datum?.timestamp) return
// datum.timestamp is already the UTC-truncated bucket boundary from timestamp_trunc(),
// so use it directly to avoid local-timezone startOf() misalignment (e.g. UTC+5:30).
const unit = interval === '1hr' ? 'minute' : 'hour'
const start = datum.timestamp
const end = dayjs.utc(datum.timestamp).add(1, unit).toISOString()
if (isUnifiedLogsEnabled) {
router.push(
buildUnifiedLogsUrl({ projectRef: projectRef!, logType: service.logType, start, end })
)
} else {
const queryParams = new URLSearchParams({ its: start, ite: end })
const separator = service.logsUrl.includes('?') ? '&' : '?'
router.push(`${service.logsUrl}${separator}${queryParams.toString()}`)
}
},
[router, interval, isUnifiedLogsEnabled, projectRef]
)
return (
<ReportPadding>
<div className="flex flex-row justify-between items-center">
<div className="flex items-center gap-3">
<ReportHeader title={REPORT_TITLE} />
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="warning">Beta</Badge>
</TooltipTrigger>
<TooltipContent>
<p>This page is subject to change</p>
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center gap-2">
<DocsButton href={OBSERVABILITY_DOCS_HREFS.overview} topic={REPORT_TITLE} />
<ShortcutTooltip
shortcutId={SHORTCUT_IDS.OBSERVABILITY_REFRESH}
label="Refresh report"
side="bottom"
>
<Button variant="outline" icon={<RefreshCw size={14} />} onClick={handleRefresh}>
Refresh
</Button>
</ShortcutTooltip>
<ChartIntervalDropdown
value={interval}
onChange={(interval) => setInterval(interval as ChartIntervalKey)}
organizationSlug={organization?.slug}
dropdownAlign="end"
tooltipSide="left"
open={showIntervalDropdown}
onOpenChange={setShowIntervalDropdown}
/>
</div>
</div>
<div className="space-y-12 mt-8">
<DatabaseInfrastructureSection
interval={interval}
refreshKey={refreshKey}
dbErrorRate={dbServiceData.errorRate}
isLoading={dbServiceData.isLoading}
slowQueriesCount={slowQueriesCount}
slowQueriesLoading={slowQueriesLoading}
/>
<ServiceHealthTable
services={enabledServices.map((service) => ({
key: service.key,
name: service.name,
description: '',
reportUrl: service.hasReport ? service.reportUrl : undefined,
logType: service.logType,
logsUrl: service.logsUrl,
}))}
serviceData={overviewData.services}
onBarClick={handleBarClick}
datetimeFormat={datetimeFormat}
/>
</div>
<ObservabilityOverviewFooter />
</ReportPadding>
)
}