mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
313 lines
8.5 KiB
TypeScript
313 lines
8.5 KiB
TypeScript
import {Component, Fragment} from 'react';
|
|
import styled from '@emotion/styled';
|
|
|
|
import SelectControl from 'sentry/components/forms/controls/selectControl';
|
|
import {t} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {Organization, Project} from 'sentry/types';
|
|
import type {
|
|
IssueAlertConfiguration,
|
|
IssueAlertGenericConditionConfig,
|
|
IssueAlertRuleAction,
|
|
IssueAlertRuleActionTemplate,
|
|
IssueAlertRuleCondition,
|
|
IssueAlertRuleConditionTemplate,
|
|
} from 'sentry/types/alerts';
|
|
import {IssueAlertActionType, IssueAlertConditionType} from 'sentry/types/alerts';
|
|
import {
|
|
CHANGE_ALERT_CONDITION_IDS,
|
|
COMPARISON_INTERVAL_CHOICES,
|
|
COMPARISON_TYPE_CHOICE_VALUES,
|
|
COMPARISON_TYPE_CHOICES,
|
|
} from 'sentry/views/alerts/utils/constants';
|
|
|
|
import {AlertRuleComparisonType} from '../metric/types';
|
|
|
|
import RuleNode from './ruleNode';
|
|
|
|
type Props = {
|
|
disabled: boolean;
|
|
error: React.ReactNode;
|
|
/**
|
|
* actions/conditions that have been added to the rule
|
|
*/
|
|
items: IssueAlertRuleAction[] | IssueAlertRuleCondition[];
|
|
/**
|
|
* All available actions or conditions
|
|
*/
|
|
nodes: IssueAlertConfiguration[keyof IssueAlertConfiguration] | null;
|
|
onAddRow: (
|
|
value: IssueAlertRuleActionTemplate | IssueAlertRuleConditionTemplate
|
|
) => void;
|
|
onDeleteRow: (ruleIndex: number) => void;
|
|
onPropertyChange: (ruleIndex: number, prop: string, val: string) => void;
|
|
onResetRow: (ruleIndex: number, name: string, value: string) => void;
|
|
organization: Organization;
|
|
/**
|
|
* Placeholder for select control
|
|
*/
|
|
placeholder: string;
|
|
project: Project;
|
|
incompatibleBanner?: number | null;
|
|
incompatibleRules?: number[] | null;
|
|
selectType?: 'grouped';
|
|
};
|
|
|
|
const createSelectOptions = (
|
|
actions: IssueAlertRuleActionTemplate[]
|
|
): Array<{
|
|
label: React.ReactNode;
|
|
value: IssueAlertRuleActionTemplate;
|
|
}> => {
|
|
return actions.map(node => {
|
|
if (node.id === IssueAlertActionType.NOTIFY_EMAIL) {
|
|
const label = t('Suggested Assignees, Team, or Member');
|
|
return {
|
|
value: node,
|
|
label,
|
|
};
|
|
}
|
|
|
|
if (node.id === IssueAlertConditionType.REAPPEARED_EVENT) {
|
|
const label = t('The issue changes state from archived to escalating');
|
|
return {
|
|
value: node,
|
|
label,
|
|
};
|
|
}
|
|
|
|
return {
|
|
value: node,
|
|
label: node.prompt ?? node.label,
|
|
};
|
|
});
|
|
};
|
|
|
|
const groupLabels = {
|
|
notify: t('Send notification to\u{2026}'),
|
|
notifyIntegration: t('Notify integration\u{2026}'),
|
|
ticket: t('Create new\u{2026}'),
|
|
change: t('Issue state change'),
|
|
frequency: t('Issue frequency'),
|
|
};
|
|
|
|
/**
|
|
* Group options by category
|
|
*/
|
|
const groupSelectOptions = (actions: IssueAlertRuleActionTemplate[]) => {
|
|
const grouped = actions.reduce<
|
|
Record<
|
|
keyof typeof groupLabels,
|
|
IssueAlertRuleActionTemplate[] | IssueAlertRuleConditionTemplate[]
|
|
>
|
|
>(
|
|
(acc, curr) => {
|
|
if (curr.actionType === 'ticket') {
|
|
acc.ticket.push(curr);
|
|
} else if (curr.id.includes('event_frequency')) {
|
|
acc.frequency.push(curr);
|
|
} else if (
|
|
curr.id.includes('sentry.rules.conditions') &&
|
|
!curr.id.includes('event_frequency')
|
|
) {
|
|
acc.change.push(curr);
|
|
} else if (curr.id.includes('sentry.integrations')) {
|
|
acc.notifyIntegration.push(curr);
|
|
} else if (curr.id.includes('notify_event')) {
|
|
acc.notifyIntegration.push(curr);
|
|
} else {
|
|
acc.notify.push(curr);
|
|
}
|
|
return acc;
|
|
},
|
|
{
|
|
notify: [],
|
|
notifyIntegration: [],
|
|
ticket: [],
|
|
change: [],
|
|
frequency: [],
|
|
}
|
|
);
|
|
|
|
return Object.entries(grouped)
|
|
.filter(([_, values]) => values.length)
|
|
.map(([key, values]) => {
|
|
return {
|
|
label: groupLabels[key],
|
|
options: createSelectOptions(values),
|
|
};
|
|
});
|
|
};
|
|
|
|
class RuleNodeList extends Component<Props> {
|
|
componentWillUnmount() {
|
|
window.clearTimeout(this.propertyChangeTimeout);
|
|
}
|
|
|
|
propertyChangeTimeout: number | undefined = undefined;
|
|
|
|
getNode = (
|
|
template: IssueAlertRuleAction | IssueAlertRuleCondition,
|
|
itemIdx: number
|
|
): IssueAlertConfiguration[keyof IssueAlertConfiguration][number] | null => {
|
|
const {nodes, items, organization, onPropertyChange} = this.props;
|
|
const node = nodes?.find(n => {
|
|
if ('sentryAppInstallationUuid' in n) {
|
|
// Match more than just the id for sentryApp actions, they share the same id
|
|
return (
|
|
n.id === template.id &&
|
|
n.sentryAppInstallationUuid === template.sentryAppInstallationUuid
|
|
);
|
|
}
|
|
|
|
return n.id === template.id;
|
|
});
|
|
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
|
|
if (
|
|
!organization.features.includes('change-alerts') ||
|
|
!CHANGE_ALERT_CONDITION_IDS.includes(node.id)
|
|
) {
|
|
return node;
|
|
}
|
|
|
|
const item = items[itemIdx];
|
|
|
|
let changeAlertNode: IssueAlertGenericConditionConfig = {
|
|
...(node as IssueAlertGenericConditionConfig),
|
|
label: node.label.replace('...', ' {comparisonType}'),
|
|
formFields: {
|
|
...(node.formFields as IssueAlertGenericConditionConfig['formFields']),
|
|
comparisonType: {
|
|
type: 'choice',
|
|
choices: COMPARISON_TYPE_CHOICES,
|
|
// give an initial value from not among choices so selector starts with none selected
|
|
initial: 'select',
|
|
},
|
|
},
|
|
};
|
|
|
|
// item.comparison type isn't backfilled and is missing for old alert rules
|
|
// this is a problem when an old alert is being edited, need to initialize it
|
|
if (!item.comparisonType && item.value && item.name) {
|
|
item.comparisonType = item.comparisonInterval === undefined ? 'count' : 'percent';
|
|
}
|
|
|
|
if (item.comparisonType) {
|
|
changeAlertNode = {
|
|
...changeAlertNode,
|
|
label: changeAlertNode.label.replace(
|
|
'{comparisonType}',
|
|
COMPARISON_TYPE_CHOICE_VALUES[item.comparisonType]
|
|
),
|
|
};
|
|
|
|
if (item.comparisonType === AlertRuleComparisonType.PERCENT) {
|
|
if (!item.comparisonInterval) {
|
|
// comparisonInterval value in IssueRuleEditor state
|
|
// is undefined even if initial value is defined
|
|
// can't directly call onPropertyChange, because
|
|
// getNode is called during render
|
|
window.clearTimeout(this.propertyChangeTimeout);
|
|
this.propertyChangeTimeout = window.setTimeout(() =>
|
|
onPropertyChange(itemIdx, 'comparisonInterval', '1w')
|
|
);
|
|
}
|
|
changeAlertNode = {
|
|
...changeAlertNode,
|
|
formFields: {
|
|
...changeAlertNode.formFields,
|
|
comparisonInterval: {
|
|
type: 'choice',
|
|
choices: COMPARISON_INTERVAL_CHOICES,
|
|
initial: '1w',
|
|
},
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
return changeAlertNode;
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
onAddRow,
|
|
onResetRow,
|
|
onDeleteRow,
|
|
onPropertyChange,
|
|
nodes,
|
|
placeholder,
|
|
items,
|
|
organization,
|
|
project,
|
|
disabled,
|
|
error,
|
|
selectType,
|
|
incompatibleRules,
|
|
incompatibleBanner,
|
|
} = this.props;
|
|
|
|
const enabledNodes = nodes ? nodes.filter(({enabled}) => enabled) : [];
|
|
|
|
const options =
|
|
selectType === 'grouped'
|
|
? groupSelectOptions(enabledNodes)
|
|
: createSelectOptions(enabledNodes);
|
|
|
|
return (
|
|
<Fragment>
|
|
<RuleNodes>
|
|
{error}
|
|
{items.map(
|
|
(item: IssueAlertRuleAction | IssueAlertRuleCondition, idx: number) => (
|
|
<RuleNode
|
|
key={idx}
|
|
index={idx}
|
|
node={this.getNode(item, idx)}
|
|
onDelete={onDeleteRow}
|
|
onPropertyChange={onPropertyChange}
|
|
onReset={onResetRow}
|
|
data={item}
|
|
organization={organization}
|
|
project={project}
|
|
disabled={disabled}
|
|
incompatibleRule={incompatibleRules?.includes(idx)}
|
|
incompatibleBanner={incompatibleBanner === idx}
|
|
/>
|
|
)
|
|
)}
|
|
</RuleNodes>
|
|
<StyledSelectControl
|
|
placeholder={placeholder}
|
|
value={null}
|
|
onChange={obj => {
|
|
onAddRow(obj.value);
|
|
}}
|
|
options={options}
|
|
disabled={disabled}
|
|
/>
|
|
</Fragment>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default RuleNodeList;
|
|
|
|
const StyledSelectControl = styled(SelectControl)`
|
|
width: 100%;
|
|
`;
|
|
|
|
const RuleNodes = styled('div')`
|
|
display: grid;
|
|
margin-bottom: ${space(1)};
|
|
gap: ${space(1)};
|
|
|
|
@media (max-width: ${p => p.theme.breakpoints.medium}) {
|
|
grid-auto-flow: row;
|
|
}
|
|
`;
|