queries.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import {Fragment, useCallback, useLayoutEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as echarts from 'echarts/core';
  4. import {Button} from 'sentry/components/button';
  5. import SwitchButton from 'sentry/components/switchButton';
  6. import {IconAdd} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {
  10. type MetricFormulaWidgetParams,
  11. MetricQueryType,
  12. type MetricQueryWidgetParams,
  13. type MetricWidgetQueryParams,
  14. } from 'sentry/utils/metrics/types';
  15. import usePageFilters from 'sentry/utils/usePageFilters';
  16. import {DDM_CHART_GROUP} from 'sentry/views/ddm/constants';
  17. import {useDDMContext} from 'sentry/views/ddm/context';
  18. import {FormulaInput} from 'sentry/views/ddm/formulaInput';
  19. import {MetricFormulaContextMenu} from 'sentry/views/ddm/metricFormulaContextMenu';
  20. import {MetricQueryContextMenu} from 'sentry/views/ddm/metricQueryContextMenu';
  21. import {QueryBuilder} from 'sentry/views/ddm/queryBuilder';
  22. import {getQuerySymbol, QuerySymbol} from 'sentry/views/ddm/querySymbol';
  23. export function Queries() {
  24. const {
  25. widgets,
  26. updateWidget,
  27. setSelectedWidgetIndex,
  28. showQuerySymbols,
  29. selectedWidgetIndex,
  30. isMultiChartMode,
  31. setIsMultiChartMode,
  32. addWidget,
  33. } = useDDMContext();
  34. const {selection} = usePageFilters();
  35. // Make sure all charts are connected to the same group whenever the widgets definition changes
  36. useLayoutEffect(() => {
  37. echarts.connect(DDM_CHART_GROUP);
  38. }, [widgets]);
  39. const handleChange = useCallback(
  40. (index: number, widget: Partial<MetricWidgetQueryParams>) => {
  41. updateWidget(index, widget);
  42. },
  43. [updateWidget]
  44. );
  45. const [querySymbols, formulaSymbols] = useMemo(() => {
  46. const querySymbolSet = new Set<string>();
  47. const formulaSymbolSet = new Set<string>();
  48. for (const widget of widgets) {
  49. const symbol = getQuerySymbol(widget.id);
  50. if (widget.type === MetricQueryType.QUERY) {
  51. querySymbolSet.add(symbol);
  52. } else {
  53. formulaSymbolSet.add(symbol);
  54. }
  55. }
  56. return [querySymbolSet, formulaSymbolSet];
  57. }, [widgets]);
  58. return (
  59. <Fragment>
  60. <Wrapper showQuerySymbols={showQuerySymbols}>
  61. {widgets.map((widget, index) => (
  62. <Row key={widget.id} onFocusCapture={() => setSelectedWidgetIndex(index)}>
  63. {widget.type === MetricQueryType.QUERY ? (
  64. <Query
  65. widget={widget}
  66. onChange={handleChange}
  67. index={index}
  68. projects={selection.projects}
  69. symbol={
  70. showQuerySymbols && (
  71. <StyledQuerySymbol
  72. queryId={widget.id}
  73. isClickable={isMultiChartMode}
  74. isSelected={index === selectedWidgetIndex}
  75. onClick={() => setSelectedWidgetIndex(index)}
  76. role={isMultiChartMode ? 'button' : undefined}
  77. aria-label={t('Select query')}
  78. />
  79. )
  80. }
  81. contextMenu={
  82. <MetricQueryContextMenu
  83. displayType={widget.displayType}
  84. widgetIndex={index}
  85. metricsQuery={{
  86. mri: widget.mri,
  87. query: widget.query,
  88. op: widget.op,
  89. groupBy: widget.groupBy,
  90. }}
  91. />
  92. }
  93. />
  94. ) : (
  95. <Formula
  96. availableVariables={querySymbols}
  97. formulaVariables={formulaSymbols}
  98. onChange={handleChange}
  99. index={index}
  100. widget={widget}
  101. symbol={
  102. showQuerySymbols && (
  103. <StyledQuerySymbol
  104. queryId={widget.id}
  105. isClickable={isMultiChartMode}
  106. isSelected={index === selectedWidgetIndex}
  107. onClick={() => setSelectedWidgetIndex(index)}
  108. role={isMultiChartMode ? 'button' : undefined}
  109. aria-label={t('Select query')}
  110. />
  111. )
  112. }
  113. contextMenu={<MetricFormulaContextMenu widgetIndex={index} />}
  114. />
  115. )}
  116. </Row>
  117. ))}
  118. </Wrapper>
  119. <ButtonBar addQuerySymbolSpacing={showQuerySymbols}>
  120. <Button
  121. size="sm"
  122. icon={<IconAdd isCircled />}
  123. onClick={() => addWidget(MetricQueryType.QUERY)}
  124. >
  125. {t('Add query')}
  126. </Button>
  127. <Button
  128. size="sm"
  129. icon={<IconAdd isCircled />}
  130. onClick={() => addWidget(MetricQueryType.FORMULA)}
  131. >
  132. {t('Add equation')}
  133. </Button>
  134. <SwitchWrapper>
  135. {t('One chart per query')}
  136. <SwitchButton
  137. isActive={isMultiChartMode}
  138. toggle={() => setIsMultiChartMode(!isMultiChartMode)}
  139. />
  140. </SwitchWrapper>
  141. </ButtonBar>
  142. </Fragment>
  143. );
  144. }
  145. interface QueryProps {
  146. index: number;
  147. onChange: (index: number, data: Partial<MetricWidgetQueryParams>) => void;
  148. projects: number[];
  149. widget: MetricQueryWidgetParams;
  150. contextMenu?: React.ReactNode;
  151. symbol?: React.ReactNode;
  152. }
  153. export function Query({
  154. widget,
  155. projects,
  156. onChange,
  157. contextMenu,
  158. symbol,
  159. index,
  160. }: QueryProps) {
  161. const metricsQuery = useMemo(
  162. () => ({
  163. mri: widget.mri,
  164. op: widget.op,
  165. groupBy: widget.groupBy,
  166. query: widget.query,
  167. }),
  168. [widget.groupBy, widget.mri, widget.op, widget.query]
  169. );
  170. const handleChange = useCallback(
  171. (data: Partial<MetricWidgetQueryParams>) => {
  172. onChange(index, data);
  173. },
  174. [index, onChange]
  175. );
  176. return (
  177. <QueryWrapper hasSymbol={!!symbol}>
  178. {symbol}
  179. <QueryBuilder
  180. onChange={handleChange}
  181. metricsQuery={metricsQuery}
  182. displayType={widget.displayType}
  183. isEdit
  184. projects={projects}
  185. />
  186. {contextMenu}
  187. </QueryWrapper>
  188. );
  189. }
  190. interface FormulaProps {
  191. availableVariables: Set<string>;
  192. formulaVariables: Set<string>;
  193. index: number;
  194. onChange: (index: number, data: Partial<MetricWidgetQueryParams>) => void;
  195. widget: MetricFormulaWidgetParams;
  196. contextMenu?: React.ReactNode;
  197. symbol?: React.ReactNode;
  198. }
  199. export function Formula({
  200. availableVariables,
  201. formulaVariables,
  202. index,
  203. widget,
  204. onChange,
  205. contextMenu,
  206. symbol,
  207. }: FormulaProps) {
  208. const handleChange = useCallback(
  209. (formula: string) => {
  210. onChange(index, {formula});
  211. },
  212. [index, onChange]
  213. );
  214. return (
  215. <QueryWrapper hasSymbol={!!symbol}>
  216. {symbol}
  217. <FormulaInput
  218. availableVariables={availableVariables}
  219. formulaVariables={formulaVariables}
  220. value={widget.formula}
  221. onChange={handleChange}
  222. />
  223. {contextMenu}
  224. </QueryWrapper>
  225. );
  226. }
  227. const QueryWrapper = styled('div')<{hasSymbol: boolean}>`
  228. display: grid;
  229. gap: ${space(1)};
  230. padding-bottom: ${space(1)};
  231. grid-template-columns: 1fr max-content;
  232. ${p => p.hasSymbol && `grid-template-columns: min-content 1fr max-content;`}
  233. `;
  234. const StyledQuerySymbol = styled(QuerySymbol)<{isClickable: boolean}>`
  235. margin-top: 10px;
  236. ${p => p.isClickable && `cursor: pointer;`}
  237. `;
  238. const Wrapper = styled('div')<{showQuerySymbols: boolean}>``;
  239. const Row = styled('div')`
  240. display: contents;
  241. `;
  242. const ButtonBar = styled('div')<{addQuerySymbolSpacing: boolean}>`
  243. align-items: center;
  244. display: flex;
  245. padding-bottom: ${space(2)};
  246. padding-top: ${space(1)};
  247. gap: ${space(2)};
  248. ${p =>
  249. p.addQuerySymbolSpacing &&
  250. `
  251. padding-left: ${space(1)};
  252. margin-left: ${space(2)};
  253. `}
  254. `;
  255. const SwitchWrapper = styled('label')`
  256. display: flex;
  257. margin: 0;
  258. align-items: center;
  259. gap: ${space(1)};
  260. `;