queryBuilder.tsx 7.3 KB

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