queries.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import {useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {navigateTo} from 'sentry/actionCreators/navigation';
  4. import {Button} from 'sentry/components/button';
  5. import type {MenuItemProps} from 'sentry/components/dropdownMenu';
  6. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  7. import {IconAdd, IconClose, IconEllipsis, IconSettings, IconSiren} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {isCustomMetric} from 'sentry/utils/metrics';
  11. import useOrganization from 'sentry/utils/useOrganization';
  12. import usePageFilters from 'sentry/utils/usePageFilters';
  13. import useRouter from 'sentry/utils/useRouter';
  14. import type {
  15. DashboardMetricsEquation,
  16. DashboardMetricsQuery,
  17. } from 'sentry/views/dashboards/metrics/types';
  18. import {
  19. filterEquationsByDisplayType,
  20. filterQueriesByDisplayType,
  21. } from 'sentry/views/dashboards/metrics/utils';
  22. import {DisplayType} from 'sentry/views/dashboards/types';
  23. import {EquationSymbol} from 'sentry/views/ddm/equationSymbol copy';
  24. import {FormulaInput} from 'sentry/views/ddm/formulaInput';
  25. import {getCreateAlert} from 'sentry/views/ddm/metricQueryContextMenu';
  26. import {QueryBuilder} from 'sentry/views/ddm/queryBuilder';
  27. import {getQuerySymbol, QuerySymbol} from 'sentry/views/ddm/querySymbol';
  28. interface Props {
  29. addEquation: () => void;
  30. addQuery: () => void;
  31. displayType: DisplayType;
  32. metricEquations: DashboardMetricsEquation[];
  33. metricQueries: DashboardMetricsQuery[];
  34. onEquationChange: (data: Partial<DashboardMetricsEquation>, index: number) => void;
  35. onQueryChange: (data: Partial<DashboardMetricsQuery>, index: number) => void;
  36. removeEquation: (index: number) => void;
  37. removeQuery: (index: number) => void;
  38. }
  39. export function Queries({
  40. displayType,
  41. metricQueries,
  42. metricEquations,
  43. onQueryChange,
  44. onEquationChange,
  45. addQuery,
  46. addEquation,
  47. removeQuery,
  48. removeEquation,
  49. }: Props) {
  50. const {selection} = usePageFilters();
  51. const availableVariables = useMemo(
  52. () => new Set(metricQueries.map(query => getQuerySymbol(query.id))),
  53. [metricQueries]
  54. );
  55. const filteredQueries = useMemo(
  56. () => filterQueriesByDisplayType(metricQueries, displayType),
  57. [metricQueries, displayType]
  58. );
  59. const filteredEquations = useMemo(
  60. () => filterEquationsByDisplayType(metricEquations, displayType),
  61. [metricEquations, displayType]
  62. );
  63. const showQuerySymbols = filteredQueries.length + filteredEquations.length > 1;
  64. return (
  65. <QueriesWrapper>
  66. {filteredQueries.map((query, index) => (
  67. <QueryWrapper key={index} hasQuerySymbol={showQuerySymbols}>
  68. {showQuerySymbols && (
  69. <StyledQuerySymbol isSelected={false} queryId={query.id} />
  70. )}
  71. <QueryBuilder
  72. onChange={data => onQueryChange(data, index)}
  73. metricsQuery={query}
  74. projects={selection.projects}
  75. />
  76. <QueryContextMenu
  77. canRemoveQuery={filteredQueries.length > 1}
  78. removeQuery={removeQuery}
  79. queryIndex={index}
  80. metricsQuery={query}
  81. />
  82. </QueryWrapper>
  83. ))}
  84. {filteredEquations.map((equation, index) => (
  85. <QueryWrapper key={index} hasQuerySymbol={showQuerySymbols}>
  86. {showQuerySymbols && (
  87. <StyledEquationSymbol isSelected={false} equationId={equation.id} />
  88. )}
  89. <FormulaInput
  90. onChange={formula => onEquationChange({formula}, index)}
  91. value={equation.formula}
  92. availableVariables={availableVariables}
  93. />
  94. <EquationContextMenu removeEquation={removeEquation} equationIndex={index} />
  95. </QueryWrapper>
  96. ))}
  97. {displayType !== DisplayType.BIG_NUMBER && (
  98. <ButtonBar addQuerySymbolSpacing={showQuerySymbols}>
  99. <Button size="sm" icon={<IconAdd isCircled />} onClick={addQuery}>
  100. {t('Add query')}
  101. </Button>
  102. <Button size="sm" icon={<IconAdd isCircled />} onClick={addEquation}>
  103. {t('Add equation')}
  104. </Button>
  105. </ButtonBar>
  106. )}
  107. </QueriesWrapper>
  108. );
  109. }
  110. interface QueryContextMenuProps {
  111. canRemoveQuery: boolean;
  112. metricsQuery: DashboardMetricsQuery;
  113. queryIndex: number;
  114. removeQuery: (index: number) => void;
  115. }
  116. function QueryContextMenu({
  117. metricsQuery,
  118. removeQuery,
  119. canRemoveQuery,
  120. queryIndex,
  121. }: QueryContextMenuProps) {
  122. const organization = useOrganization();
  123. const router = useRouter();
  124. const createAlert = useMemo(
  125. () => getCreateAlert(organization, metricsQuery),
  126. [metricsQuery, organization]
  127. );
  128. const items = useMemo<MenuItemProps[]>(() => {
  129. const customMetric = !isCustomMetric({mri: metricsQuery.mri});
  130. const addAlertItem = {
  131. leadingItems: [<IconSiren key="icon" />],
  132. key: 'add-alert',
  133. label: t('Create Alert'),
  134. disabled: !createAlert,
  135. onAction: () => {
  136. createAlert?.();
  137. },
  138. };
  139. const removeQueryItem = {
  140. leadingItems: [<IconClose key="icon" />],
  141. key: 'delete',
  142. label: t('Remove Query'),
  143. disabled: !canRemoveQuery,
  144. onAction: () => {
  145. removeQuery(queryIndex);
  146. },
  147. };
  148. const settingsItem = {
  149. leadingItems: [<IconSettings key="icon" />],
  150. key: 'settings',
  151. label: t('Metric Settings'),
  152. disabled: !customMetric,
  153. onAction: () => {
  154. navigateTo(
  155. `/settings/projects/:projectId/metrics/${encodeURIComponent(metricsQuery.mri)}`,
  156. router
  157. );
  158. },
  159. };
  160. return customMetric
  161. ? [addAlertItem, removeQueryItem, settingsItem]
  162. : [addAlertItem, removeQueryItem];
  163. }, [createAlert, metricsQuery.mri, removeQuery, canRemoveQuery, queryIndex, router]);
  164. return (
  165. <DropdownMenu
  166. items={items}
  167. triggerProps={{
  168. 'aria-label': t('Query actions'),
  169. size: 'md',
  170. showChevron: false,
  171. icon: <IconEllipsis direction="down" size="sm" />,
  172. }}
  173. position="bottom-end"
  174. />
  175. );
  176. }
  177. interface EquationContextMenuProps {
  178. equationIndex: number;
  179. removeEquation: (index: number) => void;
  180. }
  181. function EquationContextMenu({equationIndex, removeEquation}: EquationContextMenuProps) {
  182. return (
  183. <Button
  184. aria-label={t('Remove Equation')}
  185. onClick={() => removeEquation(equationIndex)}
  186. size="md"
  187. icon={<IconClose size="sm" key="icon" />}
  188. />
  189. );
  190. }
  191. const QueriesWrapper = styled('div')`
  192. padding-bottom: ${space(2)};
  193. `;
  194. const QueryWrapper = styled('div')<{hasQuerySymbol: boolean}>`
  195. display: grid;
  196. gap: ${space(1)};
  197. padding-bottom: ${space(1)};
  198. grid-template-columns: 1fr max-content;
  199. ${p =>
  200. p.hasQuerySymbol &&
  201. `
  202. grid-template-columns: max-content 1fr max-content;
  203. `}
  204. `;
  205. const StyledQuerySymbol = styled(QuerySymbol)`
  206. margin-top: 10px;
  207. `;
  208. const StyledEquationSymbol = styled(EquationSymbol)`
  209. margin-top: 10px;
  210. `;
  211. const ButtonBar = styled('div')<{addQuerySymbolSpacing: boolean}>`
  212. align-items: center;
  213. display: flex;
  214. padding-top: ${space(0.5)};
  215. gap: ${space(2)};
  216. ${p =>
  217. p.addQuerySymbolSpacing &&
  218. `
  219. padding-left: ${space(1)};
  220. margin-left: ${space(2)};
  221. `}
  222. `;