mriField.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import {Fragment, useCallback, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import SelectControl from 'sentry/components/forms/controls/selectControl';
  4. import Tag from 'sentry/components/tag';
  5. import {t} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import {Project} from 'sentry/types';
  8. import {
  9. fieldToMri,
  10. getReadableMetricType,
  11. isAllowedOp,
  12. mriToField,
  13. useMetricsMeta,
  14. } from 'sentry/utils/metrics';
  15. interface Props {
  16. aggregate: string;
  17. onChange: (value: string, meta: Record<string, any>) => void;
  18. project: Project;
  19. }
  20. function filterAndSortOperations(operations: string[]) {
  21. return operations.filter(isAllowedOp).sort((a, b) => a.localeCompare(b));
  22. }
  23. function MriField({aggregate, project, onChange}: Props) {
  24. const {data: meta} = useMetricsMeta([parseInt(project.id, 10)], {
  25. useCases: ['transactions', 'custom'],
  26. });
  27. const metaArr = useMemo(() => {
  28. return Object.values(meta).sort((a, b) => a.name.localeCompare(b.name));
  29. }, [meta]);
  30. const selectedValues = fieldToMri(aggregate);
  31. const selectedMriMeta = selectedValues.mri ? meta[selectedValues.mri] : null;
  32. useEffect(() => {
  33. // Auto-select the first mri if none of the available ones is selected
  34. if (!selectedMriMeta && metaArr.length > 0) {
  35. const newSelection = metaArr[0];
  36. onChange(
  37. mriToField(newSelection.mri, filterAndSortOperations(newSelection.operations)[0]),
  38. {}
  39. );
  40. }
  41. }, [metaArr, onChange, selectedMriMeta]);
  42. const handleMriChange = useCallback(
  43. option => {
  44. // Make sure that the selected operation matches the new metric
  45. const availableOps = filterAndSortOperations(meta[option.value].operations);
  46. const selectedOp =
  47. selectedValues.op && availableOps.includes(selectedValues.op)
  48. ? selectedValues.op
  49. : availableOps[0];
  50. onChange(mriToField(option.value, selectedOp), {});
  51. },
  52. [meta, onChange, selectedValues.op]
  53. );
  54. const operationOptions = useMemo(
  55. () =>
  56. filterAndSortOperations(selectedMriMeta?.operations ?? []).map(op => ({
  57. label: op,
  58. value: op,
  59. })),
  60. [selectedMriMeta]
  61. );
  62. // As SelectControl does not support an options size limit out of the box
  63. // we work around it by using the async variant of the control
  64. const getMriOptions = useCallback(
  65. (searchText: string) => {
  66. const filteredMeta = metaArr.filter(
  67. ({name}) =>
  68. searchText === '' || name.toLowerCase().includes(searchText.toLowerCase())
  69. );
  70. const options = filteredMeta.splice(0, 100).map<{
  71. label: React.ReactNode;
  72. value: string;
  73. disabled?: boolean;
  74. trailingItems?: React.ReactNode;
  75. }>(metric => ({
  76. label: metric.name,
  77. value: metric.mri,
  78. trailingItems: (
  79. <Fragment>
  80. <Tag tooltipText={t('Type')}>{getReadableMetricType(metric.type)}</Tag>
  81. <Tag tooltipText={t('Unit')}>{metric.unit}</Tag>
  82. </Fragment>
  83. ),
  84. }));
  85. if (filteredMeta.length > options.length) {
  86. options.push({
  87. label: (
  88. <SizeLimitMessage>{t('Use search to find more options…')}</SizeLimitMessage>
  89. ),
  90. value: '',
  91. disabled: true,
  92. });
  93. }
  94. return options;
  95. },
  96. [metaArr]
  97. );
  98. // When using the async variant of SelectControl, we need to pass in an option object instead of just the value
  99. const selectedOption = selectedMriMeta && {
  100. label: selectedMriMeta.name,
  101. value: selectedMriMeta.mri,
  102. };
  103. return (
  104. <Wrapper>
  105. <StyledSelectControl
  106. searchable
  107. placeholder={t('Select a metric')}
  108. async
  109. defaultOptions={getMriOptions('')}
  110. loadOptions={searchText => Promise.resolve(getMriOptions(searchText))}
  111. filterOption={() => true}
  112. value={selectedOption}
  113. onChange={handleMriChange}
  114. />
  115. <StyledSelectControl
  116. searchable
  117. disabled={!selectedValues.mri}
  118. placeholder={t('Select an operation')}
  119. options={operationOptions}
  120. value={selectedValues.op}
  121. onChange={option => {
  122. onChange(`${option.value}(${selectedValues.mri})`, {});
  123. }}
  124. />
  125. </Wrapper>
  126. );
  127. }
  128. export default MriField;
  129. const Wrapper = styled('div')`
  130. display: grid;
  131. gap: ${space(1)};
  132. grid-template-columns: 1fr 1fr;
  133. `;
  134. const StyledSelectControl = styled(SelectControl)`
  135. width: 200px;
  136. `;
  137. const SizeLimitMessage = styled('span')`
  138. font-size: ${p => p.theme.fontSizeSmall};
  139. display: block;
  140. width: 100%;
  141. text-align: center;
  142. `;