From a413a3835245fb8961e0469cb50e589771438bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Fri, 20 Sep 2024 14:40:39 +0100 Subject: [PATCH] chore: event timeline tooltips --- .../events/EventTimeline/EventTimeline.tsx | 11 +- .../EventTimelineEvent/EventTimelineEvent.tsx | 6 +- .../EventTimelineEventTooltip.tsx | 64 ++--- src/lib/addons/feature-event-formatter-md.ts | 223 +++++++++++------- .../events/event-search-controller.ts | 33 ++- src/lib/openapi/spec/event-schema.ts | 11 + src/lib/types/events.ts | 5 + 7 files changed, 235 insertions(+), 118 deletions(-) diff --git a/frontend/src/component/events/EventTimeline/EventTimeline.tsx b/frontend/src/component/events/EventTimeline/EventTimeline.tsx index 9cee0ca0750d..9326754f0f49 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; + description: 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 8cdedc001db5..15f72edc3ba5 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 328d35cf4d36..553daf99162b 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.description} + ); }; diff --git a/src/lib/addons/feature-event-formatter-md.ts b/src/lib/addons/feature-event-formatter-md.ts index 1e95801a9030..5613b028f713 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; } @@ -80,237 +82,297 @@ export enum LinkStyle { MD = 1, } +const bold = (text?: string) => (text ? `**${text}**` : ''); + const EVENT_MAP: Record = { [ADDON_CONFIG_CREATED]: { - action: '*{{user}}* created a new *{{event.data.provider}}* integration configuration', + label: 'Integration configuration created', + action: `${bold('{{user}}')} created a new ${bold('{{event.data.provider}}')} integration configuration`, path: '/integrations', }, [ADDON_CONFIG_DELETED]: { - action: '*{{user}}* deleted a *{{event.preData.provider}}* integration configuration', + label: 'Integration configuration deleted', + action: `${bold('{{user}}')} deleted a ${bold('{{event.preData.provider}}')} integration configuration`, path: '/integrations', }, [ADDON_CONFIG_UPDATED]: { - action: '*{{user}}* updated a *{{event.preData.provider}}* integration configuration', + label: 'Integration configuration updated', + action: `${bold('{{user}}')} updated a ${bold('{{event.preData.provider}}')} integration configuration`, path: '/integrations', }, [API_TOKEN_CREATED]: { - action: '*{{user}}* created API token *{{event.data.username}}*', + label: 'API token created', + action: `${bold('{{user}}')} created API token ${bold('{{event.data.username}}')}`, path: '/admin/api', }, [API_TOKEN_DELETED]: { - action: '*{{user}}* deleted API token *{{event.preData.username}}*', + label: 'API token deleted', + action: `${bold('{{user}}')} deleted API token ${bold('{{event.preData.username}}')}`, path: '/admin/api', }, [CHANGE_ADDED]: { - action: '*{{user}}* added a change to change request {{changeRequest}}', + label: 'Change added', + action: `${bold('{{user}}')} added a change to change request {{changeRequest}}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_DISCARDED]: { - action: '*{{user}}* discarded a change in change request {{changeRequest}}', + label: 'Change discarded', + action: `${bold('{{user}}')} discarded a change in change request {{changeRequest}}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_EDITED]: { - action: '*{{user}}* edited a change in change request {{changeRequest}}', + label: 'Change edited', + action: `${bold('{{user}}')} edited a change in change request {{changeRequest}}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_APPLIED]: { - action: '*{{user}}* applied change request {{changeRequest}}', + label: 'Change request applied', + action: `${bold('{{user}}')} applied change request {{changeRequest}}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_APPROVAL_ADDED]: { - action: '*{{user}}* added an approval to change request {{changeRequest}}', + label: 'Change request approval added', + action: `${bold('{{user}}')} added an approval to change request {{changeRequest}}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_APPROVED]: { - action: '*{{user}}* approved change request {{changeRequest}}', + label: 'Change request approved', + action: `${bold('{{user}}')} approved change request {{changeRequest}}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_CANCELLED]: { - action: '*{{user}}* cancelled change request {{changeRequest}}', + label: 'Change request cancelled', + action: `${bold('{{user}}')} cancelled change request {{changeRequest}}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_CREATED]: { - action: '*{{user}}* created change request {{changeRequest}}', + label: 'Change request created', + action: `${bold('{{user}}')} created change request {{changeRequest}}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_DISCARDED]: { - action: '*{{user}}* discarded change request {{changeRequest}}', + label: 'Change request discarded', + action: `${bold('{{user}}')} discarded change request {{changeRequest}}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_REJECTED]: { - action: '*{{user}}* rejected change request {{changeRequest}}', + label: 'Change request rejected', + action: `${bold('{{user}}')} rejected change request {{changeRequest}}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_SENT_TO_REVIEW]: { - action: '*{{user}}* sent to review change request {{changeRequest}}', + label: 'Change request sent to review', + action: `${bold('{{user}}')} sent to review change request {{changeRequest}}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_SCHEDULED]: { - action: '*{{user}}* scheduled change request {{changeRequest}} to be applied at {{event.data.scheduledDate}} in project *{{event.project}}*', + label: 'Change request scheduled', + action: `${bold('{{user}}')} scheduled change request {{changeRequest}} to be applied at {{event.data.scheduledDate}} in project ${bold('{{event.project}}')}`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_SCHEDULED_APPLICATION_SUCCESS]: { - action: '*Successfully* applied the scheduled change request {{changeRequest}} by *{{user}}* in project *{{event.project}}*.', + label: 'Scheduled change request applied successfully', + action: `${bold('Successfully')} applied the scheduled change request {{changeRequest}} by ${bold('{{user}}')} in project ${bold('{{event.project}}')}.`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_SCHEDULED_APPLICATION_FAILURE]: { - action: '*Failed* to apply the scheduled change request {{changeRequest}} by *{{user}}* in project *{{event.project}}*.', + label: 'Scheduled change request failed', + action: `${bold('Failed')} to apply the scheduled change request {{changeRequest}} by ${bold('{{user}}')} in project ${bold('{{event.project}}')}.`, path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', }, [CHANGE_REQUEST_SCHEDULE_SUSPENDED]: { - action: 'Change request {{changeRequest}} was suspended for the following reason: {{event.data.reason}}', + 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]: { - action: '*{{user}}* created context field *{{event.data.name}}*', + label: 'Context field created', + action: `${bold('{{user}}')} created context field ${bold('{{event.data.name}}')}`, path: '/context', }, [CONTEXT_FIELD_DELETED]: { - action: '*{{user}}* deleted context field *{{event.preData.name}}*', + label: 'Context field deleted', + action: `${bold('{{user}}')} deleted context field ${bold('{{event.preData.name}}')}`, path: '/context', }, [CONTEXT_FIELD_UPDATED]: { - action: '*{{user}}* updated context field *{{event.preData.name}}*', + label: 'Context field updated', + action: `${bold('{{user}}')} updated context field ${bold('{{event.preData.name}}')}`, path: '/context', }, [FEATURE_ARCHIVED]: { - action: '*{{user}}* archived *{{event.featureName}}* in project *{{project}}*', + label: 'Flag archived', + action: `${bold('{{user}}')} archived ${bold('{{event.featureName}}')} in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/archive', }, [FEATURE_CREATED]: { - action: '*{{user}}* created *{{feature}}* in project *{{project}}*', + label: 'Flag created', + action: `${bold('{{user}}')} created ${bold('{{feature}}')} in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_DELETED]: { - action: '*{{user}}* deleted *{{event.featureName}}* in project *{{project}}*', + label: 'Flag deleted', + action: `${bold('{{user}}')} deleted ${bold('{{event.featureName}}')} in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}', }, [FEATURE_ENVIRONMENT_DISABLED]: { - action: '*{{user}}* disabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', + label: 'Flag disabled', + action: `${bold('{{user}}')} disabled ${bold('{{feature}}')} for the ${bold('{{event.environment}}')} environment in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_ENVIRONMENT_ENABLED]: { - action: '*{{user}}* enabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', + label: 'Flag enabled', + action: `${bold('{{user}}')} enabled ${bold('{{feature}}')} for the ${bold('{{event.environment}}')} environment in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_ENVIRONMENT_VARIANTS_UPDATED]: { - action: '*{{user}}* updated variants for *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', + label: 'Flag variants updated', + action: `${bold('{{user}}')} updated variants for ${bold('{{feature}}')} for the ${bold('{{event.environment}}')} environment in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}/variants', }, [FEATURE_METADATA_UPDATED]: { - action: '*{{user}}* updated *{{feature}}* metadata in project *{{project}}*', + label: 'Flag metadata updated', + action: `${bold('{{user}}')} updated ${bold('{{feature}}')} metadata in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_COMPLETED]: { - action: '*{{feature}}* was marked as completed in project *{{project}}*', + label: 'Flag marked as completed', + action: `${bold('{{feature}}')} was marked as completed in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_POTENTIALLY_STALE_ON]: { - action: '*{{feature}}* was marked as potentially stale in project *{{project}}*', + label: 'Flag potentially stale', + action: `${bold('{{feature}}')} was marked as potentially stale in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_PROJECT_CHANGE]: { - action: '*{{user}}* moved *{{feature}}* from *{{event.data.oldProject}}* to *{{project}}*', + label: 'Flag moved to a new project', + action: `${bold('{{user}}')} moved ${bold('{{feature}}')} from ${bold('{{event.data.oldProject}}')} to ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_REVIVED]: { - action: '*{{user}}* revived *{{feature}}* in project *{{project}}*', + label: 'Flag revived', + action: `${bold('{{user}}')} revived ${bold('{{feature}}')} in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_STALE_OFF]: { - action: '*{{user}}* removed the stale marking on *{{feature}}* in project *{{project}}*', + label: 'Flag stale marking removed', + action: `${bold('{{user}}')} removed the stale marking on ${bold('{{feature}}')} in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_STALE_ON]: { - action: '*{{user}}* marked *{{feature}}* as stale in project *{{project}}*', + label: 'Flag marked as stale', + action: `${bold('{{user}}')} marked ${bold('{{feature}}')} as stale in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_STRATEGY_ADD]: { - action: '*{{user}}* added strategy *{{strategyTitle}}* to *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', + label: 'Flag strategy added', + action: `${bold('{{user}}')} added strategy ${bold('{{strategyTitle}}')} to ${bold('{{feature}}')} for the ${bold('{{event.environment}}')} environment in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_STRATEGY_REMOVE]: { - action: '*{{user}}* removed strategy *{{strategyTitle}}* from *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', + label: 'Flag strategy removed', + action: `${bold('{{user}}')} removed strategy ${bold('{{strategyTitle}}')} from ${bold('{{feature}}')} for the ${bold('{{event.environment}}')} environment in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_STRATEGY_UPDATE]: { - action: '*{{user}}* updated *{{feature}}* in project *{{project}}* {{strategyChangeText}}', + label: 'Flag strategy updated', + action: `${bold('{{user}}')} updated ${bold('{{feature}}')} in project ${bold('{{project}}')} {{strategyChangeText}}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_TAGGED]: { - action: '*{{user}}* tagged *{{feature}}* with *{{event.data.type}}:{{event.data.value}}* in project *{{project}}*', + label: 'Flag tagged', + action: `${bold('{{user}}')} tagged ${bold('{{feature}}')} with ${bold('{{event.data.type}}:{{event.data.value}}')} in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [FEATURE_UNTAGGED]: { - action: '*{{user}}* untagged *{{feature}}* with *{{event.preData.type}}:{{event.preData.value}}* in project *{{project}}*', + label: 'Flag untagged', + action: `${bold('{{user}}')} untagged ${bold('{{feature}}')} with ${bold('{{event.preData.type}}:{{event.preData.value}}')} in project ${bold('{{project}}')}`, path: '/projects/{{event.project}}/features/{{event.featureName}}', }, [GROUP_CREATED]: { - action: '*{{user}}* created group *{{event.data.name}}*', + label: 'Group created', + action: `${bold('{{user}}')} created group ${bold('{{event.data.name}}')}`, path: '/admin/groups', }, [GROUP_DELETED]: { - action: '*{{user}}* deleted group *{{event.preData.name}}*', + label: 'Group deleted', + action: `${bold('{{user}}')} deleted group ${bold('{{event.preData.name}}')}`, path: '/admin/groups', }, [GROUP_UPDATED]: { - action: '*{{user}}* updated group *{{event.preData.name}}*', + label: 'Group updated', + action: `${bold('{{user}}')} updated group ${bold('{{event.preData.name}}')}`, path: '/admin/groups', }, [BANNER_CREATED]: { - action: '*{{user}}* created banner *{{event.data.message}}*', + label: 'Banner created', + action: `${bold('{{user}}')} created banner ${bold('{{event.data.message}}')}`, path: '/admin/message-banners', }, [BANNER_DELETED]: { - action: '*{{user}}* deleted banner *{{event.preData.message}}*', + label: 'Banner deleted', + action: `${bold('{{user}}')} deleted banner ${bold('{{event.preData.message}}')}`, path: '/admin/message-banners', }, [BANNER_UPDATED]: { - action: '*{{user}}* updated banner *{{event.preData.message}}*', + label: 'Banner updated', + action: `${bold('{{user}}')} updated banner ${bold('{{event.preData.message}}')}`, path: '/admin/message-banners', }, [PROJECT_CREATED]: { - action: '*{{user}}* created project *{{project}}*', + label: 'Project created', + action: `${bold('{{user}}')} created project ${bold('{{project}}')}`, path: '/projects', }, [PROJECT_DELETED]: { - action: '*{{user}}* deleted project *{{event.project}}*', + label: 'Project deleted', + action: `${bold('{{user}}')} deleted project ${bold('{{event.project}}')}`, path: '/projects', }, [SEGMENT_CREATED]: { - action: '*{{user}}* created segment *{{event.data.name}}*', + label: 'Segment created', + action: `${bold('{{user}}')} created segment ${bold('{{event.data.name}}')}`, path: '/segments', }, [SEGMENT_DELETED]: { - action: '*{{user}}* deleted segment *{{event.preData.name}}*', + label: 'Segment deleted', + action: `${bold('{{user}}')} deleted segment ${bold('{{event.preData.name}}')}`, path: '/segments', }, [SEGMENT_UPDATED]: { - action: '*{{user}}* updated segment *{{event.preData.name}}*', + label: 'Segment updated', + action: `${bold('{{user}}')} updated segment ${bold('{{event.preData.name}}')}`, path: '/segments', }, [SERVICE_ACCOUNT_CREATED]: { - action: '*{{user}}* created service account *{{event.data.name}}*', + label: 'Service account created', + action: `${bold('{{user}}')} created service account ${bold('{{event.data.name}}')}`, path: '/admin/service-accounts', }, [SERVICE_ACCOUNT_DELETED]: { - action: '*{{user}}* deleted service account *{{event.preData.name}}*', + label: 'Service account deleted', + action: `${bold('{{user}}')} deleted service account ${bold('{{event.preData.name}}')}`, path: '/admin/service-accounts', }, [SERVICE_ACCOUNT_UPDATED]: { - action: '*{{user}}* updated service account *{{event.preData.name}}*', + label: 'Service account updated', + action: `${bold('{{user}}')} updated service account ${bold('{{event.preData.name}}')}`, path: '/admin/service-accounts', }, [USER_CREATED]: { - action: '*{{user}}* created user *{{event.data.name}}*', + label: 'User created', + action: `${bold('{{user}}')} created user ${bold('{{event.data.name}}')}`, path: '/admin/users', }, [USER_DELETED]: { - action: '*{{user}}* deleted user *{{event.preData.name}}*', + label: 'User deleted', + action: `${bold('{{user}}')} deleted user ${bold('{{event.preData.name}}')}`, path: '/admin/users', }, [USER_UPDATED]: { - action: '*{{user}}* updated user *{{event.preData.name}}*', + label: 'User updated', + action: `${bold('{{user}}')} updated user ${bold('{{event.preData.name}}')}`, path: '/admin/users', }, }; @@ -334,17 +396,19 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { const text = `#${changeRequestId}`; const featureLink = this.generateFeatureLink(event); const featureText = featureLink - ? ` for feature flag *${featureLink}*` + ? ` for feature flag ${bold(featureLink)}` : ''; const environmentText = environment - ? ` in the *${environment}* environment` + ? ` in the ${bold(environment)} environment` : ''; const projectLink = this.generateProjectLink(event); - const projectText = project ? ` in project *${projectLink}*` : ''; + const projectText = project + ? ` in project ${bold(projectLink)}` + : ''; if (this.linkStyle === LinkStyle.SLACK) { - return `*<${url}|${text}>*${featureText}${environmentText}${projectText}`; + return `${bold(`<${url}|${text}>`)}${featureText}${environmentText}${projectText}`; } else { - return `*[${text}](${url})*${featureText}${environmentText}${projectText}`; + return `${bold(`[${text}](${url})`)}${featureText}${environmentText}${projectText}`; } } } @@ -410,9 +474,9 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { event, ); default: - return `by updating strategy *${this.getStrategyTitle( - event, - )}* in *${environment}*`; + return `by updating strategy ${bold( + this.getStrategyTitle(event), + )} in ${bold(environment)}`; } }; @@ -462,9 +526,9 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { const strategySpecificText = [usersText, constraintText, segmentsText] .filter((x) => x.length) .join(';'); - return `by updating strategy *${this.getStrategyTitle( - event, - )}* in *${environment}*${strategySpecificText}`; + return `by updating strategy ${bold( + this.getStrategyTitle(event), + )} in ${bold(environment)}${strategySpecificText}`; } private flexibleRolloutStrategyChangeText(event: IEvent) { @@ -510,9 +574,9 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { ] .filter((txt) => txt.length) .join(';'); - return `by updating strategy *${this.getStrategyTitle( - event, - )}* in *${environment}*${strategySpecificText}`; + return `by updating strategy ${bold( + this.getStrategyTitle(event), + )} in ${bold(environment)}${strategySpecificText}`; } private defaultStrategyChangeText(event: IEvent) { @@ -528,9 +592,9 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { const strategySpecificText = [constraintText, segmentsText] .filter((txt) => txt.length) .join(';'); - return `by updating strategy *${this.getStrategyTitle( - event, - )}* in *${environment}*${strategySpecificText}`; + return `by updating strategy ${bold( + this.getStrategyTitle(event), + )} in ${bold(environment)}${strategySpecificText}`; } private constraintChangeText( @@ -598,13 +662,10 @@ 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}*`, + action: `triggered ${bold(type)}`, }; const context = { @@ -619,12 +680,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 5aea1fc83d18..61bc47203ad3 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: description } = + this.msgFormatter.format(event); + + return { + ...event, + label, + description, + }; + }); + } + 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 756d87527749..c3470ba74d65 100644 --- a/src/lib/openapi/spec/event-schema.ts +++ b/src/lib/openapi/spec/event-schema.ts @@ -92,6 +92,17 @@ export const eventSchema = { nullable: true, description: 'Any tags related to the event, if applicable.', }, + label: { + type: 'string', + nullable: true, + description: 'A concise, human-readable name for the event.', + }, + description: { + type: 'string', + nullable: true, + description: + 'A detailed description of the event, formatted in markdown.', + }, }, components: { schemas: { diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 26db0b636c26..f685d029f5d4 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; + description: string; +} + export interface IEventList { totalEvents: number; events: IEvent[];