metricQueryBuilder.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {CompactSelect} from 'sentry/components/compactSelect';
  4. import SearchBar from 'sentry/components/events/searchBar';
  5. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  6. import Tag from 'sentry/components/tag';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {MetricsTag, SavedSearchType, TagCollection} from 'sentry/types';
  10. import {
  11. defaultMetricDisplayType,
  12. getReadableMetricType,
  13. getUseCaseFromMri,
  14. isAllowedOp,
  15. MetricDisplayType,
  16. MetricsQuery,
  17. useMetricsMeta,
  18. useMetricsTags,
  19. } from 'sentry/utils/metrics';
  20. import useApi from 'sentry/utils/useApi';
  21. import useKeyPress from 'sentry/utils/useKeyPress';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import usePageFilters from 'sentry/utils/usePageFilters';
  24. import {MetricWidgetProps} from 'sentry/views/ddm/metricWidget';
  25. type QueryBuilderProps = {
  26. displayType: MetricDisplayType; // TODO(ddm): move display type out of the query builder
  27. metricsQuery: MetricsQuery;
  28. onChange: (data: Partial<MetricWidgetProps>) => void;
  29. projects: number[];
  30. powerUserMode?: boolean;
  31. };
  32. export function QueryBuilder({
  33. metricsQuery,
  34. projects,
  35. displayType,
  36. powerUserMode,
  37. onChange,
  38. }: QueryBuilderProps) {
  39. const meta = useMetricsMeta(projects);
  40. const mriModeKeyPressed = useKeyPress('`', undefined, true);
  41. const [mriMode, setMriMode] = useState(powerUserMode); // power user mode that shows raw MRI instead of metrics names
  42. useEffect(() => {
  43. if (mriModeKeyPressed && !powerUserMode) {
  44. setMriMode(!mriMode);
  45. }
  46. // eslint-disable-next-line react-hooks/exhaustive-deps
  47. }, [mriModeKeyPressed, powerUserMode]);
  48. const {data: tags = []} = useMetricsTags(metricsQuery.mri, projects);
  49. if (!meta) {
  50. return null;
  51. }
  52. return (
  53. <QueryBuilderWrapper>
  54. <QueryBuilderRow>
  55. <WrapPageFilterBar>
  56. <CompactSelect
  57. searchable
  58. triggerProps={{prefix: t('Metric'), size: 'sm'}}
  59. options={Object.values(meta)
  60. .filter(metric =>
  61. mriMode
  62. ? true
  63. : metric.mri.includes(':custom/') || metric.mri === metricsQuery.mri
  64. )
  65. .map(metric => ({
  66. label: mriMode ? metric.mri : metric.name,
  67. value: metric.mri,
  68. trailingItems: mriMode ? undefined : (
  69. <Fragment>
  70. <Tag tooltipText={t('Type')}>
  71. {getReadableMetricType(metric.type)}
  72. </Tag>
  73. <Tag tooltipText={t('Unit')}>{metric.unit}</Tag>
  74. </Fragment>
  75. ),
  76. }))}
  77. value={metricsQuery.mri}
  78. onChange={option => {
  79. const availableOps = meta[option.value]?.operations.filter(isAllowedOp);
  80. const selectedOp = availableOps.includes(metricsQuery.op ?? '')
  81. ? metricsQuery.op
  82. : availableOps[0];
  83. onChange({
  84. mri: option.value,
  85. op: selectedOp,
  86. groupBy: undefined,
  87. focusedSeries: undefined,
  88. });
  89. }}
  90. />
  91. <CompactSelect
  92. triggerProps={{prefix: t('Operation'), size: 'sm'}}
  93. options={
  94. meta[metricsQuery.mri]?.operations.filter(isAllowedOp).map(op => ({
  95. label: op,
  96. value: op,
  97. })) ?? []
  98. }
  99. disabled={!metricsQuery.mri}
  100. value={metricsQuery.op}
  101. onChange={option =>
  102. onChange({
  103. op: option.value,
  104. })
  105. }
  106. />
  107. <CompactSelect
  108. multiple
  109. triggerProps={{prefix: t('Group by'), size: 'sm'}}
  110. options={tags.map(tag => ({
  111. label: tag.key,
  112. value: tag.key,
  113. }))}
  114. disabled={!metricsQuery.mri}
  115. value={metricsQuery.groupBy}
  116. onChange={options =>
  117. onChange({
  118. groupBy: options.map(o => o.value),
  119. focusedSeries: undefined,
  120. })
  121. }
  122. />
  123. <CompactSelect
  124. triggerProps={{prefix: t('Display'), size: 'sm'}}
  125. value={displayType ?? defaultMetricDisplayType}
  126. options={[
  127. {
  128. value: MetricDisplayType.LINE,
  129. label: t('Line'),
  130. },
  131. {
  132. value: MetricDisplayType.AREA,
  133. label: t('Area'),
  134. },
  135. {
  136. value: MetricDisplayType.BAR,
  137. label: t('Bar'),
  138. },
  139. ]}
  140. onChange={({value}) => {
  141. onChange({displayType: value});
  142. }}
  143. />
  144. </WrapPageFilterBar>
  145. </QueryBuilderRow>
  146. <QueryBuilderRow>
  147. <MetricSearchBar
  148. tags={tags}
  149. mri={metricsQuery.mri}
  150. disabled={!metricsQuery.mri}
  151. onChange={query => onChange({query})}
  152. query={metricsQuery.query}
  153. />
  154. </QueryBuilderRow>
  155. </QueryBuilderWrapper>
  156. );
  157. }
  158. type MetricSearchBarProps = {
  159. mri: string;
  160. onChange: (value: string) => void;
  161. tags: MetricsTag[];
  162. disabled?: boolean;
  163. query?: string;
  164. };
  165. function MetricSearchBar({tags, mri, disabled, onChange, query}: MetricSearchBarProps) {
  166. const org = useOrganization();
  167. const api = useApi();
  168. const {selection} = usePageFilters();
  169. const supportedTags: TagCollection = useMemo(
  170. () => tags.reduce((acc, tag) => ({...acc, [tag.key]: tag}), {}),
  171. [tags]
  172. );
  173. // TODO(ddm): try to use useApiQuery here
  174. const getTagValues = useCallback(
  175. async tag => {
  176. const tagsValues = await api.requestPromise(
  177. `/organizations/${org.slug}/metrics/tags/${tag.key}/`,
  178. {
  179. query: {
  180. metric: mri,
  181. useCase: getUseCaseFromMri(mri),
  182. project: selection.projects,
  183. },
  184. }
  185. );
  186. return tagsValues.filter(tv => tv.value !== '').map(tv => tv.value);
  187. },
  188. [api, mri, org.slug, selection.projects]
  189. );
  190. const handleChange = useCallback(
  191. (value: string, {validSearch} = {validSearch: true}) => {
  192. if (validSearch) {
  193. onChange(value);
  194. }
  195. },
  196. [onChange]
  197. );
  198. return (
  199. <WideSearchBar
  200. disabled={disabled}
  201. maxMenuHeight={220}
  202. organization={org}
  203. onGetTagValues={getTagValues}
  204. supportedTags={supportedTags}
  205. onClose={handleChange}
  206. onSearch={handleChange}
  207. placeholder={t('Filter by tags')}
  208. defaultQuery={query}
  209. savedSearchType={SavedSearchType.METRIC}
  210. />
  211. );
  212. }
  213. const QueryBuilderWrapper = styled('div')`
  214. display: flex;
  215. flex-direction: column;
  216. `;
  217. const QueryBuilderRow = styled('div')`
  218. padding: ${space(1)};
  219. padding-bottom: 0;
  220. `;
  221. const WideSearchBar = styled(SearchBar)`
  222. width: 100%;
  223. opacity: ${p => (p.disabled ? '0.6' : '1')};
  224. `;
  225. const WrapPageFilterBar = styled(PageFilterBar)`
  226. max-width: max-content;
  227. height: auto;
  228. flex-wrap: wrap;
  229. `;