import {Fragment, 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 {EquationFormatter} from 'sentry/components/metrics/equationInput/syntax/formatter';
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 {getFormattedMQL, unescapeMetricsFormula} from 'sentry/utils/metrics';
import {hasMetricsNewInputs} from 'sentry/utils/metrics/features';
import {formatMRIField, MRIToField, parseMRI} from 'sentry/utils/metrics/mri';
import {MetricExpressionType} from 'sentry/utils/metrics/types';
import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsQuery';
import {useVirtualMetricsContext} from 'sentry/utils/metrics/virtualMetricsContext';
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 type {DashboardMetricsExpression} from 'sentry/views/dashboards/metrics/types';
import {
expressionsToApiQueries,
formatAlias,
getMetricExpressions,
isMetricsEquation,
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';
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 getWidgetTitle(expressions: DashboardMetricsExpression[]) {
const filteredExpressions = expressions.filter(query => !query.isQueryOnly);
if (filteredExpressions.length === 1) {
const firstQuery = filteredExpressions[0];
if (isMetricsEquation(firstQuery)) {
return (
);
}
return formatAlias(firstQuery.alias) ?? getFormattedMQL(firstQuery);
}
return filteredExpressions
.map(q =>
isMetricsEquation(q)
? formatAlias(q.alias) ?? unescapeMetricsFormula(q.formula)
: formatAlias(q.alias) ?? formatMRIField(MRIToField(q.mri, q.aggregation))
)
.join(', ');
}
export function MetricWidgetCard({
organization,
selection,
widget,
isEditingDashboard,
onDelete,
onDuplicate,
location,
router,
dashboardFilters,
renderErrorMessage,
showContextMenu = true,
}: Props) {
const metricsNewInputs = hasMetricsNewInputs(organization);
const {getVirtualMRIQuery, isLoading: isLoadingVirtualMetrics} =
useVirtualMetricsContext();
const metricExpressions = getMetricExpressions(
widget,
dashboardFilters,
getVirtualMRIQuery
);
const hasSetMetric = useMemo(
() =>
metricExpressions.some(
expression =>
expression.type === MetricExpressionType.QUERY &&
parseMRI(expression.mri)!.type === 's'
),
[metricExpressions]
);
const widgetMQL = useMemo(
() => (isLoadingVirtualMetrics ? '' : getWidgetTitle(metricExpressions)),
[isLoadingVirtualMetrics, metricExpressions]
);
const metricQueries = useMemo(() => {
const formattedAliasQueries = expressionsToApiQueries(
metricExpressions,
metricsNewInputs
).map(query => {
if (query.alias) {
return {...query, alias: formatAlias(query.alias)};
}
return query;
});
return [...formattedAliasQueries];
}, [metricExpressions, metricsNewInputs]);
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,
isPending,
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, isPending, 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)};
`;