1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +02:00

feat: group timeline events by their position on the timeline

This commit is contained in:
David Leek 2024-09-25 11:57:02 +02:00
parent a1a24ea0b1
commit a93f38d75c
No known key found for this signature in database
GPG Key ID: 515EE0F1BB6D0BE1
6 changed files with 291 additions and 26 deletions

View File

@ -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<TimeSpanOption>(
@ -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 = () => {
<StyledTimelineContainer>
<StyledTimeline />
<StyledStart />
{sortedEvents.map((event) => (
{sortedEvents.map((entry) => (
<EventTimelineEvent
key={event.id}
event={event}
startDate={startDate}
endDate={endDate}
key={entry.id}
event={entry.event ?? entry.events}
position={`${entry.position}%`}
/>
))}
<StyledEnd />

View File

@ -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 <ToggleOnIcon />;
}
@ -68,7 +71,7 @@ const getEventIcon = (type: EventSchemaType) => {
return <QuestionMarkIcon />;
};
const customEventVariants: Partial<
export const customEventVariants: Partial<
Record<EventSchemaType, CustomEventVariant>
> = {
'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 <EventTimelineGroup event={event} position={position} />;
}
const position = `${((eventTime - startDate.getTime()) / timelineDuration) * 100}%`;
if (isSignal(event)) {
return <EventTimelineSignal event={event} position={position} />;
}
const variant = customEventVariants[event.type] || 'secondary';

View File

@ -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 (
<>
<StyledTooltipHeader>
<StyledTooltipTitle>
{event.length} events occured
</StyledTooltipTitle>
<StyledDateTime>{eventDateTime}</StyledDateTime>
</StyledTooltipHeader>
{event.map((e) => (
<EventTimelineEventTooltipGroupItem key={e.id} event={e} />
))}
</>
);
}
const eventDateTime = formatDateYMDHMS(
event.createdAt,
locationSettings?.locale,
@ -41,10 +65,12 @@ export const EventTimelineEventTooltip = ({
return (
<>
<StyledTooltipHeader>
<StyledTooltipTitle>{event.label}</StyledTooltipTitle>
<StyledTooltipTitle>
{isSignal(event) ? '' : event.label}
</StyledTooltipTitle>
<StyledDateTime>{eventDateTime}</StyledDateTime>
</StyledTooltipHeader>
<Markdown>{event.summary}</Markdown>
<Markdown>{isSignal(event) ? '' : event.summary}</Markdown>
</>
);
};

View File

@ -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 (
<>
<StyledTooltipGroupItemHeader>
<StyledTooltipGroupItemIcon>
<StyledEventCircle variant={variant}>
{getEventIcon(event.type)}
</StyledEventCircle>
</StyledTooltipGroupItemIcon>
<StyledDateGroupItemText>
<Markdown key={event.id}>{event.summary}</Markdown>
</StyledDateGroupItemText>
</StyledTooltipGroupItemHeader>
</>
);
};

View File

@ -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 (
<StyledEvent position={position}>
<HtmlTooltip
title={<EventTimelineEventTooltip event={event} />}
maxWidth={320}
arrow
>
<StyledBadge badgeContent={event.length} color='primary'>
<StyledEventCircle>
<MoreHorizIcon />
</StyledEventCircle>
</StyledBadge>
</HtmlTooltip>
</StyledEvent>
);
};

View File

@ -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 (
<StyledEvent position={position}>
<HtmlTooltip
title={<EventTimelineEventTooltip event={event} />}
maxWidth={320}
arrow
>
<StyledEventCircle>
{/*getEventIcon(event.type)*/}
</StyledEventCircle>
</HtmlTooltip>
</StyledEvent>
);
};