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:
parent
6d51213f55
commit
a413a38352
@ -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() &&
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { EventSchema, EventSchemaType } from 'openapi';
|
||||
import type { EventSchemaType } from 'openapi';
|
||||
import { styled } from '@mui/material';
|
||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||
import { EventTimelineEventTooltip } from './EventTimelineEventTooltip/EventTimelineEventTooltip';
|
||||
@ -8,6 +8,7 @@ import FlagOutlinedIcon from '@mui/icons-material/FlagOutlined';
|
||||
import ExtensionOutlinedIcon from '@mui/icons-material/ExtensionOutlined';
|
||||
import SegmentsIcon from '@mui/icons-material/DonutLargeOutlined';
|
||||
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
|
||||
import type { EnrichedEvent } from '../EventTimeline';
|
||||
|
||||
type DefaultEventVariant = 'secondary';
|
||||
type CustomEventVariant = 'success' | 'neutral';
|
||||
@ -76,7 +77,7 @@ const customEventVariants: Partial<
|
||||
};
|
||||
|
||||
interface IEventTimelineEventProps {
|
||||
event: EventSchema;
|
||||
event: EnrichedEvent;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}
|
||||
@ -97,6 +98,7 @@ export const EventTimelineEvent = ({
|
||||
<StyledEvent position={position}>
|
||||
<HtmlTooltip
|
||||
title={<EventTimelineEventTooltip event={event} />}
|
||||
maxWidth={320}
|
||||
arrow
|
||||
>
|
||||
<StyledEventCircle variant={variant}>
|
||||
|
@ -1,9 +1,32 @@
|
||||
import { styled } from '@mui/material';
|
||||
import { Markdown } from 'component/common/Markdown/Markdown';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import type { EventSchema } from 'openapi';
|
||||
import { formatDateYMDHMS } from 'utils/formatDate';
|
||||
import type { EnrichedEvent } from '../../EventTimeline';
|
||||
|
||||
const StyledTooltipHeader = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: theme.spacing(1),
|
||||
gap: theme.spacing(2),
|
||||
flexWrap: 'wrap',
|
||||
}));
|
||||
|
||||
const StyledTooltipTitle = styled('div')(({ theme }) => ({
|
||||
fontWeight: theme.fontWeight.bold,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
wordBreak: 'break-word',
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
const StyledDateTime = styled('div')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
whiteSpace: 'nowrap',
|
||||
}));
|
||||
|
||||
interface IEventTimelineEventTooltipProps {
|
||||
event: EventSchema;
|
||||
event: EnrichedEvent;
|
||||
}
|
||||
|
||||
export const EventTimelineEventTooltip = ({
|
||||
@ -15,36 +38,13 @@ export const EventTimelineEventTooltip = ({
|
||||
locationSettings?.locale,
|
||||
);
|
||||
|
||||
if (event.type === 'feature-environment-enabled') {
|
||||
return (
|
||||
<div>
|
||||
<small>{eventDateTime}</small>
|
||||
<p>
|
||||
{event.createdBy} enabled {event.featureName} for the{' '}
|
||||
{event.environment} environment in project {event.project}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (event.type === 'feature-environment-disabled') {
|
||||
return (
|
||||
<div>
|
||||
<small>{eventDateTime}</small>
|
||||
<p>
|
||||
{event.createdBy} disabled {event.featureName} for the{' '}
|
||||
{event.environment} environment in project {event.project}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{eventDateTime}</div>
|
||||
<div>{event.createdBy}</div>
|
||||
<div>{event.type}</div>
|
||||
<div>{event.featureName}</div>
|
||||
<div>{event.environment}</div>
|
||||
</div>
|
||||
<>
|
||||
<StyledTooltipHeader>
|
||||
<StyledTooltipTitle>{event.label}</StyledTooltipTitle>
|
||||
<StyledDateTime>{eventDateTime}</StyledDateTime>
|
||||
</StyledTooltipHeader>
|
||||
<Markdown>{event.description}</Markdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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: {
|
||||
|
@ -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[];
|
||||
|
Loading…
Reference in New Issue
Block a user