metricSearchBar.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import {useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {useId} from '@react-aria/utils';
  4. import memoize from 'lodash/memoize';
  5. import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
  6. import SmartSearchBar from 'sentry/components/smartSearchBar';
  7. import {t} from 'sentry/locale';
  8. import {SavedSearchType, type TagCollection} from 'sentry/types/group';
  9. import type {MRI} from 'sentry/types/metrics';
  10. import {getUseCaseFromMRI} from 'sentry/utils/metrics/mri';
  11. import {useMetricsTags} from 'sentry/utils/metrics/useMetricsTags';
  12. import useApi from 'sentry/utils/useApi';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import usePageFilters from 'sentry/utils/usePageFilters';
  15. import {ensureQuotedTextFilters} from 'sentry/views/metrics/utils';
  16. import {useSelectedProjects} from 'sentry/views/metrics/utils/useSelectedProjects';
  17. interface MetricSearchBarProps extends Partial<SmartSearchBarProps> {
  18. onChange: (value: string) => void;
  19. blockedTags?: string[];
  20. disabled?: boolean;
  21. mri?: MRI;
  22. projectIds?: string[];
  23. query?: string;
  24. }
  25. const EMPTY_ARRAY = [];
  26. const EMPTY_SET = new Set<never>();
  27. export function MetricSearchBar({
  28. mri,
  29. blockedTags,
  30. disabled,
  31. onChange,
  32. query,
  33. projectIds,
  34. id: idProp,
  35. ...props
  36. }: MetricSearchBarProps) {
  37. const org = useOrganization();
  38. const api = useApi();
  39. const {selection} = usePageFilters();
  40. const selectedProjects = useSelectedProjects();
  41. const id = useId(idProp);
  42. const projectIdNumbers = useMemo(
  43. () => projectIds?.map(projectId => parseInt(projectId, 10)),
  44. [projectIds]
  45. );
  46. const {data: tags = EMPTY_ARRAY, isLoading} = useMetricsTags(
  47. mri,
  48. {
  49. ...selection,
  50. projects: projectIdNumbers,
  51. },
  52. true,
  53. blockedTags
  54. );
  55. const supportedTags: TagCollection = useMemo(
  56. () => tags.reduce((acc, tag) => ({...acc, [tag.key]: tag}), {}),
  57. [tags]
  58. );
  59. const searchConfig = useMemo(
  60. () => ({
  61. booleanKeys: EMPTY_SET,
  62. dateKeys: EMPTY_SET,
  63. durationKeys: EMPTY_SET,
  64. numericKeys: EMPTY_SET,
  65. percentageKeys: EMPTY_SET,
  66. sizeKeys: EMPTY_SET,
  67. textOperatorKeys: EMPTY_SET,
  68. supportedTags,
  69. disallowFreeText: true,
  70. }),
  71. [supportedTags]
  72. );
  73. const fetchTagValues = useMemo(() => {
  74. const fn = memoize((tagKey: string) => {
  75. // clear response from cache after 10 seconds
  76. setTimeout(() => {
  77. fn.cache.delete(tagKey);
  78. }, 10000);
  79. return api.requestPromise(`/organizations/${org.slug}/metrics/tags/${tagKey}/`, {
  80. query: {
  81. metric: mri,
  82. useCase: getUseCaseFromMRI(mri),
  83. project: selection.projects,
  84. },
  85. });
  86. });
  87. return fn;
  88. }, [api, mri, org.slug, selection.projects]);
  89. const getTagValues = useCallback(
  90. async (tag: any, search: string) => {
  91. // The tag endpoint cannot provide values for the project tag
  92. if (tag.key === 'project') {
  93. return selectedProjects.map(project => project.slug);
  94. }
  95. const tagsValues = await fetchTagValues(tag.key);
  96. return tagsValues
  97. .filter(
  98. tv =>
  99. tv.value !== '' &&
  100. tv.value.toLocaleLowerCase().includes(search.toLocaleLowerCase())
  101. )
  102. .map(tv => tv.value);
  103. },
  104. [fetchTagValues, selectedProjects]
  105. );
  106. const handleChange = useCallback(
  107. (value: string, {validSearch} = {validSearch: true}) => {
  108. if (!validSearch) {
  109. return;
  110. }
  111. onChange(ensureQuotedTextFilters(value, searchConfig));
  112. },
  113. [onChange, searchConfig]
  114. );
  115. return (
  116. <WideSearchBar
  117. id={id}
  118. disabled={disabled}
  119. maxMenuHeight={220}
  120. organization={org}
  121. onGetTagValues={getTagValues}
  122. // don't highlight tags while loading as we don't know yet if they are supported
  123. highlightUnsupportedTags={!isLoading}
  124. onClose={handleChange}
  125. onSearch={handleChange}
  126. placeholder={t('Filter by tags')}
  127. query={query}
  128. savedSearchType={SavedSearchType.METRIC}
  129. disallowWildcard
  130. {...searchConfig}
  131. {...props}
  132. />
  133. );
  134. }
  135. const WideSearchBar = styled(SmartSearchBar)`
  136. width: 100%;
  137. opacity: ${p => (p.disabled ? '0.6' : '1')};
  138. `;