| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141 |
- import dayjs from 'dayjs'
- import { ArrowRight, SearchIcon } from 'lucide-react'
- import { ReactNode, useEffect, useMemo, useState } from 'react'
- import {
- cn,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
- } from 'ui'
- import { ChartHighlight } from './useChartHighlight'
- import { useFormatDateTime } from '@/lib/datetime'
- export type UpdateDateRange = (from: string, to: string) => void
- export type ChartHighlightActionContext = {
- start: string
- end: string
- clear: () => void
- chartId?: string
- }
- 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,
- chartId,
- }: {
- chartHighlight?: ChartHighlight
- updateDateRange?: UpdateDateRange
- actions?: ChartHighlightAction[]
- chartId?: string
- }) => {
- const { left: selectedRangeStart, right: selectedRangeEnd, clearHighlight } = chartHighlight ?? {}
- const [isOpen, setIsOpen] = useState(!!chartHighlight?.popoverPosition)
- const formatChartDate = useFormatDateTime()
- useEffect(() => {
- setIsOpen(!!chartHighlight?.popoverPosition && selectedRangeStart !== selectedRangeEnd)
- }, [chartHighlight?.popoverPosition])
- const ctx: ChartHighlightActionContext | undefined =
- selectedRangeStart && selectedRangeEnd && clearHighlight
- ? { start: selectedRangeStart, end: selectedRangeEnd, clear: clearHighlight, chartId }
- : undefined
- 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}>
- <DropdownMenuTrigger
- className="w-auto p-0"
- style={{
- position: 'absolute',
- left: chartHighlight?.popoverPosition?.x + 'px' || 0,
- top: chartHighlight?.popoverPosition?.y + 'px' || 0,
- }}
- />
- <DropdownMenuContent
- className="flex flex-col gap-1 p-1 w-fit text-left"
- onEscapeKeyDown={() => clearHighlight?.()}
- onInteractOutside={(e) => {
- const target = e.target as Element | null
- // If the user clicked on a chart, handleMouseDown will manage the new selection.
- // Calling clearHighlight here would race with it and clobber the new state.
- if (target?.closest('.recharts-wrapper')) return
- clearHighlight?.()
- }}
- >
- <DropdownMenuLabel className="flex items-center justify-center text-foreground-light font-mono gap-x-2 text-xs">
- <span>{formatChartDate(selectedRangeStart!, 'MMM D, H:mm')}</span>
- <ArrowRight size={10} />
- <span>{formatChartDate(selectedRangeEnd!, 'MMM D, H:mm')}</span>
- </DropdownMenuLabel>
- <DropdownMenuSeparator className="my-0" />
- {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="grow text-left">{labelNode}</span>
- {rightNode}
- </button>
- </DropdownMenuItem>
- )
- })}
- </DropdownMenuContent>
- </DropdownMenu>
- )
- }
|