diff --git a/static/app/icons/iconRectangle.tsx b/static/app/icons/iconRectangle.tsx new file mode 100644 index 00000000000000..a4a6f43fe87ae6 --- /dev/null +++ b/static/app/icons/iconRectangle.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; + +import SvgIcon from './svgIcon'; + +type Props = React.ComponentProps; + +const IconRectangle = React.forwardRef(function IconRectangle( + props: Props, + ref: React.Ref +) { + return ( + + + + ); +}); + +IconRectangle.displayName = 'IconRectangle'; + +export {IconRectangle}; diff --git a/static/app/icons/index.tsx b/static/app/icons/index.tsx index d1ace8361dc1a5..e7cbc816f7dab2 100644 --- a/static/app/icons/index.tsx +++ b/static/app/icons/index.tsx @@ -69,6 +69,7 @@ export {IconPrint} from './iconPrint'; export {IconProject} from './iconProject'; export {IconProjects} from './iconProjects'; export {IconQuestion} from './iconQuestion'; +export {IconRectangle} from './iconRectangle'; export {IconRefresh} from './iconRefresh'; export {IconReleases} from './iconReleases'; export {IconReturn} from './iconReturn'; diff --git a/static/app/views/alerts/incidentRules/ruleForm/index.tsx b/static/app/views/alerts/incidentRules/ruleForm/index.tsx index 85e12ad6db8558..4c28cc4851a779 100644 --- a/static/app/views/alerts/incidentRules/ruleForm/index.tsx +++ b/static/app/views/alerts/incidentRules/ruleForm/index.tsx @@ -578,10 +578,6 @@ class RuleFormContainer extends AsyncComponent { this.setState({comparisonType: value, comparisonDelta, timeWindow}); }; - handleComparisonDeltaChange = (value: number) => { - this.setState({comparisonDelta: value}); - }; - handleDeleteRule = async () => { const {params} = this.props; const {orgId, projectId, ruleId} = params; diff --git a/static/app/views/alerts/rules/details/body.tsx b/static/app/views/alerts/rules/details/body.tsx index 0ee1b86a269531..dfee4eb4d02c79 100644 --- a/static/app/views/alerts/rules/details/body.tsx +++ b/static/app/views/alerts/rules/details/body.tsx @@ -21,18 +21,19 @@ import {parseSearch} from 'app/components/searchSyntax/parser'; import HighlightQuery from 'app/components/searchSyntax/renderer'; import TimeSince from 'app/components/timeSince'; import Tooltip from 'app/components/tooltip'; -import {IconCheckmark, IconFire, IconInfo, IconWarning} from 'app/icons'; +import {IconInfo, IconRectangle} from 'app/icons'; import {t, tct} from 'app/locale'; import overflowEllipsis from 'app/styles/overflowEllipsis'; import space from 'app/styles/space'; import {Actor, DateString, Organization, Project} from 'app/types'; import getDynamicText from 'app/utils/getDynamicText'; import Projects from 'app/utils/projects'; +import {COMPARISON_DELTA_OPTIONS} from 'app/views/alerts/incidentRules/constants'; import { + Action, AlertRuleThresholdType, Dataset, IncidentRule, - Trigger, } from 'app/views/alerts/incidentRules/types'; import {extractEventTypeFilterFromRule} from 'app/views/alerts/incidentRules/utils/getEventTypeFilter'; import Timeline from 'app/views/alerts/rules/details/timeline'; @@ -127,7 +128,7 @@ export default class DetailsBody extends React.Component { ); } - renderTrigger(trigger: Trigger): React.ReactNode { + renderTrigger(label: string, threshold: number, actions: Action[]): React.ReactNode { const {rule} = this.props; if (!rule) { @@ -135,28 +136,63 @@ export default class DetailsBody extends React.Component { } const status = - trigger.label === 'critical' ? ( - - Critical - - ) : trigger.label === 'warning' ? ( - - Warning - + label === 'critical' + ? t('Critical') + : label === 'warning' + ? t('Warning') + : t('Resolved'); + const statusIcon = + label === 'critical' ? ( + + ) : label === 'warning' ? ( + ) : ( - - Resolved - + ); - const thresholdTypeText = - rule.thresholdType === AlertRuleThresholdType.ABOVE ? t('above') : t('below'); + const thresholdTypeText = ( + label === 'resolved' + ? rule.thresholdType === AlertRuleThresholdType.BELOW + : rule.thresholdType === AlertRuleThresholdType.ABOVE + ) + ? rule.comparisonDelta + ? t('higher') + : t('above') + : rule.comparisonDelta + ? t('lower') + : t('below'); + + const thresholdText = rule.comparisonDelta + ? tct( + 'When [threshold]% [comparisonType] in [timeWindow] compared to [comparisonDelta]', + { + threshold, + comparisonType: thresholdTypeText, + timeWindow: this.getTimeWindow(), + comparisonDelta: ( + COMPARISON_DELTA_OPTIONS.find( + ({value}) => value === rule.comparisonDelta + ) ?? COMPARISON_DELTA_OPTIONS[0] + ).label, + } + ) + : tct('If [condition] in [timeWindow]', { + condition: `${thresholdTypeText} ${threshold}`, + timeWindow: this.getTimeWindow(), + }); return ( - - {status} - {`${thresholdTypeText} ${trigger.alertThreshold}`} - + + {statusIcon} + + {status} + {thresholdText} + {actions.map( + action => + action.desc && {action.desc} + )} + + ); } @@ -191,9 +227,21 @@ export default class DetailsBody extends React.Component { - {t('Conditions')} - {criticalTrigger && this.renderTrigger(criticalTrigger)} - {warningTrigger && this.renderTrigger(warningTrigger)} + {t('Thresholds and Actions')} + {typeof criticalTrigger?.alertThreshold === 'number' && + this.renderTrigger( + criticalTrigger.label, + criticalTrigger.alertThreshold, + criticalTrigger.actions + )} + {typeof warningTrigger?.alertThreshold === 'number' && + this.renderTrigger( + warningTrigger.label, + warningTrigger.alertThreshold, + warningTrigger.actions + )} + {typeof rule.resolveThreshold === 'number' && + this.renderTrigger('resolved', rule.resolveThreshold, [])} @@ -443,14 +491,6 @@ const DetailWrapper = styled('div')` } `; -const StatusWrapper = styled('div')` - display: flex; - align-items: center; - svg { - margin-right: ${space(0.5)}; - } -`; - const HeaderContainer = styled('div')` height: 60px; display: flex; @@ -548,16 +588,25 @@ const Filters = styled('span')` font-family: ${p => p.theme.text.familyMono}; `; +const TriggerConditionContainer = styled('div')` + display: flex; + flex-direction: row; +`; + const TriggerCondition = styled('div')` display: flex; - align-items: center; + flex-direction: column; + margin-left: ${space(0.75)}; `; const TriggerText = styled('div')` - margin-left: ${space(0.5)}; - white-space: nowrap; + color: ${p => p.theme.subText}; `; const CreatedBy = styled('div')` ${overflowEllipsis} `; + +const StyledIconRectangle = styled(IconRectangle)` + margin-top: ${space(0.75)}; +`; diff --git a/static/app/views/alerts/rules/details/metricChart.tsx b/static/app/views/alerts/rules/details/metricChart.tsx index 32ac18ea5f6fb9..46efa9c2311f6a 100644 --- a/static/app/views/alerts/rules/details/metricChart.tsx +++ b/static/app/views/alerts/rules/details/metricChart.tsx @@ -498,14 +498,14 @@ class MetricChart extends React.PureComponent { } let maxThresholdValue = 0; - if (warningTrigger?.alertThreshold) { + if (!rule.comparisonDelta && warningTrigger?.alertThreshold) { const {alertThreshold} = warningTrigger; const warningThresholdLine = createThresholdSeries(theme.yellow300, alertThreshold); series.push(warningThresholdLine); maxThresholdValue = Math.max(maxThresholdValue, alertThreshold); } - if (criticalTrigger?.alertThreshold) { + if (!rule.comparisonDelta && criticalTrigger?.alertThreshold) { const {alertThreshold} = criticalTrigger; const criticalThresholdLine = createThresholdSeries(theme.red300, alertThreshold); series.push(criticalThresholdLine);