metricSearchBar.tsx 4.5 KB

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