toolbarVisualize.tsx 6.1 KB

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