eapField.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import {useCallback, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Select} from 'sentry/components/core/select';
  4. import {t} from 'sentry/locale';
  5. import {space} from 'sentry/styles/space';
  6. import type {TagCollection} from 'sentry/types/group';
  7. import {defined} from 'sentry/utils';
  8. import {parseFunction, prettifyTagKey} from 'sentry/utils/discover/fields';
  9. import {ALLOWED_EXPLORE_VISUALIZE_AGGREGATES} from 'sentry/utils/fields';
  10. import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
  11. export const DEFAULT_EAP_FIELD = 'span.duration';
  12. export const DEFAULT_EAP_METRICS_ALERT_FIELD = `count(${DEFAULT_EAP_FIELD})`;
  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 storedTags = useSpanTags('number');
  35. const numberTags: TagCollection = useMemo(() => {
  36. const availableTags: TagCollection = storedTags;
  37. if (field && !defined(storedTags[field])) {
  38. availableTags[field] = {
  39. key: field,
  40. name: prettifyTagKey(field),
  41. };
  42. }
  43. return availableTags;
  44. }, [field, storedTags]);
  45. const fieldsArray = Object.values(numberTags);
  46. useEffect(() => {
  47. const selectedMeta = field ? numberTags[field] : undefined;
  48. if (!field || !selectedMeta) {
  49. const newSelection = fieldsArray[0];
  50. if (newSelection) {
  51. onChange(`count(${newSelection.name})`, {});
  52. } else if (aggregate !== DEFAULT_EAP_METRICS_ALERT_FIELD) {
  53. onChange(DEFAULT_EAP_METRICS_ALERT_FIELD, {});
  54. }
  55. }
  56. }, [onChange, aggregate, aggregation, field, numberTags, fieldsArray]);
  57. const handleFieldChange = useCallback(
  58. (option: any) => {
  59. const selectedMeta = numberTags[option.value];
  60. if (!selectedMeta) {
  61. return;
  62. }
  63. onChange(`${aggregation}(${selectedMeta.key})`, {});
  64. },
  65. [numberTags, onChange, aggregation]
  66. );
  67. const handleOperationChange = useCallback(
  68. (option: any) => {
  69. if (field) {
  70. onChange(`${option.value}(${field})`, {});
  71. } else {
  72. onChange(`${option.value}(${DEFAULT_EAP_FIELD})`, {});
  73. }
  74. },
  75. [field, onChange]
  76. );
  77. // As SelectControl does not support an options size limit out of the box
  78. // we work around it by using the async variant of the control
  79. const getFieldOptions = useCallback(
  80. (searchText: string) => {
  81. const filteredMeta = fieldsArray.filter(
  82. ({name}) =>
  83. searchText === '' || name.toLowerCase().includes(searchText.toLowerCase())
  84. );
  85. const options = filteredMeta.map(metric => {
  86. return {
  87. label: metric.name,
  88. value: metric.key,
  89. };
  90. });
  91. return options;
  92. },
  93. [fieldsArray]
  94. );
  95. const fieldName = fieldsArray.find(f => f.key === field)?.name;
  96. // When using the async variant of SelectControl, we need to pass in an option object instead of just the value
  97. const selectedOption = field && {
  98. label: fieldName,
  99. value: field,
  100. };
  101. return (
  102. <Wrapper>
  103. <StyledSelectControl
  104. searchable
  105. placeholder={t('Select an operation')}
  106. options={OPERATIONS}
  107. value={aggregation}
  108. onChange={handleOperationChange}
  109. />
  110. <StyledSelectControl
  111. searchable
  112. placeholder={t('Select a metric')}
  113. noOptionsMessage={() =>
  114. fieldsArray.length === 0 ? t('No metrics in this project') : t('No options')
  115. }
  116. async
  117. defaultOptions={getFieldOptions('')}
  118. loadOptions={(searchText: any) => Promise.resolve(getFieldOptions(searchText))}
  119. value={selectedOption}
  120. onChange={handleFieldChange}
  121. />
  122. </Wrapper>
  123. );
  124. }
  125. export default EAPFieldWrapper;
  126. const Wrapper = styled('div')`
  127. display: flex;
  128. gap: ${space(1)};
  129. `;
  130. const StyledSelectControl = styled(Select)`
  131. width: 200px;
  132. `;