diff --git a/frontend/src/component/events/EventTimeline/EventTimeline.tsx b/frontend/src/component/events/EventTimeline/EventTimeline.tsx index 956f588bd073..faf43cdfd81c 100644 --- a/frontend/src/component/events/EventTimeline/EventTimeline.tsx +++ b/frontend/src/component/events/EventTimeline/EventTimeline.tsx @@ -2,15 +2,19 @@ import { styled } from '@mui/material'; import type { EventSchema, EventSchemaType } from 'openapi'; import { startOfDay, sub } from 'date-fns'; import { useEventSearch } from 'hooks/api/getters/useEventSearch/useEventSearch'; -import { EventTimelineEvent } from './EventTimelineEvent/EventTimelineEvent'; +import { EventTimelineEventGroup } from './EventTimelineEventGroup/EventTimelineEventGroup'; import { EventTimelineHeader } from './EventTimelineHeader/EventTimelineHeader'; import { useEventTimeline } from './useEventTimeline'; +import { useMemo } from 'react'; export type EnrichedEvent = EventSchema & { label: string; summary: string; + timestamp: number; }; +export type TimelineEventGroup = EnrichedEvent[]; + const StyledRow = styled('div')({ display: 'flex', flexDirection: 'row', @@ -88,6 +92,8 @@ export const EventTimeline = () => { const endDate = new Date(); const startDate = sub(endDate, timeSpan.value); + const endTime = endDate.getTime(); + const startTime = startDate.getTime(); const { events: baseEvents } = useEventSearch( { @@ -98,19 +104,52 @@ export const EventTimeline = () => { { refreshInterval: 10 * 1000 }, ); - const events = baseEvents as EnrichedEvent[]; + const events = useMemo(() => { + return baseEvents.map((event) => ({ + ...event, + timestamp: new Date(event.createdAt).getTime(), + })); + }, [baseEvents]) as EnrichedEvent[]; const filteredEvents = events.filter( (event) => - new Date(event.createdAt).getTime() >= startDate.getTime() && - new Date(event.createdAt).getTime() <= endDate.getTime() && - RELEVANT_EVENT_TYPES.includes(event.type) && + event.timestamp >= startTime && + event.timestamp <= endTime && (!event.environment || !environment || event.environment === environment.name), ); - const sortedEvents = [...filteredEvents].reverse(); + const sortedEvents = filteredEvents.reverse(); + + const timespanInMs = endTime - startTime; + const groupingThresholdInMs = useMemo( + () => timespanInMs * 0.02, + [timespanInMs], + ); + + const groups = useMemo( + () => + sortedEvents.reduce((groups: TimelineEventGroup[], event) => { + if (groups.length === 0) { + groups.push([event]); + } else { + const lastGroup = groups[groups.length - 1]; + const lastEventInGroup = lastGroup[lastGroup.length - 1]; + + if ( + event.timestamp - lastEventInGroup.timestamp <= + groupingThresholdInMs + ) { + lastGroup.push(event); + } else { + groups.push([event]); + } + } + return groups; + }, []), + [sortedEvents, groupingThresholdInMs], + ); return ( <> @@ -126,10 +165,10 @@ export const EventTimeline = () => { - {sortedEvents.map((event) => ( - ( + diff --git a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx deleted file mode 100644 index db09f744d866..000000000000 --- a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { styled } from '@mui/material'; -import { Markdown } from 'component/common/Markdown/Markdown'; -import { useLocationSettings } from 'hooks/useLocationSettings'; -import { formatDateYMDHMS } from 'utils/formatDate'; -import type { EnrichedEvent } from '../../EventTimeline'; - -const StyledTooltipHeader = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: theme.spacing(1), - gap: theme.spacing(2), - flexWrap: 'wrap', -})); - -const StyledTooltipTitle = styled('div')(({ theme }) => ({ - fontWeight: theme.fontWeight.bold, - fontSize: theme.fontSizes.smallBody, - wordBreak: 'break-word', - flex: 1, -})); - -const StyledDateTime = styled('div')(({ theme }) => ({ - color: theme.palette.text.secondary, - whiteSpace: 'nowrap', -})); - -interface IEventTimelineEventTooltipProps { - event: EnrichedEvent; -} - -export const EventTimelineEventTooltip = ({ - event, -}: IEventTimelineEventTooltipProps) => { - const { locationSettings } = useLocationSettings(); - const eventDateTime = formatDateYMDHMS( - event.createdAt, - locationSettings?.locale, - ); - - return ( - <> - - {event.label} - {eventDateTime} - - {event.summary} - - ); -}; diff --git a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEvent.tsx b/frontend/src/component/events/EventTimeline/EventTimelineEventGroup/EventTimelineEventCircle.tsx similarity index 58% rename from frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEvent.tsx rename to frontend/src/component/events/EventTimeline/EventTimelineEventGroup/EventTimelineEventCircle.tsx index 15f72edc3ba5..375340c697eb 100644 --- a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEvent.tsx +++ b/frontend/src/component/events/EventTimeline/EventTimelineEventGroup/EventTimelineEventCircle.tsx @@ -1,34 +1,22 @@ import type { EventSchemaType } from 'openapi'; -import { styled } from '@mui/material'; -import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; -import { EventTimelineEventTooltip } from './EventTimelineEventTooltip/EventTimelineEventTooltip'; import ToggleOnIcon from '@mui/icons-material/ToggleOn'; import ToggleOffIcon from '@mui/icons-material/ToggleOff'; import FlagOutlinedIcon from '@mui/icons-material/FlagOutlined'; import ExtensionOutlinedIcon from '@mui/icons-material/ExtensionOutlined'; import SegmentsIcon from '@mui/icons-material/DonutLargeOutlined'; import QuestionMarkIcon from '@mui/icons-material/QuestionMark'; -import type { EnrichedEvent } from '../EventTimeline'; +import { styled } from '@mui/material'; +import type { TimelineEventGroup } from '../EventTimeline'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import type { HTMLAttributes } from 'react'; type DefaultEventVariant = 'secondary'; type CustomEventVariant = 'success' | 'neutral'; type EventVariant = DefaultEventVariant | CustomEventVariant; -const StyledEvent = styled('div', { - shouldForwardProp: (prop) => prop !== 'position', -})<{ position: string }>(({ position }) => ({ - position: 'absolute', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - left: position, - transform: 'translateX(-50%)', - zIndex: 1, -})); - const StyledEventCircle = styled('div', { shouldForwardProp: (prop) => prop !== 'variant', -})<{ variant: EventVariant }>(({ theme, variant }) => ({ +})<{ variant?: EventVariant }>(({ theme, variant = 'secondary' }) => ({ height: theme.spacing(3), width: theme.spacing(3), borderRadius: '50%', @@ -76,35 +64,31 @@ const customEventVariants: Partial< 'feature-archived': 'neutral', }; -interface IEventTimelineEventProps { - event: EnrichedEvent; - startDate: Date; - endDate: Date; +interface IEventTimelineEventCircleProps + extends HTMLAttributes { + group: TimelineEventGroup; } -export const EventTimelineEvent = ({ - event, - startDate, - endDate, -}: IEventTimelineEventProps) => { - const timelineDuration = endDate.getTime() - startDate.getTime(); - const eventTime = new Date(event.createdAt).getTime(); - - const position = `${((eventTime - startDate.getTime()) / timelineDuration) * 100}%`; +export const EventTimelineEventCircle = ({ + group, + ...props +}: IEventTimelineEventCircleProps) => { + if (group.length === 1) { + const event = group[0]; - const variant = customEventVariants[event.type] || 'secondary'; + return ( + + {getEventIcon(event.type)} + + ); + } return ( - - } - maxWidth={320} - arrow - > - - {getEventIcon(event.type)} - - - + + + ); }; diff --git a/frontend/src/component/events/EventTimeline/EventTimelineEventGroup/EventTimelineEventGroup.tsx b/frontend/src/component/events/EventTimeline/EventTimelineEventGroup/EventTimelineEventGroup.tsx new file mode 100644 index 000000000000..a9ecfba70941 --- /dev/null +++ b/frontend/src/component/events/EventTimeline/EventTimelineEventGroup/EventTimelineEventGroup.tsx @@ -0,0 +1,52 @@ +import { Badge, styled } from '@mui/material'; +import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; +import { EventTimelineEventTooltip } from './EventTimelineEventTooltip/EventTimelineEventTooltip'; +import type { TimelineEventGroup } from '../EventTimeline'; +import { EventTimelineEventCircle } from './EventTimelineEventCircle'; + +const StyledEvent = styled('div', { + shouldForwardProp: (prop) => prop !== 'position', +})<{ position: string }>(({ position }) => ({ + position: 'absolute', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + left: position, + transform: 'translateX(-50%)', + zIndex: 1, +})); + +interface IEventTimelineEventProps { + group: TimelineEventGroup; + startDate: Date; + endDate: Date; +} + +export const EventTimelineEventGroup = ({ + group, + startDate, + endDate, +}: IEventTimelineEventProps) => { + const timelineDuration = endDate.getTime() - startDate.getTime(); + const eventTime = new Date(group[0].createdAt).getTime(); + + const position = `${((eventTime - startDate.getTime()) / timelineDuration) * 100}%`; + + return ( + + } + maxWidth={320} + arrow + > + + + + + + ); +}; diff --git a/frontend/src/component/events/EventTimeline/EventTimelineEventGroup/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx b/frontend/src/component/events/EventTimeline/EventTimelineEventGroup/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx new file mode 100644 index 000000000000..97910fb29c22 --- /dev/null +++ b/frontend/src/component/events/EventTimeline/EventTimelineEventGroup/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx @@ -0,0 +1,99 @@ +import { styled } from '@mui/material'; +import { Markdown } from 'component/common/Markdown/Markdown'; +import { useLocationSettings } from 'hooks/useLocationSettings'; +import { formatDateYMDHMS } from 'utils/formatDate'; +import type { TimelineEventGroup } from '../../EventTimeline'; +import { EventTimelineEventCircle } from '../EventTimelineEventCircle'; + +const StyledTooltipHeader = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: theme.spacing(1), + gap: theme.spacing(2), + flexWrap: 'wrap', +})); + +const StyledTooltipTitle = styled('div')(({ theme }) => ({ + fontWeight: theme.fontWeight.bold, + fontSize: theme.fontSizes.smallBody, + wordBreak: 'break-word', + flex: 1, +})); + +const StyledDateTime = styled('div')(({ theme }) => ({ + color: theme.palette.text.secondary, + whiteSpace: 'nowrap', +})); + +const StyledTooltipItem = styled('div')(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1), + marginBottom: theme.spacing(1), +})); + +const StyledEventTimelineEventCircle = styled(EventTimelineEventCircle)( + ({ theme }) => ({ + marginTop: theme.spacing(0.5), + height: theme.spacing(2.5), + width: theme.spacing(2.5), + transition: 'none', + '& > svg': { + height: theme.spacing(2), + }, + '&:hover': { + transform: 'none', + }, + }), +); + +interface IEventTimelineEventTooltipProps { + group: TimelineEventGroup; +} + +export const EventTimelineEventTooltip = ({ + group, +}: IEventTimelineEventTooltipProps) => { + const { locationSettings } = useLocationSettings(); + + if (group.length === 1) { + const event = group[0]; + const eventDateTime = formatDateYMDHMS( + event.createdAt, + locationSettings?.locale, + ); + + return ( + <> + + {event.label} + {eventDateTime} + + {event.summary} + + ); + } + + const firstEvent = group[0]; + const eventDateTime = formatDateYMDHMS( + firstEvent.createdAt, + locationSettings?.locale, + ); + + return ( + <> + + + {group.length} events occurred + + {eventDateTime} + + {group.map((event) => ( + + + {event.summary} + + ))} + + ); +};