import {useMemo} from 'react'; import type {InjectedRouter} from 'react-router'; import styled from '@emotion/styled'; import {ErrorBoundary} from '@sentry/react'; import type {Location} from 'history'; import ErrorPanel from 'sentry/components/charts/errorPanel'; import {HeaderTitle} from 'sentry/components/charts/styles'; import TextOverflow from 'sentry/components/textOverflow'; import {IconWarning} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {PageFilters} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import {parseMRI} from 'sentry/utils/metrics/mri'; import {MetricExpressionType} from 'sentry/utils/metrics/types'; import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsQuery'; import {MetricBigNumberContainer} from 'sentry/views/dashboards/metrics/bigNumber'; import {MetricChartContainer} from 'sentry/views/dashboards/metrics/chart'; import {MetricTableContainer} from 'sentry/views/dashboards/metrics/table'; import { expressionsToApiQueries, getMetricExpressions, toMetricDisplayType, } from 'sentry/views/dashboards/metrics/utils'; import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types'; import {DisplayType} from 'sentry/views/dashboards/types'; import {WidgetCardPanel, WidgetTitleRow} from 'sentry/views/dashboards/widgetCard'; import {DashboardsMEPContext} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext'; import {Toolbar} from 'sentry/views/dashboards/widgetCard/toolbar'; import WidgetCardContextMenu from 'sentry/views/dashboards/widgetCard/widgetCardContextMenu'; import {useMetricsIntervalOptions} from 'sentry/views/metrics/utils/useMetricsIntervalParam'; import {getWidgetTitle} from 'sentry/views/metrics/widget'; type Props = { isEditingDashboard: boolean; location: Location; organization: Organization; router: InjectedRouter; selection: PageFilters; widget: Widget; dashboardFilters?: DashboardFilters; index?: string; isMobile?: boolean; onDelete?: () => void; onDuplicate?: () => void; onEdit?: (index: string) => void; renderErrorMessage?: (errorMessage?: string) => React.ReactNode; showContextMenu?: boolean; }; const EMPTY_FN = () => {}; export function MetricWidgetCard({ organization, selection, widget, isEditingDashboard, onDelete, onDuplicate, location, router, dashboardFilters, renderErrorMessage, showContextMenu = true, }: Props) { const metricQueries = useMemo( () => expressionsToApiQueries(getMetricExpressions(widget, dashboardFilters)), [widget, dashboardFilters] ); const hasSetMetric = useMemo( () => getMetricExpressions(widget, dashboardFilters).some( expression => expression.type === MetricExpressionType.QUERY && parseMRI(expression.mri)!.type === 's' ), [widget, dashboardFilters] ); const widgetMQL = useMemo(() => getWidgetTitle(metricQueries), [metricQueries]); const {interval: validatedInterval} = useMetricsIntervalOptions({ // TODO: Figure out why this can be undefined interval: widget.interval ?? '', hasSetMetric, datetime: selection.datetime, onIntervalChange: EMPTY_FN, }); const { data: timeseriesData, isLoading, isError, error, } = useMetricsQuery(metricQueries, selection, { interval: validatedInterval, }); const vizualizationComponent = useMemo(() => { if (widget.displayType === DisplayType.TABLE) { return ( ); } if (widget.displayType === DisplayType.BIG_NUMBER) { return ( ); } return ( ); }, [widget.displayType, metricQueries, timeseriesData, isLoading, showContextMenu]); return ( {}, }} > {widget.title || widgetMQL} {showContextMenu && !isEditingDashboard && ( { router.push({ pathname: `${location.pathname}${ location.pathname.endsWith('/') ? '' : '/' }widget/${widget.id}/`, query: location.query, }); }} router={router} location={location} onDelete={onDelete} onDuplicate={onDuplicate} /> )} {vizualizationComponent} {isEditingDashboard && } ); } function WidgetCardBody({children, isError, timeseriesData, renderErrorMessage, error}) { if (isError && !timeseriesData) { const errorMessage = error?.responseJSON?.detail?.toString() || t('Error while fetching metrics data'); return ( {renderErrorMessage?.(errorMessage)} ); } return children; } const WidgetHeaderWrapper = styled('div')` min-height: 36px; width: 100%; display: flex; align-items: flex-start; justify-content: space-between; `; const ContextMenuWrapper = styled('div')` padding: ${space(2)} ${space(1)} 0 ${space(3)}; `; const WidgetHeaderDescription = styled('div')` ${p => p.theme.overflowEllipsis}; overflow-y: visible; `; const WidgetTitle = styled(HeaderTitle)` padding-left: ${space(3)}; padding-top: ${space(2)}; padding-right: ${space(1)}; ${p => p.theme.overflowEllipsis}; font-weight: ${p => p.theme.fontWeightNormal}; `; const ErrorWrapper = styled('div')` padding-top: ${space(1)}; `;