mriField.tsx 5.5 KB

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