Skip to content

Commit

Permalink
chore: event timeline tooltips (#8205)
Browse files Browse the repository at this point in the history
https://linear.app/unleash/issue/2-2664/implement-event-tooltips

Implements event tooltips in the new event timeline.

This leverages our current `feature-event-formatter-md` to provide both
a label and a summary of the event. Whenever our new `eventTimeline`
flag is enabled, we enrich our events in our event search endpoint with
this information. We've discussed different options here and reached the
conclusion that this is the best path forward for now. This way we are
being consistent, DRY, relatively performant and it also gives us a
happy path forward if we decide to scope in the event log revamp, since
this data will already be present there.

We also added a new `label` property to each of our event types
currently in our event formatter. This way we can have a concise,
human-readable name for each event type, instead of exposing the
internal event type string.

~~We also fixed the way the event formatter handled bold text (as in,
**bold**). Before, it was wrapping them in *single asterisks*, but now
we're using **double asterisks**. We also abstracted this away into a
helper method aptly named `bold`. Of course, this change meant that a
bunch of snapshots and tests needed to be updated.~~

~~This new `bold` method also makes it super easy to revert this
decision if we choose to, for any reason. However I believe we should
stick with markdown formatting, since it is the most commonly supported
formatting syntax, so I see this as an important fix. It's also in the
name of the formatter (`md`). I also believe bold was the original
intent. If we want italic formatting we should implement it separately
at a later point.~~

Edit: It was _bold_ of me to assume this would work out of the box on
Slack. It does when you manually try it on the app, but not when using
the Slack client. See: #8222


![image](https://github.com/user-attachments/assets/31eb6296-5d4b-4400-8db0-5eb7437dd2ff)


![image](https://github.com/user-attachments/assets/ac177415-78da-4c4b-864b-0c7a1668f6b5)
  • Loading branch information
nunogois authored Sep 24, 2024
1 parent 272052c commit 7a3a5ad
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 42 deletions.
11 changes: 9 additions & 2 deletions frontend/src/component/events/EventTimeline/EventTimeline.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { styled } from '@mui/material';
import type { EventSchemaType } from 'openapi';
import type { EventSchema, EventSchemaType } from 'openapi';
import { useState } from 'react';
import { startOfDay, sub } from 'date-fns';
import type { IEnvironment } from 'interfaces/environments';
Expand All @@ -11,6 +11,11 @@ import {
timeSpanOptions,
} from './EventTimelineHeader/EventTimelineHeader';

export type EnrichedEvent = EventSchema & {
label: string;
summary: string;
};

const StyledRow = styled('div')({
display: 'flex',
flexDirection: 'row',
Expand Down Expand Up @@ -91,7 +96,7 @@ export const EventTimeline = () => {
const endDate = new Date();
const startDate = sub(endDate, timeSpan.value);

const { events } = useEventSearch(
const { events: baseEvents } = useEventSearch(
{
from: `IS:${toISODateString(startOfDay(startDate))}`,
to: `IS:${toISODateString(endDate)}`,
Expand All @@ -100,6 +105,8 @@ export const EventTimeline = () => {
{ refreshInterval: 10 * 1000 },
);

const events = baseEvents as EnrichedEvent[];

const filteredEvents = events.filter(
(event) =>
new Date(event.createdAt).getTime() >= startDate.getTime() &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EventSchema, EventSchemaType } from 'openapi';
import type { EventSchemaType } from 'openapi';
import { styled } from '@mui/material';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { EventTimelineEventTooltip } from './EventTimelineEventTooltip/EventTimelineEventTooltip';
Expand All @@ -8,6 +8,7 @@ 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';

type DefaultEventVariant = 'secondary';
type CustomEventVariant = 'success' | 'neutral';
Expand Down Expand Up @@ -76,7 +77,7 @@ const customEventVariants: Partial<
};

interface IEventTimelineEventProps {
event: EventSchema;
event: EnrichedEvent;
startDate: Date;
endDate: Date;
}
Expand All @@ -97,6 +98,7 @@ export const EventTimelineEvent = ({
<StyledEvent position={position}>
<HtmlTooltip
title={<EventTimelineEventTooltip event={event} />}
maxWidth={320}
arrow
>
<StyledEventCircle variant={variant}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
import { styled } from '@mui/material';
import { Markdown } from 'component/common/Markdown/Markdown';
import { useLocationSettings } from 'hooks/useLocationSettings';
import type { EventSchema } from 'openapi';
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: EventSchema;
event: EnrichedEvent;
}

export const EventTimelineEventTooltip = ({
Expand All @@ -15,36 +38,13 @@ export const EventTimelineEventTooltip = ({
locationSettings?.locale,
);

if (event.type === 'feature-environment-enabled') {
return (
<div>
<small>{eventDateTime}</small>
<p>
{event.createdBy} enabled {event.featureName} for the{' '}
{event.environment} environment in project {event.project}
</p>
</div>
);
}
if (event.type === 'feature-environment-disabled') {
return (
<div>
<small>{eventDateTime}</small>
<p>
{event.createdBy} disabled {event.featureName} for the{' '}
{event.environment} environment in project {event.project}
</p>
</div>
);
}

return (
<div>
<div>{eventDateTime}</div>
<div>{event.createdBy}</div>
<div>{event.type}</div>
<div>{event.featureName}</div>
<div>{event.environment}</div>
</div>
<>
<StyledTooltipHeader>
<StyledTooltipTitle>{event.label}</StyledTooltipTitle>
<StyledDateTime>{eventDateTime}</StyledDateTime>
</StyledTooltipHeader>
<Markdown>{event.summary}</Markdown>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,209 +2,239 @@

exports[`Should format specialised text for events when IPs changed 1`] = `
{
"label": "Flag strategy updated",
"text": "*[email protected]* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *remoteAddress* in *production* IPs from empty set of IPs to [127.0.0.1]; constraints from empty set of constraints to [appName is one of (x,y)]",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;

exports[`Should format specialised text for events when a scheduled change request is suspended 1`] = `
{
"label": "Change request suspended",
"text": "Change request *[#1](unleashUrl/projects/my-other-project/change-requests/1)* in the *production* environment in project *[my-other-project](unleashUrl/projects/my-other-project)* was suspended for the following reason: The user who scheduled this change request (user id: 6) has been deleted from this Unleash instance.",
"url": "unleashUrl/projects/my-other-project/change-requests/1",
}
`;

exports[`Should format specialised text for events when change request is scheduled 1`] = `
{
"label": "Change request scheduled",
"text": "*[email protected]* scheduled change request *[#1](unleashUrl/projects/my-other-project/change-requests/1)* for feature flag *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in the *production* environment in project *[my-other-project](unleashUrl/projects/my-other-project)* to be applied at in project *my-other-project*",
"url": "unleashUrl/projects/my-other-project/change-requests/1",
}
`;

exports[`Should format specialised text for events when constraints and rollout percentage and stickiness changed 1`] = `
{
"label": "Flag strategy updated",
"text": "*[email protected]* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *flexibleRollout* in *production* stickiness from default to random; rollout from 67% to 32%; constraints from empty set of constraints to [appName is one of (x,y)]",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;

exports[`Should format specialised text for events when default strategy updated 1`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from empty set of constraints to [appName is one of (x,y), appName not is one of (x)]",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated 2`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from empty set of constraints to [appName is not one of (x,y), appName not is not one of (x)]",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated 3`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from empty set of constraints to [appName is a string that contains (x,y), appName not is a string that contains (x)]",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated 4`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from empty set of constraints to [appName is a string that starts with (x,y), appName not is a string that starts with (x)]",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated 5`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from empty set of constraints to [appName is a string that ends with (x,y), appName not is a string that ends with (x)]",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated with numeric constraint DATE_AFTER 1`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a date after 4] to empty set of constraints",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated with numeric constraint DATE_BEFORE 1`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a date before 4] to empty set of constraints",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_EQ 1`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a number equal to 4] to empty set of constraints",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_GT 1`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a number greater than 4] to empty set of constraints",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_GTE 1`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a number greater than or equal to 4] to empty set of constraints",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_LT 1`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a number less than 4] to empty set of constraints",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_LTE 1`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a number less than or equal to 4] to empty set of constraints",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated with numeric constraint SEMVER_EQ 1`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a SemVer equal to 4] to empty set of constraints",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated with numeric constraint SEMVER_GT 1`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a SemVer greater than 4] to empty set of constraints",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when default strategy updated with numeric constraint SEMVER_LT 1`] = `
{
"label": "Flag strategy updated",
"text": "*admin* updated *[aaa](unleashUrl/projects/default/features/aaa)* in project *[default](unleashUrl/projects/default)* by updating strategy *default* in *production* constraints from [appName is a SemVer less than 4] to empty set of constraints",
"url": "unleashUrl/projects/default/features/aaa",
}
`;

exports[`Should format specialised text for events when groupId changed 1`] = `
{
"label": "Flag strategy updated",
"text": "*[email protected]* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *flexibleRollout* in *production* groupId from new-feature to different-feature",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;

exports[`Should format specialised text for events when host names changed 1`] = `
{
"label": "Flag strategy updated",
"text": "*[email protected]* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *applicationHostname* in *production* hostNames from empty set of hostNames to [unleash.com]; constraints from empty set of constraints to [appName is one of (x,y)]",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;

exports[`Should format specialised text for events when neither rollout percentage nor stickiness changed 1`] = `
{
"label": "Flag strategy updated",
"text": "*[email protected]* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *flexibleRollout* in *production*",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;

exports[`Should format specialised text for events when no specific text for strategy exists yet 1`] = `
{
"label": "Flag strategy updated",
"text": "*[email protected]* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *newStrategy* in *production*",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;

exports[`Should format specialised text for events when rollout percentage changed 1`] = `
{
"label": "Flag strategy updated",
"text": "*[email protected]* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *flexibleRollout* in *production* rollout from 67% to 32%",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;

exports[`Should format specialised text for events when scheduled change request fails 1`] = `
{
"label": "Scheduled change request failed",
"text": "*Failed* to apply the scheduled change request *[#1](unleashUrl/projects/my-other-project/change-requests/1)* for feature flag *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in the *production* environment in project *[my-other-project](unleashUrl/projects/my-other-project)* by *[email protected]* in project *my-other-project*.",
"url": "unleashUrl/projects/my-other-project/change-requests/1",
}
`;

exports[`Should format specialised text for events when scheduled change request succeeds 1`] = `
{
"label": "Scheduled change request applied successfully",
"text": "*Successfully* applied the scheduled change request *[#1](unleashUrl/projects/my-other-project/change-requests/1)* for feature flag *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in the *production* environment in project *[my-other-project](unleashUrl/projects/my-other-project)* by *[email protected]* in project *my-other-project*.",
"url": "unleashUrl/projects/my-other-project/change-requests/1",
}
`;

exports[`Should format specialised text for events when stickiness changed 1`] = `
{
"label": "Flag strategy updated",
"text": "*[email protected]* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *flexibleRollout* in *production* stickiness from default to random",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;

exports[`Should format specialised text for events when strategy added 1`] = `
{
"label": "Flag strategy added",
"text": "*[email protected]* added strategy *flexibleRollout* to *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* for the *production* environment in project *[my-other-project](unleashUrl/projects/my-other-project)*",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;

exports[`Should format specialised text for events when strategy removed 1`] = `
{
"label": "Flag strategy removed",
"text": "*[email protected]* removed strategy *default* from *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* for the *production* environment in project *[my-other-project](unleashUrl/projects/my-other-project)*",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
`;

exports[`Should format specialised text for events when userIds changed 1`] = `
{
"label": "Flag strategy updated",
"text": "*[email protected]* updated *[new-feature](unleashUrl/projects/my-other-project/features/new-feature)* in project *[my-other-project](unleashUrl/projects/my-other-project)* by updating strategy *userWithId* in *production* userIds from empty set of userIds to [a,b]; constraints from empty set of constraints to [appName is one of (x,y)]",
"url": "unleashUrl/projects/my-other-project/features/new-feature",
}
Expand Down
Loading

0 comments on commit 7a3a5ad

Please sign in to comment.