FE-1864 FE-1868 reports kaizen 2 (#38881)

* make opts configurable per report

* Minor nits

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Jordi Enric
2025-09-23 13:56:48 +02:00
committed by GitHub
parent 9a2140b291
commit c9033b1035
6 changed files with 153 additions and 62 deletions

View File

@@ -3,6 +3,7 @@ import { Loader2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { ComposedChart } from 'components/ui/Charts/ComposedChart'
import type { ChartHighlightAction } from 'components/ui/Charts/ChartHighlightActions'
import type { AnalyticsInterval } from 'data/analytics/constants'
import type { ReportConfig } from 'data/reports/v2/reports.types'
import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted'
@@ -10,6 +11,8 @@ import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { Card, CardContent, cn } from 'ui'
import { ReportChartUpsell } from './ReportChartUpsell'
import { useChartHighlight } from 'components/ui/Charts/useChartHighlight'
export interface ReportChartV2Props {
report: ReportConfig
projectRef: string
@@ -20,6 +23,7 @@ export interface ReportChartV2Props {
className?: string
syncId?: string
filters?: any
highlightActions?: ChartHighlightAction[]
}
export const ReportChartV2 = ({
@@ -32,6 +36,7 @@ export const ReportChartV2 = ({
className,
syncId,
filters,
highlightActions,
}: ReportChartV2Props) => {
const { data: org } = useSelectedOrganizationQuery()
const { plan: orgPlan } = useCurrentOrgPlan()
@@ -85,6 +90,7 @@ export const ReportChartV2 = ({
)
const [chartStyle, setChartStyle] = useState<string>(report.defaultChartStyle)
const chartHighlight = useChartHighlight()
if (!isAvailable) {
return <ReportChartUpsell report={report} orgSlug={org?.slug ?? ''} />
@@ -120,8 +126,8 @@ export const ReportChartV2 = ({
highlightedValue={0}
title={report.label}
customDateFormat={undefined}
chartHighlight={undefined}
chartStyle={chartStyle}
chartHighlight={chartHighlight}
showTooltip={report.showTooltip}
showLegend={report.showLegend}
showTotal={false}
@@ -133,6 +139,7 @@ export const ReportChartV2 = ({
titleTooltip={report.titleTooltip}
syncId={syncId}
sql={queryResult?.query}
highlightActions={highlightActions}
/>
</div>
)}

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import dayjs from 'dayjs'
import { ArrowRight, LogsIcon, SearchIcon } from 'lucide-react'
import { ArrowRight, SearchIcon } from 'lucide-react'
import { ReactNode, useEffect, useMemo, useState } from 'react'
import {
cn,
DropdownMenu,
@@ -12,17 +12,33 @@ import {
DropdownMenuTrigger,
} from 'ui'
import { ChartHighlight } from './useChartHighlight'
import { UpdateDateRange } from 'pages/project/[ref]/reports/database'
const ChartHighlightActions = ({
export type UpdateDateRange = (from: string, to: string) => void
export type ChartHighlightActionContext = {
start: string
end: string
clear: () => void
}
export type ChartHighlightAction = {
id: string
label: string | ((ctx: ChartHighlightActionContext) => string)
icon?: ReactNode
isDisabled?: (ctx: ChartHighlightActionContext) => boolean
rightSlot?: ReactNode | ((ctx: ChartHighlightActionContext) => ReactNode)
onSelect: (ctx: ChartHighlightActionContext) => void
}
export const ChartHighlightActions = ({
chartHighlight,
updateDateRange,
actions,
}: {
chartHighlight?: ChartHighlight
updateDateRange: UpdateDateRange
updateDateRange?: UpdateDateRange
actions?: ChartHighlightAction[]
}) => {
const router = useRouter()
const { ref } = router.query
const { left: selectedRangeStart, right: selectedRangeEnd, clearHighlight } = chartHighlight ?? {}
const [isOpen, setIsOpen] = useState(!!chartHighlight?.popoverPosition)
@@ -30,18 +46,34 @@ const ChartHighlightActions = ({
setIsOpen(!!chartHighlight?.popoverPosition && selectedRangeStart !== selectedRangeEnd)
}, [chartHighlight?.popoverPosition])
const disableZoomIn = dayjs(selectedRangeEnd).diff(dayjs(selectedRangeStart), 'minutes') < 10
const handleZoomIn = () => {
if (disableZoomIn) return
updateDateRange(selectedRangeStart!, selectedRangeEnd!)
clearHighlight && clearHighlight()
}
const ctx: ChartHighlightActionContext | undefined =
selectedRangeStart && selectedRangeEnd && clearHighlight
? { start: selectedRangeStart, end: selectedRangeEnd, clear: clearHighlight }
: undefined
const handleOpenLogsExplorer = () => {
const rangeQueryParams = `?its=${selectedRangeStart}&ite=${selectedRangeEnd}`
router.push(`/project/${ref}/logs/postgres-logs${rangeQueryParams}`)
clearHighlight && clearHighlight()
}
const defaultActions: ChartHighlightAction[] = useMemo(() => {
if (!updateDateRange || !ctx) return []
const isDisabled = dayjs(ctx.end).diff(dayjs(ctx.start), 'minutes') < 10
return [
{
id: 'zoom-in',
label: 'Zoom in',
icon: <SearchIcon className="text-foreground-lighter" size={12} />,
rightSlot: isDisabled ? <span className="text-xs">Min. 10 minutes</span> : null,
isDisabled: () => isDisabled,
onSelect: ({ start, end, clear }) => {
if (isDisabled) return
updateDateRange(start, end)
clear()
},
},
]
}, [ctx, updateDateRange])
const allActions: ChartHighlightAction[] = useMemo(() => {
const provided = actions ?? []
return [...defaultActions, ...provided]
}, [defaultActions, actions])
return (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
@@ -54,37 +86,41 @@ const ChartHighlightActions = ({
}}
/>
<DropdownMenuContent className="flex flex-col gap-1 p-1 w-fit text-left">
<DropdownMenuLabel className="flex items-center justify-center text-foreground-lighter font-mono gap-1 text-xs">
<DropdownMenuLabel className="flex items-center justify-center text-foreground-light font-mono gap-x-2 text-xs">
<span>{dayjs(selectedRangeStart).format('MMM D, H:mm')}</span>
<ArrowRight size={10} />
<span>{dayjs(selectedRangeEnd).format('MMM D, H:mm')}</span>
</DropdownMenuLabel>
<DropdownMenuSeparator className="my-0" />
<DropdownMenuItem
disabled={disableZoomIn}
className={cn('group', disableZoomIn && '!bg-transparent')}
>
<button
disabled={disableZoomIn}
onClick={handleZoomIn}
className="w-full flex items-center gap-1.5"
>
<SearchIcon className="text-foreground-lighter" size={12} />
<span className="flex-grow text-left text-foreground-light">Zoom in</span>
{disableZoomIn && <span className="text-xs">Min. 10 minutes</span>}
</button>
</DropdownMenuItem>
<DropdownMenuItem className={cn('group', disableZoomIn && '!bg-transparent')}>
<button onClick={handleOpenLogsExplorer} className="w-full flex items-center gap-1.5">
<LogsIcon className="text-foreground-lighter" size={12} />
<span className="flex-grow text-left text-foreground-light">
Open range in Logs Explorer
</span>
</button>
</DropdownMenuItem>
{allActions.map((action) => {
const disabled = ctx && action.isDisabled ? action.isDisabled(ctx) : false
let labelNode: ReactNode = null
if (typeof action.label === 'function') {
labelNode = ctx ? action.label(ctx) : null
} else {
labelNode = action.label
}
let rightNode: ReactNode = null
if (typeof action.rightSlot === 'function') {
rightNode = ctx ? action.rightSlot(ctx) : null
} else {
rightNode = action.rightSlot ?? null
}
return (
<DropdownMenuItem asChild key={action.id} disabled={disabled} className={cn('group')}>
<button
disabled={disabled}
onClick={() => ctx && action.onSelect(ctx)}
className="w-full flex items-center gap-1.5"
>
{action.icon}
<span className="flex-grow text-left">{labelNode}</span>
{rightNode}
</button>
</DropdownMenuItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
)
}
export default ChartHighlightActions

View File

@@ -19,7 +19,7 @@ import {
import { CategoricalChartState } from 'recharts/types/chart/types'
import { cn } from 'ui'
import { ChartHeader } from './ChartHeader'
import ChartHighlightActions from './ChartHighlightActions'
import { ChartHighlightActions, ChartHighlightAction } from './ChartHighlightActions'
import {
CHART_COLORS,
DateTimeFormats,
@@ -64,6 +64,7 @@ export interface ComposedChartProps<D = Datum> extends CommonChartProps<D> {
syncId?: string
docsUrl?: string
sql?: string
highlightActions?: ChartHighlightAction[]
}
export function ComposedChart({
@@ -101,6 +102,7 @@ export function ComposedChart({
syncId,
docsUrl,
sql,
highlightActions,
}: ComposedChartProps) {
const { resolvedTheme } = useTheme()
const { hoveredIndex, syncTooltip, setHover, clearHover } = useChartHoverState(
@@ -336,6 +338,7 @@ export function ComposedChart({
<RechartComposedChart
data={data}
syncId={syncId}
style={{ cursor: 'crosshair' }}
onMouseMove={(e: any) => {
setIsActiveHoveredChart(true)
if (e.activeTooltipIndex !== focusDataIndex) {
@@ -390,7 +393,7 @@ export function ComposedChart({
/>
<Tooltip
content={(props) =>
showTooltip ? (
showTooltip && !showHighlightActions ? (
<CustomTooltip
{...props}
format={format}
@@ -491,7 +494,11 @@ export function ComposedChart({
/>
)}
</RechartComposedChart>
<ChartHighlightActions chartHighlight={chartHighlight} updateDateRange={updateDateRange} />
<ChartHighlightActions
chartHighlight={chartHighlight}
updateDateRange={updateDateRange}
actions={highlightActions}
/>
</Container>
{data && (
<div

View File

@@ -1,9 +1,10 @@
import { Loader2 } from 'lucide-react'
import { List, Loader2 } from 'lucide-react'
import { useRouter } from 'next/router'
import React, { PropsWithChildren, useEffect, useMemo, useRef, useState } from 'react'
import { cn, WarningIcon } from 'ui'
import { Card, cn, WarningIcon } from 'ui'
import Panel from 'components/ui/Panel'
import type { ChartHighlightAction } from './ChartHighlightActions'
import { ComposedChart } from './ComposedChart'
import { AnalyticsInterval, DataPoint } from 'data/analytics/constants'
@@ -231,20 +232,33 @@ const ComposedChartHandler = ({
: (firstData.data[firstData.data.length - 1] as any)?.[firstAttr.attribute]
}, [highlightedValue, attributes, attributeQueries])
const highlightActions: ChartHighlightAction[] = useMemo(() => {
return [
{
id: 'open-logs',
label: 'Open in Postgres Logs',
icon: <List size={12} />,
onSelect: ({ start, end }) => {
const projectRef = ref as string
if (!projectRef) return
const url = `/project/${projectRef}/logs/postgres-logs?its=${start}&ite=${end}`
router.push(url)
},
},
]
}, [ref])
if (loading) {
return (
<Panel
<Card
className={cn(
'flex min-h-[280px] 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>
</Card>
)
}
@@ -288,6 +302,7 @@ const ComposedChartHandler = ({
valuePrecision={valuePrecision}
hideChartType={hideChartType}
syncId={syncId}
highlightActions={highlightActions}
{...otherProps}
/>
</Panel.Content>

View File

@@ -2,10 +2,10 @@ import { numberFormatter } from 'components/ui/Charts/Charts.utils'
import { ReportAttributes } from 'components/ui/Charts/ComposedChart.utils'
import { formatBytes } from 'lib/helpers'
import { Organization } from 'types'
import { Project } from '../projects/project-detail-query'
import { DiskAttributesData } from '../config/disk-attributes-query'
import { MaxConnectionsData } from '../database/max-connections-query'
import { PgbouncerConfigData } from '../database/pgbouncer-config-query'
import { Project } from '../projects/project-detail-query'
export const getReportAttributes = (
org: Organization,
@@ -111,14 +111,14 @@ export const getReportAttributes = (
{
attribute: 'disk_iops_write',
provider: 'infra-monitoring',
label: 'write IOPS',
label: 'Write IOPS',
tooltip:
'Number of write operations per second. High values indicate frequent data writes, logging, or transaction activity',
},
{
attribute: 'disk_iops_read',
provider: 'infra-monitoring',
label: 'read IOPS',
label: 'Read IOPS',
tooltip:
'Number of read operations per second. High values suggest frequent disk reads due to queries or poor caching',
},
@@ -261,7 +261,7 @@ export const getReportAttributesV2: (
{
attribute: 'ram_usage_cache_and_buffers',
provider: 'infra-monitoring',
label: 'Cache + buffers',
label: 'Cache + Buffers',
tooltip:
'RAM used by the operating system page cache and PostgreSQL buffers to accelerate disk reads/writes',
},
@@ -366,14 +366,14 @@ export const getReportAttributesV2: (
{
attribute: 'disk_iops_write',
provider: 'infra-monitoring',
label: 'write IOPS',
label: 'Write IOPS',
tooltip:
'Number of write operations per second. High values indicate frequent data writes, logging, or transaction activity',
},
{
attribute: 'disk_iops_read',
provider: 'infra-monitoring',
label: 'read IOPS',
label: 'Read IOPS',
tooltip:
'Number of read operations per second. High values suggest frequent disk reads due to queries or poor caching',
},

View File

@@ -1,7 +1,7 @@
import { useQueryClient } from '@tanstack/react-query'
import { useParams } from 'common'
import dayjs from 'dayjs'
import { ArrowRight, RefreshCw } from 'lucide-react'
import { ArrowRight, LogsIcon, RefreshCw } from 'lucide-react'
import { useState } from 'react'
import { ReportChartV2 } from 'components/interfaces/Reports/v2/ReportChartV2'
@@ -22,6 +22,8 @@ import { useReportDateRange } from 'hooks/misc/useReportDateRange'
import type { NextPageWithLayout } from 'types'
import { createAuthReportConfig } from 'data/reports/v2/auth.config'
import { ReportSettings } from 'components/ui/Charts/ReportSettings'
import type { ChartHighlightAction } from 'components/ui/Charts/ChartHighlightActions'
import { useRouter } from 'next/router'
const AuthReport: NextPageWithLayout = () => {
return (
@@ -91,6 +93,29 @@ const AuthUsage = () => {
setTimeout(() => setIsRefreshing(false), 1000)
}
const router = useRouter()
const highlightActions: ChartHighlightAction[] = [
{
id: 'api-gateway-logs',
label: 'Open in API Gateway Logs',
icon: <LogsIcon size={12} />,
onSelect: ({ start, end, clear }) => {
const url = `/project/${ref}/logs/edge-logs?its=${start}&ite=${end}&f={"product":{"auth":true}}`
router.push(url)
},
},
{
id: 'auth-logs',
label: 'Open in Auth Logs',
icon: <LogsIcon size={12} />,
onSelect: ({ start, end, clear }) => {
const url = `/project/${ref}/logs/auth-logs?its=${start}&ite=${end}`
router.push(url)
},
},
]
return (
<>
<ReportHeader title="Auth" showDatabaseSelector={false} />
@@ -149,6 +174,7 @@ const AuthUsage = () => {
filters={{
status_code: null,
}}
highlightActions={highlightActions}
/>
))}
<div>