Skip to content

Commit

Permalink
chore: event timeline signals (#8310)
Browse files Browse the repository at this point in the history
https://linear.app/unleash/issue/2-2665/show-signals-in-the-event-timeline

Implements signals in the event timeline.

This merges events and signals into a unified `TimelineEvent`
abstraction, streamlining the data structure to only include properties
relevant to the timeline.

Key changes:
- Refactors the timeline logic to handle both events and signals through
the new abstraction.
- Introduces the `useSignalQuery` hook, modeled after `useEventSearch`,
as both serve similar purposes, albeit for different resource types.

Note: The signals suggestion alert is not included and will be addressed
in a future task.


![image](https://github.com/user-attachments/assets/9dad5c21-cd36-45e6-9369-ceca25936123)
  • Loading branch information
nunogois authored Oct 1, 2024
1 parent dcb0228 commit a8eda9d
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 39 deletions.
123 changes: 99 additions & 24 deletions frontend/src/component/events/EventTimeline/EventTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ import { EventTimelineEventGroup } from './EventTimelineEventGroup/EventTimeline
import { EventTimelineHeader } from './EventTimelineHeader/EventTimelineHeader';
import { useEventTimeline } from './useEventTimeline';
import { useMemo } from 'react';
import { useSignalQuery } from 'hooks/api/getters/useSignalQuery/useSignalQuery';
import type { ISignalQuerySignal } from 'interfaces/signal';
import type { IEnvironment } from 'interfaces/environments';

export type EnrichedEvent = EventSchema & {
export type TimelineEventType = 'signal' | EventSchemaType;

type RawTimelineEvent = EventSchema | ISignalQuerySignal;

type TimelineEvent = {
id: number;
timestamp: number;
type: TimelineEventType;
label: string;
summary: string;
timestamp: number;
};

export type TimelineEventGroup = EnrichedEvent[];
export type TimelineEventGroup = TimelineEvent[];

const StyledRow = styled('div')({
display: 'flex',
Expand Down Expand Up @@ -86,6 +95,62 @@ const RELEVANT_EVENT_TYPES: EventSchemaType[] = [

const toISODateString = (date: Date) => date.toISOString().split('T')[0];

const isSignal = (event: RawTimelineEvent): event is ISignalQuerySignal =>
'sourceId' in event;

const getTimestamp = (event: RawTimelineEvent) => {
return new Date(event.createdAt).getTime();
};

const isInRange = (timestamp: number, startTime: number, endTime: number) =>
timestamp >= startTime && timestamp <= endTime;

const getTimelineEvent = (
event: RawTimelineEvent,
timestamp: number,
environment?: IEnvironment,
): TimelineEvent | undefined => {
if (isSignal(event)) {
const {
id,
sourceName = 'unknown source',
sourceDescription,
tokenName,
} = event;

const label = `Signal: ${sourceName}`;
const summary = `Signal originated from **[${sourceName} (${tokenName})](/integrations/signals)** endpoint${sourceDescription ? `: ${sourceDescription}` : ''}`;

return {
id,
timestamp,
type: 'signal',
label,
summary,
};
}

if (
!event.environment ||
!environment ||
event.environment === environment.name
) {
const {
id,
type,
label: eventLabel,
summary: eventSummary,
createdBy,
} = event;

const label = eventLabel || type;
const summary =
eventSummary || `**${createdBy}** triggered **${type}**`;

return { id, timestamp, type, label, summary };
}
};

export const EventTimeline = () => {
const { timeSpan, environment, setTimeSpan, setEnvironment } =
useEventTimeline();
Expand All @@ -103,24 +168,34 @@ export const EventTimeline = () => {
},
{ refreshInterval: 10 * 1000 },
);

const events = useMemo(() => {
return baseEvents.map((event) => ({
...event,
timestamp: new Date(event.createdAt).getTime(),
}));
}, [baseEvents]) as EnrichedEvent[];

const filteredEvents = events.filter(
(event) =>
event.timestamp >= startTime &&
event.timestamp <= endTime &&
(!event.environment ||
!environment ||
event.environment === environment.name),
const { signals: baseSignals } = useSignalQuery(
{
from: `IS:${toISODateString(startOfDay(startDate))}`,
to: `IS:${toISODateString(endDate)}`,
},
{ refreshInterval: 10 * 1000 },
);

const sortedEvents = filteredEvents.reverse();
const events = useMemo(
() =>
[...baseEvents, ...baseSignals]
.reduce<TimelineEvent[]>((acc, event) => {
const timestamp = getTimestamp(event);
if (isInRange(timestamp, startTime, endTime)) {
const timelineEvent = getTimelineEvent(
event,
timestamp,
environment,
);
if (timelineEvent) {
acc.push(timelineEvent);
}
}
return acc;
}, [])
.sort((a, b) => a.timestamp - b.timestamp),
[baseEvents, baseSignals, startTime, endTime, environment],
);

const timespanInMs = endTime - startTime;
const groupingThresholdInMs = useMemo(
Expand All @@ -130,7 +205,7 @@ export const EventTimeline = () => {

const groups = useMemo(
() =>
sortedEvents.reduce((groups: TimelineEventGroup[], event) => {
events.reduce((groups: TimelineEventGroup[], event) => {
if (groups.length === 0) {
groups.push([event]);
} else {
Expand All @@ -148,14 +223,14 @@ export const EventTimeline = () => {
}
return groups;
}, []),
[sortedEvents, groupingThresholdInMs],
[events, groupingThresholdInMs],
);

return (
<>
<StyledRow>
<EventTimelineHeader
totalEvents={sortedEvents.length}
totalEvents={events.length}
timeSpan={timeSpan}
setTimeSpan={setTimeSpan}
environment={environment}
Expand All @@ -169,8 +244,8 @@ export const EventTimeline = () => {
<EventTimelineEventGroup
key={group[0].id}
group={group}
startDate={startDate}
endDate={endDate}
startTime={startTime}
endTime={endTime}
/>
))}
<StyledEnd />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import type { EventSchemaType } from 'openapi';
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 { styled } from '@mui/material';
import type { TimelineEventGroup } from '../EventTimeline';
import type { TimelineEventGroup, TimelineEventType } from '../EventTimeline';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import type { HTMLAttributes } from 'react';
import SensorsIcon from '@mui/icons-material/Sensors';

type DefaultEventVariant = 'secondary';
type CustomEventVariant = 'success' | 'neutral';
type CustomEventVariant = 'success' | 'neutral' | 'warning';
type EventVariant = DefaultEventVariant | CustomEventVariant;

const StyledEventCircle = styled('div', {
Expand All @@ -36,7 +36,10 @@ const StyledEventCircle = styled('div', {
},
}));

const getEventIcon = (type: EventSchemaType) => {
const getEventIcon = (type: TimelineEventType) => {
if (type === 'signal') {
return <SensorsIcon />;
}
if (type === 'feature-environment-enabled') {
return <ToggleOnIcon />;
}
Expand All @@ -57,8 +60,9 @@ const getEventIcon = (type: EventSchemaType) => {
};

const customEventVariants: Partial<
Record<EventSchemaType, CustomEventVariant>
Record<TimelineEventType, CustomEventVariant>
> = {
signal: 'warning',
'feature-environment-enabled': 'success',
'feature-environment-disabled': 'neutral',
'feature-archived': 'neutral',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ const StyledEvent = styled('div', {

interface IEventTimelineEventProps {
group: TimelineEventGroup;
startDate: Date;
endDate: Date;
startTime: number;
endTime: number;
}

export const EventTimelineEventGroup = ({
group,
startDate,
endDate,
startTime,
endTime,
}: IEventTimelineEventProps) => {
const timelineDuration = endDate.getTime() - startDate.getTime();
const eventTime = new Date(group[0].createdAt).getTime();
const timelineDuration = endTime - startTime;
const eventTime = group[0].timestamp;

const position = `${((eventTime - startDate.getTime()) / timelineDuration) * 100}%`;
const position = `${((eventTime - startTime) / timelineDuration) * 100}%`;

return (
<StyledEvent position={position}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const EventTimelineEventTooltip = ({
if (group.length === 1) {
const event = group[0];
const eventDateTime = formatDateYMDHMS(
event.createdAt,
event.timestamp,
locationSettings?.locale,
);

Expand All @@ -85,7 +85,7 @@ export const EventTimelineEventTooltip = ({

const firstEvent = group[0];
const eventDate = formatDateYMD(
firstEvent.createdAt,
firstEvent.timestamp,
locationSettings?.locale,
);

Expand All @@ -103,7 +103,7 @@ export const EventTimelineEventTooltip = ({
<div>
<StyledDate>
{formatDateHMS(
event.createdAt,
event.timestamp,
locationSettings?.locale,
)}
</StyledDate>
Expand Down
Loading

0 comments on commit a8eda9d

Please sign in to comment.