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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user