queryBuilder.tsx 7.1 KB

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