queryBuilder.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import {Fragment, memo, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {navigateTo} from 'sentry/actionCreators/navigation';
  4. import {Button} from 'sentry/components/button';
  5. import {CompactSelect, SelectOption} from 'sentry/components/compactSelect';
  6. import Tag from 'sentry/components/tag';
  7. import {IconLightning, IconReleases, IconSettings} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {MetricMeta, MetricsOperation, MRI} from 'sentry/types';
  11. import {
  12. getDefaultMetricDisplayType,
  13. getReadableMetricType,
  14. isAllowedOp,
  15. isCustomMetric,
  16. isMeasurement,
  17. isTransactionDuration,
  18. MetricDisplayType,
  19. MetricsQuerySubject,
  20. MetricWidgetQueryParams,
  21. } from 'sentry/utils/metrics';
  22. import {formatMRI} from 'sentry/utils/metrics/mri';
  23. import {useIncrementQueryMetric} from 'sentry/utils/metrics/useIncrementQueryMetric';
  24. import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
  25. import {useMetricsTags} from 'sentry/utils/metrics/useMetricsTags';
  26. import {middleEllipsis} from 'sentry/utils/middleEllipsis';
  27. import useKeyPress from 'sentry/utils/useKeyPress';
  28. import useRouter from 'sentry/utils/useRouter';
  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. powerUserMode?: boolean;
  37. };
  38. const isShownByDefault = (metric: MetricMeta) =>
  39. isMeasurement(metric) || isCustomMetric(metric) || isTransactionDuration(metric);
  40. function getOpsForMRI(mri: MRI, meta: MetricMeta[]) {
  41. return meta.find(metric => metric.mri === mri)?.operations.filter(isAllowedOp) ?? [];
  42. }
  43. export const QueryBuilder = memo(function QueryBuilder({
  44. metricsQuery,
  45. projects,
  46. displayType,
  47. powerUserMode,
  48. onChange,
  49. }: QueryBuilderProps) {
  50. const {data: meta, isLoading: isMetaLoading} = useMetricsMeta(projects);
  51. const router = useRouter();
  52. const mriModeKeyPressed = useKeyPress('`', undefined, true);
  53. const [mriMode, setMriMode] = useState(powerUserMode); // power user mode that shows raw MRI instead of metrics names
  54. useEffect(() => {
  55. if (mriModeKeyPressed && !powerUserMode) {
  56. setMriMode(!mriMode);
  57. }
  58. // eslint-disable-next-line react-hooks/exhaustive-deps
  59. }, [mriModeKeyPressed, powerUserMode]);
  60. const {data: tags = []} = useMetricsTags(metricsQuery.mri, projects);
  61. const displayedMetrics = useMemo(() => {
  62. if (mriMode) {
  63. return meta;
  64. }
  65. const isSelected = (metric: MetricMeta) => metric.mri === metricsQuery.mri;
  66. return meta
  67. .filter(metric => isShownByDefault(metric) || isSelected(metric))
  68. .sort(metric => (isSelected(metric) ? -1 : 1));
  69. }, [meta, metricsQuery.mri, mriMode]);
  70. const selectedMeta = useMemo(() => {
  71. return meta.find(metric => metric.mri === metricsQuery.mri);
  72. }, [meta, metricsQuery.mri]);
  73. // Reset the query data if the selected metric is no longer available
  74. useEffect(() => {
  75. if (
  76. metricsQuery.mri &&
  77. !isMetaLoading &&
  78. !displayedMetrics.find(metric => metric.mri === metricsQuery.mri)
  79. ) {
  80. onChange({mri: '' as MRI, op: '', groupBy: []});
  81. }
  82. }, [isMetaLoading, displayedMetrics, metricsQuery.mri, onChange]);
  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}: SelectOption<MRI>) => {
  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}: SelectOption<MetricsOperation>) => {
  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
  130. ? undefined
  131. : ({isFocused}) => (
  132. <Fragment>
  133. {isFocused && isCustomMetric({mri: metric.mri}) && (
  134. <Button
  135. borderless
  136. size="zero"
  137. icon={<IconSettings />}
  138. aria-label={t('Metric Settings')}
  139. onPointerDown={() => {
  140. // not using onClick to beat the dropdown listener
  141. navigateTo(
  142. `/settings/projects/:projectId/metrics/${encodeURIComponent(
  143. metric.mri
  144. )}`,
  145. router
  146. );
  147. }}
  148. />
  149. )}
  150. <Tag tooltipText={t('Type')}>{getReadableMetricType(metric.type)}</Tag>
  151. <Tag tooltipText={t('Unit')}>{metric.unit}</Tag>
  152. </Fragment>
  153. ),
  154. })),
  155. [displayedMetrics, mriMode, router]
  156. );
  157. return (
  158. <QueryBuilderWrapper>
  159. <FlexBlock>
  160. <CompactSelect
  161. searchable
  162. sizeLimit={100}
  163. size="md"
  164. triggerLabel={middleEllipsis(formatMRI(metricsQuery.mri) ?? '', 35, /\.|-|_/)}
  165. options={mriOptions}
  166. value={metricsQuery.mri}
  167. onChange={handleMRIChange}
  168. />
  169. <FlexBlock>
  170. <CompactSelect
  171. size="md"
  172. triggerProps={{prefix: t('Op')}}
  173. options={
  174. selectedMeta?.operations.filter(isAllowedOp).map(op => ({
  175. label: op,
  176. value: op,
  177. })) ?? []
  178. }
  179. disabled={!metricsQuery.mri}
  180. value={metricsQuery.op as MetricsOperation}
  181. onChange={handleOpChange}
  182. />
  183. <CompactSelect
  184. multiple
  185. size="md"
  186. triggerProps={{prefix: t('Group by')}}
  187. options={tags.map(tag => ({
  188. label: tag.key,
  189. value: tag.key,
  190. trailingItems: (
  191. <Fragment>
  192. {tag.key === 'release' && <IconReleases size="xs" />}
  193. {tag.key === 'transaction' && <IconLightning size="xs" />}
  194. </Fragment>
  195. ),
  196. }))}
  197. disabled={!metricsQuery.mri}
  198. value={metricsQuery.groupBy}
  199. onChange={handleGroupByChange}
  200. />
  201. </FlexBlock>
  202. </FlexBlock>
  203. <SearchBarWrapper>
  204. <MetricSearchBar
  205. // TODO(aknaus): clean up projectId type in ddm
  206. projectIds={projects.map(id => id.toString())}
  207. mri={metricsQuery.mri}
  208. disabled={!metricsQuery.mri}
  209. onChange={query => {
  210. incrementQueryMetric('ddm.widget.filter', {query});
  211. onChange({query});
  212. }}
  213. query={metricsQuery.query}
  214. />
  215. </SearchBarWrapper>
  216. </QueryBuilderWrapper>
  217. );
  218. });
  219. const QueryBuilderWrapper = styled('div')`
  220. display: flex;
  221. flex-grow: 1;
  222. gap: ${space(1)};
  223. flex-wrap: wrap;
  224. `;
  225. const FlexBlock = styled('div')`
  226. display: flex;
  227. gap: ${space(1)};
  228. flex-wrap: wrap;
  229. `;
  230. const SearchBarWrapper = styled('div')`
  231. flex: 1;
  232. min-width: 300px;
  233. `;