schemaHintsList.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import debounce from 'lodash/debounce';
  4. import isEqual from 'lodash/isEqual';
  5. import omit from 'lodash/omit';
  6. import {Button} from 'sentry/components/core/button';
  7. import {getHasTag} from 'sentry/components/events/searchBar';
  8. import useDrawer from 'sentry/components/globalDrawer';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import {getFunctionTags} from 'sentry/components/performance/spanSearchQueryBuilder';
  11. import {t} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import type {Tag, TagCollection} from 'sentry/types/group';
  14. import {prettifyTagKey} from 'sentry/utils/discover/fields';
  15. import {
  16. type AggregationKey,
  17. FieldKind,
  18. FieldValueType,
  19. getFieldDefinition,
  20. } from 'sentry/utils/fields';
  21. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  22. import {useLocation} from 'sentry/utils/useLocation';
  23. import SchemaHintsDrawer from 'sentry/views/explore/components/schemaHintsDrawer';
  24. import {SCHEMA_HINTS_LIST_ORDER_KEYS} from 'sentry/views/explore/components/schemaHintsUtils/schemaHintsListOrder';
  25. import {
  26. PageParamsProvider,
  27. useExploreQuery,
  28. useSetExploreQuery,
  29. } from 'sentry/views/explore/contexts/pageParamsContext';
  30. import {SPANS_FILTER_KEY_SECTIONS} from 'sentry/views/insights/constants';
  31. interface SchemaHintsListProps {
  32. numberTags: TagCollection;
  33. stringTags: TagCollection;
  34. supportedAggregates: AggregationKey[];
  35. isLoading?: boolean;
  36. }
  37. const seeFullListTag: Tag = {
  38. key: 'seeFullList',
  39. name: t('See full list'),
  40. kind: undefined,
  41. };
  42. function getTagsFromKeys(keys: string[], tags: TagCollection): Tag[] {
  43. return keys.map(key => tags[key]).filter(tag => !!tag);
  44. }
  45. function SchemaHintsList({
  46. supportedAggregates,
  47. numberTags,
  48. stringTags,
  49. isLoading,
  50. }: SchemaHintsListProps) {
  51. const schemaHintsContainerRef = useRef<HTMLDivElement>(null);
  52. const exploreQuery = useExploreQuery();
  53. const setExploreQuery = useSetExploreQuery();
  54. const location = useLocation();
  55. const {openDrawer, isDrawerOpen} = useDrawer();
  56. const functionTags = useMemo(() => {
  57. return getFunctionTags(supportedAggregates);
  58. }, [supportedAggregates]);
  59. // sort tags by the order they show up in the query builder
  60. const filterTagsSorted = useMemo(() => {
  61. const filterTags: TagCollection = {...functionTags, ...numberTags, ...stringTags};
  62. filterTags.has = getHasTag({...stringTags});
  63. const schemaHintsPresetTags = getTagsFromKeys(
  64. SCHEMA_HINTS_LIST_ORDER_KEYS,
  65. filterTags
  66. );
  67. const sectionKeys = SPANS_FILTER_KEY_SECTIONS.flatMap(
  68. section => section.children
  69. ).filter(key => !SCHEMA_HINTS_LIST_ORDER_KEYS.includes(key));
  70. const sectionSortedTags = getTagsFromKeys(sectionKeys, filterTags);
  71. const otherKeys = Object.keys(filterTags).filter(
  72. key => !sectionKeys.includes(key) && !SCHEMA_HINTS_LIST_ORDER_KEYS.includes(key)
  73. );
  74. const otherTags = getTagsFromKeys(otherKeys, filterTags);
  75. return [...schemaHintsPresetTags, ...sectionSortedTags, ...otherTags];
  76. }, [numberTags, stringTags, functionTags]);
  77. const [visibleHints, setVisibleHints] = useState([seeFullListTag]);
  78. useEffect(() => {
  79. // debounce calculation to prevent 'flickering' when resizing
  80. const calculateVisibleHints = debounce(() => {
  81. if (!schemaHintsContainerRef.current) {
  82. return;
  83. }
  84. const container = schemaHintsContainerRef.current;
  85. const containerRect = container.getBoundingClientRect();
  86. // Create a temporary div to measure items without rendering them
  87. const measureDiv = document.createElement('div');
  88. measureDiv.style.visibility = 'hidden';
  89. document.body.appendChild(measureDiv);
  90. // Clone the container styles
  91. const styles = window.getComputedStyle(container);
  92. measureDiv.style.display = styles.display;
  93. measureDiv.style.gap = styles.gap;
  94. measureDiv.style.width = styles.width;
  95. // Render items in hidden div to measure
  96. [...filterTagsSorted, seeFullListTag].forEach(hint => {
  97. const el = container.children[0]?.cloneNode(true) as HTMLElement;
  98. el.innerHTML = getHintText(hint);
  99. measureDiv.appendChild(el);
  100. });
  101. // Get all rendered items
  102. const items = Array.from(measureDiv.children) as HTMLElement[];
  103. const seeFullListTagRect = Array.from(measureDiv.children)[
  104. Array.from(measureDiv.children).length - 1
  105. ]?.getBoundingClientRect();
  106. // Find the last item that fits within the container
  107. let lastVisibleIndex =
  108. items.findIndex(item => {
  109. const itemRect = item.getBoundingClientRect();
  110. return itemRect.right > containerRect.right - (seeFullListTagRect?.width ?? 0);
  111. }) - 1;
  112. // If all items fit, show them all
  113. if (lastVisibleIndex < 0) {
  114. lastVisibleIndex = items.length;
  115. }
  116. setVisibleHints([...filterTagsSorted.slice(0, lastVisibleIndex), seeFullListTag]);
  117. // Remove the temporary div
  118. document.body.removeChild(measureDiv);
  119. }, 30);
  120. // initial calculation
  121. calculateVisibleHints();
  122. const resizeObserver = new ResizeObserver(calculateVisibleHints);
  123. if (schemaHintsContainerRef.current) {
  124. resizeObserver.observe(schemaHintsContainerRef.current);
  125. }
  126. return () => resizeObserver.disconnect();
  127. }, [filterTagsSorted]);
  128. const onHintClick = useCallback(
  129. (hint: Tag) => {
  130. if (hint.key === seeFullListTag.key) {
  131. if (!isDrawerOpen) {
  132. openDrawer(
  133. () => (
  134. <PageParamsProvider>
  135. <SchemaHintsDrawer hints={filterTagsSorted} />
  136. </PageParamsProvider>
  137. ),
  138. {
  139. ariaLabel: t('Schema Hints Drawer'),
  140. drawerWidth: '35vw',
  141. shouldCloseOnLocationChange: newLocation => {
  142. return (
  143. location.pathname !== newLocation.pathname ||
  144. // will close if anything but the filter query has changed
  145. !isEqual(
  146. omit(location.query, ['query']),
  147. omit(newLocation.query, ['query'])
  148. )
  149. );
  150. },
  151. }
  152. );
  153. }
  154. return;
  155. }
  156. const newSearchQuery = new MutableSearch(exploreQuery);
  157. const isBoolean =
  158. getFieldDefinition(hint.key, 'span', hint.kind)?.valueType ===
  159. FieldValueType.BOOLEAN;
  160. newSearchQuery.addFilterValue(
  161. hint.key,
  162. isBoolean ? 'True' : hint.kind === FieldKind.MEASUREMENT ? '>0' : ''
  163. );
  164. setExploreQuery(newSearchQuery.formatString());
  165. },
  166. [exploreQuery, setExploreQuery, isDrawerOpen, openDrawer, filterTagsSorted, location]
  167. );
  168. const getHintText = (hint: Tag) => {
  169. if (hint.key === seeFullListTag.key) {
  170. return hint.name;
  171. }
  172. return `${prettifyTagKey(hint.name)} ${hint.kind === FieldKind.MEASUREMENT ? '>' : 'is'} ...`;
  173. };
  174. if (isLoading) {
  175. return (
  176. <SchemaHintsLoadingContainer>
  177. <LoadingIndicator mini />
  178. </SchemaHintsLoadingContainer>
  179. );
  180. }
  181. return (
  182. <SchemaHintsContainer ref={schemaHintsContainerRef}>
  183. {visibleHints.map(hint => (
  184. <SchemaHintOption
  185. key={hint.key}
  186. data-type={hint.key}
  187. onClick={() => onHintClick(hint)}
  188. >
  189. {getHintText(hint)}
  190. </SchemaHintOption>
  191. ))}
  192. </SchemaHintsContainer>
  193. );
  194. }
  195. export default SchemaHintsList;
  196. const SchemaHintsContainer = styled('div')`
  197. display: flex;
  198. flex-direction: row;
  199. gap: ${space(1)};
  200. flex-wrap: nowrap;
  201. overflow: hidden;
  202. > * {
  203. flex-shrink: 0;
  204. }
  205. `;
  206. const SchemaHintsLoadingContainer = styled('div')`
  207. display: flex;
  208. justify-content: center;
  209. align-items: center;
  210. height: 24px;
  211. `;
  212. const SchemaHintOption = styled(Button)`
  213. border: 1px solid ${p => p.theme.innerBorder};
  214. border-radius: 4px;
  215. font-size: ${p => p.theme.fontSizeSmall};
  216. font-weight: ${p => p.theme.fontWeightNormal};
  217. display: flex;
  218. padding: ${space(0.5)} ${space(1)};
  219. align-content: center;
  220. min-height: 0;
  221. height: 24px;
  222. flex-wrap: wrap;
  223. /* Ensures that filters do not grow outside of the container */
  224. min-width: fit-content;
  225. &[aria-selected='true'] {
  226. background-color: ${p => p.theme.gray100};
  227. }
  228. `;