diff --git a/frontend/src/component/events/EventTimeline/EventTimeline.tsx b/frontend/src/component/events/EventTimeline/EventTimeline.tsx index 915ed492e453..bee4e7912836 100644 --- a/frontend/src/component/events/EventTimeline/EventTimeline.tsx +++ b/frontend/src/component/events/EventTimeline/EventTimeline.tsx @@ -10,12 +10,17 @@ import { type TimeSpanOption, timeSpanOptions, } from './EventTimelineHeader/EventTimelineHeader'; +import type { ISignal } from 'interfaces/signal'; export type EnrichedEvent = EventSchema & { label: string; summary: string; }; +export type TimelineEvent = EnrichedEvent | ISignal; + +export type TimelineEventGroup = TimelineEvent[]; + const StyledRow = styled('div')({ display: 'flex', flexDirection: 'row', @@ -86,6 +91,7 @@ const RELEVANT_EVENT_TYPES: EventSchemaType[] = [ ]; const toISODateString = (date: Date) => date.toISOString().split('T')[0]; +const TIME_GROUPING_SIZE = 2; export const EventTimeline = () => { const [timeSpan, setTimeSpan] = useState( @@ -95,6 +101,19 @@ export const EventTimeline = () => { const endDate = new Date(); const startDate = sub(endDate, timeSpan.value); + const timelineDuration = endDate.getTime() - startDate.getTime(); + const timeGroups = 100 / TIME_GROUPING_SIZE; + const groups = new Array(timeGroups).fill(0).map((_, i) => { + const from = i * TIME_GROUPING_SIZE; + const to = from + TIME_GROUPING_SIZE; + const position = from + TIME_GROUPING_SIZE / 2; + return { + from, + to, + position, + events: [] as { position: number; event: EnrichedEvent }[], + }; + }); const { events: baseEvents } = useEventSearch( { @@ -117,7 +136,35 @@ export const EventTimeline = () => { event.environment === environment.name), ); - const sortedEvents = [...filteredEvents].reverse(); + filteredEvents.forEach((event) => { + const eventTime = new Date(event.createdAt).getTime(); + const position = + ((eventTime - startDate.getTime()) / timelineDuration) * 100; + const grp = groups.find( + (group) => group && group.from <= position && group.to > position, + ); + grp?.events.push({ position, event }); + }); + + const mappedEvents = groups + .filter((group) => group.events.length > 0) + .map((group) => { + return group.events.length === 1 + ? { + id: group.events[0].event.id, + position: group.events[0].position, + event: group.events[0].event, + } + : { + position: group.position, + id: group.events[0].event.id, + events: group.events.map( + (ev) => ev.event, + ) as TimelineEventGroup, + }; + }); + + const sortedEvents = [...mappedEvents].reverse(); return ( <> @@ -133,12 +180,11 @@ export const EventTimeline = () => { - {sortedEvents.map((event) => ( + {sortedEvents.map((entry) => ( ))} diff --git a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEvent.tsx b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEvent.tsx index 15f72edc3ba5..72f9896d9a7a 100644 --- a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEvent.tsx +++ b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEvent.tsx @@ -8,13 +8,16 @@ 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 type { TimelineEvent, TimelineEventGroup } from '../EventTimeline'; +import type { ISignal } from 'interfaces/signal'; +import { EventTimelineGroup } from './EventTimelineGroup'; +import { EventTimelineSignal } from './EventTimelineSignal'; type DefaultEventVariant = 'secondary'; type CustomEventVariant = 'success' | 'neutral'; type EventVariant = DefaultEventVariant | CustomEventVariant; -const StyledEvent = styled('div', { +export const StyledEvent = styled('div', { shouldForwardProp: (prop) => prop !== 'position', })<{ position: string }>(({ position }) => ({ position: 'absolute', @@ -26,11 +29,11 @@ const StyledEvent = styled('div', { zIndex: 1, })); -const StyledEventCircle = styled('div', { +export const StyledEventCircle = styled('div', { shouldForwardProp: (prop) => prop !== 'variant', })<{ variant: EventVariant }>(({ theme, variant }) => ({ - height: theme.spacing(3), - width: theme.spacing(3), + height: theme.spacing(3.75), + width: theme.spacing(3.75), borderRadius: '50%', backgroundColor: theme.palette[variant].light, border: `1px solid ${theme.palette[variant].main}`, @@ -48,7 +51,7 @@ const StyledEventCircle = styled('div', { }, })); -const getEventIcon = (type: EventSchemaType) => { +export const getEventIcon = (type: EventSchemaType) => { if (type === 'feature-environment-enabled') { return ; } @@ -68,7 +71,7 @@ const getEventIcon = (type: EventSchemaType) => { return ; }; -const customEventVariants: Partial< +export const customEventVariants: Partial< Record > = { 'feature-environment-enabled': 'success', @@ -76,21 +79,28 @@ const customEventVariants: Partial< 'feature-archived': 'neutral', }; -interface IEventTimelineEventProps { - event: EnrichedEvent; - startDate: Date; - endDate: Date; +export interface IEventTimelineEventProps { + event: TimelineEvent | TimelineEventGroup; + position: string; } +export const isSignal = ( + event: TimelineEvent | TimelineEventGroup, +): event is ISignal => { + return !Array.isArray(event) && 'source' in event; +}; + export const EventTimelineEvent = ({ event, - startDate, - endDate, + position, }: IEventTimelineEventProps) => { - const timelineDuration = endDate.getTime() - startDate.getTime(); - const eventTime = new Date(event.createdAt).getTime(); + if (Array.isArray(event)) { + return ; + } - const position = `${((eventTime - startDate.getTime()) / timelineDuration) * 100}%`; + if (isSignal(event)) { + return ; + } const variant = customEventVariants[event.type] || 'secondary'; diff --git a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx index db09f744d866..b003adbd636a 100644 --- a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx +++ b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx @@ -2,7 +2,9 @@ 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'; +import type { TimelineEvent, TimelineEventGroup } from '../../EventTimeline'; +import { isSignal } from '../EventTimelineEvent'; +import { EventTimelineEventTooltipGroupItem } from './EventTimelineEventTooltipGroupItem'; const StyledTooltipHeader = styled('div')(({ theme }) => ({ display: 'flex', @@ -26,13 +28,35 @@ const StyledDateTime = styled('div')(({ theme }) => ({ })); interface IEventTimelineEventTooltipProps { - event: EnrichedEvent; + event: TimelineEvent | TimelineEventGroup; } export const EventTimelineEventTooltip = ({ event, }: IEventTimelineEventTooltipProps) => { const { locationSettings } = useLocationSettings(); + if (Array.isArray(event)) { + const firstEvent = Array.isArray(event) + ? event[event.length - 1] + : event; + const eventDateTime = formatDateYMDHMS( + firstEvent.createdAt, + locationSettings?.locale, + ); + return ( + <> + + + {event.length} events occured + + {eventDateTime} + + {event.map((e) => ( + + ))} + + ); + } const eventDateTime = formatDateYMDHMS( event.createdAt, locationSettings?.locale, @@ -41,10 +65,12 @@ export const EventTimelineEventTooltip = ({ return ( <> - {event.label} + + {isSignal(event) ? '' : event.label} + {eventDateTime} - {event.summary} + {isSignal(event) ? '' : event.summary} ); }; diff --git a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltipGroupItem.tsx b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltipGroupItem.tsx new file mode 100644 index 000000000000..878d390d6ec6 --- /dev/null +++ b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltipGroupItem.tsx @@ -0,0 +1,79 @@ +import { styled } from '@mui/material'; +import { Markdown } from 'component/common/Markdown/Markdown'; +import { + customEventVariants, + getEventIcon, + isSignal, +} from '../EventTimelineEvent'; +import type { TimelineEvent } from '../../EventTimeline'; + +type DefaultEventVariant = 'secondary'; +type CustomEventVariant = 'success' | 'neutral'; +type EventVariant = DefaultEventVariant | CustomEventVariant; + +const StyledEventCircle = styled('div', { + shouldForwardProp: (prop) => prop !== 'variant', +})<{ variant: EventVariant }>(({ theme, variant }) => ({ + height: theme.spacing(3), + width: theme.spacing(3), + borderRadius: '50%', + backgroundColor: theme.palette[variant].light, + border: `1px solid ${theme.palette[variant].main}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'transform 0.2s', + '& svg': { + color: theme.palette[variant].main, + height: theme.spacing(2.5), + width: theme.spacing(2.5), + }, +})); + +const StyledTooltipGroupItemHeader = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: theme.spacing(1), + gap: theme.spacing(2), +})); + +const StyledTooltipGroupItemIcon = styled('div')(({ theme }) => ({ + fontWeight: theme.fontWeight.bold, + fontSize: theme.fontSizes.smallBody, + wordBreak: 'break-word', +})); + +const StyledDateGroupItemText = styled('div')(({ theme }) => ({ + color: theme.palette.text.secondary, + wordBreak: 'break-word', +})); + +interface IEventTimelineEventTooltipGroupItemProps { + event: TimelineEvent; +} + +export const EventTimelineEventTooltipGroupItem = ({ + event, +}: IEventTimelineEventTooltipGroupItemProps) => { + if (isSignal(event)) { + return null; + } + + const variant = customEventVariants[event.type] || 'secondary'; + + return ( + <> + + + + {getEventIcon(event.type)} + + + + {event.summary} + + + + ); +}; diff --git a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineGroup.tsx b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineGroup.tsx new file mode 100644 index 000000000000..e860a3fa4f5a --- /dev/null +++ b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineGroup.tsx @@ -0,0 +1,59 @@ +import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; +import { StyledEvent } from './EventTimelineEvent'; +import { EventTimelineEventTooltip } from './EventTimelineEventTooltip/EventTimelineEventTooltip'; +import { Badge, styled } from '@mui/material'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import type { TimelineEventGroup } from '../EventTimeline'; + +const StyledEventCircle = styled('div')(({ theme }) => ({ + height: theme.spacing(3.75), + width: theme.spacing(3.75), + borderRadius: '50%', + backgroundColor: theme.palette.secondary.light, + border: `1px solid ${theme.palette.primary.main}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'transform 0.2s', + '& svg': { + color: theme.palette.primary.main, + height: theme.spacing(2.5), + width: theme.spacing(2.5), + }, + '&:hover': { + transform: 'scale(1.5)', + }, +})); + +const StyledBadge = styled(Badge)(({ theme }) => ({ + '& .MuiBadge-badge': { + fontSize: theme.fontSizes.smallerBody, + fontWeight: theme.fontWeight.bold, + }, +})); + +export interface IEventTimelineGroupProps { + event: TimelineEventGroup; + position: string; +} + +export const EventTimelineGroup = ({ + event, + position, +}: IEventTimelineGroupProps) => { + return ( + + } + maxWidth={320} + arrow + > + + + + + + + + ); +}; diff --git a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineSignal.tsx b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineSignal.tsx new file mode 100644 index 000000000000..dbd9b72fa583 --- /dev/null +++ b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineSignal.tsx @@ -0,0 +1,45 @@ +import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; +import { + StyledEvent, + type IEventTimelineEventProps, +} from './EventTimelineEvent'; +import { EventTimelineEventTooltip } from './EventTimelineEventTooltip/EventTimelineEventTooltip'; +import { styled } from '@mui/material'; + +const StyledEventCircle = styled('div')(({ theme }) => ({ + height: theme.spacing(3.75), + width: theme.spacing(3.75), + borderRadius: '50%', + backgroundColor: theme.palette.warning.light, + border: `1px solid ${theme.palette.warning.main}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'transform 0.2s', + '& svg': { + color: theme.palette.warning.main, + height: theme.spacing(2.5), + width: theme.spacing(2.5), + }, + '&:hover': { + transform: 'scale(1.5)', + }, +})); +export const EventTimelineSignal = ({ + event, + position, +}: IEventTimelineEventProps) => { + return ( + + } + maxWidth={320} + arrow + > + + {/*getEventIcon(event.type)*/} + + + + ); +};