From c9033b10355881f64e21704f84c0ad751c05ea0b Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:56:48 +0200 Subject: [PATCH] FE-1864 FE-1868 reports kaizen 2 (#38881) * make opts configurable per report * Minor nits --------- Co-authored-by: Joshen Lim --- .../interfaces/Reports/v2/ReportChartV2.tsx | 9 +- .../ui/Charts/ChartHighlightActions.tsx | 124 +++++++++++------- .../components/ui/Charts/ComposedChart.tsx | 13 +- .../ui/Charts/ComposedChartHandler.tsx | 29 +++- apps/studio/data/reports/database-charts.ts | 12 +- .../pages/project/[ref]/reports/auth.tsx | 28 +++- 6 files changed, 153 insertions(+), 62 deletions(-) diff --git a/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx index 19e8e01391..57c3c07f83 100644 --- a/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx +++ b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx @@ -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(report.defaultChartStyle) + const chartHighlight = useChartHighlight() if (!isAvailable) { return @@ -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} /> )} diff --git a/apps/studio/components/ui/Charts/ChartHighlightActions.tsx b/apps/studio/components/ui/Charts/ChartHighlightActions.tsx index 21633ec2dd..7a66eaef0b 100644 --- a/apps/studio/components/ui/Charts/ChartHighlightActions.tsx +++ b/apps/studio/components/ui/Charts/ChartHighlightActions.tsx @@ -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: , + rightSlot: isDisabled ? Min. 10 minutes : 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 ( @@ -54,37 +86,41 @@ const ChartHighlightActions = ({ }} /> - + {dayjs(selectedRangeStart).format('MMM D, H:mm')} {dayjs(selectedRangeEnd).format('MMM D, H:mm')} - - - - - - + {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 ( + + + + ) + })} ) } - -export default ChartHighlightActions diff --git a/apps/studio/components/ui/Charts/ComposedChart.tsx b/apps/studio/components/ui/Charts/ComposedChart.tsx index 36a353c918..8177227a80 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.tsx @@ -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 extends CommonChartProps { 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({ { setIsActiveHoveredChart(true) if (e.activeTooltipIndex !== focusDataIndex) { @@ -390,7 +393,7 @@ export function ComposedChart({ /> - showTooltip ? ( + showTooltip && !showHighlightActions ? ( )} - + {data && (
{ + return [ + { + id: 'open-logs', + label: 'Open in Postgres Logs', + icon: , + 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 ( -

Loading data for {label}

-
+ ) } @@ -288,6 +302,7 @@ const ComposedChartHandler = ({ valuePrecision={valuePrecision} hideChartType={hideChartType} syncId={syncId} + highlightActions={highlightActions} {...otherProps} /> diff --git a/apps/studio/data/reports/database-charts.ts b/apps/studio/data/reports/database-charts.ts index 2f4eb36e21..ea287b5b8c 100644 --- a/apps/studio/data/reports/database-charts.ts +++ b/apps/studio/data/reports/database-charts.ts @@ -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', }, diff --git a/apps/studio/pages/project/[ref]/reports/auth.tsx b/apps/studio/pages/project/[ref]/reports/auth.tsx index 1a26d3fdee..e077eb62bf 100644 --- a/apps/studio/pages/project/[ref]/reports/auth.tsx +++ b/apps/studio/pages/project/[ref]/reports/auth.tsx @@ -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: , + 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: , + onSelect: ({ start, end, clear }) => { + const url = `/project/${ref}/logs/auth-logs?its=${start}&ite=${end}` + router.push(url) + }, + }, + ] + return ( <> @@ -149,6 +174,7 @@ const AuthUsage = () => { filters={{ status_code: null, }} + highlightActions={highlightActions} /> ))}