queries.tsx 10 KB

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