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

chore: event timeline tooltips

This commit is contained in:
Nuno Góis 2024-09-20 14:40:39 +01:00
parent 6d51213f55
commit a413a38352
No known key found for this signature in database
GPG Key ID: 71ECC689F1091765
7 changed files with 235 additions and 118 deletions

View File

@ -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() &&

View File

@ -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}>

View File

@ -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.description}</Markdown>
</>
);
};

View File

@ -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<string, IEventData> = {
[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,
};

View File

@ -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);

View File

@ -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: {

View File

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