queries.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {Fragment, useCallback, useLayoutEffect} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as echarts from 'echarts/core';
  4. import {Button} from 'sentry/components/button';
  5. import Input from 'sentry/components/input';
  6. import SwitchButton from 'sentry/components/switchButton';
  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 MetricWidgetQueryParams,
  15. } from 'sentry/utils/metrics/types';
  16. import useOrganization from 'sentry/utils/useOrganization';
  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 {MetricFormulaContextMenu} from 'sentry/views/ddm/metricFormulaContextMenu';
  21. import {MetricQueryContextMenu} from 'sentry/views/ddm/metricQueryContextMenu';
  22. import {QueryBuilder} from 'sentry/views/ddm/queryBuilder';
  23. import {QuerySymbol} from 'sentry/views/ddm/querySymbol';
  24. export function Queries() {
  25. const {
  26. widgets,
  27. updateWidget,
  28. setSelectedWidgetIndex,
  29. showQuerySymbols,
  30. selectedWidgetIndex,
  31. isMultiChartMode,
  32. setIsMultiChartMode,
  33. addWidget,
  34. } = useDDMContext();
  35. const {selection} = usePageFilters();
  36. const organization = useOrganization();
  37. // Make sure all charts are connected to the same group whenever the widgets definition changes
  38. useLayoutEffect(() => {
  39. echarts.connect(DDM_CHART_GROUP);
  40. }, [widgets]);
  41. const handleChange = useCallback(
  42. (index: number, widget: Partial<MetricWidgetQueryParams>) => {
  43. updateWidget(index, widget);
  44. },
  45. [updateWidget]
  46. );
  47. return (
  48. <Fragment>
  49. <Wrapper showQuerySymbols={showQuerySymbols}>
  50. {widgets.map((widget, index) => (
  51. <Row key={widget.id} onFocusCapture={() => setSelectedWidgetIndex(index)}>
  52. {widget.type === MetricQueryType.QUERY ? (
  53. <Query
  54. widget={widget}
  55. onChange={data => handleChange(index, data)}
  56. projects={selection.projects}
  57. symbol={
  58. showQuerySymbols && (
  59. <StyledQuerySymbol
  60. queryId={widget.id}
  61. isClickable={isMultiChartMode}
  62. isSelected={index === selectedWidgetIndex}
  63. onClick={() => setSelectedWidgetIndex(index)}
  64. role={isMultiChartMode ? 'button' : undefined}
  65. aria-label={t('Select query')}
  66. />
  67. )
  68. }
  69. contextMenu={
  70. <MetricQueryContextMenu
  71. displayType={widget.displayType}
  72. widgetIndex={index}
  73. metricsQuery={{
  74. mri: widget.mri,
  75. query: widget.query,
  76. op: widget.op,
  77. groupBy: widget.groupBy,
  78. }}
  79. />
  80. }
  81. />
  82. ) : (
  83. <Formula
  84. onChange={data => handleChange(index, data)}
  85. widget={widget}
  86. symbol={
  87. showQuerySymbols && (
  88. <StyledQuerySymbol
  89. queryId={widget.id}
  90. isClickable={isMultiChartMode}
  91. isSelected={index === selectedWidgetIndex}
  92. onClick={() => setSelectedWidgetIndex(index)}
  93. role={isMultiChartMode ? 'button' : undefined}
  94. aria-label={t('Select query')}
  95. />
  96. )
  97. }
  98. contextMenu={<MetricFormulaContextMenu widgetIndex={index} />}
  99. />
  100. )}
  101. </Row>
  102. ))}
  103. </Wrapper>
  104. <ButtonBar addQuerySymbolSpacing={showQuerySymbols}>
  105. <Button
  106. size="sm"
  107. icon={<IconAdd isCircled />}
  108. onClick={() => addWidget(MetricQueryType.QUERY)}
  109. >
  110. Add query
  111. </Button>
  112. {organization.features.includes('ddm-formulas') && (
  113. <Button
  114. size="sm"
  115. icon={<IconAdd isCircled />}
  116. onClick={() => addWidget(MetricQueryType.FORMULA)}
  117. >
  118. Add formula
  119. </Button>
  120. )}
  121. <SwitchWrapper>
  122. {t('One chart per query')}
  123. <SwitchButton
  124. isActive={isMultiChartMode}
  125. toggle={() => setIsMultiChartMode(!isMultiChartMode)}
  126. />
  127. </SwitchWrapper>
  128. </ButtonBar>
  129. </Fragment>
  130. );
  131. }
  132. interface QueryProps {
  133. onChange: (data: Partial<MetricWidgetQueryParams>) => void;
  134. projects: number[];
  135. widget: MetricQueryWidgetParams;
  136. contextMenu?: React.ReactNode;
  137. symbol?: React.ReactNode;
  138. }
  139. export function Query({widget, projects, onChange, contextMenu, symbol}: QueryProps) {
  140. return (
  141. <QueryWrapper hasSymbol={!!symbol}>
  142. {symbol}
  143. <QueryBuilder
  144. onChange={onChange}
  145. metricsQuery={{
  146. mri: widget.mri,
  147. op: widget.op,
  148. groupBy: widget.groupBy,
  149. query: widget.query,
  150. }}
  151. displayType={widget.displayType}
  152. isEdit
  153. projects={projects}
  154. />
  155. {contextMenu}
  156. </QueryWrapper>
  157. );
  158. }
  159. interface FormulaProps {
  160. onChange: (data: Partial<MetricWidgetQueryParams>) => void;
  161. widget: MetricFormulaWidgetParams;
  162. contextMenu?: React.ReactNode;
  163. symbol?: React.ReactNode;
  164. }
  165. export function Formula({widget, onChange, contextMenu, symbol}: FormulaProps) {
  166. return (
  167. <QueryWrapper hasSymbol={!!symbol}>
  168. {symbol}
  169. <Input value={widget.formula} onChange={e => onChange({formula: e.target.value})} />
  170. {contextMenu}
  171. </QueryWrapper>
  172. );
  173. }
  174. const QueryWrapper = styled('div')<{hasSymbol: boolean}>`
  175. display: grid;
  176. gap: ${space(1)};
  177. padding-bottom: ${space(1)};
  178. grid-template-columns: 1fr max-content;
  179. ${p => p.hasSymbol && `grid-template-columns: min-content 1fr max-content;`}
  180. `;
  181. const StyledQuerySymbol = styled(QuerySymbol)<{isClickable: boolean}>`
  182. margin-top: 10px;
  183. ${p => p.isClickable && `cursor: pointer;`}
  184. `;
  185. const Wrapper = styled('div')<{showQuerySymbols: boolean}>``;
  186. const Row = styled('div')`
  187. display: contents;
  188. `;
  189. const ButtonBar = styled('div')<{addQuerySymbolSpacing: boolean}>`
  190. align-items: center;
  191. display: flex;
  192. padding-bottom: ${space(2)};
  193. padding-top: ${space(1)};
  194. gap: ${space(2)};
  195. ${p =>
  196. p.addQuerySymbolSpacing &&
  197. `
  198. padding-left: ${space(1)};
  199. margin-left: ${space(2)};
  200. `}
  201. `;
  202. const SwitchWrapper = styled('label')`
  203. display: flex;
  204. margin: 0;
  205. align-items: center;
  206. gap: ${space(1)};
  207. `;