[APM] Elastic chart issues (#84238) (#84348)

* fixing charts

* addressing pr comments
This commit is contained in:
Cauê Marcondes 2020-11-25 18:52:54 +01:00 committed by GitHub
parent cfbb5e97d0
commit 8a40ae98b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 194 additions and 137 deletions

View file

@ -90,7 +90,12 @@ export function ErrorDistribution({ distribution, title }: Props) {
showOverlappingTicks
tickFormat={xFormatter}
/>
<Axis id="y-axis" position={Position.Left} ticks={2} showGridLines />
<Axis
id="y-axis"
position={Position.Left}
ticks={2}
gridLine={{ visible: true }}
/>
<HistogramBarSeries
minBarHeight={2}
id="errorOccurrences"

View file

@ -224,7 +224,7 @@ export function TransactionDistribution({
id="y-axis"
position={Position.Left}
ticks={3}
showGridLines
gridLine={{ visible: true }}
tickFormat={(value: number) => formatYShort(value)}
/>
<HistogramBarSeries

View file

@ -1,45 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
AnnotationDomainTypes,
LineAnnotation,
Position,
} from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { asAbsoluteDateTime } from '../../../../../common/utils/formatters';
import { useTheme } from '../../../../hooks/useTheme';
import { useAnnotations } from '../../../../hooks/use_annotations';
export function Annotations() {
const { annotations } = useAnnotations();
const theme = useTheme();
if (!annotations.length) {
return null;
}
const color = theme.eui.euiColorSecondary;
return (
<LineAnnotation
id="annotations"
domainType={AnnotationDomainTypes.XDomain}
dataValues={annotations.map((annotation) => ({
dataValue: annotation['@timestamp'],
header: asAbsoluteDateTime(annotation['@timestamp']),
details: `${i18n.translate('xpack.apm.chart.annotation.version', {
defaultMessage: 'Version',
})} ${annotation.text}`,
}))}
style={{ line: { strokeWidth: 1, stroke: color, opacity: 1 } }}
marker={<EuiIcon type="dot" color={color} />}
markerPosition={Position.Top}
/>
);
}

View file

@ -5,28 +5,35 @@
*/
import {
AnnotationDomainTypes,
AreaSeries,
Axis,
Chart,
CurveType,
LegendItemListener,
LineAnnotation,
LineSeries,
niceTimeFormatter,
Placement,
Position,
ScaleType,
Settings,
YDomainRange,
} from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { useChartTheme } from '../../../../../observability/public';
import { asAbsoluteDateTime } from '../../../../common/utils/formatters';
import { TimeSeries } from '../../../../typings/timeseries';
import { FETCH_STATUS } from '../../../hooks/useFetcher';
import { useTheme } from '../../../hooks/useTheme';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { useAnnotations } from '../../../hooks/use_annotations';
import { useChartPointerEvent } from '../../../hooks/use_chart_pointer_event';
import { unit } from '../../../style/variables';
import { Annotations } from './annotations';
import { ChartContainer } from './chart_container';
import { onBrushEnd } from './helper/helper';
@ -45,6 +52,7 @@ interface Props {
*/
yTickFormat?: (y: number) => string;
showAnnotations?: boolean;
yDomain?: YDomainRange;
}
export function TimeseriesChart({
@ -56,12 +64,16 @@ export function TimeseriesChart({
yLabelFormat,
yTickFormat,
showAnnotations = true,
yDomain,
}: Props) {
const history = useHistory();
const chartRef = React.createRef<Chart>();
const { annotations } = useAnnotations();
const chartTheme = useChartTheme();
const { pointerEvent, setPointerEvent } = useChartPointerEvent();
const { urlParams } = useUrlParams();
const theme = useTheme();
const { start, end } = urlParams;
useEffect(() => {
@ -83,6 +95,8 @@ export function TimeseriesChart({
y === null || y === undefined
);
const annotationColor = theme.eui.euiColorSecondary;
return (
<ChartContainer hasData={!isEmpty} height={height} status={fetchStatus}>
<Chart ref={chartRef} id={id}>
@ -108,17 +122,35 @@ export function TimeseriesChart({
position={Position.Bottom}
showOverlappingTicks
tickFormat={xFormatter}
gridLine={{ visible: false }}
/>
<Axis
domain={yDomain}
id="y-axis"
ticks={3}
position={Position.Left}
tickFormat={yTickFormat ? yTickFormat : yLabelFormat}
labelFormat={yLabelFormat}
showGridLines
/>
{showAnnotations && <Annotations />}
{showAnnotations && (
<LineAnnotation
id="annotations"
domainType={AnnotationDomainTypes.XDomain}
dataValues={annotations.map((annotation) => ({
dataValue: annotation['@timestamp'],
header: asAbsoluteDateTime(annotation['@timestamp']),
details: `${i18n.translate('xpack.apm.chart.annotation.version', {
defaultMessage: 'Version',
})} ${annotation.text}`,
}))}
style={{
line: { strokeWidth: 1, stroke: annotationColor, opacity: 1 },
}}
marker={<EuiIcon type="dot" color={annotationColor} />}
markerPosition={Position.Top}
/>
)}
{timeseries.map((serie) => {
const Series = serie.type === 'area' ? AreaSeries : LineSeries;

View file

@ -5,27 +5,35 @@
*/
import {
AnnotationDomainTypes,
AreaSeries,
Axis,
Chart,
CurveType,
LineAnnotation,
niceTimeFormatter,
Placement,
Position,
ScaleType,
Settings,
} from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { useChartTheme } from '../../../../../../observability/public';
import { asPercent } from '../../../../../common/utils/formatters';
import {
asAbsoluteDateTime,
asPercent,
} from '../../../../../common/utils/formatters';
import { TimeSeries } from '../../../../../typings/timeseries';
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useTheme } from '../../../../hooks/useTheme';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useAnnotations } from '../../../../hooks/use_annotations';
import { useChartPointerEvent } from '../../../../hooks/use_chart_pointer_event';
import { unit } from '../../../../style/variables';
import { Annotations } from '../../charts/annotations';
import { ChartContainer } from '../../charts/chart_container';
import { onBrushEnd } from '../../charts/helper/helper';
@ -44,9 +52,11 @@ export function TransactionBreakdownChartContents({
}: Props) {
const history = useHistory();
const chartRef = React.createRef<Chart>();
const { annotations } = useAnnotations();
const chartTheme = useChartTheme();
const { pointerEvent, setPointerEvent } = useChartPointerEvent();
const { urlParams } = useUrlParams();
const theme = useTheme();
const { start, end } = urlParams;
useEffect(() => {
@ -64,6 +74,8 @@ export function TransactionBreakdownChartContents({
const xFormatter = niceTimeFormatter([min, max]);
const annotationColor = theme.eui.euiColorSecondary;
return (
<ChartContainer height={height} hasData={!!timeseries} status={fetchStatus}>
<Chart ref={chartRef} id="timeSpentBySpan">
@ -85,6 +97,7 @@ export function TransactionBreakdownChartContents({
position={Position.Bottom}
showOverlappingTicks
tickFormat={xFormatter}
gridLine={{ visible: false }}
/>
<Axis
id="y-axis"
@ -93,7 +106,24 @@ export function TransactionBreakdownChartContents({
tickFormat={(y: number) => asPercent(y ?? 0, 1)}
/>
{showAnnotations && <Annotations />}
{showAnnotations && (
<LineAnnotation
id="annotations"
domainType={AnnotationDomainTypes.XDomain}
dataValues={annotations.map((annotation) => ({
dataValue: annotation['@timestamp'],
header: asAbsoluteDateTime(annotation['@timestamp']),
details: `${i18n.translate('xpack.apm.chart.annotation.version', {
defaultMessage: 'Version',
})} ${annotation.text}`,
}))}
style={{
line: { strokeWidth: 1, stroke: annotationColor, opacity: 1 },
}}
marker={<EuiIcon type="dot" color={annotationColor} />}
markerPosition={Position.Top}
/>
)}
{timeseries?.length ? (
timeseries.map((serie) => {

View file

@ -20,13 +20,14 @@ import {
TRANSACTION_ROUTE_CHANGE,
} from '../../../../../common/transaction_types';
import { asTransactionRate } from '../../../../../common/utils/formatters';
import { AnnotationsContextProvider } from '../../../../context/annotations_context';
import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event_context';
import { LicenseContext } from '../../../../context/LicenseContext';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { ITransactionChartData } from '../../../../selectors/chart_selectors';
import { TransactionBreakdownChart } from '../transaction_breakdown_chart';
import { TimeseriesChart } from '../timeseries_chart';
import { TransactionBreakdownChart } from '../transaction_breakdown_chart';
import { TransactionErrorRateChart } from '../transaction_error_rate_chart/';
import { getResponseTimeTickFormatter } from './helper';
import { MLHeader } from './ml_header';
@ -51,65 +52,69 @@ export function TransactionCharts({
return (
<>
<ChartPointerEventContextProvider>
<EuiFlexGrid columns={2} gutterSize="s">
<EuiFlexItem data-cy={`transaction-duration-charts`}>
<EuiPanel>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size="xs">
<span>{responseTimeLabel(transactionType)}</span>
</EuiTitle>
</EuiFlexItem>
<LicenseContext.Consumer>
{(license) => (
<MLHeader
hasValidMlLicense={license?.getFeature('ml').isAvailable}
mlJobId={charts.mlJobId}
/>
)}
</LicenseContext.Consumer>
</EuiFlexGroup>
<TimeseriesChart
fetchStatus={fetchStatus}
id="transactionDuration"
timeseries={responseTimeSeries || []}
yLabelFormat={getResponseTimeTickFormatter(formatter)}
onToggleLegend={(serie) => {
if (serie) {
toggleSerie(serie);
}
}}
/>
</EuiPanel>
</EuiFlexItem>
<AnnotationsContextProvider>
<ChartPointerEventContextProvider>
<EuiFlexGrid columns={2} gutterSize="s">
<EuiFlexItem data-cy={`transaction-duration-charts`}>
<EuiPanel>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size="xs">
<span>{responseTimeLabel(transactionType)}</span>
</EuiTitle>
</EuiFlexItem>
<LicenseContext.Consumer>
{(license) => (
<MLHeader
hasValidMlLicense={
license?.getFeature('ml').isAvailable
}
mlJobId={charts.mlJobId}
/>
)}
</LicenseContext.Consumer>
</EuiFlexGroup>
<TimeseriesChart
fetchStatus={fetchStatus}
id="transactionDuration"
timeseries={responseTimeSeries || []}
yLabelFormat={getResponseTimeTickFormatter(formatter)}
onToggleLegend={(serie) => {
if (serie) {
toggleSerie(serie);
}
}}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem style={{ flexShrink: 1 }}>
<EuiPanel>
<EuiTitle size="xs">
<span>{tpmLabel(transactionType)}</span>
</EuiTitle>
<TimeseriesChart
fetchStatus={fetchStatus}
id="requestPerMinutes"
timeseries={tpmSeries || []}
yLabelFormat={asTransactionRate}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGrid>
<EuiFlexItem style={{ flexShrink: 1 }}>
<EuiPanel>
<EuiTitle size="xs">
<span>{tpmLabel(transactionType)}</span>
</EuiTitle>
<TimeseriesChart
fetchStatus={fetchStatus}
id="requestPerMinutes"
timeseries={tpmSeries || []}
yLabelFormat={asTransactionRate}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGrid>
<EuiSpacer size="s" />
<EuiSpacer size="s" />
<EuiFlexGrid columns={2} gutterSize="s">
<EuiFlexItem>
<TransactionErrorRateChart />
</EuiFlexItem>
<EuiFlexItem>
<TransactionBreakdownChart />
</EuiFlexItem>
</EuiFlexGrid>
</ChartPointerEventContextProvider>
<EuiFlexGrid columns={2} gutterSize="s">
<EuiFlexItem>
<TransactionErrorRateChart />
</EuiFlexItem>
<EuiFlexItem>
<TransactionBreakdownChart />
</EuiFlexItem>
</EuiFlexGrid>
</ChartPointerEventContextProvider>
</AnnotationsContextProvider>
</>
);
}

View file

@ -91,6 +91,7 @@ export function TransactionErrorRateChart({
]}
yLabelFormat={yLabelFormat}
yTickFormat={yTickFormat}
yDomain={{ min: 0, max: 1 }}
/>
</EuiPanel>
);

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { createContext } from 'react';
import { useParams } from 'react-router-dom';
import { Annotation } from '../../common/annotations';
import { useFetcher } from '../hooks/useFetcher';
import { useUrlParams } from '../hooks/useUrlParams';
import { callApmApi } from '../services/rest/createCallApmApi';
export const AnnotationsContext = createContext({ annotations: [] } as {
annotations: Annotation[];
});
const INITIAL_STATE = { annotations: [] };
export function AnnotationsContextProvider({
children,
}: {
children: React.ReactNode;
}) {
const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams, uiFilters } = useUrlParams();
const { start, end } = urlParams;
const { environment } = uiFilters;
const { data = INITIAL_STATE } = useFetcher(() => {
if (start && end && serviceName) {
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/annotation/search',
params: {
path: {
serviceName,
},
query: {
start,
end,
environment,
},
},
});
}
}, [start, end, environment, serviceName]);
return <AnnotationsContext.Provider value={data} children={children} />;
}

View file

@ -3,36 +3,16 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useParams } from 'react-router-dom';
import { callApmApi } from '../services/rest/createCallApmApi';
import { useFetcher } from './useFetcher';
import { useUrlParams } from './useUrlParams';
const INITIAL_STATE = { annotations: [] };
import { useContext } from 'react';
import { AnnotationsContext } from '../context/annotations_context';
export function useAnnotations() {
const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams, uiFilters } = useUrlParams();
const { start, end } = urlParams;
const { environment } = uiFilters;
const context = useContext(AnnotationsContext);
const { data = INITIAL_STATE } = useFetcher(() => {
if (start && end && serviceName) {
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/annotation/search',
params: {
path: {
serviceName,
},
query: {
start,
end,
environment,
},
},
});
}
}, [start, end, environment, serviceName]);
if (!context) {
throw new Error('Missing Annotations context provider');
}
return data;
return context;
}