metricSearchBar.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import {useCallback, useMemo} from 'react';
  2. import {css, type SerializedStyles} from '@emotion/react';
  3. import {useId} from '@react-aria/utils';
  4. import {QueryFieldGroup} from 'sentry/components/metrics/queryFieldGroup';
  5. import {
  6. SearchQueryBuilder,
  7. type SearchQueryBuilderProps,
  8. } from 'sentry/components/searchQueryBuilder';
  9. import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
  10. import SmartSearchBar from 'sentry/components/smartSearchBar';
  11. import {t} from 'sentry/locale';
  12. import {SavedSearchType, type TagCollection} from 'sentry/types/group';
  13. import type {MRI} from 'sentry/types/metrics';
  14. import {
  15. hasMetricsNewInputs,
  16. hasMetricsNewSearchQueryBuilder,
  17. } from 'sentry/utils/metrics/features';
  18. import {getUseCaseFromMRI} from 'sentry/utils/metrics/mri';
  19. import type {MetricTag} from 'sentry/utils/metrics/types';
  20. import {useMetricsTags} from 'sentry/utils/metrics/useMetricsTags';
  21. import useApi from 'sentry/utils/useApi';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import usePageFilters from 'sentry/utils/usePageFilters';
  24. import {INSIGHTS_METRICS} from 'sentry/views/alerts/rules/metric/utils/isInsightsMetricAlert';
  25. import {SpanMetricsField} from 'sentry/views/insights/types';
  26. import {ensureQuotedTextFilters} from 'sentry/views/metrics/utils';
  27. import {useSelectedProjects} from 'sentry/views/metrics/utils/useSelectedProjects';
  28. export interface MetricSearchBarProps
  29. extends Partial<Omit<SmartSearchBarProps, 'projectIds'>> {
  30. onChange: (value: string) => void;
  31. blockedTags?: string[];
  32. disabled?: boolean;
  33. mri?: MRI;
  34. projectIds?: string[];
  35. query?: string;
  36. }
  37. const EMPTY_ARRAY = [];
  38. const EMPTY_SET = new Set<never>();
  39. const INSIGHTS_ADDITIONAL_TAG_FILTERS: MetricTag[] = [
  40. {
  41. key: 'has',
  42. },
  43. {
  44. key: SpanMetricsField.SPAN_MODULE,
  45. },
  46. ];
  47. export function MetricSearchBar({
  48. mri,
  49. blockedTags,
  50. disabled,
  51. onChange,
  52. query,
  53. projectIds,
  54. id: idProp,
  55. ...props
  56. }: MetricSearchBarProps) {
  57. const organization = useOrganization();
  58. const api = useApi();
  59. const {selection} = usePageFilters();
  60. const selectedProjects = useSelectedProjects();
  61. const id = useId(idProp);
  62. const projectIdNumbers = useMemo(
  63. () => projectIds?.map(projectId => parseInt(projectId, 10)),
  64. [projectIds]
  65. );
  66. const {data: tags = EMPTY_ARRAY, isPending} = useMetricsTags(
  67. mri,
  68. {
  69. ...selection,
  70. projects: projectIdNumbers,
  71. },
  72. true,
  73. blockedTags
  74. );
  75. const additionalTags: MetricTag[] = useMemo(
  76. () =>
  77. // Insights metrics allow the `has` filter.
  78. // `span.module` is a discover field alias that does not appear in the metrics meta endpoint.
  79. INSIGHTS_METRICS.includes(mri as string) ? INSIGHTS_ADDITIONAL_TAG_FILTERS : [],
  80. [mri]
  81. );
  82. const supportedTags: TagCollection = useMemo(
  83. () =>
  84. [...tags, ...additionalTags].reduce((acc, tag) => ({...acc, [tag.key]: tag}), {}),
  85. [tags, additionalTags]
  86. );
  87. const searchConfig = useMemo(
  88. () => ({
  89. booleanKeys: EMPTY_SET,
  90. dateKeys: EMPTY_SET,
  91. durationKeys: EMPTY_SET,
  92. numericKeys: EMPTY_SET,
  93. percentageKeys: EMPTY_SET,
  94. sizeKeys: EMPTY_SET,
  95. textOperatorKeys: EMPTY_SET,
  96. supportedTags,
  97. disallowFreeText: true,
  98. }),
  99. [supportedTags]
  100. );
  101. const fetchTagValues = useCallback(
  102. (tagKey: string, search: string) => {
  103. return api.requestPromise(
  104. `/organizations/${organization.slug}/metrics/tags/${tagKey}/`,
  105. {
  106. query: {
  107. prefix: search,
  108. metric: mri,
  109. useCase: getUseCaseFromMRI(mri),
  110. project: selection.projects,
  111. },
  112. }
  113. );
  114. },
  115. [api, mri, organization.slug, selection.projects]
  116. );
  117. const getTagValues = useCallback(
  118. async (tag: any, search: string) => {
  119. // The tag endpoint cannot provide values for the project tag
  120. if (tag.key === 'project') {
  121. return selectedProjects.map(project => project.slug);
  122. }
  123. const tagsValues = await fetchTagValues(tag.key, search);
  124. return tagsValues.filter(tv => tv.value !== '').map(tv => tv.value);
  125. },
  126. [fetchTagValues, selectedProjects]
  127. );
  128. const handleChange = useCallback(
  129. (value: string, {validSearch} = {validSearch: true}) => {
  130. if (!validSearch) {
  131. return;
  132. }
  133. onChange(ensureQuotedTextFilters(value, searchConfig));
  134. },
  135. [onChange, searchConfig]
  136. );
  137. const searchQueryBuilderProps: SearchQueryBuilderProps & {css: SerializedStyles} = {
  138. disabled,
  139. onChange: (value, {queryIsValid}) => handleChange(value, {validSearch: queryIsValid}),
  140. placeholder: t('Filter by tags'),
  141. initialQuery: query ?? '',
  142. getTagValues,
  143. recentSearches: SavedSearchType.METRIC,
  144. // don't highlight tags while loading as we don't know yet if they are supported
  145. disallowUnsupportedFilters: !isPending,
  146. filterKeys: searchConfig.supportedTags,
  147. disallowFreeText: searchConfig.disallowFreeText,
  148. searchSource: props.searchSource ?? 'metrics',
  149. css: wideSearchBarCss(disabled),
  150. };
  151. const smartSearchProps: Partial<SmartSearchBarProps> & {css: SerializedStyles} = {
  152. id,
  153. disabled,
  154. maxMenuHeight: 220,
  155. organization,
  156. onGetTagValues: getTagValues,
  157. // don't highlight tags while loading as we don't know yet if they are supported
  158. highlightUnsupportedTags: !isPending,
  159. onClose: handleChange,
  160. onSearch: handleChange,
  161. placeholder: t('Filter by tags'),
  162. query,
  163. savedSearchType: SavedSearchType.METRIC,
  164. css: wideSearchBarCss(disabled),
  165. ...props,
  166. ...searchConfig,
  167. };
  168. if (hasMetricsNewInputs(organization)) {
  169. if (hasMetricsNewSearchQueryBuilder(organization)) {
  170. return <QueryFieldGroup.SearchQueryBuilder {...searchQueryBuilderProps} />;
  171. }
  172. return <QueryFieldGroup.SmartSearchBar {...smartSearchProps} />;
  173. }
  174. if (hasMetricsNewSearchQueryBuilder(organization)) {
  175. return <SearchQueryBuilder {...searchQueryBuilderProps} />;
  176. }
  177. return <SmartSearchBar {...smartSearchProps} />;
  178. }
  179. function wideSearchBarCss(disabled?: boolean) {
  180. return css`
  181. width: 100%;
  182. opacity: ${disabled ? '0.6' : '1'};
  183. `;
  184. }