Files
2024-02-15 17:01:50 +00:00

263 lines
7.7 KiB
TypeScript

import {Component} from 'react';
import styled from '@emotion/styled';
import Feature from 'sentry/components/acl/feature';
import SelectControl from 'sentry/components/forms/controls/selectControl';
import Input from 'sentry/components/input';
import NumberDragControl from 'sentry/components/numberDragControl';
import {Tooltip} from 'sentry/components/tooltip';
import {t, tct, tn} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {ThresholdControlValue} from 'sentry/views/alerts/rules/metric/types';
import {
AlertRuleComparisonType,
AlertRuleThresholdType,
} from 'sentry/views/alerts/rules/metric/types';
type Props = ThresholdControlValue & {
comparisonType: AlertRuleComparisonType;
disableThresholdType: boolean;
disabled: boolean;
onChange: (value: ThresholdControlValue, e: React.FormEvent) => void;
onThresholdPeriodChange: (value: number) => void;
onThresholdTypeChange: (thresholdType: AlertRuleThresholdType) => void;
placeholder: string;
thresholdPeriod: number | null;
type: string;
hideControl?: boolean;
};
type State = {
currentValue: string | null;
};
class ThresholdControl extends Component<Props, State> {
state: State = {
currentValue: null,
};
handleThresholdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const {value} = e.target;
// Only allow number and partial number inputs
if (!/^[0-9]*\.?[0-9]*$/.test(value)) {
return;
}
const {onChange, thresholdType} = this.props;
// Empty input
if (value === '') {
this.setState({currentValue: null});
onChange({thresholdType, threshold: ''}, e);
return;
}
// Only call onChange if the new number is valid, and not partially typed
// (eg writing out the decimal '5.')
if (/\.+0*$/.test(value)) {
this.setState({currentValue: value});
return;
}
const numberValue = Number(value);
this.setState({currentValue: null});
onChange({thresholdType, threshold: numberValue}, e);
};
/**
* Coerce the currentValue to a number and trigger the onChange.
*/
handleThresholdBlur = (e: React.FocusEvent<HTMLInputElement>) => {
if (this.state.currentValue === null) {
return;
}
const {onChange, thresholdType} = this.props;
onChange({thresholdType, threshold: Number(this.state.currentValue)}, e);
this.setState({currentValue: null});
};
handleTypeChange = ({value}) => {
const {onThresholdTypeChange} = this.props;
onThresholdTypeChange(value);
};
handleDragChange = (delta: number, e: React.MouseEvent) => {
const {onChange, thresholdType, threshold} = this.props;
const currentValue = threshold || 0;
onChange({thresholdType, threshold: currentValue + delta}, e);
};
handleThresholdPeriodChange = ({value}) => {
this.props.onThresholdPeriodChange(value);
};
render() {
const {currentValue} = this.state;
const {
thresholdPeriod,
thresholdType,
comparisonType,
hideControl,
threshold,
placeholder,
type,
onChange: _,
onThresholdTypeChange: __,
disabled,
disableThresholdType,
} = this.props;
return (
<Wrapper>
<Container comparisonType={comparisonType}>
<SelectContainer>
<SelectControl
isDisabled={disabled || disableThresholdType}
name={`${type}ThresholdType`}
value={thresholdType}
options={[
{
value: AlertRuleThresholdType.BELOW,
label:
comparisonType === AlertRuleComparisonType.COUNT
? hideControl
? t('When below Critical or Warning')
: t('Below')
: hideControl
? t('When lower than Critical or Warning')
: t('Lower than'),
},
{
value: AlertRuleThresholdType.ABOVE,
label:
comparisonType === AlertRuleComparisonType.COUNT
? hideControl
? t('When above Critical or Warning')
: t('Above')
: hideControl
? t('When higher than Critical or Warning')
: t('Higher than'),
},
]}
components={disableThresholdType ? {DropdownIndicator: null} : undefined}
styles={
disableThresholdType
? {
control: provided => ({
...provided,
cursor: 'not-allowed',
pointerEvents: 'auto',
}),
}
: undefined
}
onChange={this.handleTypeChange}
/>
</SelectContainer>
{!hideControl && (
<ThresholdContainer comparisonType={comparisonType}>
<ThresholdInput>
<Input
size="md"
disabled={disabled}
name={`${type}Threshold`}
data-test-id={`${type}-threshold`}
placeholder={placeholder}
value={currentValue ?? threshold ?? ''}
onChange={this.handleThresholdChange}
onBlur={this.handleThresholdBlur}
// Disable lastpass autocomplete
data-lpignore="true"
/>
<DragContainer>
<Tooltip
title={tct(
'Drag to adjust threshold[break]You can hold shift to fine tune',
{
break: <br />,
}
)}
>
<NumberDragControl
step={5}
axis="y"
onChange={this.handleDragChange}
/>
</Tooltip>
</DragContainer>
</ThresholdInput>
{comparisonType === AlertRuleComparisonType.CHANGE && (
<PercentWrapper>%</PercentWrapper>
)}
</ThresholdContainer>
)}
</Container>
{!hideControl && (
<Feature features="metric-alert-threshold-period">
<SelectContainer>
<SelectControl
isDisabled={disabled}
name="thresholdPeriod"
value={thresholdPeriod}
options={[1, 2, 5, 10, 20].map(value => ({
value,
label: tn('For %s minute', 'For %s minutes', value),
}))}
onChange={this.handleThresholdPeriodChange}
/>
</SelectContainer>
</Feature>
)}
</Wrapper>
);
}
}
const Wrapper = styled('div')`
display: flex;
align-items: center;
gap: ${space(1)};
`;
const Container = styled('div')<{comparisonType: AlertRuleComparisonType}>`
flex: 2;
display: flex;
align-items: center;
flex-direction: ${p =>
p.comparisonType === AlertRuleComparisonType.COUNT ? 'row' : 'row-reverse'};
gap: ${space(1)};
`;
const SelectContainer = styled('div')`
flex: 1;
`;
const ThresholdContainer = styled('div')<{comparisonType: AlertRuleComparisonType}>`
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
`;
const ThresholdInput = styled('div')`
position: relative;
display: flex;
flex-direction: row;
align-items: center;
`;
const PercentWrapper = styled('div')`
margin-left: ${space(1)};
`;
const DragContainer = styled('div')`
position: absolute;
top: 4px;
right: 12px;
`;
export default ThresholdControl;