queryBuilder.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {CompactSelect} from 'sentry/components/compactSelect';
  4. import SearchBar from 'sentry/components/events/searchBar';
  5. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  6. import Tag from 'sentry/components/tag';
  7. import {IconLightning, IconReleases} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {MetricsTag, SavedSearchType, TagCollection} from 'sentry/types';
  11. import {
  12. defaultMetricDisplayType,
  13. getReadableMetricType,
  14. getUseCaseFromMri,
  15. isAllowedOp,
  16. MetricDisplayType,
  17. MetricsQuery,
  18. useMetricsMeta,
  19. useMetricsTags,
  20. } from 'sentry/utils/metrics';
  21. import useApi from 'sentry/utils/useApi';
  22. import useKeyPress from 'sentry/utils/useKeyPress';
  23. import useOrganization from 'sentry/utils/useOrganization';
  24. import usePageFilters from 'sentry/utils/usePageFilters';
  25. import {MetricWidgetProps} from 'sentry/views/ddm/widget';
  26. type QueryBuilderProps = {
  27. displayType: MetricDisplayType; // TODO(ddm): move display type out of the query builder
  28. metricsQuery: Pick<MetricsQuery, 'mri' | 'op' | 'query' | 'groupBy'>;
  29. onChange: (data: Partial<MetricWidgetProps>) => void;
  30. projects: number[];
  31. powerUserMode?: boolean;
  32. };
  33. export function QueryBuilder({
  34. metricsQuery,
  35. projects,
  36. displayType,
  37. powerUserMode,
  38. onChange,
  39. }: QueryBuilderProps) {
  40. const meta = useMetricsMeta(projects);
  41. const mriModeKeyPressed = useKeyPress('`', undefined, true);
  42. const [mriMode, setMriMode] = useState(powerUserMode); // power user mode that shows raw MRI instead of metrics names
  43. useEffect(() => {
  44. if (mriModeKeyPressed && !powerUserMode) {
  45. setMriMode(!mriMode);
  46. }
  47. // eslint-disable-next-line react-hooks/exhaustive-deps
  48. }, [mriModeKeyPressed, powerUserMode]);
  49. const {data: tags = []} = useMetricsTags(metricsQuery.mri, projects);
  50. if (!meta) {
  51. return null;
  52. }
  53. return (
  54. <QueryBuilderWrapper>
  55. <QueryBuilderRow>
  56. <WrapPageFilterBar>
  57. <CompactSelect
  58. searchable
  59. triggerProps={{prefix: t('Metric'), size: 'sm'}}
  60. options={Object.values(meta)
  61. .filter(metric =>
  62. mriMode
  63. ? true
  64. : metric.mri.includes(':custom/') || metric.mri === metricsQuery.mri
  65. )
  66. .map(metric => ({
  67. label: mriMode ? metric.mri : metric.name,
  68. value: metric.mri,
  69. trailingItems: mriMode ? undefined : (
  70. <Fragment>
  71. <Tag tooltipText={t('Type')}>
  72. {getReadableMetricType(metric.type)}
  73. </Tag>
  74. <Tag tooltipText={t('Unit')}>{metric.unit}</Tag>
  75. </Fragment>
  76. ),
  77. }))}
  78. value={metricsQuery.mri}
  79. onChange={option => {
  80. const availableOps = meta[option.value]?.operations.filter(isAllowedOp);
  81. const selectedOp = availableOps.includes(metricsQuery.op ?? '')
  82. ? metricsQuery.op
  83. : availableOps[0];
  84. onChange({
  85. mri: option.value,
  86. op: selectedOp,
  87. groupBy: undefined,
  88. focusedSeries: undefined,
  89. });
  90. }}
  91. />
  92. <CompactSelect
  93. triggerProps={{prefix: t('Op'), size: 'sm'}}
  94. options={
  95. meta[metricsQuery.mri]?.operations.filter(isAllowedOp).map(op => ({
  96. label: op,
  97. value: op,
  98. })) ?? []
  99. }
  100. disabled={!metricsQuery.mri}
  101. value={metricsQuery.op}
  102. onChange={option =>
  103. onChange({
  104. op: option.value,
  105. })
  106. }
  107. />
  108. <CompactSelect
  109. multiple
  110. triggerProps={{prefix: t('Group by'), size: 'sm'}}
  111. options={tags.map(tag => ({
  112. label: tag.key,
  113. value: tag.key,
  114. trailingItems: (
  115. <Fragment>
  116. {tag.key === 'release' && <IconReleases size="xs" />}
  117. {tag.key === 'transaction' && <IconLightning size="xs" />}
  118. </Fragment>
  119. ),
  120. }))}
  121. disabled={!metricsQuery.mri}
  122. value={metricsQuery.groupBy}
  123. onChange={options =>
  124. onChange({
  125. groupBy: options.map(o => o.value),
  126. focusedSeries: undefined,
  127. })
  128. }
  129. />
  130. <CompactSelect
  131. triggerProps={{prefix: t('Display'), size: 'sm'}}
  132. value={displayType ?? defaultMetricDisplayType}
  133. options={[
  134. {
  135. value: MetricDisplayType.LINE,
  136. label: t('Line'),
  137. },
  138. {
  139. value: MetricDisplayType.AREA,
  140. label: t('Area'),
  141. },
  142. {
  143. value: MetricDisplayType.BAR,
  144. label: t('Bar'),
  145. },
  146. ]}
  147. onChange={({value}) => {
  148. onChange({displayType: value});
  149. }}
  150. />
  151. </WrapPageFilterBar>
  152. </QueryBuilderRow>
  153. <QueryBuilderRow>
  154. <MetricSearchBar
  155. tags={tags}
  156. mri={metricsQuery.mri}
  157. disabled={!metricsQuery.mri}
  158. onChange={query => onChange({query})}
  159. query={metricsQuery.query}
  160. />
  161. </QueryBuilderRow>
  162. </QueryBuilderWrapper>
  163. );
  164. }
  165. type MetricSearchBarProps = {
  166. mri: string;
  167. onChange: (value: string) => void;
  168. tags: MetricsTag[];
  169. disabled?: boolean;
  170. query?: string;
  171. };
  172. function MetricSearchBar({tags, mri, disabled, onChange, query}: MetricSearchBarProps) {
  173. const org = useOrganization();
  174. const api = useApi();
  175. const {selection} = usePageFilters();
  176. const supportedTags: TagCollection = useMemo(
  177. () => tags.reduce((acc, tag) => ({...acc, [tag.key]: tag}), {}),
  178. [tags]
  179. );
  180. // TODO(ddm): try to use useApiQuery here
  181. const getTagValues = useCallback(
  182. async tag => {
  183. const tagsValues = await api.requestPromise(
  184. `/organizations/${org.slug}/metrics/tags/${tag.key}/`,
  185. {
  186. query: {
  187. metric: mri,
  188. useCase: getUseCaseFromMri(mri),
  189. project: selection.projects,
  190. },
  191. }
  192. );
  193. return tagsValues.filter(tv => tv.value !== '').map(tv => tv.value);
  194. },
  195. [api, mri, org.slug, selection.projects]
  196. );
  197. const handleChange = useCallback(
  198. (value: string, {validSearch} = {validSearch: true}) => {
  199. if (validSearch) {
  200. onChange(value);
  201. }
  202. },
  203. [onChange]
  204. );
  205. return (
  206. <WideSearchBar
  207. disabled={disabled}
  208. maxMenuHeight={220}
  209. organization={org}
  210. onGetTagValues={getTagValues}
  211. supportedTags={supportedTags}
  212. onClose={handleChange}
  213. onSearch={handleChange}
  214. placeholder={t('Filter by tags')}
  215. defaultQuery={query}
  216. savedSearchType={SavedSearchType.METRIC}
  217. />
  218. );
  219. }
  220. const QueryBuilderWrapper = styled('div')`
  221. display: flex;
  222. flex-direction: column;
  223. `;
  224. const QueryBuilderRow = styled('div')`
  225. padding: ${space(1)};
  226. padding-bottom: 0;
  227. `;
  228. const WideSearchBar = styled(SearchBar)`
  229. width: 100%;
  230. opacity: ${p => (p.disabled ? '0.6' : '1')};
  231. `;
  232. const WrapPageFilterBar = styled(PageFilterBar)`
  233. max-width: max-content;
  234. height: auto;
  235. flex-wrap: wrap;
  236. `;