mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
305 lines
8.8 KiB
TypeScript
305 lines
8.8 KiB
TypeScript
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>
|
|
);
|
|
}
|