import {Fragment, useEffect, useRef, useState} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import isEqual from 'lodash/isEqual'; import {Button} from 'sentry/components/button'; import {openConfirmModal} from 'sentry/components/confirm'; import SlideOverPanel from 'sentry/components/slideOverPanel'; import {IconClose} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents'; import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery'; import useMedia from 'sentry/utils/useMedia'; import useOrganization from 'sentry/utils/useOrganization'; import {useValidateWidgetQuery} from 'sentry/views/dashboards/hooks/useValidateWidget'; import { type DashboardDetails, type DashboardFilters, DisplayType, type Widget, } from 'sentry/views/dashboards/types'; import WidgetBuilderDatasetSelector from 'sentry/views/dashboards/widgetBuilder/components/datasetSelector'; import WidgetBuilderFilterBar from 'sentry/views/dashboards/widgetBuilder/components/filtersBar'; import WidgetBuilderGroupBySelector from 'sentry/views/dashboards/widgetBuilder/components/groupBySelector'; import WidgetBuilderNameAndDescription from 'sentry/views/dashboards/widgetBuilder/components/nameAndDescFields'; import { type ThresholdMetaState, WidgetPreviewContainer, } from 'sentry/views/dashboards/widgetBuilder/components/newWidgetBuilder'; import WidgetBuilderQueryFilterBuilder from 'sentry/views/dashboards/widgetBuilder/components/queryFilterBuilder'; import SaveButton from 'sentry/views/dashboards/widgetBuilder/components/saveButton'; import WidgetBuilderSortBySelector from 'sentry/views/dashboards/widgetBuilder/components/sortBySelector'; import ThresholdsSection from 'sentry/views/dashboards/widgetBuilder/components/thresholds'; import WidgetBuilderTypeSelector from 'sentry/views/dashboards/widgetBuilder/components/typeSelector'; import Visualize from 'sentry/views/dashboards/widgetBuilder/components/visualize'; import WidgetTemplatesList from 'sentry/views/dashboards/widgetBuilder/components/widgetTemplatesList'; import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext'; import useDashboardWidgetSource from 'sentry/views/dashboards/widgetBuilder/hooks/useDashboardWidgetSource'; import useIsEditingWidget from 'sentry/views/dashboards/widgetBuilder/hooks/useIsEditingWidget'; import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget'; type WidgetBuilderSlideoutProps = { dashboard: DashboardDetails; dashboardFilters: DashboardFilters; isOpen: boolean; isWidgetInvalid: boolean; onClose: () => void; onQueryConditionChange: (valid: boolean) => void; onSave: ({index, widget}: {index: number; widget: Widget}) => void; openWidgetTemplates: boolean; setIsPreviewDraggable: (draggable: boolean) => void; setOpenWidgetTemplates: (openWidgetTemplates: boolean) => void; onDataFetched?: (tableData: TableDataWithTitle[]) => void; thresholdMetaState?: ThresholdMetaState; }; function WidgetBuilderSlideout({ isOpen, onClose, onSave, onQueryConditionChange, dashboard, dashboardFilters, setIsPreviewDraggable, isWidgetInvalid, openWidgetTemplates, setOpenWidgetTemplates, onDataFetched, thresholdMetaState, }: WidgetBuilderSlideoutProps) { const organization = useOrganization(); const {state} = useWidgetBuilderContext(); const [initialState] = useState(state); const [error, setError] = useState>({}); const theme = useTheme(); const isEditing = useIsEditingWidget(); const source = useDashboardWidgetSource(); const validatedWidgetResponse = useValidateWidgetQuery( convertBuilderStateToWidget(state) ); useEffect(() => { if (!openWidgetTemplates) { trackAnalytics('dashboards_views.widget_builder.opened', { builder_version: WidgetBuilderVersion.SLIDEOUT, new_widget: !isEditing, organization, from: source, }); } // Ignore isEditing because it won't change during the // useful lifetime of the widget builder, but it // flickers when an edited widget is saved. // eslint-disable-next-line react-hooks/exhaustive-deps }, [openWidgetTemplates, organization]); const title = openWidgetTemplates ? t('Add from Widget Library') : isEditing ? t('Edit Widget') : t('Create Custom Widget'); const isChartWidget = state.displayType !== DisplayType.BIG_NUMBER && state.displayType !== DisplayType.TABLE; const customPreviewRef = useRef(null); const templatesPreviewRef = useRef(null); const isSmallScreen = useMedia(`(max-width: ${theme.breakpoints.small})`); const showSortByStep = (isChartWidget && state.fields && state.fields.length > 0) || state.displayType === DisplayType.TABLE; useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { setIsPreviewDraggable(!entry!.isIntersecting); }, {threshold: 0} ); // need two different refs to account for preview when customizing templates if (customPreviewRef.current) { observer.observe(customPreviewRef.current); } if (templatesPreviewRef.current) { observer.observe(templatesPreviewRef.current); } return () => observer.disconnect(); }, [setIsPreviewDraggable, openWidgetTemplates]); return ( {title} } onClick={() => { openConfirmModal({ bypass: isEqual(initialState, state), message: t('You have unsaved changes. Are you sure you want to leave?'), priority: 'danger', onConfirm: onClose, }); }} > {t('Close')} {!openWidgetTemplates ? (
{isSmallScreen && (
)}
{state.displayType === DisplayType.BIG_NUMBER && (
)} {isChartWidget && (
)} {showSortByStep && (
)}
) : (
{isSmallScreen && (
)}
)}
); } export default WidgetBuilderSlideout; const CloseButton = styled(Button)` color: ${p => p.theme.gray300}; height: fit-content; &:hover { color: ${p => p.theme.gray400}; } z-index: 100; `; const SlideoutTitle = styled('h5')` margin: 0; `; const SlideoutHeaderWrapper = styled('div')` padding: ${space(3)} ${space(4)}; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid ${p => p.theme.border}; `; const SlideoutBodyWrapper = styled('div')` padding: ${space(4)}; `; const Section = styled('div')` margin-bottom: 24px; `;