queries.tsx 9.2 KB

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