Files
supabase/apps/studio/components/ui/Charts/LogChartHandler.tsx
f65884f341 Reports consolidation 1: Edge Functions (#38129)
* wip

* add formatTimestamp for reports

* add tests for formatTimestamp

* fix type err

* rm old chart logic

* do not retry replication sources

* update report config schema

* use report config

* fix charts, simplify config

* sync charts

* rm comment

* usefilltimeseries

* move chart upsell to its own component

* cleanup v2 chart component

* fix chartheader function format bug

* make attributes dynamic

* move types to its own file

* update reportv2chart types

* update reportv2 chart type

* use new edgefn report

* rm mock

* rm old code

* fix type err

* fix type err

* rename hook

* return sql in dataprovider

* add button to open in log explorer in chart header

* fix type

* fix chart type toggle

* fix type error

* export data transformation for testing

* add tests for data transformation

* rm dupped code

* add report settings to edge fn report

* rm mocked data

* rename

* fix type err

* Apply suggestion from @joshenlim

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>

* rm unnecessary div

* rm unnecessary props from report upsell

* cleaner query keys

* move loadin/error/empty states to inside panel content to prevent layout shift

* use card instead of panel

* use buttontooltip in chartheader

* use buttontooltips in buttons with tooltips

* rm export default

* rm export defualt

* rm i from key

* fix status code transformation

* fix filltimeseries

* add tests for filltimeseries

* fix data transform

* pass xaxis key to chart

* refactor hover state to use timestamp not index

* fix tests

* fix areachart

* fix barchart

* fix chartheader

* fix composedchart

* fix stacked

* fix type missing xaxiskey

* type fix

* Revert "refactor hover state to use timestamp not index"

This reverts commit cfd7886460.

* undo charthover changes, fix data aggregation

* Revert "fix tests"

This reverts commit e5200a2035.

* move transforms to testable functions, add tests

* nit refactors and clean up

* feat: add aws nimbus cloud provider (#38312)

* feat: add aws nimbus cloud provider

* Fix return type of useCustomContent hook

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>

* add utc plugin to dayjs

* move plugins to vitest setup

* rm customparser plugin

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Alaister Young <alaister@users.noreply.github.com>
2025-09-01 11:10:30 +02:00

250 lines
7.4 KiB
TypeScript

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 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
syncId?: 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.
*/
export const LogChartHandler = ({
label,
attributes,
customDateFormat,
children = null,
defaultChartStyle = 'bar',
hideChartType = false,
data,
isLoading,
format,
highlightedValue,
className,
showTooltip,
showLegend,
showMaxValue,
showTotal,
updateDateRange,
valuePrecision,
titleTooltip,
id,
syncId,
...otherProps
}: PropsWithChildren<LogChartHandlerProps>) => {
const [chartStyle, setChartStyle] = useState<string>(defaultChartStyle)
const chartHighlight = useChartHighlight()
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 w-full overflow-hidden scroll-mt-16', className)}
wrapWithLoading={false}
id={id ?? label.toLowerCase().replaceAll(' ', '-')}
>
{isLoading && (
<div className="absolute inset-0 rounded-md flex w-full flex-col items-center justify-center gap-y-2 bg-surface-100 backdrop z-20">
<Loader2 size={18} className="animate-spin text-border-strong" />
<p className="text-xs text-foreground-lighter">Loading data for {label}</p>
</div>
)}
<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}
syncId={syncId}
{...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 infraAttributes = attributes.filter((attr) => attr.provider === 'infra-monitoring')
const dailyStatsAttributes = attributes.filter((attr) => attr.provider === 'daily-stats')
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 === '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>
)
}