import {Fragment, useMemo, useRef} from 'react';
import type {InjectedRouter} from 'react-router';
import styled from '@emotion/styled';
import type {Location} from 'history';
import ErrorPanel from 'sentry/components/charts/errorPanel';
import {HeaderTitle} from 'sentry/components/charts/styles';
import TransitionChart from 'sentry/components/charts/transitionChart';
import EmptyMessage from 'sentry/components/emptyMessage';
import TextOverflow from 'sentry/components/textOverflow';
import {IconSearch, IconWarning} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Organization, PageFilters} from 'sentry/types';
import type {ReactEchartsRef} from 'sentry/types/echarts';
import {getWidgetTitle} from 'sentry/utils/metrics';
import type {MetricQueryWidgetParams} from 'sentry/utils/metrics/types';
import {MetricDisplayType} from 'sentry/utils/metrics/types';
import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsQuery';
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 {MetricChart} from 'sentry/views/ddm/chart';
import {createChartPalette} from 'sentry/views/ddm/utils/metricsChartPalette';
import {getChartTimeseries} from 'sentry/views/ddm/widget';
import {LoadingScreen} from 'sentry/views/starfish/components/chart';
import {convertToMetricWidget} from '../../../utils/metrics/dashboard';
import {DASHBOARD_CHART_GROUP} from '../dashboard';
import type {DashboardFilters, Widget} from '../types';
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;
};
export function MetricWidgetCard({
organization,
selection,
widget,
isEditingDashboard,
onDelete,
onDuplicate,
location,
router,
dashboardFilters,
renderErrorMessage,
showContextMenu = true,
}: Props) {
const metricWidgetQueries = useMemo(() => convertToMetricWidget(widget), [widget]);
const widgetMQL = useMemo(
() => getWidgetTitle(metricWidgetQueries),
[metricWidgetQueries]
);
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}
/>
)}
{isEditingDashboard && }
);
}
type MetricWidgetChartContainerProps = {
selection: PageFilters;
widget: Widget;
chartHeight?: number;
dashboardFilters?: DashboardFilters;
metricWidgetQueries?: MetricQueryWidgetParams[];
renderErrorMessage?: (errorMessage?: string) => React.ReactNode;
};
export function MetricWidgetChartContainer({
selection,
dashboardFilters,
renderErrorMessage,
metricWidgetQueries,
widget,
chartHeight,
}: MetricWidgetChartContainerProps) {
// TODO: Remove this and the widget prop once this component is no longer used in widgetViewerModal
const metricQueries = metricWidgetQueries || convertToMetricWidget(widget);
const chartQueries = useMemo(() => {
return metricQueries.map(({mri, op, groupBy, query}) => {
return {
mri,
op,
query: extendQuery(query, dashboardFilters),
groupBy,
};
});
}, [metricQueries, dashboardFilters]);
const displayType = metricQueries[0].displayType;
const {
data: timeseriesData,
isLoading,
isError,
error,
} = useMetricsQuery(chartQueries, selection, {
intervalLadder: displayType === MetricDisplayType.BAR ? 'bar' : 'dashboard',
});
const chartRef = useRef(null);
const chartSeries = useMemo(() => {
return timeseriesData
? getChartTimeseries(timeseriesData, chartQueries, {
getChartPalette: createChartPalette,
})
: [];
}, [timeseriesData, chartQueries]);
if (isError) {
const errorMessage =
error?.responseJSON?.detail?.toString() || t('Error while fetching metrics data');
return (
{renderErrorMessage?.(errorMessage)}
);
}
if (timeseriesData?.data.length === 0) {
return (
}
title={t('No results')}
description={t('No results found for the given query')}
/>
);
}
return (
);
}
function extendQuery(query = '', dashboardFilters?: DashboardFilters) {
if (!dashboardFilters?.release?.length) {
return query;
}
const releaseQuery = convertToQuery(dashboardFilters);
return `${query} ${releaseQuery}`;
}
function convertToQuery(dashboardFilters: DashboardFilters) {
const {release} = dashboardFilters;
if (!release?.length) {
return '';
}
if (release.length === 1) {
return `release:${release[0]}`;
}
return `release:[${release.join(',')}]`;
}
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: normal;
`;
const MetricWidgetChartWrapper = styled('div')`
height: 100%;
width: 100%;
padding: ${space(3)};
padding-top: ${space(2)};
`;