widgetBuilderSlideout.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import {useEffect, useRef, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import isEqual from 'lodash/isEqual';
  5. import {Button} from 'sentry/components/button';
  6. import {openConfirmModal} from 'sentry/components/confirm';
  7. import SlideOverPanel from 'sentry/components/slideOverPanel';
  8. import {IconClose} from 'sentry/icons';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import useMedia from 'sentry/utils/useMedia';
  12. import useOrganization from 'sentry/utils/useOrganization';
  13. import {useParams} from 'sentry/utils/useParams';
  14. import {
  15. type DashboardDetails,
  16. type DashboardFilters,
  17. DisplayType,
  18. type Widget,
  19. WidgetType,
  20. } from 'sentry/views/dashboards/types';
  21. import WidgetBuilderDatasetSelector from 'sentry/views/dashboards/widgetBuilder/components/datasetSelector';
  22. import WidgetBuilderFilterBar from 'sentry/views/dashboards/widgetBuilder/components/filtersBar';
  23. import WidgetBuilderGroupBySelector from 'sentry/views/dashboards/widgetBuilder/components/groupBySelector';
  24. import WidgetBuilderNameAndDescription from 'sentry/views/dashboards/widgetBuilder/components/nameAndDescFields';
  25. import {WidgetPreviewContainer} from 'sentry/views/dashboards/widgetBuilder/components/newWidgetBuilder';
  26. import WidgetBuilderQueryFilterBuilder from 'sentry/views/dashboards/widgetBuilder/components/queryFilterBuilder';
  27. import RPCToggle from 'sentry/views/dashboards/widgetBuilder/components/rpcToggle';
  28. import SaveButton from 'sentry/views/dashboards/widgetBuilder/components/saveButton';
  29. import WidgetBuilderSortBySelector from 'sentry/views/dashboards/widgetBuilder/components/sortBySelector';
  30. import WidgetBuilderTypeSelector from 'sentry/views/dashboards/widgetBuilder/components/typeSelector';
  31. import Visualize from 'sentry/views/dashboards/widgetBuilder/components/visualize';
  32. import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
  33. type WidgetBuilderSlideoutProps = {
  34. dashboard: DashboardDetails;
  35. dashboardFilters: DashboardFilters;
  36. isOpen: boolean;
  37. isWidgetInvalid: boolean;
  38. onClose: () => void;
  39. onQueryConditionChange: (valid: boolean) => void;
  40. onSave: ({index, widget}: {index: number; widget: Widget}) => void;
  41. setIsPreviewDraggable: (draggable: boolean) => void;
  42. };
  43. function WidgetBuilderSlideout({
  44. isOpen,
  45. onClose,
  46. onSave,
  47. onQueryConditionChange,
  48. dashboard,
  49. dashboardFilters,
  50. setIsPreviewDraggable,
  51. isWidgetInvalid,
  52. }: WidgetBuilderSlideoutProps) {
  53. const organization = useOrganization();
  54. const {state} = useWidgetBuilderContext();
  55. const [initialState] = useState(state);
  56. const [error, setError] = useState<Record<string, any>>({});
  57. const {widgetIndex} = useParams();
  58. const theme = useTheme();
  59. const isEditing = widgetIndex !== undefined;
  60. const title = isEditing ? t('Edit Widget') : t('Create Custom Widget');
  61. const isChartWidget =
  62. state.displayType !== DisplayType.BIG_NUMBER &&
  63. state.displayType !== DisplayType.TABLE;
  64. const previewRef = useRef<HTMLDivElement>(null);
  65. const isSmallScreen = useMedia(`(max-width: ${theme.breakpoints.small})`);
  66. const showSortByStep =
  67. (isChartWidget && state.fields && state.fields.length > 0) ||
  68. state.displayType === DisplayType.TABLE;
  69. useEffect(() => {
  70. const observer = new IntersectionObserver(
  71. ([entry]) => {
  72. setIsPreviewDraggable(!entry!.isIntersecting);
  73. },
  74. {threshold: 0}
  75. );
  76. if (previewRef.current) {
  77. observer.observe(previewRef.current);
  78. }
  79. return () => observer.disconnect();
  80. }, [setIsPreviewDraggable]);
  81. return (
  82. <SlideOverPanel
  83. collapsed={!isOpen}
  84. slidePosition="left"
  85. data-test-id="widget-slideout"
  86. >
  87. <SlideoutHeaderWrapper>
  88. <SlideoutTitle>{title}</SlideoutTitle>
  89. <CloseButton
  90. priority="link"
  91. size="zero"
  92. borderless
  93. aria-label={t('Close Widget Builder')}
  94. icon={<IconClose size="sm" />}
  95. onClick={() => {
  96. openConfirmModal({
  97. bypass: isEqual(initialState, state),
  98. message: t('You have unsaved changes. Are you sure you want to leave?'),
  99. priority: 'danger',
  100. onConfirm: onClose,
  101. });
  102. }}
  103. >
  104. {t('Close')}
  105. </CloseButton>
  106. </SlideoutHeaderWrapper>
  107. <SlideoutBodyWrapper>
  108. <Section>
  109. <WidgetBuilderFilterBar />
  110. </Section>
  111. <Section>
  112. <WidgetBuilderDatasetSelector />
  113. </Section>
  114. {organization.features.includes('visibility-explore-dataset') &&
  115. state.dataset === WidgetType.SPANS && (
  116. <Section>
  117. <RPCToggle />
  118. </Section>
  119. )}
  120. <Section>
  121. <WidgetBuilderTypeSelector error={error} setError={setError} />
  122. </Section>
  123. <div ref={previewRef}>
  124. {isSmallScreen && (
  125. <Section>
  126. <WidgetPreviewContainer
  127. dashboard={dashboard}
  128. dashboardFilters={dashboardFilters}
  129. isWidgetInvalid={isWidgetInvalid}
  130. />
  131. </Section>
  132. )}
  133. </div>
  134. <Section>
  135. <Visualize />
  136. </Section>
  137. <Section>
  138. <WidgetBuilderQueryFilterBuilder
  139. onQueryConditionChange={onQueryConditionChange}
  140. error={error}
  141. />
  142. </Section>
  143. {isChartWidget && (
  144. <Section>
  145. <WidgetBuilderGroupBySelector />
  146. </Section>
  147. )}
  148. {showSortByStep && (
  149. <Section>
  150. <WidgetBuilderSortBySelector />
  151. </Section>
  152. )}
  153. <Section>
  154. <WidgetBuilderNameAndDescription error={error} setError={setError} />
  155. </Section>
  156. <SaveButton isEditing={isEditing} onSave={onSave} setError={setError} />
  157. </SlideoutBodyWrapper>
  158. </SlideOverPanel>
  159. );
  160. }
  161. export default WidgetBuilderSlideout;
  162. const CloseButton = styled(Button)`
  163. color: ${p => p.theme.gray300};
  164. height: fit-content;
  165. &:hover {
  166. color: ${p => p.theme.gray400};
  167. }
  168. z-index: 100;
  169. `;
  170. const SlideoutTitle = styled('h5')`
  171. margin: 0;
  172. `;
  173. const SlideoutHeaderWrapper = styled('div')`
  174. padding: ${space(3)} ${space(4)};
  175. display: flex;
  176. align-items: center;
  177. justify-content: space-between;
  178. border-bottom: 1px solid ${p => p.theme.border};
  179. `;
  180. const SlideoutBodyWrapper = styled('div')`
  181. padding: ${space(4)};
  182. `;
  183. const Section = styled('div')`
  184. margin-bottom: ${space(4)};
  185. `;