queryBuilder.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import {Fragment, memo, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {SelectOption} from 'sentry/components/compactSelect';
  4. import {CompactSelect} from 'sentry/components/compactSelect';
  5. import Tag from 'sentry/components/tag';
  6. import {IconLightning, IconReleases} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import type {MetricMeta, MetricsOperation, MRI} from 'sentry/types';
  10. import {
  11. getDefaultMetricDisplayType,
  12. isAllowedOp,
  13. isCustomMetric,
  14. isMeasurement,
  15. isSpanMetric,
  16. isTransactionDuration,
  17. } from 'sentry/utils/metrics';
  18. import {getReadableMetricType} from 'sentry/utils/metrics/formatters';
  19. import {formatMRI} from 'sentry/utils/metrics/mri';
  20. import type {
  21. MetricDisplayType,
  22. MetricsQuerySubject,
  23. MetricWidgetQueryParams,
  24. } from 'sentry/utils/metrics/types';
  25. import {useBreakpoints} from 'sentry/utils/metrics/useBreakpoints';
  26. import {useIncrementQueryMetric} from 'sentry/utils/metrics/useIncrementQueryMetric';
  27. import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
  28. import {useMetricsTags} from 'sentry/utils/metrics/useMetricsTags';
  29. import {middleEllipsis} from 'sentry/utils/middleEllipsis';
  30. import useKeyPress from 'sentry/utils/useKeyPress';
  31. import usePageFilters from 'sentry/utils/usePageFilters';
  32. import {MetricSearchBar} from 'sentry/views/ddm/metricSearchBar';
  33. type QueryBuilderProps = {
  34. displayType: MetricDisplayType;
  35. isEdit: boolean;
  36. metricsQuery: MetricsQuerySubject;
  37. onChange: (data: Partial<MetricWidgetQueryParams>) => void;
  38. projects: number[];
  39. fixedWidth?: boolean;
  40. powerUserMode?: boolean;
  41. };
  42. const isShownByDefault = (metric: MetricMeta) =>
  43. isCustomMetric(metric) ||
  44. isTransactionDuration(metric) ||
  45. isMeasurement(metric) ||
  46. isSpanMetric(metric);
  47. function getOpsForMRI(mri: MRI, meta: MetricMeta[]) {
  48. return meta.find(metric => metric.mri === mri)?.operations.filter(isAllowedOp) ?? [];
  49. }
  50. export const QueryBuilder = memo(function QueryBuilder({
  51. metricsQuery,
  52. projects,
  53. displayType,
  54. powerUserMode,
  55. onChange,
  56. }: QueryBuilderProps) {
  57. const pageFilters = usePageFilters();
  58. const {data: meta} = useMetricsMeta(pageFilters.selection);
  59. const mriModeKeyPressed = useKeyPress('`', undefined, true);
  60. const [mriMode, setMriMode] = useState(powerUserMode); // power user mode that shows raw MRI instead of metrics names
  61. const breakpoints = useBreakpoints();
  62. useEffect(() => {
  63. if (mriModeKeyPressed && !powerUserMode) {
  64. setMriMode(!mriMode);
  65. }
  66. // eslint-disable-next-line react-hooks/exhaustive-deps
  67. }, [mriModeKeyPressed, powerUserMode]);
  68. const {data: tags = [], isLoading: tagsIsLoading} = useMetricsTags(metricsQuery.mri, {
  69. projects,
  70. });
  71. const displayedMetrics = useMemo(() => {
  72. if (mriMode) {
  73. return meta;
  74. }
  75. const isSelected = (metric: MetricMeta) => metric.mri === metricsQuery.mri;
  76. return meta
  77. .filter(metric => isShownByDefault(metric) || isSelected(metric))
  78. .sort(metric => (isSelected(metric) ? -1 : 1));
  79. }, [meta, metricsQuery.mri, mriMode]);
  80. const selectedMeta = useMemo(() => {
  81. return meta.find(metric => metric.mri === metricsQuery.mri);
  82. }, [meta, metricsQuery.mri]);
  83. const incrementQueryMetric = useIncrementQueryMetric({
  84. displayType,
  85. op: metricsQuery.op,
  86. groupBy: metricsQuery.groupBy,
  87. query: metricsQuery.query,
  88. mri: metricsQuery.mri,
  89. });
  90. const handleMRIChange = ({value}) => {
  91. const availableOps = getOpsForMRI(value, meta);
  92. const selectedOp = availableOps.includes((metricsQuery.op ?? '') as MetricsOperation)
  93. ? metricsQuery.op
  94. : availableOps?.[0];
  95. const queryChanges = {
  96. mri: value,
  97. op: selectedOp,
  98. groupBy: undefined,
  99. displayType: getDefaultMetricDisplayType(value, selectedOp),
  100. };
  101. incrementQueryMetric('ddm.widget.metric', queryChanges);
  102. onChange({
  103. ...queryChanges,
  104. focusedSeries: undefined,
  105. });
  106. };
  107. const handleOpChange = ({value}) => {
  108. incrementQueryMetric('ddm.widget.operation', {op: value});
  109. onChange({
  110. op: value,
  111. });
  112. };
  113. const handleGroupByChange = (options: SelectOption<string>[]) => {
  114. incrementQueryMetric('ddm.widget.group', {
  115. groupBy: options.map(o => o.value),
  116. });
  117. onChange({
  118. groupBy: options.map(o => o.value),
  119. focusedSeries: undefined,
  120. });
  121. };
  122. const mriOptions = useMemo(
  123. () =>
  124. displayedMetrics.map<SelectOption<MRI>>(metric => ({
  125. label: mriMode ? metric.mri : 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: mriMode ? undefined : (
  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, mriMode]
  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. placeholder={t('Select a metric')}
  151. options={mriOptions}
  152. value={metricsQuery.mri}
  153. onChange={handleMRIChange}
  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. disabled={!metricsQuery.mri}
  166. value={metricsQuery.op}
  167. onChange={handleOpChange}
  168. />
  169. <CompactSelect
  170. multiple
  171. size="md"
  172. triggerProps={{prefix: t('Group by')}}
  173. options={tags.map(tag => ({
  174. label: tag.key,
  175. value: tag.key,
  176. trailingItems: (
  177. <Fragment>
  178. {tag.key === 'release' && <IconReleases size="xs" />}
  179. {tag.key === 'transaction' && <IconLightning size="xs" />}
  180. </Fragment>
  181. ),
  182. }))}
  183. disabled={!metricsQuery.mri || tagsIsLoading}
  184. value={metricsQuery.groupBy}
  185. onChange={handleGroupByChange}
  186. />
  187. </FlexBlock>
  188. </FlexBlock>
  189. <SearchBarWrapper>
  190. <MetricSearchBar
  191. mri={metricsQuery.mri}
  192. disabled={!metricsQuery.mri}
  193. onChange={query => {
  194. incrementQueryMetric('ddm.widget.filter', {query});
  195. onChange({query});
  196. }}
  197. query={metricsQuery.query}
  198. />
  199. </SearchBarWrapper>
  200. </QueryBuilderWrapper>
  201. );
  202. });
  203. const QueryBuilderWrapper = styled('div')`
  204. display: flex;
  205. flex-grow: 1;
  206. gap: ${space(1)};
  207. flex-wrap: wrap;
  208. `;
  209. const FlexBlock = styled('div')`
  210. display: flex;
  211. gap: ${space(1)};
  212. flex-wrap: wrap;
  213. `;
  214. const MetricSelect = styled(CompactSelect)`
  215. min-width: 200px;
  216. & > button {
  217. width: 100%;
  218. }
  219. `;
  220. const OpSelect = styled(CompactSelect)`
  221. width: 120px;
  222. & > button {
  223. width: 100%;
  224. }
  225. `;
  226. const SearchBarWrapper = styled('div')`
  227. flex: 1;
  228. min-width: 200px;
  229. `;