123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- import {type CSSProperties, Fragment, useCallback, 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 cloneDeep from 'lodash/cloneDeep';
- import omit from 'lodash/omit';
- import {t} from 'sentry/locale';
- import PreferencesStore from 'sentry/stores/preferencesStore';
- import {useLegacyStore} from 'sentry/stores/useLegacyStore';
- import {space} from 'sentry/styles/space';
- import {CustomMeasurementsProvider} from 'sentry/utils/customMeasurements/customMeasurementsProvider';
- import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
- 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 {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,
- } 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';
- export interface ThresholdMetaState {
- dataType?: string;
- dataUnit?: string;
- }
- type WidgetBuilderV2Props = {
- dashboard: DashboardDetails;
- dashboardFilters: DashboardFilters;
- isOpen: boolean;
- onClose: () => void;
- onSave: ({index, widget}: {index: number; widget: Widget}) => void;
- openWidgetTemplates: boolean;
- setOpenWidgetTemplates: (openWidgetTemplates: boolean) => void;
- };
- function WidgetBuilderV2({
- isOpen,
- onClose,
- onSave,
- dashboardFilters,
- dashboard,
- setOpenWidgetTemplates,
- openWidgetTemplates,
- }: WidgetBuilderV2Props) {
- const organization = useOrganization();
- const {selection} = usePageFilters();
- const [queryConditionsValid, setQueryConditionsValid] = useState<boolean>(true);
- const theme = useTheme();
- const [isPreviewDraggable, setIsPreviewDraggable] = useState(false);
- const [thresholdMetaState, setThresholdMetaState] = useState<ThresholdMetaState>({});
- const isSmallScreen = useMedia(`(max-width: ${theme.breakpoints.small})`);
- const [translate, setTranslate] = useState<WidgetDragPositioning>(
- DEFAULT_WIDGET_DRAG_POSITIONING
- );
- const handleDragEnd = ({over}: any) => {
- setTranslate(snapPreviewToCorners(over));
- };
- const handleDragMove = ({delta}: any) => {
- setTranslate(previousTranslate => ({
- ...previousTranslate,
- initialTranslate: previousTranslate.initialTranslate,
- translate: {
- x: previousTranslate.initialTranslate.x + delta.x,
- y: previousTranslate.initialTranslate.y + delta.y,
- },
- }));
- };
- const handleWidgetDataFetched = useCallback(
- (tableData: TableDataWithTitle[]) => {
- const tableMeta = {...tableData[0]!.meta};
- const keys = Object.keys(tableMeta);
- const field = keys[0]!;
- const dataType = tableMeta[field];
- const dataUnit = tableMeta.units?.[field];
- const newState = cloneDeep(thresholdMetaState);
- newState.dataType = dataType;
- newState.dataUnit = dataUnit;
- setThresholdMetaState(newState);
- },
- [thresholdMetaState]
- );
- // reset the drag position when the draggable preview is not visible
- useEffect(() => {
- if (!isPreviewDraggable) {
- setTranslate(DEFAULT_WIDGET_DRAG_POSITIONING);
- }
- }, [isPreviewDraggable]);
- const preferences = useLegacyStore(PreferencesStore);
- const hasNewNav = organization?.features.includes('navigation-sidebar-v2');
- const sidebarCollapsed = hasNewNav ? true : !!preferences.collapsed;
- return (
- <Fragment>
- {isOpen && <Backdrop style={{opacity: 0.5, pointerEvents: 'auto'}} />}
- <AnimatePresence>
- {isOpen && (
- <WidgetBuilderProvider>
- <CustomMeasurementsProvider organization={organization} selection={selection}>
- <SpanTagsProvider
- dataset={DiscoverDatasets.SPANS_EAP}
- enabled={organization.features.includes('dashboards-eap')}
- >
- <ContainerWithoutSidebar sidebarCollapsed={sidebarCollapsed}>
- <WidgetBuilderContainer>
- <SlideoutContainer>
- <WidgetBuilderSlideout
- isOpen={isOpen}
- onClose={() => {
- onClose();
- setTranslate(DEFAULT_WIDGET_DRAG_POSITIONING);
- }}
- onSave={onSave}
- onQueryConditionChange={setQueryConditionsValid}
- dashboard={dashboard}
- dashboardFilters={dashboardFilters}
- setIsPreviewDraggable={setIsPreviewDraggable}
- isWidgetInvalid={!queryConditionsValid}
- openWidgetTemplates={openWidgetTemplates}
- setOpenWidgetTemplates={setOpenWidgetTemplates}
- onDataFetched={handleWidgetDataFetched}
- thresholdMetaState={thresholdMetaState}
- />
- </SlideoutContainer>
- {(!isSmallScreen || isPreviewDraggable) && (
- <DndContext
- onDragEnd={handleDragEnd}
- onDragMove={handleDragMove}
- collisionDetection={closestCorners}
- >
- <SurroundingWidgetContainer>
- <WidgetPreviewContainer
- dashboardFilters={dashboardFilters}
- dashboard={dashboard}
- dragPosition={translate}
- isDraggable={isPreviewDraggable}
- isWidgetInvalid={!queryConditionsValid}
- onDataFetched={handleWidgetDataFetched}
- openWidgetTemplates={openWidgetTemplates}
- />
- </SurroundingWidgetContainer>
- </DndContext>
- )}
- </WidgetBuilderContainer>
- </ContainerWithoutSidebar>
- </SpanTagsProvider>
- </CustomMeasurementsProvider>
- </WidgetBuilderProvider>
- )}
- </AnimatePresence>
- </Fragment>
- );
- }
- export default WidgetBuilderV2;
- export function WidgetPreviewContainer({
- dashboardFilters,
- dashboard,
- isWidgetInvalid,
- dragPosition,
- isDraggable,
- onDataFetched,
- openWidgetTemplates,
- }: {
- dashboard: DashboardDetails;
- dashboardFilters: DashboardFilters;
- isWidgetInvalid: boolean;
- dragPosition?: WidgetDragPositioning;
- isDraggable?: boolean;
- onDataFetched?: (tableData: TableDataWithTitle[]) => void;
- openWidgetTemplates?: boolean;
- }) {
- const {state} = useWidgetBuilderContext();
- const organization = useOrganization();
- const location = useLocation();
- const theme = useTheme();
- 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(${isDragging ? translate?.x : 0}px, ${isDragging ? 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
- : isSmallScreen
- ? theme.zIndex.initial
- : // if not responsive, set z-index to default in styled component
- undefined,
- cursor: isDragEnabled ? 'grab' : undefined,
- margin: isDragEnabled ? '0' : undefined,
- alignSelf: isDragEnabled ? 'flex-start' : undefined,
- position: isDragEnabled ? 'fixed' : undefined,
- };
- // check if the state is in the url because the state variable has default values
- const hasUrlParams =
- Object.keys(
- omit(location.query, [
- 'environment',
- 'project',
- 'release',
- 'start',
- 'end',
- 'statsPeriod',
- ])
- ).length > 0;
- const getPreviewHeight = () => {
- if (isDragEnabled) {
- return DRAGGABLE_PREVIEW_HEIGHT_PX;
- }
- // if none of the widget templates are selected
- if (openWidgetTemplates && !hasUrlParams) {
- return PREVIEW_HEIGHT_PX;
- }
- if (state.displayType === DisplayType.TABLE) {
- return 'auto';
- }
- if (state.displayType === DisplayType.BIG_NUMBER && !isSmallScreen) {
- return '20vw';
- }
- return PREVIEW_HEIGHT_PX;
- };
- return (
- <DashboardsMEPProvider>
- <MetricsCardinalityProvider organization={organization} location={location}>
- <MetricsDataSwitcher
- organization={organization}
- location={location}
- hideLoadingIndicator
- eventView={EventView.fromLocation(location)}
- >
- {metricsDataSide => (
- <MEPSettingProvider
- location={location}
- forceTransactions={metricsDataSide.forceTransactionsOnly}
- >
- {isDragEnabled && <DroppablePreviewContainer />}
- <DraggableWidgetContainer
- ref={setNodeRef}
- id={WIDGET_PREVIEW_DRAG_ID}
- style={draggableStyle}
- aria-label={t('Draggable Widget Preview')}
- {...attributes}
- {...listeners}
- >
- {!isSmallScreen && (
- <WidgetPreviewTitle
- initial={{opacity: 0, x: '50%', y: 0}}
- animate={{opacity: 1, x: 0, y: 0}}
- exit={{opacity: 0, x: '50%', y: 0}}
- transition={{
- type: 'spring',
- stiffness: 500,
- damping: 50,
- }}
- >
- {t('Widget Preview')}
- </WidgetPreviewTitle>
- )}
- <SampleWidgetCard
- initial={{opacity: 0, x: '50%', y: 0}}
- animate={{opacity: 1, x: 0, y: 0}}
- exit={{opacity: 0, x: '50%', y: 0}}
- transition={{
- type: 'spring',
- stiffness: 500,
- damping: 50,
- }}
- style={{
- width: isDragEnabled ? DRAGGABLE_PREVIEW_WIDTH_PX : undefined,
- height: getPreviewHeight(),
- outline: isDragEnabled
- ? `${space(1)} solid ${theme.border}`
- : undefined,
- }}
- >
- {openWidgetTemplates && !hasUrlParams ? (
- <WidgetPreviewPlaceholder>
- <h6 style={{margin: 0}}>{t('Widget Title')}</h6>
- <TemplateWidgetPreviewPlaceholder>
- <p style={{margin: 0}}>{t('Select a widget to preview')}</p>
- </TemplateWidgetPreviewPlaceholder>
- </WidgetPreviewPlaceholder>
- ) : (
- <WidgetPreview
- dashboardFilters={dashboardFilters}
- dashboard={dashboard}
- isWidgetInvalid={isWidgetInvalid}
- onDataFetched={onDataFetched}
- shouldForceDescriptionTooltip={!isSmallScreen}
- />
- )}
- </SampleWidgetCard>
- </DraggableWidgetContainer>
- </MEPSettingProvider>
- )}
- </MetricsDataSwitcher>
- </MetricsCardinalityProvider>
- </DashboardsMEPProvider>
- );
- }
- function DroppablePreviewContainer() {
- const containers = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
- return (
- <DroppableGrid>
- {containers.map(id => (
- <Droppable key={id} id={id} />
- ))}
- </DroppableGrid>
- );
- }
- function Droppable({id}: {id: string}) {
- const {setNodeRef} = useDroppable({
- id,
- });
- return <div ref={setNodeRef} id={id} />;
- }
- const fullPageCss = css`
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- `;
- 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: 40vw;
- 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: 30vw;
- min-width: 100px;
- }
- `;
- 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')<{sidebarCollapsed: boolean}>`
- z-index: ${p => p.theme.zIndex.widgetBuilderDrawer};
- position: fixed;
- top: 0;
- left: ${p =>
- p.sidebarCollapsed ? p.theme.sidebar.collapsedWidth : p.theme.sidebar.expandedWidth};
- right: 0;
- bottom: 0;
- @media (max-width: ${p => p.theme.breakpoints.medium}) {
- left: 0;
- top: ${p => p.theme.sidebar.mobileHeight};
- }
- `;
- const WidgetBuilderContainer = styled('div')`
- z-index: ${p => p.theme.zIndex.widgetBuilderDrawer};
- display: flex;
- align-items: center;
- position: absolute;
- inset: 0;
- `;
- 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;
- `;
- const TemplateWidgetPreviewPlaceholder = styled('div')`
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: 95%;
- color: ${p => p.theme.subText};
- font-style: italic;
- font-size: ${p => p.theme.fontSizeMedium};
- font-weight: ${p => p.theme.fontWeightNormal};
- `;
- const WidgetPreviewPlaceholder = styled('div')`
- width: 100%;
- height: 100%;
- padding: ${space(2)};
- `;
- const SlideoutContainer = styled('div')`
- height: 100%;
- `;
- const SurroundingWidgetContainer = styled('div')`
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- `;
- const WidgetPreviewTitle = styled(motion.h5)`
- margin-bottom: ${space(1)};
- margin-left: ${space(1)};
- color: ${p => p.theme.white};
- font-weight: ${p => p.theme.fontWeightBold};
- `;
|