queryBuilder.tsx 7.3 KB

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