queryBuilder.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import {Fragment, memo, useCallback, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import uniqBy from 'lodash/uniqBy';
  4. import type {SelectOption} from 'sentry/components/compactSelect';
  5. import {CompactSelect} from 'sentry/components/compactSelect';
  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 type {MetricMeta, MetricsOperation, MRI} from 'sentry/types';
  11. import {trackAnalytics} from 'sentry/utils/analytics';
  12. import {
  13. isAllowedOp,
  14. isCustomMetric,
  15. isSpanMeasurement,
  16. isSpanSelfTime,
  17. isTransactionDuration,
  18. isTransactionMeasurement,
  19. } from 'sentry/utils/metrics';
  20. import {getReadableMetricType} from 'sentry/utils/metrics/formatters';
  21. import {formatMRI} from 'sentry/utils/metrics/mri';
  22. import type {MetricsQuery} from 'sentry/utils/metrics/types';
  23. import {useBreakpoints} from 'sentry/utils/metrics/useBreakpoints';
  24. import {useIncrementQueryMetric} from 'sentry/utils/metrics/useIncrementQueryMetric';
  25. import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
  26. import {useMetricsTags} from 'sentry/utils/metrics/useMetricsTags';
  27. import {middleEllipsis} from 'sentry/utils/middleEllipsis';
  28. import useKeyPress from 'sentry/utils/useKeyPress';
  29. import useOrganization from 'sentry/utils/useOrganization';
  30. import usePageFilters from 'sentry/utils/usePageFilters';
  31. import {MetricSearchBar} from 'sentry/views/ddm/metricSearchBar';
  32. type QueryBuilderProps = {
  33. metricsQuery: MetricsQuery;
  34. onChange: (data: Partial<MetricsQuery>) => void;
  35. projects: number[];
  36. };
  37. const isVisibleTransactionMetric = (metric: MetricMeta) =>
  38. isTransactionDuration(metric) || isTransactionMeasurement(metric);
  39. const isVisibleSpanMetric = (metric: MetricMeta) =>
  40. isSpanSelfTime(metric) || isSpanMeasurement(metric);
  41. const isShownByDefault = (metric: MetricMeta) =>
  42. isCustomMetric(metric) ||
  43. isVisibleTransactionMetric(metric) ||
  44. isVisibleSpanMetric(metric);
  45. function getOpsForMRI(mri: MRI, meta: MetricMeta[]) {
  46. return meta.find(metric => metric.mri === mri)?.operations.filter(isAllowedOp) ?? [];
  47. }
  48. function useMriMode() {
  49. const [mriMode, setMriMode] = useState(false);
  50. const mriModeKeyPressed = useKeyPress('`', undefined, true);
  51. useEffect(() => {
  52. if (mriModeKeyPressed) {
  53. setMriMode(value => !value);
  54. }
  55. // eslint-disable-next-line react-hooks/exhaustive-deps
  56. }, [mriModeKeyPressed]);
  57. return mriMode;
  58. }
  59. export const QueryBuilder = memo(function QueryBuilder({
  60. metricsQuery,
  61. projects,
  62. onChange,
  63. }: QueryBuilderProps) {
  64. const organization = useOrganization();
  65. const pageFilters = usePageFilters();
  66. const {data: meta} = useMetricsMeta(pageFilters.selection);
  67. const breakpoints = useBreakpoints();
  68. const mriMode = useMriMode();
  69. const {data: tagsData = [], isLoading: tagsIsLoading} = useMetricsTags(
  70. metricsQuery.mri,
  71. {
  72. projects,
  73. }
  74. );
  75. const tags = useMemo(() => {
  76. return uniqBy(tagsData, 'key');
  77. }, [tagsData]);
  78. const displayedMetrics = useMemo(() => {
  79. const isSelected = (metric: MetricMeta) => metric.mri === metricsQuery.mri;
  80. return meta
  81. .filter(metric => isShownByDefault(metric) || isSelected(metric))
  82. .sort(metric => (isSelected(metric) ? -1 : 1));
  83. }, [meta, metricsQuery.mri]);
  84. const selectedMeta = useMemo(() => {
  85. return meta.find(metric => metric.mri === metricsQuery.mri);
  86. }, [meta, metricsQuery.mri]);
  87. const incrementQueryMetric = useIncrementQueryMetric({
  88. ...metricsQuery,
  89. });
  90. const handleMRIChange = useCallback(
  91. ({value}) => {
  92. const availableOps = getOpsForMRI(value, meta);
  93. const selectedOp = availableOps.includes(
  94. (metricsQuery.op ?? '') as MetricsOperation
  95. )
  96. ? metricsQuery.op
  97. : availableOps?.[0];
  98. const queryChanges = {
  99. mri: value,
  100. op: selectedOp,
  101. groupBy: undefined,
  102. };
  103. trackAnalytics('ddm.widget.metric', {organization});
  104. incrementQueryMetric('ddm.widget.metric', queryChanges);
  105. onChange(queryChanges);
  106. },
  107. [incrementQueryMetric, meta, metricsQuery.op, onChange, organization]
  108. );
  109. const handleOpChange = useCallback(
  110. ({value}) => {
  111. trackAnalytics('ddm.widget.operation', {organization});
  112. incrementQueryMetric('ddm.widget.operation', {op: value});
  113. onChange({
  114. op: value,
  115. });
  116. },
  117. [incrementQueryMetric, onChange, organization]
  118. );
  119. const handleGroupByChange = useCallback(
  120. (options: SelectOption<string>[]) => {
  121. trackAnalytics('ddm.widget.group', {organization});
  122. incrementQueryMetric('ddm.widget.group', {
  123. groupBy: options.map(o => o.value),
  124. });
  125. onChange({
  126. groupBy: options.map(o => o.value),
  127. });
  128. },
  129. [incrementQueryMetric, onChange, organization]
  130. );
  131. const handleQueryChange = useCallback(
  132. (query: string) => {
  133. trackAnalytics('ddm.widget.filter', {organization});
  134. incrementQueryMetric('ddm.widget.filter', {query});
  135. onChange({query});
  136. },
  137. [incrementQueryMetric, onChange, organization]
  138. );
  139. const mriOptions = useMemo(
  140. () =>
  141. displayedMetrics.map<SelectOption<MRI>>(metric => ({
  142. label: mriMode ? metric.mri : formatMRI(metric.mri),
  143. // enable search by mri, name, unit (millisecond), type (c:), and readable type (counter)
  144. textValue: `${metric.mri}${getReadableMetricType(metric.type)}`,
  145. value: metric.mri,
  146. trailingItems: mriMode ? undefined : (
  147. <Fragment>
  148. <Tag tooltipText={t('Type')}>{getReadableMetricType(metric.type)}</Tag>
  149. <Tag tooltipText={t('Unit')}>{metric.unit}</Tag>
  150. </Fragment>
  151. ),
  152. })),
  153. [displayedMetrics, mriMode]
  154. );
  155. return (
  156. <QueryBuilderWrapper>
  157. <FlexBlock>
  158. <MetricSelect
  159. searchable
  160. sizeLimit={100}
  161. size="md"
  162. triggerLabel={middleEllipsis(
  163. formatMRI(metricsQuery.mri) ?? '',
  164. breakpoints.large ? (breakpoints.xlarge ? 70 : 45) : 30,
  165. /\.|-|_/
  166. )}
  167. options={mriOptions}
  168. value={metricsQuery.mri}
  169. onChange={handleMRIChange}
  170. shouldUseVirtualFocus
  171. />
  172. <FlexBlock>
  173. <OpSelect
  174. size="md"
  175. triggerProps={{prefix: t('Op')}}
  176. options={
  177. selectedMeta?.operations.filter(isAllowedOp).map(op => ({
  178. label: op,
  179. value: op,
  180. })) ?? []
  181. }
  182. triggerLabel={metricsQuery.op}
  183. disabled={!selectedMeta}
  184. value={metricsQuery.op}
  185. onChange={handleOpChange}
  186. />
  187. <CompactSelect
  188. multiple
  189. size="md"
  190. triggerProps={{prefix: t('Group by')}}
  191. options={tags.map(tag => ({
  192. label: tag.key,
  193. value: tag.key,
  194. trailingItems: (
  195. <Fragment>
  196. {tag.key === 'release' && <IconReleases size="xs" />}
  197. {tag.key === 'transaction' && <IconLightning size="xs" />}
  198. </Fragment>
  199. ),
  200. }))}
  201. disabled={!metricsQuery.mri || tagsIsLoading}
  202. value={metricsQuery.groupBy}
  203. onChange={handleGroupByChange}
  204. />
  205. </FlexBlock>
  206. </FlexBlock>
  207. <SearchBarWrapper>
  208. <MetricSearchBar
  209. mri={metricsQuery.mri}
  210. disabled={!metricsQuery.mri}
  211. onChange={handleQueryChange}
  212. query={metricsQuery.query}
  213. blockedTags={selectedMeta?.blockingStatus?.flatMap(s => s.blockedTags) ?? []}
  214. />
  215. </SearchBarWrapper>
  216. </QueryBuilderWrapper>
  217. );
  218. });
  219. const QueryBuilderWrapper = styled('div')`
  220. display: flex;
  221. flex-grow: 1;
  222. gap: ${space(1)};
  223. flex-wrap: wrap;
  224. `;
  225. const FlexBlock = styled('div')`
  226. display: flex;
  227. gap: ${space(1)};
  228. flex-wrap: wrap;
  229. `;
  230. const MetricSelect = styled(CompactSelect)`
  231. min-width: 200px;
  232. & > button {
  233. width: 100%;
  234. }
  235. `;
  236. const OpSelect = styled(CompactSelect)`
  237. width: 120px;
  238. & > button {
  239. width: 100%;
  240. }
  241. `;
  242. const SearchBarWrapper = styled('div')`
  243. flex: 1;
  244. min-width: 200px;
  245. `;