eapField.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  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 type {Project} from 'sentry/types/project';
  7. import {parseFunction} from 'sentry/utils/discover/fields';
  8. import {ALLOWED_EXPLORE_VISUALIZE_AGGREGATES} from 'sentry/utils/fields';
  9. export const DEFAULT_EAP_FIELD = 'span.duration';
  10. export const DEFAULT_EAP_METRICS_ALERT_FIELD = `count(${DEFAULT_EAP_FIELD})`;
  11. interface Props {
  12. aggregate: string;
  13. onChange: (value: string, meta: Record<string, any>) => void;
  14. project: Project;
  15. }
  16. // Use the same aggregates/operations available in the explore view
  17. const OPERATIONS = [
  18. ...ALLOWED_EXPLORE_VISUALIZE_AGGREGATES.map(aggregate => ({
  19. label: aggregate,
  20. value: aggregate,
  21. })),
  22. ];
  23. // TODD(edward): Just hardcode the EAP fields for now. We should use SpanTagsProvider in the future to match the Explore UI.
  24. const EAP_FIELD_OPTIONS = [
  25. {
  26. name: 'span.duration',
  27. },
  28. {
  29. name: 'span.self_time',
  30. },
  31. ];
  32. function EAPField({aggregate, onChange}: Props) {
  33. // We parse out the aggregation and field from the aggregate string.
  34. // This only works for aggregates with <= 1 argument.
  35. const {
  36. name: aggregation,
  37. arguments: [field],
  38. } = parseFunction(aggregate) ?? {arguments: [undefined]};
  39. useEffect(() => {
  40. const selectedMriMeta = EAP_FIELD_OPTIONS.find(metric => metric.name === field);
  41. if (field && !selectedMriMeta) {
  42. const newSelection = EAP_FIELD_OPTIONS[0];
  43. if (newSelection) {
  44. onChange(`count(${newSelection.name})`, {});
  45. } else if (aggregate !== DEFAULT_EAP_METRICS_ALERT_FIELD) {
  46. onChange(DEFAULT_EAP_METRICS_ALERT_FIELD, {});
  47. }
  48. }
  49. }, [onChange, aggregate, aggregation, field]);
  50. const handleFieldChange = useCallback(
  51. option => {
  52. const selectedMeta = EAP_FIELD_OPTIONS.find(metric => metric.name === option.value);
  53. if (!selectedMeta) {
  54. return;
  55. }
  56. onChange(`${aggregation}(${option.value})`, {});
  57. },
  58. [onChange, aggregation]
  59. );
  60. const handleOperationChange = useCallback(
  61. option => {
  62. if (field) {
  63. onChange(`${option.value}(${field})`, {});
  64. } else {
  65. onChange(`${option.value}(${DEFAULT_EAP_FIELD})`, {});
  66. }
  67. },
  68. [field, onChange]
  69. );
  70. // As SelectControl does not support an options size limit out of the box
  71. // we work around it by using the async variant of the control
  72. const getFieldOptions = useCallback((searchText: string) => {
  73. const filteredMeta = EAP_FIELD_OPTIONS.filter(
  74. ({name}) =>
  75. searchText === '' || name.toLowerCase().includes(searchText.toLowerCase())
  76. );
  77. const options = filteredMeta.map(metric => {
  78. return {
  79. label: metric.name,
  80. value: metric.name,
  81. };
  82. });
  83. return options;
  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. EAP_FIELD_OPTIONS.length === 0
  104. ? t('No metrics in this project')
  105. : t('No options')
  106. }
  107. async
  108. defaultOptions={getFieldOptions('')}
  109. loadOptions={searchText => Promise.resolve(getFieldOptions(searchText))}
  110. filterOption={() => true}
  111. value={selectedOption}
  112. onChange={handleFieldChange}
  113. />
  114. </Wrapper>
  115. );
  116. }
  117. export default EAPField;
  118. const Wrapper = styled('div')`
  119. display: flex;
  120. gap: ${space(1)};
  121. `;
  122. const StyledSelectControl = styled(SelectControl)`
  123. width: 200px;
  124. `;