diff --git a/frontend/src/component/events/EventTimeline/EventTimeline.tsx b/frontend/src/component/events/EventTimeline/EventTimeline.tsx index 9cee0ca075..915ed492e4 100644 --- a/frontend/src/component/events/EventTimeline/EventTimeline.tsx +++ b/frontend/src/component/events/EventTimeline/EventTimeline.tsx @@ -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() && diff --git a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEvent.tsx b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEvent.tsx index 8cdedc001d..15f72edc3b 100644 --- a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEvent.tsx +++ b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEvent.tsx @@ -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 = ({ } + maxWidth={320} arrow > diff --git a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx index 328d35cf4d..db09f744d8 100644 --- a/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx +++ b/frontend/src/component/events/EventTimeline/EventTimelineEvent/EventTimelineEventTooltip/EventTimelineEventTooltip.tsx @@ -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 ( -
- {eventDateTime} -

- {event.createdBy} enabled {event.featureName} for the{' '} - {event.environment} environment in project {event.project} -

-
- ); - } - if (event.type === 'feature-environment-disabled') { - return ( -
- {eventDateTime} -

- {event.createdBy} disabled {event.featureName} for the{' '} - {event.environment} environment in project {event.project} -

-
- ); - } - return ( -
-
{eventDateTime}
-
{event.createdBy}
-
{event.type}
-
{event.featureName}
-
{event.environment}
-
+ <> + + {event.label} + {eventDateTime} + + {event.summary} + ); }; diff --git a/src/lib/addons/__snapshots__/feature-event-formatter-md.test.ts.snap b/src/lib/addons/__snapshots__/feature-event-formatter-md.test.ts.snap index bf49309fe6..8ce76de746 100644 --- a/src/lib/addons/__snapshots__/feature-event-formatter-md.test.ts.snap +++ b/src/lib/addons/__snapshots__/feature-event-formatter-md.test.ts.snap @@ -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", } diff --git a/src/lib/addons/feature-event-formatter-md.ts b/src/lib/addons/feature-event-formatter-md.ts index 1e95801a90..09015e2df5 100644 --- a/src/lib/addons/feature-event-formatter-md.ts +++ b/src/lib/addons/feature-event-formatter-md.ts @@ -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 = { [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, }; diff --git a/src/lib/features/events/event-search-controller.ts b/src/lib/features/events/event-search-controller.ts index 5aea1fc83d..ee005a837b 100644 --- a/src/lib/features/events/event-search-controller.ts +++ b/src/lib/features/events/event-search-controller.ts @@ -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); diff --git a/src/lib/openapi/spec/event-schema.ts b/src/lib/openapi/spec/event-schema.ts index 756d875277..78f8640e51 100644 --- a/src/lib/openapi/spec/event-schema.ts +++ b/src/lib/openapi/spec/event-schema.ts @@ -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: { diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 26db0b636c..d71c28df5f 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -374,6 +374,11 @@ export interface IEvent extends Omit { createdAt: Date; } +export interface IEnrichedEvent extends IEvent { + label: string; + summary: string; +} + export interface IEventList { totalEvents: number; events: IEvent[];