eapField.tsx 4.1 KB

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