mirror of
https://github.com/Unleash/unleash.git
synced 2024-11-01 19:07:38 +01:00
4f14549fa1
## About the changes Adds more specifics to addon notifications when a flexibleRollout is updated. - [x] Specific text for all strategy types - [x] Add constraint differences for all strategy types <!-- Does it close an issue? Multiple? --> Closes [#3140](https://github.com/Unleash/unleash/issues/3140)
362 lines
12 KiB
TypeScript
362 lines
12 KiB
TypeScript
import {
|
|
FEATURE_ARCHIVED,
|
|
FEATURE_CREATED,
|
|
FEATURE_ENVIRONMENT_DISABLED,
|
|
FEATURE_ENVIRONMENT_ENABLED,
|
|
FEATURE_METADATA_UPDATED,
|
|
FEATURE_PROJECT_CHANGE,
|
|
FEATURE_REVIVED,
|
|
FEATURE_STALE_OFF,
|
|
FEATURE_STALE_ON,
|
|
FEATURE_STRATEGY_ADD,
|
|
FEATURE_STRATEGY_REMOVE,
|
|
FEATURE_STRATEGY_UPDATE,
|
|
FEATURE_UPDATED,
|
|
FEATURE_VARIANTS_UPDATED,
|
|
IConstraint,
|
|
IEvent,
|
|
} from '../types';
|
|
|
|
export interface FeatureEventFormatter {
|
|
format: (event: IEvent) => string;
|
|
featureLink: (event: IEvent) => string;
|
|
}
|
|
|
|
export enum LinkStyle {
|
|
SLACK,
|
|
MD,
|
|
}
|
|
|
|
export class FeatureEventFormatterMd implements FeatureEventFormatter {
|
|
private readonly unleashUrl: string;
|
|
|
|
private readonly linkStyle: LinkStyle;
|
|
|
|
constructor(unleashUrl: string, linkStyle: LinkStyle = LinkStyle.MD) {
|
|
this.unleashUrl = unleashUrl;
|
|
this.linkStyle = linkStyle;
|
|
}
|
|
|
|
generateArchivedText(event: IEvent): string {
|
|
const { createdBy, type } = event;
|
|
const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived';
|
|
const feature = this.generateFeatureLink(event);
|
|
return ` ${createdBy} just ${action} feature toggle *${feature}*`;
|
|
}
|
|
|
|
generateFeatureLink(event: IEvent): string {
|
|
if (this.linkStyle === LinkStyle.SLACK) {
|
|
return `<${this.featureLink(event)}|${event.featureName}>`;
|
|
} else {
|
|
return `[${event.featureName}](${this.featureLink(event)})`;
|
|
}
|
|
}
|
|
|
|
generateStaleText(event: IEvent): string {
|
|
const { createdBy, type } = event;
|
|
const isStale = type === FEATURE_STALE_ON;
|
|
const feature = this.generateFeatureLink(event);
|
|
|
|
if (isStale) {
|
|
return `${createdBy} marked ${feature} as stale and this feature toggle is now *ready to be removed* from the code.`;
|
|
}
|
|
return `${createdBy} removed the stale marking on *${feature}*.`;
|
|
}
|
|
|
|
generateEnvironmentToggleText(event: IEvent): string {
|
|
const { createdBy, environment, type, project } = event;
|
|
const toggleStatus =
|
|
type === FEATURE_ENVIRONMENT_ENABLED ? 'enabled' : 'disabled';
|
|
const feature = this.generateFeatureLink(event);
|
|
return `${createdBy} *${toggleStatus}* ${feature} in *${environment}* environment in project *${project}*`;
|
|
}
|
|
|
|
generateStrategyChangeText(event: IEvent): string {
|
|
const { createdBy, environment, project, data, preData } = event;
|
|
const feature = this.generateFeatureLink(event);
|
|
const strategyText = () => {
|
|
switch (data.name) {
|
|
case 'flexibleRollout':
|
|
return this.flexibleRolloutStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment,
|
|
);
|
|
case 'default':
|
|
return this.defaultStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment,
|
|
);
|
|
case 'userWithId':
|
|
return this.userWithIdStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment,
|
|
);
|
|
case 'remoteAddress':
|
|
return this.remoteAddressStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment,
|
|
);
|
|
case 'applicationHostname':
|
|
return this.applicationHostnameStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment,
|
|
);
|
|
default:
|
|
return `by updating strategy ${data?.name} in *${environment}*`;
|
|
}
|
|
};
|
|
|
|
return `${createdBy} updated *${feature}* in project *${project}* ${strategyText()}`;
|
|
}
|
|
|
|
private applicationHostnameStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment: string | undefined,
|
|
) {
|
|
return this.listOfValuesStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment,
|
|
'hostNames',
|
|
);
|
|
}
|
|
|
|
private remoteAddressStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment: string | undefined,
|
|
) {
|
|
return this.listOfValuesStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment,
|
|
'IPs',
|
|
);
|
|
}
|
|
|
|
private userWithIdStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment: string | undefined,
|
|
) {
|
|
return this.listOfValuesStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment,
|
|
'userIds',
|
|
);
|
|
}
|
|
|
|
private listOfValuesStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment: string | undefined,
|
|
propertyName: string,
|
|
) {
|
|
const userIdText = (values) =>
|
|
values.length === 0
|
|
? `empty set of ${propertyName}`
|
|
: `[${values}]`;
|
|
const usersText =
|
|
preData.parameters[propertyName] === data.parameters[propertyName]
|
|
? ''
|
|
: ` ${propertyName} from ${userIdText(
|
|
preData.parameters[propertyName],
|
|
)} to ${userIdText(data.parameters[propertyName])}`;
|
|
const constraintText = this.constraintChangeText(
|
|
preData.constraints,
|
|
data.constraints,
|
|
);
|
|
const strategySpecificText = [usersText, constraintText]
|
|
.filter((x) => x.length)
|
|
.join(';');
|
|
return `by updating strategy ${data?.name} in *${environment}*${strategySpecificText}`;
|
|
}
|
|
|
|
private flexibleRolloutStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment: string | undefined,
|
|
) {
|
|
const {
|
|
rollout: oldRollout,
|
|
stickiness: oldStickiness,
|
|
groupId: oldGroupId,
|
|
} = preData.parameters;
|
|
const { rollout, stickiness, groupId } = data.parameters;
|
|
const stickinessText =
|
|
oldStickiness === stickiness
|
|
? ''
|
|
: ` stickiness from ${oldStickiness} to ${stickiness}`;
|
|
const rolloutText =
|
|
oldRollout === rollout
|
|
? ''
|
|
: ` rollout from ${oldRollout}% to ${rollout}%`;
|
|
const groupIdText =
|
|
oldGroupId === groupId
|
|
? ''
|
|
: ` groupId from ${oldGroupId} to ${groupId}`;
|
|
const constraintText = this.constraintChangeText(
|
|
preData.constraints,
|
|
data.constraints,
|
|
);
|
|
const strategySpecificText = [
|
|
stickinessText,
|
|
rolloutText,
|
|
groupIdText,
|
|
constraintText,
|
|
]
|
|
.filter((txt) => txt.length)
|
|
.join(';');
|
|
return `by updating strategy ${data?.name} in *${environment}*${strategySpecificText}`;
|
|
}
|
|
|
|
private defaultStrategyChangeText(
|
|
preData,
|
|
data,
|
|
environment: string | undefined,
|
|
) {
|
|
return `by updating strategy ${
|
|
data?.name
|
|
} in *${environment}*${this.constraintChangeText(
|
|
preData.constraints,
|
|
data.constraints,
|
|
)}`;
|
|
}
|
|
|
|
private constraintChangeText(
|
|
oldConstraints: IConstraint[],
|
|
newConstraints: IConstraint[],
|
|
) {
|
|
const formatConstraints = (constraints: IConstraint[]) => {
|
|
const constraintOperatorDescriptions = {
|
|
IN: 'is one of',
|
|
NOT_IN: 'is not one of',
|
|
STR_CONTAINS: 'is a string that contains',
|
|
STR_STARTS_WITH: 'is a string that starts with',
|
|
STR_ENDS_WITH: 'is a string that ends with',
|
|
NUM_EQ: 'is a number equal to',
|
|
NUM_GT: 'is a number greater than',
|
|
NUM_GTE: 'is a number greater than or equal to',
|
|
NUM_LT: 'is a number less than',
|
|
NUM_LTE: 'is a number less than or equal to',
|
|
DATE_BEFORE: 'is a date before',
|
|
DATE_AFTER: 'is a date after',
|
|
SEMVER_EQ: 'is a SemVer equal to',
|
|
SEMVER_GT: 'is a SemVer greater than',
|
|
SEMVER_LT: 'is a SemVer less than',
|
|
};
|
|
const formatConstraint = (constraint: IConstraint) => {
|
|
const val = constraint.hasOwnProperty('value')
|
|
? constraint.value
|
|
: `(${constraint.values.join(',')})`;
|
|
const operator = constraintOperatorDescriptions.hasOwnProperty(
|
|
constraint.operator,
|
|
)
|
|
? constraintOperatorDescriptions[constraint.operator]
|
|
: constraint.operator;
|
|
|
|
return `${constraint.contextName} ${
|
|
constraint.inverted ? 'not ' : ''
|
|
}${operator} ${val}`;
|
|
};
|
|
|
|
return constraints.length === 0
|
|
? 'empty set of constraints'
|
|
: `[${constraints.map(formatConstraint).join(', ')}]`;
|
|
};
|
|
const oldConstraintText = formatConstraints(oldConstraints);
|
|
const newConstraintText = formatConstraints(newConstraints);
|
|
return oldConstraintText === newConstraintText
|
|
? ''
|
|
: ` constraints from ${oldConstraintText} to ${newConstraintText}`;
|
|
}
|
|
|
|
generateStrategyRemoveText(event: IEvent): string {
|
|
const { createdBy, environment, project, preData } = event;
|
|
const feature = this.generateFeatureLink(event);
|
|
return `${createdBy} updated *${feature}* in project *${project}* by removing strategy ${preData?.name} in *${environment}*`;
|
|
}
|
|
|
|
generateStrategyAddText(event: IEvent): string {
|
|
const { createdBy, environment, project, data } = event;
|
|
const feature = this.generateFeatureLink(event);
|
|
return `${createdBy} updated *${feature}* in project *${project}* by adding strategy ${data?.name} in *${environment}*`;
|
|
}
|
|
|
|
generateMetadataText(event: IEvent): string {
|
|
const { createdBy, project } = event;
|
|
const feature = this.generateFeatureLink(event);
|
|
return `${createdBy} updated the metadata for ${feature} in project *${project}*`;
|
|
}
|
|
|
|
generateProjectChangeText(event: IEvent): string {
|
|
const { createdBy, project, featureName } = event;
|
|
return `${createdBy} moved ${featureName} to ${project}`;
|
|
}
|
|
|
|
featureLink(event: IEvent): string {
|
|
const { type, project = '', featureName } = event;
|
|
if (type === FEATURE_ARCHIVED) {
|
|
if (project) {
|
|
return `${this.unleashUrl}/projects/${project}/archive`;
|
|
}
|
|
return `${this.unleashUrl}/archive`;
|
|
}
|
|
|
|
return `${this.unleashUrl}/projects/${project}/features/${featureName}`;
|
|
}
|
|
|
|
getAction(type: string): string {
|
|
switch (type) {
|
|
case FEATURE_CREATED:
|
|
return 'created';
|
|
case FEATURE_UPDATED:
|
|
return 'updated';
|
|
case FEATURE_VARIANTS_UPDATED:
|
|
return 'updated variants for';
|
|
default:
|
|
return type;
|
|
}
|
|
}
|
|
|
|
defaultText(event: IEvent): string {
|
|
const { createdBy, project, type } = event;
|
|
const action = this.getAction(type);
|
|
const feature = this.generateFeatureLink(event);
|
|
return `${createdBy} ${action} feature toggle ${feature} in project *${project}*`;
|
|
}
|
|
|
|
format(event: IEvent): string {
|
|
switch (event.type) {
|
|
case FEATURE_ARCHIVED:
|
|
case FEATURE_REVIVED:
|
|
return this.generateArchivedText(event);
|
|
case FEATURE_STALE_ON:
|
|
case FEATURE_STALE_OFF:
|
|
return this.generateStaleText(event);
|
|
case FEATURE_ENVIRONMENT_DISABLED:
|
|
case FEATURE_ENVIRONMENT_ENABLED:
|
|
return this.generateEnvironmentToggleText(event);
|
|
case FEATURE_STRATEGY_REMOVE:
|
|
return this.generateStrategyRemoveText(event);
|
|
case FEATURE_STRATEGY_ADD:
|
|
return this.generateStrategyAddText(event);
|
|
case FEATURE_STRATEGY_UPDATE:
|
|
return this.generateStrategyChangeText(event);
|
|
case FEATURE_METADATA_UPDATED:
|
|
return this.generateMetadataText(event);
|
|
case FEATURE_PROJECT_CHANGE:
|
|
return this.generateProjectChangeText(event);
|
|
default:
|
|
return this.defaultText(event);
|
|
}
|
|
}
|
|
}
|