import {Component, Fragment} from 'react'; import {InjectedRouter} from 'react-router'; import styled from '@emotion/styled'; import {LineSeriesOption} from 'echarts'; import {Location} from 'history'; import isEqual from 'lodash/isEqual'; import {Client} from 'sentry/api'; import {AreaChart} from 'sentry/components/charts/areaChart'; import {BarChart} from 'sentry/components/charts/barChart'; import EventsChart from 'sentry/components/charts/eventsChart'; import {getInterval, getPreviousSeriesName} from 'sentry/components/charts/utils'; import {WorldMapChart} from 'sentry/components/charts/worldMapChart'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import {Panel} from 'sentry/components/panels'; import Placeholder from 'sentry/components/placeholder'; import {t} from 'sentry/locale'; import {Organization, SelectValue} from 'sentry/types'; import {valueIsEqual} from 'sentry/utils'; import {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements'; import {CustomMeasurementsContext} from 'sentry/utils/customMeasurements/customMeasurementsContext'; import {getUtcToLocalDateObject} from 'sentry/utils/dates'; import EventView from 'sentry/utils/discover/eventView'; import { getAggregateArg, isEquation, stripEquationPrefix, } from 'sentry/utils/discover/fields'; import { DisplayModes, MULTI_Y_AXIS_SUPPORTED_DISPLAY_MODES, TOP_EVENT_MODES, TOP_N, } from 'sentry/utils/discover/types'; import getDynamicText from 'sentry/utils/getDynamicText'; import {decodeScalar} from 'sentry/utils/queryString'; import withApi from 'sentry/utils/withApi'; import {isCustomMeasurement} from '../dashboardsV2/utils'; import ChartFooter from './chartFooter'; type ResultsChartProps = { api: Client; confirmedQuery: boolean; eventView: EventView; location: Location; organization: Organization; router: InjectedRouter; yAxisValue: string[]; customMeasurements?: CustomMeasurementCollection | undefined; loadingProcessedEventsBaseline?: boolean; processedLineSeries?: LineSeriesOption[]; reloadingProcessedEventsBaseline?: boolean; }; class ResultsChart extends Component { shouldComponentUpdate(nextProps: ResultsChartProps) { const {eventView, ...restProps} = this.props; const {eventView: nextEventView, ...restNextProps} = nextProps; if (!eventView.isEqualTo(nextEventView)) { return true; } return !isEqual(restProps, restNextProps); } render() { const { api, eventView, location, organization, router, confirmedQuery, yAxisValue, processedLineSeries, customMeasurements, loadingProcessedEventsBaseline, reloadingProcessedEventsBaseline, } = this.props; const hasPerformanceChartInterpolation = organization.features.includes( 'performance-chart-interpolation' ); const globalSelection = eventView.getPageFilters(); const start = globalSelection.datetime.start ? getUtcToLocalDateObject(globalSelection.datetime.start) : null; const end = globalSelection.datetime.end ? getUtcToLocalDateObject(globalSelection.datetime.end) : null; const {utc} = normalizeDateTimeParams(location.query); const apiPayload = eventView.getEventsAPIPayload(location); const display = eventView.getDisplayMode(); const isTopEvents = display === DisplayModes.TOP5 || display === DisplayModes.DAILYTOP5; const isPeriod = display === DisplayModes.DEFAULT || display === DisplayModes.TOP5; const isDaily = display === DisplayModes.DAILYTOP5 || display === DisplayModes.DAILY; const isPrevious = display === DisplayModes.PREVIOUS; const referrer = `api.discover.${display}-chart`; const topEvents = eventView.topEvents ? parseInt(eventView.topEvents, 10) : TOP_N; const aggregateParam = getAggregateArg(yAxisValue[0]) || ''; const customPerformanceMetricFieldType = isCustomMeasurement(aggregateParam) ? customMeasurements ? customMeasurements[aggregateParam]?.fieldType : null : null; const chartComponent = display === DisplayModes.WORLDMAP ? WorldMapChart : display === DisplayModes.BAR ? BarChart : customPerformanceMetricFieldType === 'size' && isTopEvents ? AreaChart : undefined; const interval = display === DisplayModes.BAR ? getInterval( { start, end, period: globalSelection.datetime.period, utc: utc === 'true', }, 'low' ) : eventView.interval; const seriesLabels = yAxisValue.map(stripEquationPrefix); const disableableSeries = [ ...seriesLabels, ...seriesLabels.map(getPreviousSeriesName), ]; if (processedLineSeries?.length) { processedLineSeries.forEach(series => disableableSeries.push((series.name as string) ?? '') ); } return ( {getDynamicText({ value: ( ), fixed: , })} ); } } type ContainerProps = { api: Client; confirmedQuery: boolean; disableProcessedBaselineToggle: boolean; eventView: EventView; location: Location; onAxisChange: (value: string[]) => void; onDisplayChange: (value: string) => void; onIntervalChange: (value: string | undefined) => void; onTopEventsChange: (value: string) => void; organization: Organization; router: InjectedRouter; setShowBaseline: (value: boolean) => void; showBaseline: boolean; // chart footer props total: number | null; yAxis: string[]; loadingProcessedEventsBaseline?: boolean; loadingProcessedTotals?: boolean; processedLineSeries?: LineSeriesOption[]; processedTotal?: number; reloadingProcessedEventsBaseline?: boolean; }; type ContainerState = { yAxisOptions: SelectValue[]; }; class ResultsChartContainer extends Component { state: ContainerState = { yAxisOptions: this.getYAxisOptions(this.props.eventView), }; componentWillReceiveProps(nextProps) { const yAxisOptions = this.getYAxisOptions(this.props.eventView); const nextYAxisOptions = this.getYAxisOptions(nextProps.eventView); if (!valueIsEqual(yAxisOptions, nextYAxisOptions, true)) { this.setState({yAxisOptions: nextYAxisOptions}); } } shouldComponentUpdate(nextProps: ContainerProps) { const {eventView, ...restProps} = this.props; const {eventView: nextEventView, ...restNextProps} = nextProps; if ( !eventView.isEqualTo(nextEventView) || this.props.confirmedQuery !== nextProps.confirmedQuery ) { return true; } if ( nextProps.reloadingProcessedEventsBaseline || nextProps.loadingProcessedEventsBaseline ) { return false; } return !isEqual(restProps, restNextProps); } getYAxisOptions(eventView) { const yAxisOptions = eventView.getYAxisOptions(); // Equations on World Map isn't supported on the events-geo endpoint // Disabling equations as an option to prevent erroring out if (eventView.getDisplayMode() === DisplayModes.WORLDMAP) { return yAxisOptions.filter(({value}) => !isEquation(value)); } return yAxisOptions; } render() { const { api, eventView, location, router, total, onAxisChange, onDisplayChange, onIntervalChange, onTopEventsChange, organization, confirmedQuery, yAxis, disableProcessedBaselineToggle, processedLineSeries, showBaseline, setShowBaseline, processedTotal, loadingProcessedTotals, loadingProcessedEventsBaseline, reloadingProcessedEventsBaseline, } = this.props; const {yAxisOptions} = this.state; const hasQueryFeature = organization.features.includes('discover-query'); const displayOptions = eventView .getDisplayOptions() .filter(opt => { // top5 modes are only available with larger packages in saas. // We remove instead of disable here as showing tooltips in dropdown // menus is clunky. if (TOP_EVENT_MODES.includes(opt.value) && !hasQueryFeature) { return false; } return true; }) .map(opt => { // Can only use default display or total daily with multi y axis if (TOP_EVENT_MODES.includes(opt.value)) { opt.label = DisplayModes.TOP5 === opt.value ? 'Top Period' : 'Top Daily'; } if ( yAxis.length > 1 && !MULTI_Y_AXIS_SUPPORTED_DISPLAY_MODES.includes(opt.value as DisplayModes) ) { return { ...opt, disabled: true, tooltip: t( 'Change the Y-Axis dropdown to display only 1 function to use this view.' ), }; } return opt; }); return ( {(yAxis.length > 0 && ( {contextValue => ( )} )) || {t('No Y-Axis selected.')}} ); } } export default withApi(ResultsChartContainer); const StyledPanel = styled(Panel)` @media (min-width: ${p => p.theme.breakpoints.large}) { margin: 0; } `; const NoChartContainer = styled('div')<{height?: string}>` display: flex; flex-direction: column; justify-content: center; align-items: center; flex: 1; flex-shrink: 0; overflow: hidden; height: ${p => p.height || '200px'}; position: relative; border-color: transparent; margin-bottom: 0; color: ${p => p.theme.gray300}; font-size: ${p => p.theme.fontSizeExtraLarge}; `;