123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
- import styled from '@emotion/styled';
- import Alert from 'sentry/components/alert';
- import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
- import {CompactSelect} from 'sentry/components/compactSelect';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import {Tooltip} from 'sentry/components/tooltip';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {MetricsQueryApiResponse} from 'sentry/types';
- import {DEFAULT_SORT_STATE} from 'sentry/utils/metrics/constants';
- import type {FocusedMetricsSeries, SortState} from 'sentry/utils/metrics/types';
- import {
- type MetricsQueryApiQueryParams,
- useMetricsQuery,
- } from 'sentry/utils/metrics/useMetricsQuery';
- import usePageFilters from 'sentry/utils/usePageFilters';
- import {DASHBOARD_CHART_GROUP} from 'sentry/views/dashboards/dashboard';
- import {BigNumber, getBigNumberData} from 'sentry/views/dashboards/metrics/bigNumber';
- import {getTableData, MetricTable} from 'sentry/views/dashboards/metrics/table';
- import type {Order} from 'sentry/views/dashboards/metrics/types';
- import {toMetricDisplayType} from 'sentry/views/dashboards/metrics/utils';
- import {DisplayType} from 'sentry/views/dashboards/types';
- import {displayTypes} from 'sentry/views/dashboards/widgetBuilder/utils';
- import {LoadingScreen} from 'sentry/views/dashboards/widgetCard/widgetCardChartContainer';
- import {getIngestionSeriesId, MetricChart} from 'sentry/views/ddm/chart/chart';
- import {SummaryTable} from 'sentry/views/ddm/summaryTable';
- import {useSeriesHover} from 'sentry/views/ddm/useSeriesHover';
- import {createChartPalette} from 'sentry/views/ddm/utils/metricsChartPalette';
- import {getChartTimeseries, getWidgetTitle} from 'sentry/views/ddm/widget';
- function useFocusedSeries({
- timeseriesData,
- queries,
- onChange,
- }: {
- queries: MetricsQueryApiQueryParams[];
- timeseriesData: MetricsQueryApiResponse | null;
- onChange?: () => void;
- }) {
- const [focusedSeries, setFocusedSeries] = useState<FocusedMetricsSeries[]>([]);
- const chartSeries = useMemo(() => {
- return timeseriesData
- ? getChartTimeseries(timeseriesData, queries, {
- getChartPalette: createChartPalette,
- focusedSeries: focusedSeries && new Set(focusedSeries?.map(s => s.id)),
- })
- : [];
- }, [timeseriesData, focusedSeries, queries]);
- const toggleSeriesVisibility = useCallback(
- (series: FocusedMetricsSeries) => {
- onChange?.();
- // The focused series array is not populated yet, so we can add all series except the one that was de-selected
- if (!focusedSeries || focusedSeries.length === 0) {
- setFocusedSeries(
- chartSeries
- .filter(s => s.id !== series.id)
- .map(s => ({
- id: s.id,
- groupBy: s.groupBy,
- }))
- );
- return;
- }
- const filteredSeries = focusedSeries.filter(s => s.id !== series.id);
- if (filteredSeries.length === focusedSeries.length) {
- // The series was not focused before so we can add it
- filteredSeries.push(series);
- }
- setFocusedSeries(filteredSeries);
- },
- [chartSeries, focusedSeries, onChange]
- );
- const setSeriesVisibility = useCallback(
- (series: FocusedMetricsSeries) => {
- onChange?.();
- if (focusedSeries?.length === 1 && focusedSeries[0].id === series.id) {
- setFocusedSeries([]);
- return;
- }
- setFocusedSeries([series]);
- },
- [focusedSeries, onChange]
- );
- useEffect(() => {
- setFocusedSeries([]);
- }, [queries]);
- return {
- toggleSeriesVisibility,
- setSeriesVisibility,
- chartSeries,
- };
- }
- const supportedDisplayTypes = Object.keys(displayTypes).map(value => ({
- label: displayTypes[value],
- value,
- }));
- interface MetricVisualizationProps {
- displayType: DisplayType;
- onDisplayTypeChange: (displayType: DisplayType) => void;
- queries: MetricsQueryApiQueryParams[];
- onOrderChange?: ({id, order}: {id: number; order: Order}) => void;
- }
- export function MetricVisualization({
- queries,
- displayType,
- onDisplayTypeChange,
- onOrderChange,
- }: MetricVisualizationProps) {
- const {selection} = usePageFilters();
- const {
- data: timeseriesData,
- isLoading,
- isError,
- error,
- } = useMetricsQuery(queries, selection, {
- intervalLadder: displayType === DisplayType.BAR ? 'bar' : 'dashboard',
- });
- const widgetMQL = useMemo(() => getWidgetTitle(queries), [queries]);
- const visualizationComponent = useMemo(() => {
- if (!timeseriesData) {
- return null;
- }
- if (displayType === DisplayType.TABLE) {
- return (
- <MetricTableVisualization
- isLoading={isLoading}
- timeseriesData={timeseriesData}
- queries={queries}
- onOrderChange={onOrderChange}
- />
- );
- }
- if (displayType === DisplayType.BIG_NUMBER) {
- return (
- <MetricBigNumberVisualization
- timeseriesData={timeseriesData}
- isLoading={isLoading}
- queries={queries}
- />
- );
- }
- return (
- <MetricChartVisualization
- isLoading={isLoading}
- timeseriesData={timeseriesData}
- queries={queries}
- displayType={displayType}
- />
- );
- }, [timeseriesData, displayType, isLoading, queries, onOrderChange]);
- if (!timeseriesData || isError) {
- return (
- <StyledMetricChartContainer>
- {isLoading && <LoadingIndicator />}
- {isError && (
- <Alert type="error">
- {(error?.responseJSON?.detail as string) ||
- t('Error while fetching metrics data')}
- </Alert>
- )}
- </StyledMetricChartContainer>
- );
- }
- return (
- <StyledOuterContainer>
- <ViualizationHeader>
- <WidgetTitle>
- <StyledTooltip
- title={widgetMQL}
- showOnlyOnOverflow
- delay={500}
- overlayStyle={{maxWidth: '90vw'}}
- >
- {widgetMQL}
- </StyledTooltip>
- </WidgetTitle>
- <CompactSelect
- size="sm"
- triggerProps={{prefix: t('Visualization')}}
- value={displayType}
- options={supportedDisplayTypes}
- onChange={({value}) => onDisplayTypeChange(value as DisplayType)}
- />
- </ViualizationHeader>
- {visualizationComponent}
- </StyledOuterContainer>
- );
- }
- interface MetricTableVisualizationProps {
- isLoading: boolean;
- queries: MetricsQueryApiQueryParams[];
- timeseriesData: MetricsQueryApiResponse;
- onOrderChange?: ({id, order}: {id: number; order: Order}) => void;
- }
- function MetricTableVisualization({
- timeseriesData,
- queries,
- isLoading,
- onOrderChange,
- }: MetricTableVisualizationProps) {
- const tableData = useMemo(() => {
- return getTableData(timeseriesData, queries);
- }, [timeseriesData, queries]);
- const handleOrderChange = useCallback(
- (column: {id: number; order: Order}) => {
- onOrderChange?.(column);
- },
- [onOrderChange]
- );
- return (
- <Fragment>
- <TransparentLoadingMask visible={isLoading} />
- <MetricTable
- isLoading={isLoading}
- data={tableData}
- onOrderChange={handleOrderChange}
- />
- </Fragment>
- );
- }
- function MetricBigNumberVisualization({
- timeseriesData,
- queries,
- isLoading,
- }: MetricTableVisualizationProps) {
- const bigNumberData = useMemo(() => {
- return timeseriesData ? getBigNumberData(timeseriesData, queries) : undefined;
- }, [timeseriesData, queries]);
- if (!bigNumberData) {
- return null;
- }
- return (
- <Fragment>
- <LoadingScreen loading={isLoading} />
- <BigNumber>{bigNumberData}</BigNumber>
- </Fragment>
- );
- }
- interface MetricChartVisualizationProps extends MetricTableVisualizationProps {
- displayType: DisplayType;
- }
- function MetricChartVisualization({
- timeseriesData,
- queries,
- displayType,
- isLoading,
- }: MetricChartVisualizationProps) {
- const {chartRef, setHoveredSeries} = useSeriesHover();
- const handleHoverSeries = useCallback(
- (seriesId: string) => {
- setHoveredSeries([seriesId, getIngestionSeriesId(seriesId)]);
- },
- [setHoveredSeries]
- );
- const {chartSeries, toggleSeriesVisibility, setSeriesVisibility} = useFocusedSeries({
- timeseriesData,
- queries,
- onChange: () => handleHoverSeries(''),
- });
- const [tableSort, setTableSort] = useState<SortState>(DEFAULT_SORT_STATE);
- return (
- <Fragment>
- <TransparentLoadingMask visible={isLoading} />
- <MetricChart
- ref={chartRef}
- series={chartSeries}
- displayType={toMetricDisplayType(displayType)}
- group={DASHBOARD_CHART_GROUP}
- height={200}
- />
- <SummaryTable
- series={chartSeries}
- onSortChange={setTableSort}
- sort={tableSort}
- onRowClick={setSeriesVisibility}
- onColorDotClick={toggleSeriesVisibility}
- onRowHover={handleHoverSeries}
- />
- </Fragment>
- );
- }
- const StyledOuterContainer = styled('div')`
- display: flex;
- flex-direction: column;
- gap: ${space(3)};
- `;
- const StyledMetricChartContainer = styled('div')`
- gap: ${space(3)};
- display: flex;
- flex-direction: column;
- justify-content: center;
- height: 100%;
- `;
- const ViualizationHeader = styled('div')`
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: ${space(1)};
- `;
- const WidgetTitle = styled('div')`
- flex-grow: 1;
- font-size: ${p => p.theme.fontSizeMedium};
- display: inline-grid;
- grid-auto-flow: column;
- `;
- const StyledTooltip = styled(Tooltip)`
- ${p => p.theme.overflowEllipsis};
- `;
|