1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

chore: event timeline tooltips (#8205)

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: https://github.com/Unleash/unleash/pull/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)
This commit is contained in:
Nuno Góis 2024-09-24 08:45:08 +01:00 committed by GitHub
parent 272052c59b
commit 7a3a5ad33c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 186 additions and 42 deletions

View File

@ -1,5 +1,5 @@
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import type { EventSchemaType } from 'openapi'; import type { EventSchema, EventSchemaType } from 'openapi';
import { useState } from 'react'; import { useState } from 'react';
import { startOfDay, sub } from 'date-fns'; import { startOfDay, sub } from 'date-fns';
import type { IEnvironment } from 'interfaces/environments'; import type { IEnvironment } from 'interfaces/environments';
@ -11,6 +11,11 @@ import {
timeSpanOptions, timeSpanOptions,
} from './EventTimelineHeader/EventTimelineHeader'; } from './EventTimelineHeader/EventTimelineHeader';
export type EnrichedEvent = EventSchema & {
label: string;
summary: string;
};
const StyledRow = styled('div')({ const StyledRow = styled('div')({
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
@ -91,7 +96,7 @@ export const EventTimeline = () => {
const endDate = new Date(); const endDate = new Date();
const startDate = sub(endDate, timeSpan.value); const startDate = sub(endDate, timeSpan.value);
const { events } = useEventSearch( const { events: baseEvents } = useEventSearch(
{ {
from: `IS:${toISODateString(startOfDay(startDate))}`, from: `IS:${toISODateString(startOfDay(startDate))}`,
to: `IS:${toISODateString(endDate)}`, to: `IS:${toISODateString(endDate)}`,
@ -100,6 +105,8 @@ export const EventTimeline = () => {
{ refreshInterval: 10 * 1000 }, { refreshInterval: 10 * 1000 },
); );
const events = baseEvents as EnrichedEvent[];
const filteredEvents = events.filter( const filteredEvents = events.filter(
(event) => (event) =>
new Date(event.createdAt).getTime() >= startDate.getTime() && new Date(event.createdAt).getTime() >= startDate.getTime() &&

View File

@ -1,4 +1,4 @@
import type { EventSchema, EventSchemaType } from 'openapi'; import type { EventSchemaType } from 'openapi';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { EventTimelineEventTooltip } from './EventTimelineEventTooltip/EventTimelineEventTooltip'; import { EventTimelineEventTooltip } from './EventTimelineEventTooltip/EventTimelineEventTooltip';
@ -8,6 +8,7 @@ import FlagOutlinedIcon from '@mui/icons-material/FlagOutlined';
import ExtensionOutlinedIcon from '@mui/icons-material/ExtensionOutlined'; import ExtensionOutlinedIcon from '@mui/icons-material/ExtensionOutlined';
import SegmentsIcon from '@mui/icons-material/DonutLargeOutlined'; import SegmentsIcon from '@mui/icons-material/DonutLargeOutlined';
import QuestionMarkIcon from '@mui/icons-material/QuestionMark'; import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
import type { EnrichedEvent } from '../EventTimeline';
type DefaultEventVariant = 'secondary'; type DefaultEventVariant = 'secondary';
type CustomEventVariant = 'success' | 'neutral'; type CustomEventVariant = 'success' | 'neutral';
@ -76,7 +77,7 @@ const customEventVariants: Partial<
}; };
interface IEventTimelineEventProps { interface IEventTimelineEventProps {
event: EventSchema; event: EnrichedEvent;
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
} }
@ -97,6 +98,7 @@ export const EventTimelineEvent = ({
<StyledEvent position={position}> <StyledEvent position={position}>
<HtmlTooltip <HtmlTooltip
title={<EventTimelineEventTooltip event={event} />} title={<EventTimelineEventTooltip event={event} />}
maxWidth={320}
arrow arrow
> >
<StyledEventCircle variant={variant}> <StyledEventCircle variant={variant}>

View File

@ -1,9 +1,32 @@
import { styled } from '@mui/material';
import { Markdown } from 'component/common/Markdown/Markdown';
import { useLocationSettings } from 'hooks/useLocationSettings'; import { useLocationSettings } from 'hooks/useLocationSettings';
import type { EventSchema } from 'openapi';
import { formatDateYMDHMS } from 'utils/formatDate'; 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 { interface IEventTimelineEventTooltipProps {
event: EventSchema; event: EnrichedEvent;
} }
export const EventTimelineEventTooltip = ({ export const EventTimelineEventTooltip = ({
@ -15,36 +38,13 @@ export const EventTimelineEventTooltip = ({
locationSettings?.locale, 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 ( return (
<div> <>
<div>{eventDateTime}</div> <StyledTooltipHeader>
<div>{event.createdBy}</div> <StyledTooltipTitle>{event.label}</StyledTooltipTitle>
<div>{event.type}</div> <StyledDateTime>{eventDateTime}</StyledDateTime>
<div>{event.featureName}</div> </StyledTooltipHeader>
<div>{event.environment}</div> <Markdown>{event.summary}</Markdown>
</div> </>
); );
}; };

View File

@ -2,6 +2,7 @@
exports[`Should format specialised text for events when IPs changed 1`] = ` exports[`Should format specialised text for events when IPs changed 1`] = `
{ {
"label": "Flag strategy updated",
"text": "*user@company.com* 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)]", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/features/new-feature",
} }
@ -9,6 +10,7 @@ exports[`Should format specialised text for events when IPs changed 1`] = `
exports[`Should format specialised text for events when a scheduled change request is suspended 1`] = ` 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.", "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", "url": "unleashUrl/projects/my-other-project/change-requests/1",
} }
@ -16,6 +18,7 @@ exports[`Should format specialised text for events when a scheduled change reque
exports[`Should format specialised text for events when change request is scheduled 1`] = ` exports[`Should format specialised text for events when change request is scheduled 1`] = `
{ {
"label": "Change request scheduled",
"text": "*user@company.com* 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*", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/change-requests/1",
} }
@ -23,6 +26,7 @@ exports[`Should format specialised text for events when change request is schedu
exports[`Should format specialised text for events when constraints and rollout percentage and stickiness changed 1`] = ` exports[`Should format specialised text for events when constraints and rollout percentage and stickiness changed 1`] = `
{ {
"label": "Flag strategy updated",
"text": "*user@company.com* 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)]", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/features/new-feature",
} }
@ -30,6 +34,7 @@ exports[`Should format specialised text for events when constraints and rollout
exports[`Should format specialised text for events when default strategy updated 1`] = ` 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)]", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -37,6 +42,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated 2`] = ` 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)]", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -44,6 +50,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated 3`] = ` 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)]", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -51,6 +58,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated 4`] = ` 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)]", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -58,6 +66,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated 5`] = ` 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)]", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -65,6 +74,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated with numeric constraint DATE_AFTER 1`] = ` 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", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -72,6 +82,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated with numeric constraint DATE_BEFORE 1`] = ` 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", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -79,6 +90,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_EQ 1`] = ` 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", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -86,6 +98,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_GT 1`] = ` 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", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -93,6 +106,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_GTE 1`] = ` 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", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -100,6 +114,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_LT 1`] = ` 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", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -107,6 +122,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated with numeric constraint NUM_LTE 1`] = ` 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", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -114,6 +130,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated with numeric constraint SEMVER_EQ 1`] = ` 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", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -121,6 +138,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated with numeric constraint SEMVER_GT 1`] = ` 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", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -128,6 +146,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when default strategy updated with numeric constraint SEMVER_LT 1`] = ` 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", "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", "url": "unleashUrl/projects/default/features/aaa",
} }
@ -135,6 +154,7 @@ exports[`Should format specialised text for events when default strategy updated
exports[`Should format specialised text for events when groupId changed 1`] = ` exports[`Should format specialised text for events when groupId changed 1`] = `
{ {
"label": "Flag strategy updated",
"text": "*user@company.com* 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", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/features/new-feature",
} }
@ -142,6 +162,7 @@ exports[`Should format specialised text for events when groupId changed 1`] = `
exports[`Should format specialised text for events when host names changed 1`] = ` exports[`Should format specialised text for events when host names changed 1`] = `
{ {
"label": "Flag strategy updated",
"text": "*user@company.com* 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)]", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/features/new-feature",
} }
@ -149,6 +170,7 @@ exports[`Should format specialised text for events when host names changed 1`] =
exports[`Should format specialised text for events when neither rollout percentage nor stickiness changed 1`] = ` exports[`Should format specialised text for events when neither rollout percentage nor stickiness changed 1`] = `
{ {
"label": "Flag strategy updated",
"text": "*user@company.com* 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*", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/features/new-feature",
} }
@ -156,6 +178,7 @@ exports[`Should format specialised text for events when neither rollout percenta
exports[`Should format specialised text for events when no specific text for strategy exists yet 1`] = ` exports[`Should format specialised text for events when no specific text for strategy exists yet 1`] = `
{ {
"label": "Flag strategy updated",
"text": "*user@company.com* 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*", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/features/new-feature",
} }
@ -163,6 +186,7 @@ exports[`Should format specialised text for events when no specific text for str
exports[`Should format specialised text for events when rollout percentage changed 1`] = ` exports[`Should format specialised text for events when rollout percentage changed 1`] = `
{ {
"label": "Flag strategy updated",
"text": "*user@company.com* 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%", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/features/new-feature",
} }
@ -170,6 +194,7 @@ exports[`Should format specialised text for events when rollout percentage chang
exports[`Should format specialised text for events when scheduled change request fails 1`] = ` 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 *user@company.com* in project *my-other-project*.", "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 *user@company.com* in project *my-other-project*.",
"url": "unleashUrl/projects/my-other-project/change-requests/1", "url": "unleashUrl/projects/my-other-project/change-requests/1",
} }
@ -177,6 +202,7 @@ exports[`Should format specialised text for events when scheduled change request
exports[`Should format specialised text for events when scheduled change request succeeds 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 *user@company.com* in project *my-other-project*.", "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 *user@company.com* in project *my-other-project*.",
"url": "unleashUrl/projects/my-other-project/change-requests/1", "url": "unleashUrl/projects/my-other-project/change-requests/1",
} }
@ -184,6 +210,7 @@ exports[`Should format specialised text for events when scheduled change request
exports[`Should format specialised text for events when stickiness changed 1`] = ` exports[`Should format specialised text for events when stickiness changed 1`] = `
{ {
"label": "Flag strategy updated",
"text": "*user@company.com* 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", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/features/new-feature",
} }
@ -191,6 +218,7 @@ exports[`Should format specialised text for events when stickiness changed 1`] =
exports[`Should format specialised text for events when strategy added 1`] = ` exports[`Should format specialised text for events when strategy added 1`] = `
{ {
"label": "Flag strategy added",
"text": "*user@company.com* 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)*", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/features/new-feature",
} }
@ -198,6 +226,7 @@ exports[`Should format specialised text for events when strategy added 1`] = `
exports[`Should format specialised text for events when strategy removed 1`] = ` exports[`Should format specialised text for events when strategy removed 1`] = `
{ {
"label": "Flag strategy removed",
"text": "*user@company.com* 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)*", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/features/new-feature",
} }
@ -205,6 +234,7 @@ exports[`Should format specialised text for events when strategy removed 1`] = `
exports[`Should format specialised text for events when userIds changed 1`] = ` exports[`Should format specialised text for events when userIds changed 1`] = `
{ {
"label": "Flag strategy updated",
"text": "*user@company.com* 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)]", "text": "*user@company.com* 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", "url": "unleashUrl/projects/my-other-project/features/new-feature",
} }

View File

@ -63,11 +63,13 @@ import {
} from '../types'; } from '../types';
interface IEventData { interface IEventData {
label: string;
action: string; action: string;
path?: string; path?: string;
} }
interface IFormattedEventData { interface IFormattedEventData {
label: string;
text: string; text: string;
url?: string; url?: string;
} }
@ -82,234 +84,292 @@ export enum LinkStyle {
const EVENT_MAP: Record<string, IEventData> = { const EVENT_MAP: Record<string, IEventData> = {
[ADDON_CONFIG_CREATED]: { [ADDON_CONFIG_CREATED]: {
label: 'Integration configuration created',
action: '*{{user}}* created a new *{{event.data.provider}}* integration configuration', action: '*{{user}}* created a new *{{event.data.provider}}* integration configuration',
path: '/integrations', path: '/integrations',
}, },
[ADDON_CONFIG_DELETED]: { [ADDON_CONFIG_DELETED]: {
label: 'Integration configuration deleted',
action: '*{{user}}* deleted a *{{event.preData.provider}}* integration configuration', action: '*{{user}}* deleted a *{{event.preData.provider}}* integration configuration',
path: '/integrations', path: '/integrations',
}, },
[ADDON_CONFIG_UPDATED]: { [ADDON_CONFIG_UPDATED]: {
label: 'Integration configuration updated',
action: '*{{user}}* updated a *{{event.preData.provider}}* integration configuration', action: '*{{user}}* updated a *{{event.preData.provider}}* integration configuration',
path: '/integrations', path: '/integrations',
}, },
[API_TOKEN_CREATED]: { [API_TOKEN_CREATED]: {
label: 'API token created',
action: '*{{user}}* created API token *{{event.data.username}}*', action: '*{{user}}* created API token *{{event.data.username}}*',
path: '/admin/api', path: '/admin/api',
}, },
[API_TOKEN_DELETED]: { [API_TOKEN_DELETED]: {
label: 'API token deleted',
action: '*{{user}}* deleted API token *{{event.preData.username}}*', action: '*{{user}}* deleted API token *{{event.preData.username}}*',
path: '/admin/api', path: '/admin/api',
}, },
[CHANGE_ADDED]: { [CHANGE_ADDED]: {
label: 'Change added',
action: '*{{user}}* added a change to change request {{changeRequest}}', action: '*{{user}}* added a change to change request {{changeRequest}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_DISCARDED]: { [CHANGE_DISCARDED]: {
label: 'Change discarded',
action: '*{{user}}* discarded a change in change request {{changeRequest}}', action: '*{{user}}* discarded a change in change request {{changeRequest}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_EDITED]: { [CHANGE_EDITED]: {
label: 'Change edited',
action: '*{{user}}* edited a change in change request {{changeRequest}}', action: '*{{user}}* edited a change in change request {{changeRequest}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_APPLIED]: { [CHANGE_REQUEST_APPLIED]: {
label: 'Change request applied',
action: '*{{user}}* applied change request {{changeRequest}}', action: '*{{user}}* applied change request {{changeRequest}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_APPROVAL_ADDED]: { [CHANGE_REQUEST_APPROVAL_ADDED]: {
label: 'Change request approval added',
action: '*{{user}}* added an approval to change request {{changeRequest}}', action: '*{{user}}* added an approval to change request {{changeRequest}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_APPROVED]: { [CHANGE_REQUEST_APPROVED]: {
label: 'Change request approved',
action: '*{{user}}* approved change request {{changeRequest}}', action: '*{{user}}* approved change request {{changeRequest}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_CANCELLED]: { [CHANGE_REQUEST_CANCELLED]: {
label: 'Change request cancelled',
action: '*{{user}}* cancelled change request {{changeRequest}}', action: '*{{user}}* cancelled change request {{changeRequest}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_CREATED]: { [CHANGE_REQUEST_CREATED]: {
label: 'Change request created',
action: '*{{user}}* created change request {{changeRequest}}', action: '*{{user}}* created change request {{changeRequest}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_DISCARDED]: { [CHANGE_REQUEST_DISCARDED]: {
label: 'Change request discarded',
action: '*{{user}}* discarded change request {{changeRequest}}', action: '*{{user}}* discarded change request {{changeRequest}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_REJECTED]: { [CHANGE_REQUEST_REJECTED]: {
label: 'Change request rejected',
action: '*{{user}}* rejected change request {{changeRequest}}', action: '*{{user}}* rejected change request {{changeRequest}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_SENT_TO_REVIEW]: { [CHANGE_REQUEST_SENT_TO_REVIEW]: {
label: 'Change request sent to review',
action: '*{{user}}* sent to review change request {{changeRequest}}', action: '*{{user}}* sent to review change request {{changeRequest}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_SCHEDULED]: { [CHANGE_REQUEST_SCHEDULED]: {
label: 'Change request scheduled',
action: '*{{user}}* scheduled change request {{changeRequest}} to be applied at {{event.data.scheduledDate}} in project *{{event.project}}*', action: '*{{user}}* scheduled change request {{changeRequest}} to be applied at {{event.data.scheduledDate}} in project *{{event.project}}*',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_SCHEDULED_APPLICATION_SUCCESS]: { [CHANGE_REQUEST_SCHEDULED_APPLICATION_SUCCESS]: {
label: 'Scheduled change request applied successfully',
action: '*Successfully* applied the scheduled change request {{changeRequest}} by *{{user}}* in project *{{event.project}}*.', action: '*Successfully* applied the scheduled change request {{changeRequest}} by *{{user}}* in project *{{event.project}}*.',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_SCHEDULED_APPLICATION_FAILURE]: { [CHANGE_REQUEST_SCHEDULED_APPLICATION_FAILURE]: {
label: 'Scheduled change request failed',
action: '*Failed* to apply the scheduled change request {{changeRequest}} by *{{user}}* in project *{{event.project}}*.', action: '*Failed* to apply the scheduled change request {{changeRequest}} by *{{user}}* in project *{{event.project}}*.',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CHANGE_REQUEST_SCHEDULE_SUSPENDED]: { [CHANGE_REQUEST_SCHEDULE_SUSPENDED]: {
label: 'Change request suspended',
action: 'Change request {{changeRequest}} was suspended for the following reason: {{event.data.reason}}', action: 'Change request {{changeRequest}} was suspended for the following reason: {{event.data.reason}}',
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
}, },
[CONTEXT_FIELD_CREATED]: { [CONTEXT_FIELD_CREATED]: {
label: 'Context field created',
action: '*{{user}}* created context field *{{event.data.name}}*', action: '*{{user}}* created context field *{{event.data.name}}*',
path: '/context', path: '/context',
}, },
[CONTEXT_FIELD_DELETED]: { [CONTEXT_FIELD_DELETED]: {
label: 'Context field deleted',
action: '*{{user}}* deleted context field *{{event.preData.name}}*', action: '*{{user}}* deleted context field *{{event.preData.name}}*',
path: '/context', path: '/context',
}, },
[CONTEXT_FIELD_UPDATED]: { [CONTEXT_FIELD_UPDATED]: {
label: 'Context field updated',
action: '*{{user}}* updated context field *{{event.preData.name}}*', action: '*{{user}}* updated context field *{{event.preData.name}}*',
path: '/context', path: '/context',
}, },
[FEATURE_ARCHIVED]: { [FEATURE_ARCHIVED]: {
label: 'Flag archived',
action: '*{{user}}* archived *{{event.featureName}}* in project *{{project}}*', action: '*{{user}}* archived *{{event.featureName}}* in project *{{project}}*',
path: '/projects/{{event.project}}/archive', path: '/projects/{{event.project}}/archive',
}, },
[FEATURE_CREATED]: { [FEATURE_CREATED]: {
label: 'Flag created',
action: '*{{user}}* created *{{feature}}* in project *{{project}}*', action: '*{{user}}* created *{{feature}}* in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_DELETED]: { [FEATURE_DELETED]: {
label: 'Flag deleted',
action: '*{{user}}* deleted *{{event.featureName}}* in project *{{project}}*', action: '*{{user}}* deleted *{{event.featureName}}* in project *{{project}}*',
path: '/projects/{{event.project}}', path: '/projects/{{event.project}}',
}, },
[FEATURE_ENVIRONMENT_DISABLED]: { [FEATURE_ENVIRONMENT_DISABLED]: {
label: 'Flag disabled',
action: '*{{user}}* disabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', action: '*{{user}}* disabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_ENVIRONMENT_ENABLED]: { [FEATURE_ENVIRONMENT_ENABLED]: {
label: 'Flag enabled',
action: '*{{user}}* enabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', action: '*{{user}}* enabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_ENVIRONMENT_VARIANTS_UPDATED]: { [FEATURE_ENVIRONMENT_VARIANTS_UPDATED]: {
label: 'Flag variants updated',
action: '*{{user}}* updated variants for *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', action: '*{{user}}* updated variants for *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}/variants', path: '/projects/{{event.project}}/features/{{event.featureName}}/variants',
}, },
[FEATURE_METADATA_UPDATED]: { [FEATURE_METADATA_UPDATED]: {
label: 'Flag metadata updated',
action: '*{{user}}* updated *{{feature}}* metadata in project *{{project}}*', action: '*{{user}}* updated *{{feature}}* metadata in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_COMPLETED]: { [FEATURE_COMPLETED]: {
label: 'Flag marked as completed',
action: '*{{feature}}* was marked as completed in project *{{project}}*', action: '*{{feature}}* was marked as completed in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_POTENTIALLY_STALE_ON]: { [FEATURE_POTENTIALLY_STALE_ON]: {
label: 'Flag potentially stale',
action: '*{{feature}}* was marked as potentially stale in project *{{project}}*', action: '*{{feature}}* was marked as potentially stale in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_PROJECT_CHANGE]: { [FEATURE_PROJECT_CHANGE]: {
label: 'Flag moved to a new project',
action: '*{{user}}* moved *{{feature}}* from *{{event.data.oldProject}}* to *{{project}}*', action: '*{{user}}* moved *{{feature}}* from *{{event.data.oldProject}}* to *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_REVIVED]: { [FEATURE_REVIVED]: {
label: 'Flag revived',
action: '*{{user}}* revived *{{feature}}* in project *{{project}}*', action: '*{{user}}* revived *{{feature}}* in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_STALE_OFF]: { [FEATURE_STALE_OFF]: {
label: 'Flag stale marking removed',
action: '*{{user}}* removed the stale marking on *{{feature}}* in project *{{project}}*', action: '*{{user}}* removed the stale marking on *{{feature}}* in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_STALE_ON]: { [FEATURE_STALE_ON]: {
label: 'Flag marked as stale',
action: '*{{user}}* marked *{{feature}}* as stale in project *{{project}}*', action: '*{{user}}* marked *{{feature}}* as stale in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_STRATEGY_ADD]: { [FEATURE_STRATEGY_ADD]: {
label: 'Flag strategy added',
action: '*{{user}}* added strategy *{{strategyTitle}}* to *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', action: '*{{user}}* added strategy *{{strategyTitle}}* to *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_STRATEGY_REMOVE]: { [FEATURE_STRATEGY_REMOVE]: {
label: 'Flag strategy removed',
action: '*{{user}}* removed strategy *{{strategyTitle}}* from *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', action: '*{{user}}* removed strategy *{{strategyTitle}}* from *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_STRATEGY_UPDATE]: { [FEATURE_STRATEGY_UPDATE]: {
label: 'Flag strategy updated',
action: '*{{user}}* updated *{{feature}}* in project *{{project}}* {{strategyChangeText}}', action: '*{{user}}* updated *{{feature}}* in project *{{project}}* {{strategyChangeText}}',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_TAGGED]: { [FEATURE_TAGGED]: {
label: 'Flag tagged',
action: '*{{user}}* tagged *{{feature}}* with *{{event.data.type}}:{{event.data.value}}* in project *{{project}}*', action: '*{{user}}* tagged *{{feature}}* with *{{event.data.type}}:{{event.data.value}}* in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[FEATURE_UNTAGGED]: { [FEATURE_UNTAGGED]: {
label: 'Flag untagged',
action: '*{{user}}* untagged *{{feature}}* with *{{event.preData.type}}:{{event.preData.value}}* in project *{{project}}*', action: '*{{user}}* untagged *{{feature}}* with *{{event.preData.type}}:{{event.preData.value}}* in project *{{project}}*',
path: '/projects/{{event.project}}/features/{{event.featureName}}', path: '/projects/{{event.project}}/features/{{event.featureName}}',
}, },
[GROUP_CREATED]: { [GROUP_CREATED]: {
label: 'Group created',
action: '*{{user}}* created group *{{event.data.name}}*', action: '*{{user}}* created group *{{event.data.name}}*',
path: '/admin/groups', path: '/admin/groups',
}, },
[GROUP_DELETED]: { [GROUP_DELETED]: {
label: 'Group deleted',
action: '*{{user}}* deleted group *{{event.preData.name}}*', action: '*{{user}}* deleted group *{{event.preData.name}}*',
path: '/admin/groups', path: '/admin/groups',
}, },
[GROUP_UPDATED]: { [GROUP_UPDATED]: {
label: 'Group updated',
action: '*{{user}}* updated group *{{event.preData.name}}*', action: '*{{user}}* updated group *{{event.preData.name}}*',
path: '/admin/groups', path: '/admin/groups',
}, },
[BANNER_CREATED]: { [BANNER_CREATED]: {
label: 'Banner created',
action: '*{{user}}* created banner *{{event.data.message}}*', action: '*{{user}}* created banner *{{event.data.message}}*',
path: '/admin/message-banners', path: '/admin/message-banners',
}, },
[BANNER_DELETED]: { [BANNER_DELETED]: {
label: 'Banner deleted',
action: '*{{user}}* deleted banner *{{event.preData.message}}*', action: '*{{user}}* deleted banner *{{event.preData.message}}*',
path: '/admin/message-banners', path: '/admin/message-banners',
}, },
[BANNER_UPDATED]: { [BANNER_UPDATED]: {
label: 'Banner updated',
action: '*{{user}}* updated banner *{{event.preData.message}}*', action: '*{{user}}* updated banner *{{event.preData.message}}*',
path: '/admin/message-banners', path: '/admin/message-banners',
}, },
[PROJECT_CREATED]: { [PROJECT_CREATED]: {
label: 'Project created',
action: '*{{user}}* created project *{{project}}*', action: '*{{user}}* created project *{{project}}*',
path: '/projects', path: '/projects',
}, },
[PROJECT_DELETED]: { [PROJECT_DELETED]: {
label: 'Project deleted',
action: '*{{user}}* deleted project *{{event.project}}*', action: '*{{user}}* deleted project *{{event.project}}*',
path: '/projects', path: '/projects',
}, },
[SEGMENT_CREATED]: { [SEGMENT_CREATED]: {
label: 'Segment created',
action: '*{{user}}* created segment *{{event.data.name}}*', action: '*{{user}}* created segment *{{event.data.name}}*',
path: '/segments', path: '/segments',
}, },
[SEGMENT_DELETED]: { [SEGMENT_DELETED]: {
label: 'Segment deleted',
action: '*{{user}}* deleted segment *{{event.preData.name}}*', action: '*{{user}}* deleted segment *{{event.preData.name}}*',
path: '/segments', path: '/segments',
}, },
[SEGMENT_UPDATED]: { [SEGMENT_UPDATED]: {
label: 'Segment updated',
action: '*{{user}}* updated segment *{{event.preData.name}}*', action: '*{{user}}* updated segment *{{event.preData.name}}*',
path: '/segments', path: '/segments',
}, },
[SERVICE_ACCOUNT_CREATED]: { [SERVICE_ACCOUNT_CREATED]: {
label: 'Service account created',
action: '*{{user}}* created service account *{{event.data.name}}*', action: '*{{user}}* created service account *{{event.data.name}}*',
path: '/admin/service-accounts', path: '/admin/service-accounts',
}, },
[SERVICE_ACCOUNT_DELETED]: { [SERVICE_ACCOUNT_DELETED]: {
label: 'Service account deleted',
action: '*{{user}}* deleted service account *{{event.preData.name}}*', action: '*{{user}}* deleted service account *{{event.preData.name}}*',
path: '/admin/service-accounts', path: '/admin/service-accounts',
}, },
[SERVICE_ACCOUNT_UPDATED]: { [SERVICE_ACCOUNT_UPDATED]: {
label: 'Service account updated',
action: '*{{user}}* updated service account *{{event.preData.name}}*', action: '*{{user}}* updated service account *{{event.preData.name}}*',
path: '/admin/service-accounts', path: '/admin/service-accounts',
}, },
[USER_CREATED]: { [USER_CREATED]: {
label: 'User created',
action: '*{{user}}* created user *{{event.data.name}}*', action: '*{{user}}* created user *{{event.data.name}}*',
path: '/admin/users', path: '/admin/users',
}, },
[USER_DELETED]: { [USER_DELETED]: {
label: 'User deleted',
action: '*{{user}}* deleted user *{{event.preData.name}}*', action: '*{{user}}* deleted user *{{event.preData.name}}*',
path: '/admin/users', path: '/admin/users',
}, },
[USER_UPDATED]: { [USER_UPDATED]: {
label: 'User updated',
action: '*{{user}}* updated user *{{event.preData.name}}*', action: '*{{user}}* updated user *{{event.preData.name}}*',
path: '/admin/users', path: '/admin/users',
}, },
@ -598,10 +658,7 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
: ` segments from ${oldSegmentsText} to ${newSegmentsText}`; : ` segments from ${oldSegmentsText} to ${newSegmentsText}`;
} }
format(event: IEvent): { format(event: IEvent): IFormattedEventData {
text: string;
url?: string;
} {
const { createdBy, type } = event; const { createdBy, type } = event;
const { action, path } = EVENT_MAP[type] || { const { action, path } = EVENT_MAP[type] || {
action: `triggered *${type}*`, action: `triggered *${type}*`,
@ -619,12 +676,14 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
Mustache.escape = (text) => text; Mustache.escape = (text) => text;
const label = EVENT_MAP[type]?.label || type;
const text = Mustache.render(action, context); const text = Mustache.render(action, context);
const url = path const url = path
? `${this.unleashUrl}${Mustache.render(path, context)}` ? `${this.unleashUrl}${Mustache.render(path, context)}`
: undefined; : undefined;
return { return {
label,
text, text,
url, url,
}; };

View File

@ -18,8 +18,12 @@ import {
import { normalizeQueryParams } from '../../features/feature-search/search-utils'; import { normalizeQueryParams } from '../../features/feature-search/search-utils';
import Controller from '../../routes/controller'; import Controller from '../../routes/controller';
import type { IAuthRequest } from '../../server-impl'; import type { IAuthRequest } from '../../server-impl';
import type { IEvent } from '../../types'; import type { IEnrichedEvent, IEvent } from '../../types';
import { anonymiseKeys, extractUserIdFromUser } from '../../util'; import { anonymiseKeys, extractUserIdFromUser } from '../../util';
import {
FeatureEventFormatterMd,
type FeatureEventFormatter,
} from '../../addons/feature-event-formatter-md';
const ANON_KEYS = ['email', 'username', 'createdBy']; const ANON_KEYS = ['email', 'username', 'createdBy'];
const version = 1 as const; const version = 1 as const;
@ -28,6 +32,8 @@ export default class EventSearchController extends Controller {
private flagResolver: IFlagResolver; private flagResolver: IFlagResolver;
private msgFormatter: FeatureEventFormatter;
private openApiService: OpenApiService; private openApiService: OpenApiService;
constructor( constructor(
@ -41,6 +47,9 @@ export default class EventSearchController extends Controller {
this.eventService = eventService; this.eventService = eventService;
this.flagResolver = config.flagResolver; this.flagResolver = config.flagResolver;
this.openApiService = openApiService; this.openApiService = openApiService;
this.msgFormatter = new FeatureEventFormatterMd(
config.server.unleashUrl,
);
this.route({ this.route({
method: 'get', method: 'get',
@ -85,17 +94,37 @@ export default class EventSearchController extends Controller {
extractUserIdFromUser(user), extractUserIdFromUser(user),
); );
const enrichedEvents = this.enrichEvents(events);
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,
res, res,
eventSearchResponseSchema.$id, eventSearchResponseSchema.$id,
serializeDates({ serializeDates({
events: serializeDates(this.maybeAnonymiseEvents(events)), events: serializeDates(
this.maybeAnonymiseEvents(enrichedEvents),
),
total: totalEvents, total: totalEvents,
}), }),
); );
} }
enrichEvents(events: IEvent[]): IEvent[] | IEnrichedEvent[] {
if (this.flagResolver.isEnabled('eventTimeline')) {
return events.map((event) => {
const { label, text: summary } =
this.msgFormatter.format(event);
return {
...event,
label,
summary,
};
});
}
return events;
}
maybeAnonymiseEvents(events: IEvent[]): IEvent[] { maybeAnonymiseEvents(events: IEvent[]): IEvent[] {
if (this.flagResolver.isEnabled('anonymiseEventLog')) { if (this.flagResolver.isEnabled('anonymiseEventLog')) {
return anonymiseKeys(events, ANON_KEYS); return anonymiseKeys(events, ANON_KEYS);

View File

@ -92,6 +92,18 @@ export const eventSchema = {
nullable: true, nullable: true,
description: 'Any tags related to the event, if applicable.', description: 'Any tags related to the event, if applicable.',
}, },
label: {
type: 'string',
nullable: true,
description:
'**[Experimental]** The concise, human-readable name of the event.',
},
summary: {
type: 'string',
nullable: true,
description:
'**[Experimental]** A markdown-formatted summary of the event.',
},
}, },
components: { components: {
schemas: { schemas: {

View File

@ -374,6 +374,11 @@ export interface IEvent extends Omit<IBaseEvent, 'ip'> {
createdAt: Date; createdAt: Date;
} }
export interface IEnrichedEvent extends IEvent {
label: string;
summary: string;
}
export interface IEventList { export interface IEventList {
totalEvents: number; totalEvents: number;
events: IEvent[]; events: IEvent[];