// eslint-disable-next-line no-restricted-imports import {browserHistory, InjectedRouter, withRouter, WithRouterProps} from 'react-router'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import color from 'color'; import {Location} from 'history'; import ChartZoom from 'sentry/components/charts/chartZoom'; import MarkPoint from 'sentry/components/charts/components/markPoint'; import ErrorPanel from 'sentry/components/charts/errorPanel'; import EventsRequest from 'sentry/components/charts/eventsRequest'; import {LineChart, LineChartProps} from 'sentry/components/charts/lineChart'; import {SectionHeading} from 'sentry/components/charts/styles'; import TransitionChart from 'sentry/components/charts/transitionChart'; import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask'; import {getInterval} from 'sentry/components/charts/utils'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import Placeholder from 'sentry/components/placeholder'; import QuestionTooltip from 'sentry/components/questionTooltip'; import {IconWarning} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {Organization} from 'sentry/types'; import {getUtcToLocalDateObject} from 'sentry/utils/dates'; import {tooltipFormatter} from 'sentry/utils/discover/charts'; import EventView from 'sentry/utils/discover/eventView'; import {aggregateOutputType} from 'sentry/utils/discover/fields'; import {QueryError} from 'sentry/utils/discover/genericDiscoverQuery'; import { formatAbbreviatedNumber, formatFloat, formatPercentage, } from 'sentry/utils/formatters'; import getDynamicText from 'sentry/utils/getDynamicText'; import AnomaliesQuery from 'sentry/utils/performance/anomalies/anomaliesQuery'; import {decodeScalar} from 'sentry/utils/queryString'; import useApi from 'sentry/utils/useApi'; import {getTermHelp, PERFORMANCE_TERM} from 'sentry/views/performance/data'; import { anomaliesRouteWithQuery, ANOMALY_FLAG, anomalyToColor, } from '../transactionAnomalies/utils'; type ContainerProps = WithRouterProps & { error: QueryError | null; eventView: EventView; isLoading: boolean; location: Location; organization: Organization; totals: Record | null; transactionName: string; }; type Props = Pick & { chartData: { chartOptions: Omit; errored: boolean; loading: boolean; reloading: boolean; series: LineChartProps['series']; }; eventView: EventView; location: Location; router: InjectedRouter; transactionName: string; utc: boolean; end?: Date; start?: Date; statsPeriod?: string | null; }; function SidebarCharts({ organization, isLoading, error, totals, start, end, utc, router, statsPeriod, chartData, eventView, location, transactionName, }: Props) { const useAggregateAlias = !organization.features.includes( 'performance-frontend-use-events-endpoint' ); const theme = useTheme(); return ( {t('Apdex')} {t('Failure Rate')} {t('TPM')} {results => ( {zoomRenderProps => { const {errored, loading, reloading, chartOptions, series} = chartData; if (errored) { return ( ); } if (organization.features.includes(ANOMALY_FLAG)) { const epmSeries = series.find( s => s.seriesName.includes('epm') || s.seriesName.includes('tpm') ); if (epmSeries && results.data) { epmSeries.markPoint = MarkPoint({ data: results.data.anomalies.map(a => ({ name: a.id, yAxis: epmSeries.data.find(({name}) => name > (a.end + a.start) / 2) ?.value, // TODO: the above is O(n*m), remove after we change the api to include the midpoint of y. xAxis: a.start, itemStyle: { borderColor: color(anomalyToColor(a.confidence, theme)).string(), color: color(anomalyToColor(a.confidence, theme)) .alpha(0.2) .rgb() .string(), }, onClick: () => { const target = anomaliesRouteWithQuery({ orgSlug: organization.slug, query: location.query, projectID: decodeScalar(location.query.project), transaction: transactionName, }); browserHistory.push(target); }, })), symbol: 'circle', symbolSize: 16, }); } } return ( {getDynamicText({ value: ( ), fixed: , })} ); }} )} ); } function SidebarChartsContainer({ location, eventView, organization, router, isLoading, error, totals, transactionName, }: ContainerProps) { const api = useApi(); const theme = useTheme(); const colors = theme.charts.getColorPalette(3); const statsPeriod = eventView.statsPeriod; const start = eventView.start ? getUtcToLocalDateObject(eventView.start) : undefined; const end = eventView.end ? getUtcToLocalDateObject(eventView.end) : undefined; const project = eventView.project; const environment = eventView.environment; const query = eventView.query; const utc = normalizeDateTimeParams(location.query).utc === 'true'; const axisLineConfig = { scale: true, axisLine: { show: false, }, axisTick: { show: false, }, splitLine: { show: false, }, }; const chartOptions: Omit = { height: 480, grid: [ { top: '60px', left: '10px', right: '10px', height: '100px', }, { top: '220px', left: '10px', right: '10px', height: '100px', }, { top: '380px', left: '10px', right: '10px', height: '120px', }, ], axisPointer: { // Link each x-axis together. link: [{xAxisIndex: [0, 1, 2]}], }, xAxes: Array.from(new Array(3)).map((_i, index) => ({ gridIndex: index, type: 'time', show: false, })), yAxes: [ { // apdex gridIndex: 0, interval: 0.2, axisLabel: { formatter: (value: number) => `${formatFloat(value, 1)}`, color: theme.chartLabel, }, ...axisLineConfig, }, { // failure rate gridIndex: 1, splitNumber: 4, interval: 0.5, max: 1.0, axisLabel: { formatter: (value: number) => formatPercentage(value, 0), color: theme.chartLabel, }, ...axisLineConfig, }, { // throughput gridIndex: 2, splitNumber: 4, axisLabel: { formatter: formatAbbreviatedNumber, color: theme.chartLabel, }, ...axisLineConfig, }, ], utc, isGroupedByDate: true, showTimeInTooltip: true, colors: [colors[0], colors[1], colors[2]], tooltip: { trigger: 'axis', truncate: 80, valueFormatter: (value, label) => tooltipFormatter(value, aggregateOutputType(label)), nameFormatter(value: string) { return value === 'epm()' ? 'tpm()' : value; }, }, }; const requestCommonProps = { api, start, end, period: statsPeriod, project, environment, query, }; const contentCommonProps = { organization, router, error, isLoading, start, end, utc, totals, }; const datetimeSelection = { start: start || null, end: end || null, period: statsPeriod, }; return ( {({results, errored, loading, reloading}) => { const series = results ? results.map((values, i: number) => ({ ...values, yAxisIndex: i, xAxisIndex: i, })) : []; return ( ); }} ); } type ChartValueProps = { 'data-test-id': string; error: QueryError | null; isLoading: boolean; value: React.ReactNode; }; function ChartSummaryValue({error, isLoading, value, ...props}: ChartValueProps) { if (error) { return
{'\u2014'}
; } if (isLoading) { return ; } return {value}; } const RelativeBox = styled('div')` position: relative; `; const ChartTitle = styled(SectionHeading)` margin: 0; `; const ChartLabel = styled('div')<{top: string}>` position: absolute; top: ${p => p.top}; z-index: 1; `; const ChartValue = styled('div')` font-size: ${p => p.theme.fontSizeExtraLarge}; `; export default withRouter(SidebarChartsContainer);