eapField.tsx 4.3 KB

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