123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- import {browserHistory} from 'react-router';
- import {useTheme} from '@emotion/react';
- import ChartZoom from 'sentry/components/charts/chartZoom';
- import ErrorPanel from 'sentry/components/charts/errorPanel';
- import EventsRequest from 'sentry/components/charts/eventsRequest';
- import type {LineChartProps} from 'sentry/components/charts/lineChart';
- import {LineChart} from 'sentry/components/charts/lineChart';
- import ReleaseSeries from 'sentry/components/charts/releaseSeries';
- import {ChartContainer, HeaderTitleLegend} from 'sentry/components/charts/styles';
- import TransitionChart from 'sentry/components/charts/transitionChart';
- import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
- import Panel from 'sentry/components/panels/panel';
- import QuestionTooltip from 'sentry/components/questionTooltip';
- import {IconWarning} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import type {DateString, OrganizationSummary} from 'sentry/types';
- import type {Series} from 'sentry/types/echarts';
- import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
- import {aggregateOutputType} from 'sentry/utils/discover/fields';
- import {WebVital} from 'sentry/utils/fields';
- import getDynamicText from 'sentry/utils/getDynamicText';
- import useApi from 'sentry/utils/useApi';
- import {useLocation} from 'sentry/utils/useLocation';
- import useRouter from 'sentry/utils/useRouter';
- import {replaceSeriesName, transformEventStatsSmoothed} from '../trends/utils';
- import type {ViewProps} from '../types';
- import {
- getMaxOfSeries,
- getVitalChartDefinitions,
- getVitalChartTitle,
- vitalNameFromLocation,
- VitalState,
- vitalStateColors,
- } from './utils';
- type Props = Omit<ViewProps, 'start' | 'end'> & {
- end: DateString | null;
- interval: string;
- organization: OrganizationSummary;
- start: DateString | null;
- };
- function VitalChart({
- project,
- environment,
- organization,
- query,
- statsPeriod,
- start,
- end,
- interval,
- }: Props) {
- const location = useLocation();
- const router = useRouter();
- const api = useApi();
- const theme = useTheme();
- const vitalName = vitalNameFromLocation(location);
- const yAxis = `p75(${vitalName})`;
- const {utc, legend, vitalPoor, markLines, chartOptions} = getVitalChartDefinitions({
- theme,
- location,
- yAxis,
- vital: vitalName,
- });
- function handleLegendSelectChanged(legendChange: {
- name: string;
- selected: Record<string, boolean>;
- type: string;
- }) {
- const {selected} = legendChange;
- const unselected = Object.keys(selected).filter(key => !selected[key]);
- const to = {
- ...location,
- query: {
- ...location.query,
- unselectedSeries: unselected,
- },
- };
- browserHistory.push(to);
- }
- return (
- <Panel>
- <ChartContainer>
- <HeaderTitleLegend>
- {getVitalChartTitle(vitalName)}
- <QuestionTooltip
- size="sm"
- position="top"
- title={t('The durations shown should fall under the vital threshold.')}
- />
- </HeaderTitleLegend>
- <ChartZoom router={router} period={statsPeriod} start={start} end={end} utc={utc}>
- {zoomRenderProps => (
- <EventsRequest
- api={api}
- organization={organization}
- period={statsPeriod}
- project={project}
- environment={environment}
- start={start}
- end={end}
- interval={interval}
- showLoading={false}
- query={query}
- includePrevious={false}
- yAxis={[yAxis]}
- partial
- >
- {({timeseriesData: results, errored, loading, reloading}) => {
- if (errored) {
- return (
- <ErrorPanel>
- <IconWarning color="gray500" size="lg" />
- </ErrorPanel>
- );
- }
- const colors =
- (results && theme.charts.getColorPalette(results.length - 2)) || [];
- const {smoothedResults} = transformEventStatsSmoothed(results);
- const smoothedSeries = smoothedResults
- ? smoothedResults.map(({seriesName, ...rest}, i: number) => {
- return {
- seriesName: replaceSeriesName(seriesName) || 'p75',
- ...rest,
- color: colors[i],
- lineStyle: {
- opacity: 1,
- width: 2,
- },
- };
- })
- : [];
- const seriesMax = getMaxOfSeries(smoothedSeries);
- const yAxisMax = Math.max(seriesMax, vitalPoor);
- chartOptions.yAxis!.max = yAxisMax * 1.1;
- return (
- <ReleaseSeries
- start={start}
- end={end}
- period={statsPeriod}
- utc={utc}
- projects={project}
- environments={environment}
- >
- {({releaseSeries}) => (
- <TransitionChart loading={loading} reloading={reloading}>
- <TransparentLoadingMask visible={reloading} />
- {getDynamicText({
- value: (
- <LineChart
- {...zoomRenderProps}
- {...chartOptions}
- legend={legend}
- onLegendSelectChanged={handleLegendSelectChanged}
- series={[...markLines, ...releaseSeries, ...smoothedSeries]}
- />
- ),
- fixed: 'Web Vitals Chart',
- })}
- </TransitionChart>
- )}
- </ReleaseSeries>
- );
- }}
- </EventsRequest>
- )}
- </ChartZoom>
- </ChartContainer>
- </Panel>
- );
- }
- export default VitalChart;
- export type _VitalChartProps = {
- field: string;
- grid: LineChartProps['grid'];
- loading: boolean;
- reloading: boolean;
- data?: Series[];
- height?: number;
- utc?: boolean;
- vitalFields?: {
- goodCountField: string;
- mehCountField: string;
- poorCountField: string;
- };
- };
- function fieldToVitalType(
- seriesName: string,
- vitalFields: _VitalChartProps['vitalFields']
- ): VitalState | undefined {
- if (seriesName === vitalFields?.poorCountField.replace('equation|', '')) {
- return VitalState.POOR;
- }
- if (seriesName === vitalFields?.mehCountField.replace('equation|', '')) {
- return VitalState.MEH;
- }
- if (seriesName === vitalFields?.goodCountField.replace('equation|', '')) {
- return VitalState.GOOD;
- }
- return undefined;
- }
- export function _VitalChart(props: _VitalChartProps) {
- const {
- field: yAxis,
- data: _results,
- loading,
- reloading,
- height,
- grid,
- utc,
- vitalFields,
- } = props;
- const theme = useTheme();
- if (!_results || !vitalFields) {
- return null;
- }
- const chartOptions: Omit<LineChartProps, 'series'> = {
- grid,
- seriesOptions: {
- showSymbol: false,
- },
- tooltip: {
- trigger: 'axis',
- valueFormatter: (value: number, seriesName?: string) => {
- return tooltipFormatter(
- value,
- aggregateOutputType(vitalFields[0] === WebVital.CLS ? seriesName : yAxis)
- );
- },
- },
- xAxis: {
- show: false,
- },
- xAxes: undefined,
- yAxis: {
- axisLabel: {
- color: theme.chartLabel,
- showMaxLabel: false,
- formatter: (value: number) =>
- axisLabelFormatter(value, aggregateOutputType(yAxis)),
- },
- },
- utc,
- isGroupedByDate: true,
- showTimeInTooltip: true,
- };
- const results = _results.filter(s => !!fieldToVitalType(s.seriesName, vitalFields));
- const smoothedSeries = results?.length
- ? results.map(({seriesName, ...rest}) => {
- const adjustedSeries = fieldToVitalType(seriesName, vitalFields) || 'count';
- return {
- seriesName: adjustedSeries,
- ...rest,
- color: theme[vitalStateColors[adjustedSeries]],
- lineStyle: {
- opacity: 1,
- width: 2,
- },
- };
- })
- : [];
- return (
- <div>
- <TransitionChart loading={loading} reloading={reloading}>
- <TransparentLoadingMask visible={reloading} />
- {getDynamicText({
- value: (
- <LineChart
- height={height}
- {...chartOptions}
- onLegendSelectChanged={() => {}}
- series={[...smoothedSeries]}
- isGroupedByDate
- />
- ),
- fixed: 'Web Vitals Chart',
- })}
- </TransitionChart>
- </div>
- );
- }
|