123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- import {Component, Fragment, PureComponent} from 'react';
- import styled from '@emotion/styled';
- import {fetchOrgMembers} from 'sentry/actionCreators/members';
- import type {Client} from 'sentry/api';
- import FieldGroup from 'sentry/components/forms/fieldGroup';
- import {IconDiamond} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Organization} from 'sentry/types/organization';
- import type {Project} from 'sentry/types/project';
- import type {Config} from 'sentry/types/system';
- import withApi from 'sentry/utils/withApi';
- import withConfig from 'sentry/utils/withConfig';
- import {getThresholdUnits} from 'sentry/views/alerts/rules/metric/constants';
- import ThresholdControl from 'sentry/views/alerts/rules/metric/triggers/thresholdControl';
- import {isSessionAggregate} from '../../../utils';
- import type {
- AlertRuleThresholdType,
- ThresholdControlValue,
- Trigger,
- UnsavedMetricRule,
- UnsavedTrigger,
- } from '../types';
- import {AlertRuleComparisonType, AlertRuleTriggerType} from '../types';
- type Props = {
- aggregate: UnsavedMetricRule['aggregate'];
- api: Client;
- comparisonType: AlertRuleComparisonType;
- config: Config;
- disabled: boolean;
- fieldHelp: React.ReactNode;
- isCritical: boolean;
- onChange: (trigger: Trigger, changeObj: Partial<Trigger>) => void;
- onThresholdPeriodChange: (value: number) => void;
- onThresholdTypeChange: (thresholdType: AlertRuleThresholdType) => void;
- organization: Organization;
- placeholder: string;
- projects: Project[];
- resolveThreshold: UnsavedMetricRule['resolveThreshold'];
- thresholdPeriod: UnsavedMetricRule['thresholdPeriod'];
- thresholdType: UnsavedMetricRule['thresholdType'];
- trigger: Trigger;
- triggerIndex: number;
- triggerLabel: React.ReactNode;
- /**
- * Map of fieldName -> errorMessage
- */
- error?: {[fieldName: string]: string};
- hideControl?: boolean;
- };
- class TriggerFormItem extends PureComponent<Props> {
- /**
- * Handler for threshold changes coming from slider or chart.
- * Needs to sync state with the form.
- */
- handleChangeThreshold = (value: ThresholdControlValue) => {
- const {onChange, trigger} = this.props;
- onChange(
- {
- ...trigger,
- alertThreshold: value.threshold,
- },
- {alertThreshold: value.threshold}
- );
- };
- render() {
- const {
- disabled,
- error,
- trigger,
- isCritical,
- thresholdType,
- thresholdPeriod,
- hideControl,
- comparisonType,
- fieldHelp,
- triggerLabel,
- placeholder,
- onThresholdTypeChange,
- onThresholdPeriodChange,
- } = this.props;
- return (
- <StyledField
- label={triggerLabel}
- help={fieldHelp}
- required={isCritical}
- error={error?.alertThreshold}
- >
- <ThresholdControl
- disabled={disabled}
- disableThresholdType={!isCritical}
- type={trigger.label}
- thresholdType={thresholdType}
- thresholdPeriod={thresholdPeriod}
- hideControl={hideControl}
- threshold={trigger.alertThreshold}
- comparisonType={comparisonType}
- placeholder={placeholder}
- onChange={this.handleChangeThreshold}
- onThresholdTypeChange={onThresholdTypeChange}
- onThresholdPeriodChange={onThresholdPeriodChange}
- />
- </StyledField>
- );
- }
- }
- type TriggerFormContainerProps = Omit<
- React.ComponentProps<typeof TriggerFormItem>,
- | 'onChange'
- | 'isCritical'
- | 'error'
- | 'triggerIndex'
- | 'trigger'
- | 'fieldHelp'
- | 'triggerHelp'
- | 'triggerLabel'
- | 'placeholder'
- > & {
- onChange: (triggerIndex: number, trigger: Trigger, changeObj: Partial<Trigger>) => void;
- onResolveThresholdChange: (
- resolveThreshold: UnsavedMetricRule['resolveThreshold']
- ) => void;
- triggers: Trigger[];
- errors?: Map<number, {[fieldName: string]: string}>;
- };
- class TriggerFormContainer extends Component<TriggerFormContainerProps> {
- componentDidMount() {
- const {api, organization} = this.props;
- fetchOrgMembers(api, organization.slug);
- }
- handleChangeTrigger =
- (triggerIndex: number) => (trigger: Trigger, changeObj: Partial<Trigger>) => {
- const {onChange} = this.props;
- onChange(triggerIndex, trigger, changeObj);
- };
- handleChangeResolveTrigger = (trigger: Trigger, _: Partial<Trigger>) => {
- const {onResolveThresholdChange} = this.props;
- onResolveThresholdChange(trigger.alertThreshold);
- };
- getCriticalThresholdPlaceholder(
- aggregate: string,
- comparisonType: AlertRuleComparisonType
- ) {
- if (aggregate.includes('failure_rate')) {
- return '0.05';
- }
- if (isSessionAggregate(aggregate)) {
- return '97';
- }
- if (comparisonType === AlertRuleComparisonType.CHANGE) {
- return '100';
- }
- return '300';
- }
- getIndicator(type: AlertRuleTriggerType) {
- if (type === AlertRuleTriggerType.CRITICAL) {
- return <StyledIconDiamond color="errorText" size="sm" />;
- }
- if (type === AlertRuleTriggerType.WARNING) {
- return <StyledIconDiamond color="warningText" size="sm" />;
- }
- return <StyledIconDiamond color="successText" size="sm" />;
- }
- render() {
- const {
- api,
- config,
- disabled,
- errors,
- organization,
- triggers,
- thresholdType,
- thresholdPeriod,
- comparisonType,
- aggregate,
- resolveThreshold,
- projects,
- onThresholdTypeChange,
- onThresholdPeriodChange,
- } = this.props;
- const resolveTrigger: UnsavedTrigger = {
- label: AlertRuleTriggerType.RESOLVE,
- alertThreshold: resolveThreshold,
- actions: [],
- };
- const thresholdUnits = getThresholdUnits(aggregate, comparisonType);
- return (
- <Fragment>
- {triggers.map((trigger, index) => {
- const isCritical = index === 0;
- // eslint-disable-next-line no-use-before-define
- return (
- <TriggerFormItem
- key={index}
- api={api}
- config={config}
- disabled={disabled}
- error={errors?.get(index)}
- trigger={trigger}
- thresholdPeriod={thresholdPeriod}
- thresholdType={thresholdType}
- comparisonType={comparisonType}
- aggregate={aggregate}
- resolveThreshold={resolveThreshold}
- organization={organization}
- projects={projects}
- triggerIndex={index}
- isCritical={isCritical}
- fieldHelp={null}
- triggerLabel={
- <TriggerLabel>
- {this.getIndicator(
- isCritical
- ? AlertRuleTriggerType.CRITICAL
- : AlertRuleTriggerType.WARNING
- )}
- {isCritical ? t('Critical') : t('Warning')}
- </TriggerLabel>
- }
- placeholder={
- isCritical
- ? `${this.getCriticalThresholdPlaceholder(aggregate, comparisonType)}${
- comparisonType === AlertRuleComparisonType.COUNT
- ? thresholdUnits
- : ''
- }`
- : t('None')
- }
- onChange={this.handleChangeTrigger(index)}
- onThresholdTypeChange={onThresholdTypeChange}
- onThresholdPeriodChange={onThresholdPeriodChange}
- />
- );
- })}
- <TriggerFormItem
- api={api}
- config={config}
- disabled={disabled}
- error={errors?.get(2)}
- trigger={resolveTrigger}
- // Flip rule thresholdType to opposite
- thresholdPeriod={thresholdPeriod}
- thresholdType={+!thresholdType}
- comparisonType={comparisonType}
- aggregate={aggregate}
- resolveThreshold={resolveThreshold}
- organization={organization}
- projects={projects}
- triggerIndex={2}
- isCritical={false}
- fieldHelp={null}
- triggerLabel={
- <TriggerLabel>
- {this.getIndicator(AlertRuleTriggerType.RESOLVE)}
- {t('Resolved')}
- </TriggerLabel>
- }
- placeholder={t('Automatic')}
- onChange={this.handleChangeResolveTrigger}
- onThresholdTypeChange={onThresholdTypeChange}
- onThresholdPeriodChange={onThresholdPeriodChange}
- />
- </Fragment>
- );
- }
- }
- const TriggerLabel = styled('div')`
- display: flex;
- flex-direction: row;
- align-items: center;
- `;
- const StyledIconDiamond = styled(IconDiamond)`
- margin-right: ${space(0.75)};
- `;
- const StyledField = styled(FieldGroup)`
- & > label > div:first-child > span {
- display: flex;
- flex-direction: row;
- }
- `;
- export default withConfig(withApi(TriggerFormContainer));
|