queryBuilder.tsx 7.2 KB

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