queries.tsx 11 KB

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