Files
Jordi Enric c228ca4f61 fix(studio): restore function deployment annotations FE-3921 (#50362)
## Problem

The Edge Function overview converted the numeric deployment timestamp to
a string before parsing it as a date. This produced an invalid date, so
the invocations chart omitted the deployment annotation.

## Fix

Preserve the numeric timestamp returned by the API and allow the
annotation helper to parse both numeric and string timestamps. Show
deployment details in an accessible tooltip when hovering or focusing
the rocket marker, and add regression coverage for numeric timestamps
and the existing invalid, missing, and out-of-range cases.

## How to test

- Run the focused EdgeFunctionOverview utility test suite.
- Open an Edge Function whose latest deployment is within the selected
chart interval.
- Hover or focus the rocket marker.
- Expected result: the invocations chart shows the dashed deployment
line and rocket marker, and the marker tooltip shows the deployment
time.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Invocation update annotations now display correctly when function
update times are provided as numbers.
  - Timestamps at the start of the Unix epoch are now supported.
- Annotations no longer appear when update times are invalid or chart
data is incomplete.

- **Accessibility**
- Deployment markers in invocation charts are now keyboard-focusable and
include accessible labels.
  - Deployment timestamps are available in a tooltip on hover or focus.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-15 12:04:15 +02:00

138 lines
4.3 KiB
TypeScript

import { Rocket } from 'lucide-react'
import { useMemo } from 'react'
import {
Bar,
CartesianGrid,
BarChart as RechartBarChart,
ReferenceLine,
XAxis,
YAxis,
} from 'recharts'
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import {
formatChartTimestamp,
getChartTimeRangeLabels,
INVOCATION_CHART_CONFIG,
} from './EdgeFunctionOverview.utils'
import type { InvocationChartDatum, InvocationUpdateAnnotation } from './EdgeFunctionOverview.utils'
interface EdgeFunctionInvocationsChartProps {
chartData: InvocationChartDatum[]
dateTimeFormat: string
onChartClick: (timestamp: string) => void
updateAnnotation?: InvocationUpdateAnnotation
}
export const EdgeFunctionInvocationsChart = ({
chartData,
dateTimeFormat,
onChartClick,
updateAnnotation,
}: EdgeFunctionInvocationsChartProps) => {
const timeRangeLabels = useMemo(
() => getChartTimeRangeLabels(chartData, dateTimeFormat),
[chartData, dateTimeFormat]
)
return (
<div className="flex flex-col gap-1">
<div className="relative h-40 w-full overflow-visible">
<ChartContainer config={INVOCATION_CHART_CONFIG} className="aspect-auto! h-full! w-full!">
<RechartBarChart
data={chartData}
className="cursor-pointer"
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
onClick={(tooltipData) => {
const timestamp = tooltipData?.activePayload?.[0]?.payload?.timestamp
if (typeof timestamp === 'string') onChartClick(timestamp)
}}
>
<CartesianGrid vertical={false} />
<YAxis hide width={0} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tick={false}
minTickGap={32}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
className="text-foreground-light"
labelFormatter={(value) =>
formatChartTimestamp(value as string | number | undefined, dateTimeFormat)
}
indicator="dot"
/>
}
/>
<Bar
dataKey="error_count"
stackId="invocations"
fill="var(--color-error_count)"
maxBarSize={24}
/>
<Bar
dataKey="warning_count"
stackId="invocations"
fill="var(--color-warning_count)"
maxBarSize={24}
/>
<Bar
dataKey="ok_count"
stackId="invocations"
fill="var(--color-ok_count)"
maxBarSize={24}
/>
{updateAnnotation && (
<ReferenceLine
x={updateAnnotation.timestamp}
stroke="var(--foreground-default)"
strokeDasharray="4 4"
strokeWidth={1.5}
/>
)}
</RechartBarChart>
</ChartContainer>
{updateAnnotation && (
<Tooltip>
<TooltipTrigger asChild>
<span
className="pointer-events-auto absolute bottom-0 z-10 flex h-6 w-6 -translate-x-1/2 translate-y-1/2 items-center justify-center rounded-full border border-foreground/20 bg-background text-foreground shadow-xs"
style={{ left: `${updateAnnotation.position}%` }}
role="img"
tabIndex={0}
aria-label={`Deployment at ${formatChartTimestamp(
updateAnnotation.updatedAt,
dateTimeFormat
)}`}
>
<Rocket size={12} strokeWidth={1.75} aria-hidden />
</span>
</TooltipTrigger>
<TooltipContent side="top" align="center">
Deployment at {formatChartTimestamp(updateAnnotation.updatedAt, dateTimeFormat)}
</TooltipContent>
</Tooltip>
)}
</div>
{timeRangeLabels && (
<div className="-mt-6 flex items-center justify-between text-[10px] font-mono text-foreground-lighter">
<span>{timeRangeLabels.start}</span>
<span>{timeRangeLabels.end}</span>
</div>
)}
</div>
)
}