queryBuilder.tsx 7.4 KB

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