mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
263 lines
7.7 KiB
TypeScript
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;
|