eapField.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import {useCallback, useEffect} from 'react';
  2. import styled from '@emotion/styled';
  3. import SelectControl from 'sentry/components/forms/controls/selectControl';
  4. import {t} from 'sentry/locale';
  5. import {space} from 'sentry/styles/space';
  6. import {parseFunction} from 'sentry/utils/discover/fields';
  7. import {ALLOWED_EXPLORE_VISUALIZE_AGGREGATES} from 'sentry/utils/fields';
  8. import {
  9. DEFAULT_EAP_FIELD,
  10. DEFAULT_EAP_METRICS_ALERT_FIELD,
  11. } from 'sentry/utils/metrics/mri';
  12. import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
  13. interface Props {
  14. aggregate: string;
  15. onChange: (value: string, meta: Record<string, any>) => void;
  16. }
  17. // Use the same aggregates/operations available in the explore view
  18. const OPERATIONS = [
  19. ...ALLOWED_EXPLORE_VISUALIZE_AGGREGATES.map(aggregate => ({
  20. label: aggregate,
  21. value: aggregate,
  22. })),
  23. ];
  24. function EAPFieldWrapper({aggregate, onChange}: Props) {
  25. return <EAPField aggregate={aggregate} onChange={onChange} />;
  26. }
  27. function EAPField({aggregate, onChange}: Props) {
  28. // We parse out the aggregation and field from the aggregate string.
  29. // This only works for aggregates with <= 1 argument.
  30. const {
  31. name: aggregation,
  32. arguments: [field],
  33. } = parseFunction(aggregate) ?? {arguments: [undefined]};
  34. const numberTags = useSpanTags('number');
  35. const fieldsArray = Object.values(numberTags);
  36. useEffect(() => {
  37. const selectedMeta = field ? numberTags[field] : undefined;
  38. if (field && !selectedMeta) {
  39. const newSelection = fieldsArray[0];
  40. if (newSelection) {
  41. onChange(`count(${newSelection.name})`, {});
  42. } else if (aggregate !== DEFAULT_EAP_METRICS_ALERT_FIELD) {
  43. onChange(DEFAULT_EAP_METRICS_ALERT_FIELD, {});
  44. }
  45. }
  46. }, [onChange, aggregate, aggregation, field, numberTags, fieldsArray]);
  47. const handleFieldChange = useCallback(
  48. option => {
  49. const selectedMeta = numberTags[option.value];
  50. if (!selectedMeta) {
  51. return;
  52. }
  53. onChange(`${aggregation}(${selectedMeta.key})`, {});
  54. },
  55. [numberTags, onChange, aggregation]
  56. );
  57. const handleOperationChange = useCallback(
  58. option => {
  59. if (field) {
  60. onChange(`${option.value}(${field})`, {});
  61. } else {
  62. onChange(`${option.value}(${DEFAULT_EAP_FIELD})`, {});
  63. }
  64. },
  65. [field, onChange]
  66. );
  67. // As SelectControl does not support an options size limit out of the box
  68. // we work around it by using the async variant of the control
  69. const getFieldOptions = useCallback(
  70. (searchText: string) => {
  71. const filteredMeta = fieldsArray.filter(
  72. ({name}) =>
  73. searchText === '' || name.toLowerCase().includes(searchText.toLowerCase())
  74. );
  75. const options = filteredMeta.map(metric => {
  76. return {
  77. label: metric.name,
  78. value: metric.key,
  79. };
  80. });
  81. return options;
  82. },
  83. [fieldsArray]
  84. );
  85. // When using the async variant of SelectControl, we need to pass in an option object instead of just the value
  86. const selectedOption = field && {
  87. label: field,
  88. value: field,
  89. };
  90. return (
  91. <Wrapper>
  92. <StyledSelectControl
  93. searchable
  94. placeholder={t('Select an operation')}
  95. options={OPERATIONS}
  96. value={aggregation}
  97. onChange={handleOperationChange}
  98. />
  99. <StyledSelectControl
  100. searchable
  101. placeholder={t('Select a metric')}
  102. noOptionsMessage={() =>
  103. fieldsArray.length === 0 ? t('No metrics in this project') : t('No options')
  104. }
  105. async
  106. defaultOptions={getFieldOptions('')}
  107. loadOptions={searchText => Promise.resolve(getFieldOptions(searchText))}
  108. filterOption={() => true}
  109. value={selectedOption}
  110. onChange={handleFieldChange}
  111. />
  112. </Wrapper>
  113. );
  114. }
  115. export default EAPFieldWrapper;
  116. const Wrapper = styled('div')`
  117. display: flex;
  118. gap: ${space(1)};
  119. `;
  120. const StyledSelectControl = styled(SelectControl)`
  121. width: 200px;
  122. `;