import { Label } from '@ui/components/shadcn/ui/label' import { RadioGroup, RadioGroupItem } from '@ui/components/shadcn/ui/radio-group' import dayjs from 'dayjs' import { Clock, HistoryIcon, Lock } from 'lucide-react' import type { PropsWithChildren } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { Button, ButtonProps, Calendar, cn, copyToClipboard, Input, Popover, PopoverContent, PopoverTrigger, } from 'ui' import { LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD } from './Logs.constants' import type { DatetimeHelper } from './Logs.types' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { TimeSplitInput } from '@/components/ui/DatePicker/TimeSplitInput' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' type Unit = 'minute' | 'hour' | 'day' export type ParsedCustomInput = | { type: 'number'; value: number } | { type: 'unit'; value: number; unit: Unit } | { type: 'invalid' } export const parseCustomInput = (input: string): ParsedCustomInput => { const trimmed = input.trim().toLowerCase() if (!trimmed) return { type: 'invalid' } // Try to match "number + optional space + unit prefix" const match = trimmed.match(/^(\d+)\s*([a-z]*)$/) if (!match) return { type: 'invalid' } const [, numStr, unitStr] = match const value = parseInt(numStr, 10) if (isNaN(value) || value <= 0) return { type: 'invalid' } if (!unitStr) { return { type: 'number', value } } // Match if unitStr is a prefix of any unit name or its first letter const units: Unit[] = ['minute', 'hour', 'day'] const matchedUnit = units.find((u) => u.startsWith(unitStr) || u[0] === unitStr) if (!matchedUnit) return { type: 'invalid' } return { type: 'unit', value, unit: matchedUnit } } export const generateDynamicHelper = (value: number, unit: Unit): DatetimeHelper => { return { text: `Last ${value} ${unit}${value === 1 ? '' : 's'}`, calcFrom: () => dayjs().subtract(value, unit).toISOString(), calcTo: () => dayjs().toISOString(), } } export const generateDynamicHelpers = (value: number): DatetimeHelper[] => { const units: Unit[] = ['minute', 'hour', 'day'] return units.map((unit) => generateDynamicHelper(value, unit)) } export const generateHelpersFromInput = (input: string): DatetimeHelper[] | null => { const parsed = parseCustomInput(input) switch (parsed.type) { case 'number': return generateDynamicHelpers(parsed.value) case 'unit': return [generateDynamicHelper(parsed.value, parsed.unit)] case 'invalid': return null } } export type DatePickerValue = { to: string from: string isHelper?: boolean text?: string } interface LogsDatePickerProps { value: DatePickerValue helpers: DatetimeHelper[] onSubmit: (value: DatePickerValue) => void buttonTriggerProps?: ButtonProps popoverContentProps?: typeof PopoverContent hideWarnings?: boolean align?: 'start' | 'end' | 'center' open?: boolean onOpenChange?: (open: boolean) => void } export const LogsDatePicker = ({ onSubmit, helpers, value, buttonTriggerProps, popoverContentProps, hideWarnings, align = 'end', open: openProp, onOpenChange, }: PropsWithChildren) => { const [internalOpen, setInternalOpen] = useState(false) const isControlled = openProp !== undefined const open = isControlled ? openProp : internalOpen const setOpen = (next: boolean) => { if (!isControlled) setInternalOpen(next) onOpenChange?.(next) } const [customValue, setCustomValue] = useState('') const displayedHelpers = useMemo(() => { if (!customValue.trim()) return helpers const generated = generateHelpersFromInput(customValue) return generated ?? [] }, [customValue, helpers]) // Reset the state when the popover closes useEffect(() => { if (!open) { setCustomValue('') setStartDate(value.from ? new Date(value.from) : null) const defaultEndDate = value.to ? new Date(value.to) : new Date() setEndDate(defaultEndDate) setCurrentMonth(new Date(defaultEndDate)) const fromDate = value.from ? new Date(value.from) : null const toDate = value.to ? new Date(value.to) : null setStartTime({ HH: fromDate?.getHours().toString().padStart(2, '0') || '00', mm: fromDate?.getMinutes().toString().padStart(2, '0') || '00', ss: fromDate?.getSeconds().toString().padStart(2, '0') || '00', }) const now = new Date() const nowHH = now.getHours().toString().padStart(2, '0') const nowMM = now.getMinutes().toString().padStart(2, '0') const nowSS = now.getSeconds().toString().padStart(2, '0') setEndTime({ HH: toDate?.getHours().toString().padStart(2, '0') || nowHH, mm: toDate?.getMinutes().toString().padStart(2, '0') || nowMM, ss: toDate?.getSeconds().toString().padStart(2, '0') || nowSS, }) } }, [open, value]) const handleHelperChange = (newValue: string) => { const selectedHelper = displayedHelpers.find((h) => h.text === newValue) if (onSubmit && selectedHelper) { onSubmit({ to: selectedHelper.calcTo(), from: selectedHelper.calcFrom(), isHelper: true, text: selectedHelper.text, }) } setOpen(false) } const [startDate, setStartDate] = useState(value.from ? new Date(value.from) : null) const [endDate, setEndDate] = useState(value.to ? new Date(value.to) : new Date()) const [currentMonth, setCurrentMonth] = useState(() => value.to ? new Date(value.to) : new Date() ) const [startTime, setStartTime] = useState({ HH: startDate?.getHours().toString() || '00', mm: startDate?.getMinutes().toString() || '00', ss: startDate?.getSeconds().toString() || '00', }) const [endTime, setEndTime] = useState({ HH: endDate?.getHours().toString() || '23', mm: endDate?.getMinutes().toString() || '59', ss: endDate?.getSeconds().toString() || '59', }) function handleDatePickerChange(dates: [from: Date | null, to: Date | null]) { const [from, to] = dates setStartDate(from) setEndDate(to) } function handleApply() { const from = startDate || new Date() const to = endDate || new Date() // Add Time to the dates const finalFrom = new Date(from.setHours(+startTime.HH, +startTime.mm, +startTime.ss)) const finalTo = new Date(to.setHours(+endTime.HH, +endTime.mm, +endTime.ss)) onSubmit({ from: finalFrom.toISOString(), to: finalTo.toISOString(), isHelper: false, }) setOpen(false) } const [copied, setCopied] = useState(false) const [pasted, setPasted] = useState(false) useEffect(() => { if (copied) { setTimeout(() => { setCopied(false) }, 2000) } }, [copied]) function handlePaste() { navigator.clipboard .readText() .then((text) => { try { const json = JSON.parse(text) if (!json.from || !json.to) { console.warn('Invalid date range format in clipboard') return } const fromDate = new Date(json.from) const toDate = new Date(json.to) // Check if dates are valid if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) { console.warn('Invalid date values in clipboard') return } setStartDate(fromDate) setEndDate(toDate) setCurrentMonth(new Date(toDate)) // Update time states setStartTime({ HH: fromDate.getHours().toString(), mm: fromDate.getMinutes().toString(), ss: fromDate.getSeconds().toString(), }) setEndTime({ HH: toDate.getHours().toString(), mm: toDate.getMinutes().toString(), ss: toDate.getSeconds().toString(), }) setPasted(true) } catch (error) { console.warn('Failed to parse clipboard content as date range:', error) } }) .catch((error) => { console.warn('Failed to read clipboard:', error) }) } const handleCopy = useCallback(() => { if (!startDate || !endDate) return const fromDate = new Date(startDate) const toDate = new Date(endDate) // Add time from time states fromDate.setHours(+startTime.HH, +startTime.mm, +startTime.ss) toDate.setHours(+endTime.HH, +endTime.mm, +endTime.ss) copyToClipboard( JSON.stringify({ from: fromDate.toISOString(), to: toDate.toISOString(), }) ) setCopied(true) }, [startDate, endDate, startTime, endTime]) useEffect(() => { if (pasted) { setTimeout(() => { setPasted(false) }, 2000) } }, [pasted]) useEffect(() => { if (open) { document.addEventListener('paste', handlePaste) document.addEventListener('copy', handleCopy) } return () => { document.removeEventListener('paste', handlePaste) document.removeEventListener('copy', handleCopy) } }, [open, startDate, endDate, handleCopy]) const isLargeRange = Math.abs(dayjs(startDate).diff(dayjs(endDate), 'days')) > LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD - 1 const { getEntitlementNumericValue } = useCheckEntitlements('log.retention_days') const entitledToAuditLogDays = getEntitlementNumericValue() const showHelperBadge = (helper?: DatetimeHelper) => { if (!helper) return false if (!entitledToAuditLogDays) return false const day = Math.abs(dayjs().diff(dayjs(helper.calcFrom()), 'day')) if (day <= entitledToAuditLogDays) return false return true } return (
setCustomValue(e.target.value)} className="mb-2 text-xs h-7 rounded-xs" /> {displayedHelpers.map((helper) => ( ))}
} type="text" size="tiny" className="px-1.5" onClick={() => { setStartTime({ HH: '00', mm: '00', ss: '00' }) setEndTime({ HH: '00', mm: '00', ss: '00' }) }} >
setCurrentMonth(new Date(month))} selected={{ from: startDate ?? undefined, to: endDate ?? undefined }} onSelect={(range) => { handleDatePickerChange([range?.from ?? null, range?.to ?? null]) }} />
{isLargeRange && !hideWarnings && (
Large ranges may result in memory errors for
big projects.
)}
{startDate && endDate ? ( ) : null}
) }