Auth + Database Report Fix (#36768)

rollback previous files and move auth changes to new files
This commit is contained in:
Jordi Enric authored and GitHub committed 2025-06-30 16:04:33 +02:00
1 parent 4984392edd
commit 92eabe3e3e
5 files changed
+446 -77

No files matched your search

@@ -7,7 +7,7 @@
* This component acts as a bridge between the data-fetching logic and the
* presentational chart component.
*/
import ComposedChartHandler from 'components/ui/Charts/ComposedChartHandler'
import LogChartHandler from 'components/ui/Charts/LogChartHandler'
import { useChartData } from 'hooks/useChartData'
import type { UpdateDateRange } from 'pages/project/[ref]/reports/database'
import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
@@ -43,7 +43,7 @@ const ReportChart = ({
})
return (
<ComposedChartHandler
<LogChartHandler
{...chart}
attributes={
(chartAttributes.length > 0 ? chartAttributes : chart.attributes) as MultiAttribute[]
@@ -1,11 +1,5 @@
/**
* ComposedChartHandler
*
* A presentational component for rendering charts.
* It is responsible only for rendering the chart UI based on the data and loading state passed to it as props.
* All the complex data fetching logic has been moved to the useChartData hook.
*/
import React, { PropsWithChildren, useState, useEffect, useRef } from 'react'
import React, { PropsWithChildren, useState, useMemo, useEffect, useRef } from 'react'
import { useRouter } from 'next/router'
import { Loader2 } from 'lucide-react'
import { cn, WarningIcon } from 'ui'
@@ -17,14 +11,14 @@ import { InfraMonitoringAttribute } from 'data/analytics/infra-monitoring-query'
import { useInfraMonitoringQueries } from 'data/analytics/infra-monitoring-queries'
import { ProjectDailyStatsAttribute } from 'data/analytics/project-daily-stats-query'
import { useProjectDailyStatsQueries } from 'data/analytics/project-daily-stats-queries'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { useChartHighlight } from './useChartHighlight'
import { getMockDataForAttribute } from 'data/reports/auth-charts'
import type { ChartData } from './Charts.types'
import type { UpdateDateRange } from 'pages/project/[ref]/reports/database'
import type { MultiAttribute } from './ComposedChart.utils'
import { MultiAttribute } from './ComposedChart.utils'
interface ComposedChartHandlerProps {
export interface ComposedChartHandlerProps {
id?: string
label: string
attributes: MultiAttribute[]
@@ -46,7 +40,6 @@ interface ComposedChartHandlerProps {
updateDateRange: UpdateDateRange
valuePrecision?: number
isVisible?: boolean
titleTooltip?: string
docsUrl?: string
}
@@ -98,6 +91,9 @@ const LazyChartWrapper = ({ children }: PropsWithChildren) => {
const ComposedChartHandler = ({
label,
attributes,
startDate,
endDate,
interval,
customDateFormat,
children = null,
defaultChartStyle = 'bar',
@@ -113,14 +109,131 @@ const ComposedChartHandler = ({
showTotal,
updateDateRange,
valuePrecision,
titleTooltip,
isVisible = true,
id,
...otherProps
}: PropsWithChildren<ComposedChartHandlerProps>) => {
const router = useRouter()
const { ref } = router.query
const state = useDatabaseSelectorStateSnapshot()
const [chartStyle, setChartStyle] = useState<string>(defaultChartStyle)
const chartHighlight = useChartHighlight()
if (isLoading) {
const databaseIdentifier = state.selectedDatabaseId
// Use the custom hook at the top level of the component
const attributeQueries = useAttributeQueries(
attributes,
ref,
startDate,
endDate,
interval as AnalyticsInterval,
databaseIdentifier,
data,
isVisible
)
// Combine all the data into a single dataset
const combinedData = useMemo(() => {
if (data) return data
const isLoading = attributeQueries.some((query: any) => query.isLoading)
if (isLoading) return undefined
const hasError = attributeQueries.some((query: any) => !query.data)
if (hasError) return undefined
// Get all unique timestamps from all datasets
const timestamps = new Set<string>()
attributeQueries.forEach((query: any) => {
query.data?.data?.forEach((point: any) => {
if (point?.period_start) {
timestamps.add(point.period_start)
}
})
})
const referenceLineQueries = attributeQueries.filter(
(_, index) => attributes[index].provider === 'reference-line'
)
// Combine data points for each timestamp
const combined = Array.from(timestamps)
.sort()
.map((timestamp) => {
const point: any = { timestamp }
// Add regular attributes
attributes.forEach((attr, index) => {
if (!attr) return
// Handle custom value attributes (like disk size)
if (attr.customValue !== undefined) {
point[attr.attribute] = attr.customValue
return
}
// Skip reference line attributes here, we'll add them below
if (attr.provider === 'reference-line') return
const queryData = attributeQueries[index]?.data?.data
const matchingPoint = queryData?.find((p: any) => p.period_start === timestamp)
let value = matchingPoint?.[attr.attribute] ?? 0
// Apply value manipulation if provided
if (attr.manipulateValue && typeof attr.manipulateValue === 'function') {
// Ensure value is a number before manipulation
const numericValue = typeof value === 'number' ? value : Number(value) || 0
value = attr.manipulateValue(numericValue)
}
point[attr.attribute] = value
})
// Add reference line values for each timestamp
referenceLineQueries.forEach((query: any) => {
const attr = query.data.attribute
const value = query.data.total
point[attr] = value
})
return point as DataPoint
})
return combined as DataPoint[]
}, [data, attributeQueries, attributes])
const loading = isLoading || attributeQueries.some((query: any) => query.isLoading)
// Calculate highlighted value based on the first attribute's data
const _highlightedValue = useMemo(() => {
if (highlightedValue !== undefined) return highlightedValue
const firstAttr = attributes[0]
const firstQuery = attributeQueries[0]
const firstData = firstQuery?.data
if (!firstData) return undefined
const shouldHighlightMaxValue =
firstAttr.provider === 'daily-stats' &&
!firstAttr.attribute.includes('ingress') &&
!firstAttr.attribute.includes('egress') &&
'maximum' in firstData
const shouldHighlightTotalGroupedValue = 'totalGrouped' in firstData
return shouldHighlightMaxValue
? firstData.maximum
: firstAttr.provider === 'daily-stats'
? firstData.total
: shouldHighlightTotalGroupedValue
? firstData.totalGrouped?.[firstAttr.attribute as keyof typeof firstData.totalGrouped]
: (firstData.data[firstData.data.length - 1] as any)?.[firstAttr.attribute]
}, [highlightedValue, attributes, attributeQueries])
if (loading) {
return (
<Panel
className={cn(
@@ -137,7 +250,7 @@ const ComposedChartHandler = ({
)
}
if (!data) {
if (!combinedData) {
return (
<div className="flex h-52 w-full flex-col items-center justify-center gap-y-2">
<WarningIcon />
@@ -159,11 +272,11 @@ const ComposedChartHandler = ({
<div className="absolute right-6 z-50 flex justify-between scroll-mt-16">{children}</div>
<ComposedChart
attributes={attributes}
data={data as any}
data={combinedData as DataPoint[]}
format={format}
xAxisKey="period_start"
yAxisKey={attributes[0].attribute}
highlightedValue={highlightedValue}
highlightedValue={_highlightedValue}
title={label}
customDateFormat={customDateFormat}
chartHighlight={chartHighlight}
@@ -176,7 +289,6 @@ const ComposedChartHandler = ({
updateDateRange={updateDateRange}
valuePrecision={valuePrecision}
hideChartType={hideChartType}
titleTooltip={titleTooltip}
{...otherProps}
/>
</Panel.Content>
@@ -184,7 +296,7 @@ const ComposedChartHandler = ({
)
}
export const useAttributeQueries = (
const useAttributeQueries = (
attributes: MultiAttribute[],
ref: string | string[] | undefined,
startDate: string,
@@ -194,15 +306,16 @@ export const useAttributeQueries = (
data: ChartData | undefined,
isVisible: boolean
) => {
const projectRef = typeof ref === 'string' ? ref : Array.isArray(ref) ? ref[0] : ''
const infraAttributes = attributes.filter((attr) => attr.provider === 'infra-monitoring')
const dailyStatsAttributes = attributes.filter((attr) => attr.provider === 'daily-stats')
const mockAttributes = attributes.filter((attr) => attr.provider === 'mock')
const referenceLineAttributes = attributes.filter((attr) => attr.provider === 'reference-line')
const infraAttributes = attributes
.filter((attr) => attr?.provider === 'infra-monitoring')
.map((attr) => attr.attribute as InfraMonitoringAttribute)
const dailyStatsAttributes = attributes
.filter((attr) => attr?.provider === 'daily-stats')
.map((attr) => attr.attribute as ProjectDailyStatsAttribute)
const referenceLines = attributes.filter((attr) => attr?.provider === 'reference-line')
const infraQueries = useInfraMonitoringQueries(
infraAttributes.map((attr) => attr.attribute as InfraMonitoringAttribute),
infraAttributes,
ref,
startDate,
endDate,
@@ -212,7 +325,7 @@ export const useAttributeQueries = (
isVisible
)
const dailyStatsQueries = useProjectDailyStatsQueries(
dailyStatsAttributes.map((attr) => attr.attribute as ProjectDailyStatsAttribute),
dailyStatsAttributes,
ref,
startDate,
endDate,
@@ -222,48 +335,23 @@ export const useAttributeQueries = (
isVisible
)
let infraIdx = 0
let dailyStatsIdx = 0
return attributes
.filter((attr) => attr.provider !== 'logs')
.map((attr) => {
if (attr.provider === 'infra-monitoring') {
return {
...infraQueries[infraIdx++],
data: { ...infraQueries[infraIdx - 1]?.data, provider: 'infra-monitoring' },
}
} else if (attr.provider === 'daily-stats') {
return {
...dailyStatsQueries[dailyStatsIdx++],
data: { ...dailyStatsQueries[dailyStatsIdx - 1]?.data, provider: 'daily-stats' },
}
} else if (attr.provider === 'mock') {
const mockData = getMockDataForAttribute(attr.attribute)
return {
isLoading: false,
data: { ...mockData, provider: 'mock', attribute: attr.attribute },
}
} else if (attr.provider === 'reference-line') {
let value = attr.value || 0
return {
data: {
data: [],
attribute: attr.attribute,
total: value,
maximum: value,
totalGrouped: { [attr.attribute]: value },
provider: 'reference-line',
},
isLoading: false,
isError: false,
}
} else {
return {
isLoading: false,
data: undefined,
}
}
})
const referenceLineQueries = referenceLines.map((line) => {
let value = line.value || 0
return {
data: {
data: [], // Will be populated in combinedData
attribute: line.attribute,
total: value,
maximum: value,
totalGrouped: { [line.attribute]: value },
},
isLoading: false,
isError: false,
}
})
return [...infraQueries, ...dailyStatsQueries, ...referenceLineQueries]
}
export default function LazyComposedChartHandler(props: ComposedChartHandlerProps) {
@@ -0,0 +1,268 @@
import React, { PropsWithChildren, useState, useEffect, useRef } from 'react'
import { Loader2 } from 'lucide-react'
import { cn, WarningIcon } from 'ui'
import Panel from 'components/ui/Panel'
import ComposedChart from './ComposedChart'
import { AnalyticsInterval, DataPoint } from 'data/analytics/constants'
import { InfraMonitoringAttribute } from 'data/analytics/infra-monitoring-query'
import { useInfraMonitoringQueries } from 'data/analytics/infra-monitoring-queries'
import { ProjectDailyStatsAttribute } from 'data/analytics/project-daily-stats-query'
import { useProjectDailyStatsQueries } from 'data/analytics/project-daily-stats-queries'
import { useChartHighlight } from './useChartHighlight'
import { getMockDataForAttribute } from 'data/reports/auth-charts'
import type { ChartData } from './Charts.types'
import type { UpdateDateRange } from 'pages/project/[ref]/reports/database'
import type { MultiAttribute } from './ComposedChart.utils'
interface LogChartHandlerProps {
id?: string
label: string
attributes: MultiAttribute[]
startDate: string
endDate: string
interval: string
customDateFormat?: string
defaultChartStyle?: 'bar' | 'line' | 'stackedAreaLine'
hideChartType?: boolean
data?: ChartData
isLoading?: boolean
format?: string
highlightedValue?: string | number
className?: string
showTooltip?: boolean
showLegend?: boolean
showTotal?: boolean
showMaxValue?: boolean
updateDateRange: UpdateDateRange
valuePrecision?: number
isVisible?: boolean
titleTooltip?: string
docsUrl?: string
}
/**
* Wrapper component that handles intersection observer logic for lazy loading
*/
const LazyChartWrapper = ({ children }: PropsWithChildren) => {
const [isVisible, setIsVisible] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
observer.disconnect()
}
},
{
rootMargin: '150px 0px', // Start loading before the component enters viewport
threshold: 0,
}
)
const currentRef = ref.current
if (currentRef) {
observer.observe(currentRef)
}
return () => {
if (currentRef) {
observer.unobserve(currentRef)
}
}
}, [])
return <div ref={ref}>{React.cloneElement(children as React.ReactElement, { isVisible })}</div>
}
/**
* Controls chart display state. Optionally fetches static chart data if data is not provided.
*
* If the `data` prop is provided, it will disable automatic chart data fetching and pass the data directly to the chart render.
* - loading state can also be provided through the `isLoading` prop, to display loading placeholders. Ignored if `data` key not provided.
* - if `isLoading=true` and `data` is `undefined`, loading error message will be shown.
*
* Provided data must be in the expected chart format.
*/
const LogChartHandler = ({
label,
attributes,
customDateFormat,
children = null,
defaultChartStyle = 'bar',
hideChartType = false,
data,
isLoading,
format,
highlightedValue,
className,
showTooltip,
showLegend,
showMaxValue,
showTotal,
updateDateRange,
valuePrecision,
titleTooltip,
id,
...otherProps
}: PropsWithChildren<LogChartHandlerProps>) => {
const [chartStyle, setChartStyle] = useState<string>(defaultChartStyle)
const chartHighlight = useChartHighlight()
if (isLoading) {
return (
<Panel
className={cn(
'flex min-h-[320px] w-full flex-col items-center justify-center gap-y-2',
className
)}
wrapWithLoading={false}
noMargin
noHideOverflow
>
<Loader2 size={18} className="animate-spin text-border-strong" />
<p className="text-xs text-foreground-lighter">Loading data for {label}</p>
</Panel>
)
}
if (!data) {
return (
<div className="flex h-52 w-full flex-col items-center justify-center gap-y-2">
<WarningIcon />
<p className="text-xs text-foreground-lighter">Unable to load data for {label}</p>
</div>
)
}
// Rest of the component remains similar, but pass all attributes to charts
return (
<Panel
noMargin
noHideOverflow
className={cn('relative py-2 w-full scroll-mt-16', className)}
wrapWithLoading={false}
id={id ?? label.toLowerCase().replaceAll(' ', '-')}
>
<Panel.Content className="flex flex-col gap-4">
<div className="absolute right-6 z-50 flex justify-between scroll-mt-16">{children}</div>
<ComposedChart
attributes={attributes}
data={data as any}
format={format}
xAxisKey="period_start"
yAxisKey={attributes[0].attribute}
highlightedValue={highlightedValue}
title={label}
customDateFormat={customDateFormat}
chartHighlight={chartHighlight}
chartStyle={chartStyle}
showTooltip={showTooltip}
showLegend={showLegend}
showTotal={showTotal}
showMaxValue={showMaxValue}
onChartStyleChange={setChartStyle}
updateDateRange={updateDateRange}
valuePrecision={valuePrecision}
hideChartType={hideChartType}
titleTooltip={titleTooltip}
{...otherProps}
/>
</Panel.Content>
</Panel>
)
}
export const useAttributeQueries = (
attributes: MultiAttribute[],
ref: string | string[] | undefined,
startDate: string,
endDate: string,
interval: AnalyticsInterval,
databaseIdentifier: string | undefined,
data: ChartData | undefined,
isVisible: boolean
) => {
const projectRef = typeof ref === 'string' ? ref : Array.isArray(ref) ? ref[0] : ''
const infraAttributes = attributes.filter((attr) => attr.provider === 'infra-monitoring')
const dailyStatsAttributes = attributes.filter((attr) => attr.provider === 'daily-stats')
const mockAttributes = attributes.filter((attr) => attr.provider === 'mock')
const referenceLineAttributes = attributes.filter((attr) => attr.provider === 'reference-line')
const infraQueries = useInfraMonitoringQueries(
infraAttributes.map((attr) => attr.attribute as InfraMonitoringAttribute),
ref,
startDate,
endDate,
interval,
databaseIdentifier,
data,
isVisible
)
const dailyStatsQueries = useProjectDailyStatsQueries(
dailyStatsAttributes.map((attr) => attr.attribute as ProjectDailyStatsAttribute),
ref,
startDate,
endDate,
interval,
databaseIdentifier,
data,
isVisible
)
let infraIdx = 0
let dailyStatsIdx = 0
return attributes
.filter((attr) => attr.provider !== 'logs')
.map((attr) => {
if (attr.provider === 'infra-monitoring') {
return {
...infraQueries[infraIdx++],
data: { ...infraQueries[infraIdx - 1]?.data, provider: 'infra-monitoring' },
}
} else if (attr.provider === 'daily-stats') {
return {
...dailyStatsQueries[dailyStatsIdx++],
data: { ...dailyStatsQueries[dailyStatsIdx - 1]?.data, provider: 'daily-stats' },
}
} else if (attr.provider === 'mock') {
const mockData = getMockDataForAttribute(attr.attribute)
return {
isLoading: false,
data: { ...mockData, provider: 'mock', attribute: attr.attribute },
}
} else if (attr.provider === 'reference-line') {
let value = attr.value || 0
return {
data: {
data: [],
attribute: attr.attribute,
total: value,
maximum: value,
totalGrouped: { [attr.attribute]: value },
provider: 'reference-line',
},
isLoading: false,
isError: false,
}
} else {
return {
isLoading: false,
data: undefined,
}
}
})
}
export default function LazyLogChartHandler(props: LogChartHandlerProps) {
return (
<LazyChartWrapper>
<LogChartHandler {...props} />
</LazyChartWrapper>
)
}
+1 -1
View File
@@ -16,7 +16,7 @@ import type { AnalyticsInterval, DataPoint } from 'data/analytics/constants'
import { useAuthLogsReport } from 'data/reports/auth-report-query'
import type { ChartData } from 'components/ui/Charts/Charts.types'
import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
import { useAttributeQueries } from 'components/ui/Charts/ComposedChartHandler'
import { useAttributeQueries } from 'components/ui/Charts/LogChartHandler'
export const useChartData = ({
attributes,
@@ -21,6 +21,7 @@ import ChartHandler from 'components/ui/Charts/ChartHandler'
import Panel from 'components/ui/Panel'
import ShimmerLine from 'components/ui/ShimmerLine'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import ComposedChartHandler from 'components/ui/Charts/ComposedChartHandler'
import { DateRangePicker } from 'components/ui/DateRangePicker'
import GrafanaPromoBanner from 'components/ui/GrafanaPromoBanner'
@@ -36,8 +37,8 @@ import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { TIME_PERIODS_INFRA } from 'lib/constants/metrics'
import { formatBytes } from 'lib/helpers'
import ReportChart from 'components/interfaces/Reports/ReportChart'
import type { NextPageWithLayout } from 'types'
import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
const DatabaseReport: NextPageWithLayout = () => {
return (
@@ -207,10 +208,15 @@ const DatabaseUsage = () => {
return (
<>
<ReportHeader showDatabaseSelector title="Database" />
<div className="w-full flex flex-col gap-1">
<div className="h-2 w-full">
<ShimmerLine active={report.isLoading} />
</div>
</div>
<GrafanaPromoBanner />
<section className="relative pt-16 -mt-2">
<div className="absolute inset-0 z-40 pointer-events-none flex flex-col gap-4">
<div className="sticky top-0 py-4 mb-4 flex items-center space-x-3 pointer-events-auto dark:bg-background-200 bg-background">
<div className="sticky top-0 bg-200 py-4 mb-4 flex items-center space-x-3 pointer-events-auto">
<ButtonTooltip
type="default"
disabled={isRefreshing}
@@ -253,13 +259,20 @@ const DatabaseUsage = () => {
<div className="grid grid-cols-1 gap-4">
{dateRange &&
REPORT_ATTRIBUTES_V2.filter((chart) => !chart.hide).map((chart) => (
<ReportChart
<ComposedChartHandler
key={chart.id}
chart={chart}
{...chart}
attributes={chart.attributes as MultiAttribute[]}
interval={dateRange.interval}
startDate={dateRange?.period_start?.date}
endDate={dateRange?.period_end?.date}
interval={dateRange.interval}
updateDateRange={updateDateRange}
defaultChartStyle={chart.defaultChartStyle as 'line' | 'bar' | 'stackedAreaLine'}
showMaxValue={
chart.id === 'client-connections' || chart.id === 'pgbouncer-connections'
? true
: chart.showMaxValue
}
/>
))}
</div>