import {type CSSProperties, Fragment, useEffect, useState} from 'react'; import {closestCorners, DndContext, useDraggable, useDroppable} from '@dnd-kit/core'; import {css, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {AnimatePresence, motion} from 'framer-motion'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {CustomMeasurementsProvider} from 'sentry/utils/customMeasurements/customMeasurementsProvider'; import EventView from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality'; import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import {decodeBoolean} from 'sentry/utils/queryString'; import useLocationQuery from 'sentry/utils/url/useLocationQuery'; import useKeyPress from 'sentry/utils/useKeyPress'; import {useLocation} from 'sentry/utils/useLocation'; import useMedia from 'sentry/utils/useMedia'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import { type DashboardDetails, type DashboardFilters, DisplayType, type Widget, WidgetType, } from 'sentry/views/dashboards/types'; import { DEFAULT_WIDGET_DRAG_POSITIONING, DRAGGABLE_PREVIEW_HEIGHT_PX, DRAGGABLE_PREVIEW_WIDTH_PX, PREVIEW_HEIGHT_PX, SIDEBAR_HEIGHT, snapPreviewToCorners, WIDGET_PREVIEW_DRAG_ID, type WidgetDragPositioning, } from 'sentry/views/dashboards/widgetBuilder/components/common/draggableUtils'; import WidgetBuilderSlideout from 'sentry/views/dashboards/widgetBuilder/components/widgetBuilderSlideout'; import WidgetPreview from 'sentry/views/dashboards/widgetBuilder/components/widgetPreview'; import { useWidgetBuilderContext, WidgetBuilderProvider, } from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext'; import {DashboardsMEPProvider} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext'; import {SpanTagsProvider} from 'sentry/views/explore/contexts/spanTagsContext'; import {MetricsDataSwitcher} from 'sentry/views/performance/landing/metricsDataSwitcher'; type WidgetBuilderV2Props = { dashboard: DashboardDetails; dashboardFilters: DashboardFilters; isOpen: boolean; onClose: () => void; onSave: ({index, widget}: {index: number; widget: Widget}) => void; }; function WidgetBuilderV2({ isOpen, onClose, onSave, dashboardFilters, dashboard, }: WidgetBuilderV2Props) { const escapeKeyPressed = useKeyPress('Escape'); const organization = useOrganization(); const {selection} = usePageFilters(); const [queryConditionsValid, setQueryConditionsValid] = useState(true); const theme = useTheme(); const [isPreviewDraggable, setIsPreviewDraggable] = useState(false); const isSmallScreen = useMedia(`(max-width: ${theme.breakpoints.small})`); const [translate, setTranslate] = useState( DEFAULT_WIDGET_DRAG_POSITIONING ); // do we want to keep this? useEffect(() => { if (escapeKeyPressed) { if (isOpen) { onClose?.(); } } }, [escapeKeyPressed, isOpen, onClose]); const handleDragEnd = ({over}) => { setTranslate(snapPreviewToCorners(over)); }; const handleDragMove = ({delta}) => { setTranslate(previousTranslate => ({ ...previousTranslate, initialTranslate: previousTranslate.initialTranslate, translate: { x: previousTranslate.initialTranslate.x + delta.x, y: previousTranslate.initialTranslate.y + delta.y, }, })); }; return ( {isOpen && } {isOpen && ( { onClose(); setTranslate(DEFAULT_WIDGET_DRAG_POSITIONING); }} onSave={onSave} onQueryConditionChange={setQueryConditionsValid} dashboard={dashboard} dashboardFilters={dashboardFilters} setIsPreviewDraggable={setIsPreviewDraggable} isWidgetInvalid={!queryConditionsValid} /> {(!isSmallScreen || isPreviewDraggable) && ( )} )} ); } export default WidgetBuilderV2; export function WidgetPreviewContainer({ dashboardFilters, dashboard, isWidgetInvalid, dragPosition, isDraggable, }: { dashboard: DashboardDetails; dashboardFilters: DashboardFilters; isWidgetInvalid: boolean; dragPosition?: WidgetDragPositioning; isDraggable?: boolean; }) { const {state} = useWidgetBuilderContext(); const organization = useOrganization(); const location = useLocation(); const theme = useTheme(); const {useRpc} = useLocationQuery({ fields: { useRpc: decodeBoolean, }, }); const isSmallScreen = useMedia(`(max-width: ${theme.breakpoints.small})`); // if small screen and draggable, enable dragging const isDragEnabled = isSmallScreen && isDraggable; const {attributes, listeners, setNodeRef, isDragging} = useDraggable({ id: WIDGET_PREVIEW_DRAG_ID, disabled: !isDragEnabled, // May need to add 'handle' prop if we want to drag the preview by a specific area }); const {translate, top, left} = dragPosition ?? {}; const draggableStyle: CSSProperties = { transform: isDragEnabled ? `translate3d(${translate?.x ?? 0}px, ${translate?.y ?? 0}px, 0)` : undefined, top: isDragEnabled ? top ?? 0 : undefined, left: isDragEnabled ? left ?? 0 : undefined, opacity: isDragging ? 0.5 : 1, zIndex: isDragEnabled ? theme.zIndex.modal : theme.zIndex.initial, cursor: isDragEnabled ? 'grab' : undefined, margin: isDragEnabled ? '0' : undefined, alignSelf: isDragEnabled ? 'flex-start' : undefined, position: isDragEnabled ? 'fixed' : undefined, }; const getPreviewHeight = () => { if (isDragEnabled) { return DRAGGABLE_PREVIEW_HEIGHT_PX; } if (state.displayType === DisplayType.TABLE) { return 'auto'; } if (state.displayType === DisplayType.BIG_NUMBER && !isSmallScreen) { return '20vw'; } return PREVIEW_HEIGHT_PX; }; return ( {metricsDataSide => ( {isDragEnabled && } )} ); } function DroppablePreviewContainer() { const containers = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; return ( {containers.map(id => ( ))} ); } function Droppable({id}: {id: string}) { const {setNodeRef} = useDroppable({ id, }); return
; } const fullPageCss = css` position: absolute; top: 0; right: 0; bottom: 0; left: 0; `; const fillAvailableCss = css` height: 100%; /* default */ height: -webkit-fill-available; /* Chrome */ height: -moz-available; /* Firefox */ height: fill-available; /* others */ width: -webkit-fill-available; /* Chrome */ width: -moz-available; /* Firefox */ width: fill-available; /* others */ `; const Backdrop = styled('div')` ${fullPageCss}; z-index: ${p => p.theme.zIndex.widgetBuilderDrawer}; background: ${p => p.theme.black}; will-change: opacity; transition: opacity 200ms; pointer-events: none; opacity: 0; `; const SampleWidgetCard = styled(motion.div)` width: 100%; min-width: 100%; border: 1px dashed ${p => p.theme.gray300}; border-radius: ${p => p.theme.borderRadius}; background-color: ${p => p.theme.background}; z-index: ${p => p.theme.zIndex.initial}; position: relative; @media (min-width: ${p => p.theme.breakpoints.small}) { width: 30vw; min-width: 300px; z-index: ${p => p.theme.zIndex.modal}; cursor: auto; } @media (max-width: ${p => p.theme.breakpoints.large}) and (min-width: ${p => p.theme.breakpoints.medium}) { width: 25vw; min-width: 100px; max-width: 300px; } `; const DraggableWidgetContainer = styled(`div`)` align-content: center; z-index: ${p => p.theme.zIndex.initial}; position: relative; margin: auto; cursor: auto; @media (min-width: ${p => p.theme.breakpoints.small}) { z-index: ${p => p.theme.zIndex.modal}; transform: none; cursor: auto; } `; const ContainerWithoutSidebar = styled('div')` position: absolute; top: 0; left: 0; `; const WidgetBuilderContainer = styled('div')` z-index: ${p => p.theme.zIndex.widgetBuilderDrawer}; display: flex; align-items: center; justify-content: space-between; position: fixed; ${fillAvailableCss}; `; const DroppableGrid = styled('div')` display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; position: fixed; gap: ${space(4)}; margin: ${space(2)}; top: ${SIDEBAR_HEIGHT}px; right: ${space(2)}; bottom: ${space(2)}; left: 0; `;