toolbarVisualize.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import {Fragment, useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import {CompactSelect, type SelectOption} from 'sentry/components/compactSelect';
  5. import {Tooltip} from 'sentry/components/tooltip';
  6. import {IconAdd} from 'sentry/icons';
  7. import {IconDelete} from 'sentry/icons/iconDelete';
  8. import {t} from 'sentry/locale';
  9. import {defined} from 'sentry/utils';
  10. import type {ParsedFunction} from 'sentry/utils/discover/fields';
  11. import {parseFunction} from 'sentry/utils/discover/fields';
  12. import {ALLOWED_EXPLORE_VISUALIZE_AGGREGATES} from 'sentry/utils/fields';
  13. import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
  14. import type {Visualize} from 'sentry/views/explore/hooks/useVisualizes';
  15. import {
  16. DEFAULT_VISUALIZATION,
  17. useVisualizes,
  18. } from 'sentry/views/explore/hooks/useVisualizes';
  19. import {ChartType} from 'sentry/views/insights/common/components/chart';
  20. import {
  21. ToolbarFooter,
  22. ToolbarFooterButton,
  23. ToolbarHeader,
  24. ToolbarHeaderButton,
  25. ToolbarLabel,
  26. ToolbarRow,
  27. ToolbarSection,
  28. } from './styles';
  29. type ParsedVisualize = {
  30. func: ParsedFunction;
  31. label: string;
  32. };
  33. interface ToolbarVisualizeProps {}
  34. export function ToolbarVisualize({}: ToolbarVisualizeProps) {
  35. const [visualizes, setVisualizes] = useVisualizes();
  36. const numberTags = useSpanTags('number');
  37. const parsedVisualizeGroups: ParsedVisualize[][] = useMemo(() => {
  38. return visualizes.map(visualize =>
  39. visualize.yAxes
  40. .map(parseFunction)
  41. .filter(defined)
  42. .map(func => {
  43. return {
  44. func,
  45. label: visualize.label,
  46. };
  47. })
  48. );
  49. }, [visualizes]);
  50. const fieldOptions: SelectOption<string>[] = useMemo(() => {
  51. const options = Object.values(numberTags).map(tag => {
  52. return {
  53. label: tag.name,
  54. value: tag.key,
  55. textValue: tag.name,
  56. };
  57. });
  58. options.sort((a, b) => {
  59. if (a.label < b.label) {
  60. return -1;
  61. }
  62. if (a.label > b.label) {
  63. return 1;
  64. }
  65. return 0;
  66. });
  67. return options;
  68. }, [numberTags]);
  69. const aggregateOptions: SelectOption<string>[] = useMemo(() => {
  70. return ALLOWED_EXPLORE_VISUALIZE_AGGREGATES.map(aggregate => {
  71. return {
  72. label: aggregate,
  73. value: aggregate,
  74. textValue: aggregate,
  75. };
  76. });
  77. }, []);
  78. const addChart = useCallback(() => {
  79. setVisualizes([
  80. ...visualizes,
  81. {yAxes: [DEFAULT_VISUALIZATION], chartType: ChartType.LINE},
  82. ]);
  83. }, [setVisualizes, visualizes]);
  84. const addOverlay = useCallback(
  85. (group: number) => {
  86. const newVisualizes = visualizes.slice();
  87. newVisualizes[group].yAxes.push(DEFAULT_VISUALIZATION);
  88. setVisualizes(newVisualizes);
  89. },
  90. [setVisualizes, visualizes]
  91. );
  92. const setChartField = useCallback(
  93. (group: number, index: number, {value}: SelectOption<string>) => {
  94. const newVisualizes = visualizes.slice();
  95. newVisualizes[group].yAxes[index] =
  96. `${parsedVisualizeGroups[group][index].func.name}(${value})`;
  97. setVisualizes(newVisualizes);
  98. },
  99. [parsedVisualizeGroups, setVisualizes, visualizes]
  100. );
  101. const setChartAggregate = useCallback(
  102. (group: number, index: number, {value}: SelectOption<string>) => {
  103. const newVisualizes = visualizes.slice();
  104. newVisualizes[group].yAxes[index] =
  105. `${value}(${parsedVisualizeGroups[group][index].func.arguments[0]})`;
  106. setVisualizes(newVisualizes);
  107. },
  108. [parsedVisualizeGroups, setVisualizes, visualizes]
  109. );
  110. const deleteOverlay = useCallback(
  111. (group: number, index: number) => {
  112. const newVisualizes: Visualize[] = visualizes
  113. .map((visualize, orgGroup) => {
  114. if (group !== orgGroup) {
  115. return visualize;
  116. }
  117. return {
  118. ...visualize,
  119. yAxes: visualize.yAxes.filter((_, orgIndex) => index !== orgIndex),
  120. };
  121. })
  122. .filter(visualize => visualize.yAxes.length > 0);
  123. setVisualizes(newVisualizes);
  124. },
  125. [setVisualizes, visualizes]
  126. );
  127. const lastVisualization =
  128. parsedVisualizeGroups
  129. .map(parsedVisualizeGroup => parsedVisualizeGroup.length)
  130. .reduce((a, b) => a + b, 0) <= 1;
  131. const shouldRenderLabel = visualizes.length > 1;
  132. return (
  133. <ToolbarSection data-test-id="section-visualizes">
  134. <ToolbarHeader>
  135. <Tooltip
  136. position="right"
  137. title={t(
  138. 'Primary metric that appears in your chart, overlays and/or equations.'
  139. )}
  140. >
  141. <ToolbarLabel>{t('Visualize')}</ToolbarLabel>
  142. </Tooltip>
  143. <ToolbarHeaderButton
  144. size="zero"
  145. icon={<IconAdd />}
  146. onClick={addChart}
  147. aria-label={t('Add Chart')}
  148. borderless
  149. />
  150. </ToolbarHeader>
  151. <div>
  152. {parsedVisualizeGroups.map((parsedVisualizeGroup, group) => {
  153. return (
  154. <Fragment key={group}>
  155. {parsedVisualizeGroup.map((parsedVisualize, index) => (
  156. <ToolbarRow key={index}>
  157. {shouldRenderLabel && <ChartLabel>{parsedVisualize.label}</ChartLabel>}
  158. <CompactSelect
  159. searchable
  160. options={fieldOptions}
  161. value={parsedVisualize.func.arguments[0]}
  162. onChange={newField => setChartField(group, index, newField)}
  163. />
  164. <CompactSelect
  165. options={aggregateOptions}
  166. value={parsedVisualize.func.name}
  167. onChange={newAggregate =>
  168. setChartAggregate(group, index, newAggregate)
  169. }
  170. />
  171. <Button
  172. borderless
  173. icon={<IconDelete />}
  174. size="zero"
  175. disabled={lastVisualization}
  176. onClick={() => deleteOverlay(group, index)}
  177. aria-label={t('Remove Overlay')}
  178. />
  179. </ToolbarRow>
  180. ))}
  181. <ToolbarFooter>
  182. <ToolbarFooterButton
  183. borderless
  184. size="zero"
  185. icon={<IconAdd />}
  186. onClick={() => addOverlay(group)}
  187. priority="link"
  188. aria-label={t('Add Series')}
  189. >
  190. {t('Add Series')}
  191. </ToolbarFooterButton>
  192. </ToolbarFooter>
  193. </Fragment>
  194. );
  195. })}
  196. </div>
  197. </ToolbarSection>
  198. );
  199. }
  200. const ChartLabel = styled('div')`
  201. background-color: ${p => p.theme.purple100};
  202. border-radius: ${p => p.theme.borderRadius};
  203. text-align: center;
  204. min-width: 32px;
  205. color: ${p => p.theme.purple400};
  206. white-space: nowrap;
  207. font-weight: ${p => p.theme.fontWeightBold};
  208. align-content: center;
  209. `;