import {Component, Fragment} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import cloneDeep from 'lodash/cloneDeep'; import set from 'lodash/set'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import type {Client} from 'sentry/api'; import {Button} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import SelectControl from 'sentry/components/forms/controls/selectControl'; import FieldGroup from 'sentry/components/forms/fieldGroup'; import Input from 'sentry/components/input'; import Link from 'sentry/components/links/link'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {defined} from 'sentry/utils'; import type EventView from 'sentry/utils/discover/eventView'; import withApi from 'sentry/utils/withApi'; import withProjects from 'sentry/utils/withProjects'; import {transactionSummaryRouteWithQuery} from './utils'; export enum TransactionThresholdMetric { TRANSACTION_DURATION = 'duration', LARGEST_CONTENTFUL_PAINT = 'lcp', } export const METRIC_CHOICES = [ {label: t('Transaction Duration'), value: 'duration'}, {label: t('Largest Contentful Paint'), value: 'lcp'}, ]; type Props = { api: Client; eventView: EventView; organization: Organization; projects: Project[]; transactionName: string; transactionThreshold: number | undefined; transactionThresholdMetric: TransactionThresholdMetric | undefined; onApply?: (threshold, metric) => void; project?: string; } & ModalRenderProps; type State = { error: string | null; metric: TransactionThresholdMetric | undefined; threshold: number | undefined; }; class TransactionThresholdModal extends Component { state: State = { threshold: this.props.transactionThreshold, metric: this.props.transactionThresholdMetric, error: null, }; getProject() { const {projects, eventView, project} = this.props; if (defined(project)) { return projects.find(proj => proj.id === project); } const projectId = String(eventView.project[0]); return projects.find(proj => proj.id === projectId); } handleApply = (event: React.FormEvent) => { event.preventDefault(); const {api, closeModal, organization, transactionName, onApply} = this.props; const project = this.getProject(); if (!defined(project)) { return; } const transactionThresholdUrl = `/organizations/${organization.slug}/project-transaction-threshold-override/`; api .requestPromise(transactionThresholdUrl, { method: 'POST', includeAllArgs: true, query: { project: project.id, }, data: { transaction: transactionName, threshold: this.state.threshold, metric: this.state.metric, }, }) .then(() => { closeModal(); if (onApply) { onApply(this.state.threshold, this.state.metric); } }) .catch(err => { this.setState({ error: err, }); const errorMessage = err.responseJSON?.threshold ?? err.responseJSON?.non_field_errors ?? null; addErrorMessage(errorMessage); }); }; handleFieldChange = (field: string) => (value: string) => { this.setState(prevState => { const newState = cloneDeep(prevState); set(newState, field, value); return {...newState, errors: undefined}; }); }; handleReset = (event: React.FormEvent) => { event.preventDefault(); const {api, closeModal, organization, transactionName, onApply} = this.props; const project = this.getProject(); if (!defined(project)) { return; } const transactionThresholdUrl = `/organizations/${organization.slug}/project-transaction-threshold-override/`; api .requestPromise(transactionThresholdUrl, { method: 'DELETE', includeAllArgs: true, query: { project: project.id, }, data: { transaction: transactionName, }, }) .then(() => { const projectThresholdUrl = `/projects/${organization.slug}/${project.slug}/transaction-threshold/configure/`; this.props.api .requestPromise(projectThresholdUrl, { method: 'GET', includeAllArgs: true, query: { project: project.id, }, }) .then(([data]) => { this.setState({ threshold: data.threshold, metric: data.metric, }); closeModal(); if (onApply) { onApply(this.state.threshold, this.state.metric); } }) .catch(err => { const errorMessage = err.responseJSON?.threshold ?? null; addErrorMessage(errorMessage); }); }) .catch(err => { this.setState({ error: err, }); }); }; renderModalFields() { return ( { this.handleFieldChange('metric')(option.value); }} /> ) => { this.handleFieldChange('threshold')(event.target.value); }} value={this.state.threshold} step={100} min={100} /> ); } render() { const {Header, Body, Footer, organization, transactionName, eventView} = this.props; const project = this.getProject(); const summaryView = eventView.clone(); summaryView.query = summaryView.getQueryWithAdditionalConditions(); const target = transactionSummaryRouteWithQuery({ orgSlug: organization.slug, transaction: transactionName, query: summaryView.generateQueryStringObject(), projectID: project?.id, }); return (

{t('Transaction Settings')}

{tct( 'The changes below will only be applied to [transaction]. To set it at a more global level, go to [projectSettings: Project Settings].', { transaction: {transactionName}, projectSettings: ( ), } )} {this.renderModalFields()}
); } } const Instruction = styled('div')` margin-bottom: ${space(4)}; `; export const modalCss = css` width: 100%; max-width: 650px; `; export default withApi(withProjects(TransactionThresholdModal));