mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-11 00:08:30 +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:
parent
272052c59b
commit
7a3a5ad33c
@ -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';
|
||||
@ -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',
|
||||
@ -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)}`,
|
||||
@ -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() &&
|
||||
|
@ -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';
|
||||
@ -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';
|
||||
@ -76,7 +77,7 @@ const customEventVariants: Partial<
|
||||
};
|
||||
|
||||
interface IEventTimelineEventProps {
|
||||
event: EventSchema;
|
||||
event: EnrichedEvent;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}
|
||||
@ -97,6 +98,7 @@ export const EventTimelineEvent = ({
|
||||
<StyledEvent position={position}>
|
||||
<HtmlTooltip
|
||||
title={<EventTimelineEventTooltip event={event} />}
|
||||
maxWidth={320}
|
||||
arrow
|
||||
>
|
||||
<StyledEventCircle variant={variant}>
|
||||
|
@ -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 = ({
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
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)]",
|
||||
"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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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*",
|
||||
"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`] = `
|
||||
{
|
||||
"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)]",
|
||||
"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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
}
|
||||
@ -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`] = `
|
||||
{
|
||||
"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",
|
||||
"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`] = `
|
||||
{
|
||||
"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)]",
|
||||
"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`] = `
|
||||
{
|
||||
"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*",
|
||||
"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`] = `
|
||||
{
|
||||
"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*",
|
||||
"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`] = `
|
||||
{
|
||||
"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%",
|
||||
"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`] = `
|
||||
{
|
||||
"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*.",
|
||||
"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`] = `
|
||||
{
|
||||
"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*.",
|
||||
"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`] = `
|
||||
{
|
||||
"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",
|
||||
"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`] = `
|
||||
{
|
||||
"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)*",
|
||||
"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`] = `
|
||||
{
|
||||
"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)*",
|
||||
"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`] = `
|
||||
{
|
||||
"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)]",
|
||||
"url": "unleashUrl/projects/my-other-project/features/new-feature",
|
||||
}
|
||||
|
@ -63,11 +63,13 @@ import {
|
||||
} from '../types';
|
||||
|
||||
interface IEventData {
|
||||
label: string;
|
||||
action: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
interface IFormattedEventData {
|
||||
label: string;
|
||||
text: string;
|
||||
url?: string;
|
||||
}
|
||||
@ -82,234 +84,292 @@ export enum LinkStyle {
|
||||
|
||||
const EVENT_MAP: Record<string, IEventData> = {
|
||||
[ADDON_CONFIG_CREATED]: {
|
||||
label: 'Integration configuration created',
|
||||
action: '*{{user}}* created a new *{{event.data.provider}}* integration configuration',
|
||||
path: '/integrations',
|
||||
},
|
||||
[ADDON_CONFIG_DELETED]: {
|
||||
label: 'Integration configuration deleted',
|
||||
action: '*{{user}}* deleted a *{{event.preData.provider}}* integration configuration',
|
||||
path: '/integrations',
|
||||
},
|
||||
[ADDON_CONFIG_UPDATED]: {
|
||||
label: 'Integration configuration updated',
|
||||
action: '*{{user}}* updated a *{{event.preData.provider}}* integration configuration',
|
||||
path: '/integrations',
|
||||
},
|
||||
[API_TOKEN_CREATED]: {
|
||||
label: 'API token created',
|
||||
action: '*{{user}}* created API token *{{event.data.username}}*',
|
||||
path: '/admin/api',
|
||||
},
|
||||
[API_TOKEN_DELETED]: {
|
||||
label: 'API token deleted',
|
||||
action: '*{{user}}* deleted API token *{{event.preData.username}}*',
|
||||
path: '/admin/api',
|
||||
},
|
||||
[CHANGE_ADDED]: {
|
||||
label: 'Change added',
|
||||
action: '*{{user}}* added a change to change request {{changeRequest}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_DISCARDED]: {
|
||||
label: 'Change discarded',
|
||||
action: '*{{user}}* discarded a change in change request {{changeRequest}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_EDITED]: {
|
||||
label: 'Change edited',
|
||||
action: '*{{user}}* edited a change in change request {{changeRequest}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_REQUEST_APPLIED]: {
|
||||
label: 'Change request applied',
|
||||
action: '*{{user}}* applied change request {{changeRequest}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_REQUEST_APPROVAL_ADDED]: {
|
||||
label: 'Change request approval added',
|
||||
action: '*{{user}}* added an approval to change request {{changeRequest}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_REQUEST_APPROVED]: {
|
||||
label: 'Change request approved',
|
||||
action: '*{{user}}* approved change request {{changeRequest}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_REQUEST_CANCELLED]: {
|
||||
label: 'Change request cancelled',
|
||||
action: '*{{user}}* cancelled change request {{changeRequest}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_REQUEST_CREATED]: {
|
||||
label: 'Change request created',
|
||||
action: '*{{user}}* created change request {{changeRequest}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_REQUEST_DISCARDED]: {
|
||||
label: 'Change request discarded',
|
||||
action: '*{{user}}* discarded change request {{changeRequest}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_REQUEST_REJECTED]: {
|
||||
label: 'Change request rejected',
|
||||
action: '*{{user}}* rejected change request {{changeRequest}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_REQUEST_SENT_TO_REVIEW]: {
|
||||
label: 'Change request sent to review',
|
||||
action: '*{{user}}* sent to review change request {{changeRequest}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_REQUEST_SCHEDULED]: {
|
||||
label: 'Change request scheduled',
|
||||
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}}',
|
||||
},
|
||||
[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}}*.',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[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}}*.',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CHANGE_REQUEST_SCHEDULE_SUSPENDED]: {
|
||||
label: 'Change request suspended',
|
||||
action: 'Change request {{changeRequest}} was suspended for the following reason: {{event.data.reason}}',
|
||||
path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}',
|
||||
},
|
||||
[CONTEXT_FIELD_CREATED]: {
|
||||
label: 'Context field created',
|
||||
action: '*{{user}}* created context field *{{event.data.name}}*',
|
||||
path: '/context',
|
||||
},
|
||||
[CONTEXT_FIELD_DELETED]: {
|
||||
label: 'Context field deleted',
|
||||
action: '*{{user}}* deleted context field *{{event.preData.name}}*',
|
||||
path: '/context',
|
||||
},
|
||||
[CONTEXT_FIELD_UPDATED]: {
|
||||
label: 'Context field updated',
|
||||
action: '*{{user}}* updated context field *{{event.preData.name}}*',
|
||||
path: '/context',
|
||||
},
|
||||
[FEATURE_ARCHIVED]: {
|
||||
label: 'Flag archived',
|
||||
action: '*{{user}}* archived *{{event.featureName}}* in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/archive',
|
||||
},
|
||||
[FEATURE_CREATED]: {
|
||||
label: 'Flag created',
|
||||
action: '*{{user}}* created *{{feature}}* in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_DELETED]: {
|
||||
label: 'Flag deleted',
|
||||
action: '*{{user}}* deleted *{{event.featureName}}* in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}',
|
||||
},
|
||||
[FEATURE_ENVIRONMENT_DISABLED]: {
|
||||
label: 'Flag disabled',
|
||||
action: '*{{user}}* disabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_ENVIRONMENT_ENABLED]: {
|
||||
label: 'Flag enabled',
|
||||
action: '*{{user}}* enabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_ENVIRONMENT_VARIANTS_UPDATED]: {
|
||||
label: 'Flag variants updated',
|
||||
action: '*{{user}}* updated variants for *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}/variants',
|
||||
},
|
||||
[FEATURE_METADATA_UPDATED]: {
|
||||
label: 'Flag metadata updated',
|
||||
action: '*{{user}}* updated *{{feature}}* metadata in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_COMPLETED]: {
|
||||
label: 'Flag marked as completed',
|
||||
action: '*{{feature}}* was marked as completed in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_POTENTIALLY_STALE_ON]: {
|
||||
label: 'Flag potentially stale',
|
||||
action: '*{{feature}}* was marked as potentially stale in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_PROJECT_CHANGE]: {
|
||||
label: 'Flag moved to a new project',
|
||||
action: '*{{user}}* moved *{{feature}}* from *{{event.data.oldProject}}* to *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_REVIVED]: {
|
||||
label: 'Flag revived',
|
||||
action: '*{{user}}* revived *{{feature}}* in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_STALE_OFF]: {
|
||||
label: 'Flag stale marking removed',
|
||||
action: '*{{user}}* removed the stale marking on *{{feature}}* in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_STALE_ON]: {
|
||||
label: 'Flag marked as stale',
|
||||
action: '*{{user}}* marked *{{feature}}* as stale in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_STRATEGY_ADD]: {
|
||||
label: 'Flag strategy added',
|
||||
action: '*{{user}}* added strategy *{{strategyTitle}}* to *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_STRATEGY_REMOVE]: {
|
||||
label: 'Flag strategy removed',
|
||||
action: '*{{user}}* removed strategy *{{strategyTitle}}* from *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_STRATEGY_UPDATE]: {
|
||||
label: 'Flag strategy updated',
|
||||
action: '*{{user}}* updated *{{feature}}* in project *{{project}}* {{strategyChangeText}}',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_TAGGED]: {
|
||||
label: 'Flag tagged',
|
||||
action: '*{{user}}* tagged *{{feature}}* with *{{event.data.type}}:{{event.data.value}}* in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[FEATURE_UNTAGGED]: {
|
||||
label: 'Flag untagged',
|
||||
action: '*{{user}}* untagged *{{feature}}* with *{{event.preData.type}}:{{event.preData.value}}* in project *{{project}}*',
|
||||
path: '/projects/{{event.project}}/features/{{event.featureName}}',
|
||||
},
|
||||
[GROUP_CREATED]: {
|
||||
label: 'Group created',
|
||||
action: '*{{user}}* created group *{{event.data.name}}*',
|
||||
path: '/admin/groups',
|
||||
},
|
||||
[GROUP_DELETED]: {
|
||||
label: 'Group deleted',
|
||||
action: '*{{user}}* deleted group *{{event.preData.name}}*',
|
||||
path: '/admin/groups',
|
||||
},
|
||||
[GROUP_UPDATED]: {
|
||||
label: 'Group updated',
|
||||
action: '*{{user}}* updated group *{{event.preData.name}}*',
|
||||
path: '/admin/groups',
|
||||
},
|
||||
[BANNER_CREATED]: {
|
||||
label: 'Banner created',
|
||||
action: '*{{user}}* created banner *{{event.data.message}}*',
|
||||
path: '/admin/message-banners',
|
||||
},
|
||||
[BANNER_DELETED]: {
|
||||
label: 'Banner deleted',
|
||||
action: '*{{user}}* deleted banner *{{event.preData.message}}*',
|
||||
path: '/admin/message-banners',
|
||||
},
|
||||
[BANNER_UPDATED]: {
|
||||
label: 'Banner updated',
|
||||
action: '*{{user}}* updated banner *{{event.preData.message}}*',
|
||||
path: '/admin/message-banners',
|
||||
},
|
||||
[PROJECT_CREATED]: {
|
||||
label: 'Project created',
|
||||
action: '*{{user}}* created project *{{project}}*',
|
||||
path: '/projects',
|
||||
},
|
||||
[PROJECT_DELETED]: {
|
||||
label: 'Project deleted',
|
||||
action: '*{{user}}* deleted project *{{event.project}}*',
|
||||
path: '/projects',
|
||||
},
|
||||
[SEGMENT_CREATED]: {
|
||||
label: 'Segment created',
|
||||
action: '*{{user}}* created segment *{{event.data.name}}*',
|
||||
path: '/segments',
|
||||
},
|
||||
[SEGMENT_DELETED]: {
|
||||
label: 'Segment deleted',
|
||||
action: '*{{user}}* deleted segment *{{event.preData.name}}*',
|
||||
path: '/segments',
|
||||
},
|
||||
[SEGMENT_UPDATED]: {
|
||||
label: 'Segment updated',
|
||||
action: '*{{user}}* updated segment *{{event.preData.name}}*',
|
||||
path: '/segments',
|
||||
},
|
||||
[SERVICE_ACCOUNT_CREATED]: {
|
||||
label: 'Service account created',
|
||||
action: '*{{user}}* created service account *{{event.data.name}}*',
|
||||
path: '/admin/service-accounts',
|
||||
},
|
||||
[SERVICE_ACCOUNT_DELETED]: {
|
||||
label: 'Service account deleted',
|
||||
action: '*{{user}}* deleted service account *{{event.preData.name}}*',
|
||||
path: '/admin/service-accounts',
|
||||
},
|
||||
[SERVICE_ACCOUNT_UPDATED]: {
|
||||
label: 'Service account updated',
|
||||
action: '*{{user}}* updated service account *{{event.preData.name}}*',
|
||||
path: '/admin/service-accounts',
|
||||
},
|
||||
[USER_CREATED]: {
|
||||
label: 'User created',
|
||||
action: '*{{user}}* created user *{{event.data.name}}*',
|
||||
path: '/admin/users',
|
||||
},
|
||||
[USER_DELETED]: {
|
||||
label: 'User deleted',
|
||||
action: '*{{user}}* deleted user *{{event.preData.name}}*',
|
||||
path: '/admin/users',
|
||||
},
|
||||
[USER_UPDATED]: {
|
||||
label: 'User updated',
|
||||
action: '*{{user}}* updated user *{{event.preData.name}}*',
|
||||
path: '/admin/users',
|
||||
},
|
||||
@ -598,10 +658,7 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
|
||||
: ` segments from ${oldSegmentsText} to ${newSegmentsText}`;
|
||||
}
|
||||
|
||||
format(event: IEvent): {
|
||||
text: string;
|
||||
url?: string;
|
||||
} {
|
||||
format(event: IEvent): IFormattedEventData {
|
||||
const { createdBy, type } = event;
|
||||
const { action, path } = EVENT_MAP[type] || {
|
||||
action: `triggered *${type}*`,
|
||||
@ -619,12 +676,14 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
|
||||
|
||||
Mustache.escape = (text) => text;
|
||||
|
||||
const label = EVENT_MAP[type]?.label || type;
|
||||
const text = Mustache.render(action, context);
|
||||
const url = path
|
||||
? `${this.unleashUrl}${Mustache.render(path, context)}`
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
label,
|
||||
text,
|
||||
url,
|
||||
};
|
||||
|
@ -18,8 +18,12 @@ import {
|
||||
import { normalizeQueryParams } from '../../features/feature-search/search-utils';
|
||||
import Controller from '../../routes/controller';
|
||||
import type { IAuthRequest } from '../../server-impl';
|
||||
import type { IEvent } from '../../types';
|
||||
import type { IEnrichedEvent, IEvent } from '../../types';
|
||||
import { anonymiseKeys, extractUserIdFromUser } from '../../util';
|
||||
import {
|
||||
FeatureEventFormatterMd,
|
||||
type FeatureEventFormatter,
|
||||
} from '../../addons/feature-event-formatter-md';
|
||||
|
||||
const ANON_KEYS = ['email', 'username', 'createdBy'];
|
||||
const version = 1 as const;
|
||||
@ -28,6 +32,8 @@ export default class EventSearchController extends Controller {
|
||||
|
||||
private flagResolver: IFlagResolver;
|
||||
|
||||
private msgFormatter: FeatureEventFormatter;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
constructor(
|
||||
@ -41,6 +47,9 @@ export default class EventSearchController extends Controller {
|
||||
this.eventService = eventService;
|
||||
this.flagResolver = config.flagResolver;
|
||||
this.openApiService = openApiService;
|
||||
this.msgFormatter = new FeatureEventFormatterMd(
|
||||
config.server.unleashUrl,
|
||||
);
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
@ -85,17 +94,37 @@ export default class EventSearchController extends Controller {
|
||||
extractUserIdFromUser(user),
|
||||
);
|
||||
|
||||
const enrichedEvents = this.enrichEvents(events);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
eventSearchResponseSchema.$id,
|
||||
serializeDates({
|
||||
events: serializeDates(this.maybeAnonymiseEvents(events)),
|
||||
events: serializeDates(
|
||||
this.maybeAnonymiseEvents(enrichedEvents),
|
||||
),
|
||||
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[] {
|
||||
if (this.flagResolver.isEnabled('anonymiseEventLog')) {
|
||||
return anonymiseKeys(events, ANON_KEYS);
|
||||
|
@ -92,6 +92,18 @@ export const eventSchema = {
|
||||
nullable: true,
|
||||
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: {
|
||||
schemas: {
|
||||
|
@ -374,6 +374,11 @@ export interface IEvent extends Omit<IBaseEvent, 'ip'> {
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface IEnrichedEvent extends IEvent {
|
||||
label: string;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface IEventList {
|
||||
totalEvents: number;
|
||||
events: IEvent[];
|
||||
|
Loading…
Reference in New Issue
Block a user