queryBuilder.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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. displayType: getWidgetDisplayType(option.value, selectedOp),
  90. });
  91. }}
  92. />
  93. <CompactSelect
  94. triggerProps={{prefix: t('Op'), size: 'sm'}}
  95. options={
  96. meta[metricsQuery.mri]?.operations.filter(isAllowedOp).map(op => ({
  97. label: op,
  98. value: op,
  99. })) ?? []
  100. }
  101. disabled={!metricsQuery.mri}
  102. value={metricsQuery.op}
  103. onChange={option =>
  104. onChange({
  105. op: option.value,
  106. })
  107. }
  108. />
  109. <CompactSelect
  110. multiple
  111. triggerProps={{prefix: t('Group by'), size: 'sm'}}
  112. options={tags.map(tag => ({
  113. label: tag.key,
  114. value: tag.key,
  115. trailingItems: (
  116. <Fragment>
  117. {tag.key === 'release' && <IconReleases size="xs" />}
  118. {tag.key === 'transaction' && <IconLightning size="xs" />}
  119. </Fragment>
  120. ),
  121. }))}
  122. disabled={!metricsQuery.mri}
  123. value={metricsQuery.groupBy}
  124. onChange={options =>
  125. onChange({
  126. groupBy: options.map(o => o.value),
  127. focusedSeries: undefined,
  128. })
  129. }
  130. />
  131. <CompactSelect
  132. triggerProps={{prefix: t('Display'), size: 'sm'}}
  133. value={displayType ?? defaultMetricDisplayType}
  134. options={[
  135. {
  136. value: MetricDisplayType.LINE,
  137. label: t('Line'),
  138. },
  139. {
  140. value: MetricDisplayType.AREA,
  141. label: t('Area'),
  142. },
  143. {
  144. value: MetricDisplayType.BAR,
  145. label: t('Bar'),
  146. },
  147. ]}
  148. onChange={({value}) => {
  149. onChange({displayType: value});
  150. }}
  151. />
  152. </WrapPageFilterBar>
  153. </QueryBuilderRow>
  154. <QueryBuilderRow>
  155. <MetricSearchBar
  156. tags={tags}
  157. mri={metricsQuery.mri}
  158. disabled={!metricsQuery.mri}
  159. onChange={query => onChange({query})}
  160. query={metricsQuery.query}
  161. />
  162. </QueryBuilderRow>
  163. </QueryBuilderWrapper>
  164. );
  165. }
  166. type MetricSearchBarProps = {
  167. mri: string;
  168. onChange: (value: string) => void;
  169. tags: MetricsTag[];
  170. disabled?: boolean;
  171. query?: string;
  172. };
  173. function MetricSearchBar({tags, mri, disabled, onChange, query}: MetricSearchBarProps) {
  174. const org = useOrganization();
  175. const api = useApi();
  176. const {selection} = usePageFilters();
  177. const supportedTags: TagCollection = useMemo(
  178. () => tags.reduce((acc, tag) => ({...acc, [tag.key]: tag}), {}),
  179. [tags]
  180. );
  181. // TODO(ddm): try to use useApiQuery here
  182. const getTagValues = useCallback(
  183. async tag => {
  184. const tagsValues = await api.requestPromise(
  185. `/organizations/${org.slug}/metrics/tags/${tag.key}/`,
  186. {
  187. query: {
  188. metric: mri,
  189. useCase: getUseCaseFromMri(mri),
  190. project: selection.projects,
  191. },
  192. }
  193. );
  194. return tagsValues.filter(tv => tv.value !== '').map(tv => tv.value);
  195. },
  196. [api, mri, org.slug, selection.projects]
  197. );
  198. const handleChange = useCallback(
  199. (value: string, {validSearch} = {validSearch: true}) => {
  200. if (validSearch) {
  201. onChange(value);
  202. }
  203. },
  204. [onChange]
  205. );
  206. return (
  207. <WideSearchBar
  208. disabled={disabled}
  209. maxMenuHeight={220}
  210. organization={org}
  211. onGetTagValues={getTagValues}
  212. supportedTags={supportedTags}
  213. onClose={handleChange}
  214. onSearch={handleChange}
  215. placeholder={t('Filter by tags')}
  216. query={query}
  217. savedSearchType={SavedSearchType.METRIC}
  218. />
  219. );
  220. }
  221. function getWidgetDisplayType(
  222. mri: MetricsQuery['mri'],
  223. op: MetricsQuery['op']
  224. ): MetricDisplayType {
  225. if (mri?.startsWith('c') || op === 'count') {
  226. return MetricDisplayType.BAR;
  227. }
  228. return MetricDisplayType.LINE;
  229. }
  230. const QueryBuilderWrapper = styled('div')`
  231. display: flex;
  232. flex-direction: column;
  233. `;
  234. const QueryBuilderRow = styled('div')`
  235. padding: ${space(1)};
  236. padding-bottom: 0;
  237. `;
  238. const WideSearchBar = styled(SearchBar)`
  239. width: 100%;
  240. opacity: ${p => (p.disabled ? '0.6' : '1')};
  241. `;
  242. const WrapPageFilterBar = styled(PageFilterBar)`
  243. max-width: max-content;
  244. height: auto;
  245. flex-wrap: wrap;
  246. `;