import {css} from '@emotion/react'; import styled from '@emotion/styled'; import SelectControl from 'sentry/components/forms/controls/selectControl'; import type {FormFieldProps} from 'sentry/components/forms/formField'; import FormField from 'sentry/components/forms/formField'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization, Project} from 'sentry/types'; import type {QueryFieldValue} from 'sentry/utils/discover/fields'; import {explodeFieldString, generateFieldAsString} from 'sentry/utils/discover/fields'; import {hasCustomMetrics} from 'sentry/utils/metrics/features'; import MriField from 'sentry/views/alerts/rules/metric/mriField'; import type {Dataset} from 'sentry/views/alerts/rules/metric/types'; import type {AlertType} from 'sentry/views/alerts/wizard/options'; import { AlertWizardAlertNames, AlertWizardRuleTemplates, } from 'sentry/views/alerts/wizard/options'; import {QueryField} from 'sentry/views/discover/table/queryField'; import {FieldValueKind} from 'sentry/views/discover/table/types'; import {generateFieldOptions} from 'sentry/views/discover/utils'; import {getFieldOptionConfig} from './metricField'; type MenuOption = {label: string; value: AlertType}; type GroupedMenuOption = {label: string; options: Array}; type Props = Omit & { organization: Organization; project: Project; alertType?: AlertType; /** * Optionally set a width for each column of selector */ columnWidth?: number; inFieldLabels?: boolean; }; export default function WizardField({ organization, columnWidth, inFieldLabels, alertType, project, ...fieldProps }: Props) { const menuOptions: GroupedMenuOption[] = [ { label: t('ERRORS'), options: [ { label: AlertWizardAlertNames.num_errors, value: 'num_errors', }, { label: AlertWizardAlertNames.users_experiencing_errors, value: 'users_experiencing_errors', }, ], }, ...((organization.features.includes('crash-rate-alerts') ? [ { label: t('SESSIONS'), options: [ { label: AlertWizardAlertNames.crash_free_sessions, value: 'crash_free_sessions', }, { label: AlertWizardAlertNames.crash_free_users, value: 'crash_free_users', }, ], }, ] : []) as GroupedMenuOption[]), { label: t('PERFORMANCE'), options: [ { label: AlertWizardAlertNames.throughput, value: 'throughput', }, { label: AlertWizardAlertNames.trans_duration, value: 'trans_duration', }, { label: AlertWizardAlertNames.apdex, value: 'apdex', }, { label: AlertWizardAlertNames.failure_rate, value: 'failure_rate', }, { label: AlertWizardAlertNames.lcp, value: 'lcp', }, { label: AlertWizardAlertNames.fid, value: 'fid', }, { label: AlertWizardAlertNames.cls, value: 'cls', }, ...(hasCustomMetrics(organization) ? [ { label: AlertWizardAlertNames.custom_transactions, value: 'custom_transactions' as const, }, ] : []), ], }, { label: hasCustomMetrics(organization) ? t('METRICS') : t('CUSTOM'), options: [ hasCustomMetrics(organization) ? { label: AlertWizardAlertNames.custom_metrics, value: 'custom_metrics', } : { label: AlertWizardAlertNames.custom_transactions, value: 'custom_transactions', }, ], }, ]; return ( {({onChange, model, disabled}) => { const aggregate = model.getValue('aggregate'); const dataset: Dataset = model.getValue('dataset'); const selectedTemplate: AlertType = alertType || 'custom_metrics'; const {fieldOptionsConfig, hidePrimarySelector, hideParameterSelector} = getFieldOptionConfig({ dataset: dataset as Dataset, alertType, }); const fieldOptions = generateFieldOptions({organization, ...fieldOptionsConfig}); const fieldValue = getFieldValue(aggregate ?? '', model); const fieldKey = fieldValue?.kind === FieldValueKind.FUNCTION ? `function:${fieldValue.function[0]}` : ''; const selectedField = fieldOptions[fieldKey]?.value; const numParameters: number = selectedField?.kind === FieldValueKind.FUNCTION ? selectedField.meta.parameters.length : 0; const gridColumns = 1 + numParameters - (hideParameterSelector ? 1 : 0) - (hidePrimarySelector ? 1 : 0); return ( { const template = AlertWizardRuleTemplates[option.value]; model.setValue('aggregate', template.aggregate); model.setValue('dataset', template.dataset); model.setValue('eventTypes', [template.eventTypes]); // Keep alertType last model.setValue('alertType', option.value); }} /> {hasCustomMetrics(organization) && alertType === 'custom_metrics' ? ( onChange(newAggregate, {})} /> ) : ( option.value.kind === FieldValueKind.FUNCTION } fieldOptions={fieldOptions} fieldValue={fieldValue} onChange={v => onChange(generateFieldAsString(v), {})} columnWidth={columnWidth} gridColumns={gridColumns} inFieldLabels={inFieldLabels} shouldRenderTag={false} disabled={disabled} hideParameterSelector={hideParameterSelector} hidePrimarySelector={hidePrimarySelector} /> )} ); }} ); } // swaps out custom percentile values for known percentiles, used while we fade out custom percentiles in metric alerts // TODO(telemetry-experience): remove once we migrate all custom percentile alerts const getFieldValue = (aggregate: string | undefined, model) => { const fieldValue = explodeFieldString(aggregate ?? ''); if (fieldValue?.kind !== FieldValueKind.FUNCTION) { return fieldValue; } if (fieldValue.function[0] !== 'percentile') { return fieldValue; } const newFieldValue: QueryFieldValue = { kind: FieldValueKind.FUNCTION, function: [ getApproximateKnownPercentile(fieldValue.function[2] as string), fieldValue.function[1], undefined, undefined, ], alias: fieldValue.alias, }; model.setValue('aggregate', generateFieldAsString(newFieldValue)); return newFieldValue; }; const getApproximateKnownPercentile = (customPercentile: string) => { const percentile = parseFloat(customPercentile); if (percentile <= 0.5) { return 'p50'; } if (percentile <= 0.75) { return 'p75'; } if (percentile <= 0.9) { return 'p90'; } if (percentile <= 0.95) { return 'p95'; } if (percentile <= 0.99) { return 'p99'; } return 'p100'; }; const Container = styled('div')<{hideGap: boolean}>` display: grid; grid-template-columns: 1fr auto; gap: ${p => (p.hideGap ? space(0) : space(1))}; `; const StyledQueryField = styled(QueryField)<{gridColumns: number; columnWidth?: number}>` ${p => p.columnWidth && css` width: ${p.gridColumns * p.columnWidth}px; `} `;