metricField.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import React from 'react';
  2. import {css} from '@emotion/core';
  3. import styled from '@emotion/styled';
  4. import Button from 'app/components/button';
  5. import Tooltip from 'app/components/tooltip';
  6. import {t, tct} from 'app/locale';
  7. import space from 'app/styles/space';
  8. import {Organization} from 'app/types';
  9. import {
  10. Aggregation,
  11. AGGREGATIONS,
  12. ColumnType,
  13. explodeFieldString,
  14. FIELDS,
  15. generateFieldAsString,
  16. } from 'app/utils/discover/fields';
  17. import {QueryField} from 'app/views/eventsV2/table/queryField';
  18. import {FieldValueKind} from 'app/views/eventsV2/table/types';
  19. import {generateFieldOptions} from 'app/views/eventsV2/utils';
  20. import FormField from 'app/views/settings/components/forms/formField';
  21. import FormModel from 'app/views/settings/components/forms/model';
  22. import {errorFieldConfig, transactionFieldConfig} from './constants';
  23. import {PRESET_AGGREGATES} from './presets';
  24. import {Dataset} from './types';
  25. type Props = Omit<FormField['props'], 'children'> & {
  26. organization: Organization;
  27. /**
  28. * Optionally set a width for each column of selector
  29. */
  30. columnWidth?: number;
  31. inFieldLabels?: boolean;
  32. };
  33. const getFieldOptionConfig = (dataset: Dataset) => {
  34. const config = dataset === Dataset.ERRORS ? errorFieldConfig : transactionFieldConfig;
  35. const aggregations = Object.fromEntries<Aggregation>(
  36. config.aggregations.map(key => {
  37. // TODO(scttcper): Temporary hack for default value while we handle the translation of user
  38. if (key === 'count_unique') {
  39. const agg = AGGREGATIONS[key] as Aggregation;
  40. agg.generateDefaultValue = () => 'tags[sentry:user]';
  41. return [key, agg];
  42. }
  43. return [key, AGGREGATIONS[key]];
  44. })
  45. );
  46. const fields = Object.fromEntries<ColumnType>(
  47. config.fields.map(key => {
  48. // XXX(epurkhiser): Temporary hack while we handle the translation of user ->
  49. // tags[sentry:user].
  50. if (key === 'user') {
  51. return ['tags[sentry:user]', 'string'];
  52. }
  53. return [key, FIELDS[key]];
  54. })
  55. );
  56. const {measurementKeys} = config;
  57. return {aggregations, fields, measurementKeys};
  58. };
  59. const help = ({name, model}: {name: string; model: FormModel}) => {
  60. const aggregate = model.getValue(name) as string;
  61. const presets = PRESET_AGGREGATES.filter(preset =>
  62. preset.validDataset.includes(model.getValue('dataset') as Dataset)
  63. )
  64. .map(preset => ({...preset, selected: preset.match.test(aggregate)}))
  65. .map((preset, i, list) => (
  66. <React.Fragment key={preset.name}>
  67. <Tooltip title={t('This preset is selected')} disabled={!preset.selected}>
  68. <PresetButton
  69. type="button"
  70. onClick={() => model.setValue(name, preset.default)}
  71. disabled={preset.selected}
  72. >
  73. {preset.name}
  74. </PresetButton>
  75. </Tooltip>
  76. {i + 1 < list.length && ', '}
  77. </React.Fragment>
  78. ));
  79. return tct(
  80. 'Choose an aggregate function. Not sure what to select, try a preset: [presets]',
  81. {presets}
  82. );
  83. };
  84. const MetricField = ({organization, columnWidth, inFieldLabels, ...props}: Props) => (
  85. <FormField help={help} {...props}>
  86. {({onChange, value, model, disabled}) => {
  87. const dataset = model.getValue('dataset');
  88. const fieldOptionsConfig = getFieldOptionConfig(dataset);
  89. const fieldOptions = generateFieldOptions({organization, ...fieldOptionsConfig});
  90. const fieldValue = explodeFieldString(value ?? '');
  91. const fieldKey =
  92. fieldValue?.kind === FieldValueKind.FUNCTION
  93. ? `function:${fieldValue.function[0]}`
  94. : '';
  95. const selectedField = fieldOptions[fieldKey]?.value;
  96. const numParameters: number =
  97. selectedField?.kind === FieldValueKind.FUNCTION
  98. ? selectedField.meta.parameters.length
  99. : 0;
  100. return (
  101. <React.Fragment>
  102. {!inFieldLabels && (
  103. <AggregateHeader>
  104. <div>{t('Function')}</div>
  105. {numParameters > 0 && <div>{t('Parameter')}</div>}
  106. {numParameters > 1 && <div>{t('Value')}</div>}
  107. </AggregateHeader>
  108. )}
  109. <StyledQueryField
  110. filterPrimaryOptions={option => option.value.kind === FieldValueKind.FUNCTION}
  111. fieldOptions={fieldOptions}
  112. fieldValue={fieldValue}
  113. onChange={v => onChange(generateFieldAsString(v), {})}
  114. columnWidth={columnWidth}
  115. gridColumns={numParameters}
  116. inFieldLabels={inFieldLabels}
  117. shouldRenderTag={false}
  118. disabled={disabled}
  119. />
  120. </React.Fragment>
  121. );
  122. }}
  123. </FormField>
  124. );
  125. const StyledQueryField = styled(QueryField)<{gridColumns: number; columnWidth?: number}>`
  126. ${p =>
  127. p.columnWidth &&
  128. css`
  129. width: ${(p.gridColumns + 1) * p.columnWidth}px;
  130. `}
  131. `;
  132. const AggregateHeader = styled('div')`
  133. display: grid;
  134. grid-auto-flow: column;
  135. grid-auto-columns: 1fr;
  136. grid-gap: ${space(1)};
  137. text-transform: uppercase;
  138. font-size: ${p => p.theme.fontSizeSmall};
  139. color: ${p => p.theme.gray300};
  140. font-weight: bold;
  141. margin-bottom: ${space(1)};
  142. `;
  143. const PresetButton = styled(Button)<{disabled: boolean}>`
  144. ${p =>
  145. p.disabled &&
  146. css`
  147. color: ${p.theme.textColor};
  148. &:hover,
  149. &:focus {
  150. color: ${p.theme.textColor};
  151. }
  152. `}
  153. `;
  154. PresetButton.defaultProps = {
  155. priority: 'link',
  156. borderless: true,
  157. };
  158. export default MetricField;