Add error rate chart to overview (#82082)

* Add error rate chart to overview

Take most of the work directly from #80298 to add the error rate chart to the overview.

Rename the existing chart that's on the transactions overview so it still keeps using the old chart for the time being. We don't want to mix chart types (react-vis + elastic-charts) on the same page becuase the interactions are different. We'll switch the transactions page to use elastic charts in a future PR.

Hide the error rate chart on RUM services.
This commit is contained in:
Nathan L Smith 2020-11-02 10:24:03 -06:00 committed by GitHub
parent e2f0f94a49
commit 2f504a05a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 622 additions and 202 deletions

View file

@ -48,7 +48,9 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) {
})}
</ServiceOverviewLink>
),
render: () => <ServiceOverview serviceName={serviceName} />,
render: () => (
<ServiceOverview agentName={agentName} serviceName={serviceName} />
),
name: 'overview',
};

View file

@ -15,7 +15,7 @@ import React, { useMemo } from 'react';
import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts';
import { MetricsChart } from '../../shared/charts/MetricsChart';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
import { LegacyChartsSyncContextProvider as ChartsSyncContextProvider } from '../../../context/charts_sync_context';
import { Projection } from '../../../../common/projections';
import { LocalUIFilters } from '../../shared/LocalUIFilters';

View file

@ -22,7 +22,7 @@ import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import styled from 'styled-components';
import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes';
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
import { LegacyChartsSyncContextProvider as ChartsSyncContextProvider } from '../../../context/charts_sync_context';
import { useAgentName } from '../../../hooks/useAgentName';
import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts';

View file

@ -24,7 +24,7 @@ import { TransactionCharts } from '../../shared/charts/TransactionCharts';
import { TransactionDistribution } from './Distribution';
import { WaterfallWithSummmary } from './WaterfallWithSummmary';
import { FETCH_STATUS } from '../../../hooks/useFetcher';
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
import { LegacyChartsSyncContextProvider as ChartsSyncContextProvider } from '../../../context/charts_sync_context';
import { useTrackPageview } from '../../../../../observability/public';
import { Projection } from '../../../../common/projections';
import { fromQuery, toQuery } from '../../shared/Links/url_helpers';

View file

@ -22,7 +22,7 @@ import React, { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useTrackPageview } from '../../../../../observability/public';
import { Projection } from '../../../../common/projections';
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
import { LegacyChartsSyncContextProvider as ChartsSyncContextProvider } from '../../../context/charts_sync_context';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes';
import { useTransactionCharts } from '../../../hooks/useTransactionCharts';

View file

@ -9,6 +9,9 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import styled from 'styled-components';
import { useTrackPageview } from '../../../../../observability/public';
import { isRumAgentName } from '../../../../common/agent_name';
import { ChartsSyncContextProvider } from '../../../context/charts_sync_context';
import { ErroneousTransactionsRateChart } from '../../shared/charts/erroneous_transactions_rate_chart';
import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink';
import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink';
import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink';
@ -31,216 +34,225 @@ const TableLinkFlexItem = styled(EuiFlexItem)`
`;
interface ServiceOverviewProps {
agentName?: string;
serviceName: string;
}
export function ServiceOverview({ serviceName }: ServiceOverviewProps) {
export function ServiceOverview({
agentName,
serviceName,
}: ServiceOverviewProps) {
useTrackPageview({ app: 'apm', path: 'service_overview' });
useTrackPageview({ app: 'apm', path: 'service_overview', delay: 15000 });
return (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup
gutterSize="xs"
style={{ marginTop: 16, marginBottom: 8 }}
>
<EuiFlexItem grow={2}>
<EuiPanel>Search bar</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel>Comparison picker</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel>Date picker</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<LatencyChartRow>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.apm.serviceOverview.latencyChartTitle', {
defaultMessage: 'Latency',
})}
</h2>
</EuiTitle>
</EuiPanel>
</LatencyChartRow>
<Row>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.trafficChartTitle',
{
defaultMessage: 'Traffic',
}
)}
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.transactionsTableTitle',
{
defaultMessage: 'Transactions',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<TableLinkFlexItem>
<TransactionOverviewLink serviceName={serviceName}>
<ChartsSyncContextProvider>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup
gutterSize="xs"
style={{ marginTop: 16, marginBottom: 8 }}
>
<EuiFlexItem grow={2}>
<EuiPanel>Search bar</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel>Comparison picker</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel>Date picker</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<LatencyChartRow>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.apm.serviceOverview.latencyChartTitle', {
defaultMessage: 'Latency',
})}
</h2>
</EuiTitle>
</EuiPanel>
</LatencyChartRow>
<Row>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.transactionsTableLinkText',
'xpack.apm.serviceOverview.trafficChartTitle',
{
defaultMessage: 'View transactions',
defaultMessage: 'Traffic',
}
)}
</TransactionOverviewLink>
</TableLinkFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</Row>
<Row>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.errorRateChartTitle',
{
defaultMessage: 'Error rate',
}
)}
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem>
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.transactionsTableTitle',
{
defaultMessage: 'Transactions',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<TableLinkFlexItem>
<TransactionOverviewLink serviceName={serviceName}>
{i18n.translate(
'xpack.apm.serviceOverview.transactionsTableLinkText',
{
defaultMessage: 'View transactions',
}
)}
</TransactionOverviewLink>
</TableLinkFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</Row>
<Row>
<EuiFlexGroup gutterSize="s">
{!isRumAgentName(agentName) && (
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.errorsTableTitle',
'xpack.apm.serviceOverview.errorRateChartTitle',
{
defaultMessage: 'Errors',
defaultMessage: 'Error rate',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<TableLinkFlexItem>
<ErrorOverviewLink serviceName={serviceName}>
<ErroneousTransactionsRateChart />
</EuiPanel>
</EuiFlexItem>
)}
<EuiFlexItem grow={6}>
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.errorsTableTitle',
{
defaultMessage: 'Errors',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<TableLinkFlexItem>
<ErrorOverviewLink serviceName={serviceName}>
{i18n.translate(
'xpack.apm.serviceOverview.errorsTableLinkText',
{
defaultMessage: 'View errors',
}
)}
</ErrorOverviewLink>
</TableLinkFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</Row>
<Row>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.averageDurationBySpanTypeChartTitle',
{
defaultMessage: 'Average duration by span type',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.dependenciesTableTitle',
{
defaultMessage: 'Dependencies',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<TableLinkFlexItem>
<ServiceMapLink serviceName={serviceName}>
{i18n.translate(
'xpack.apm.serviceOverview.dependenciesTableLinkText',
{
defaultMessage: 'View service map',
}
)}
</ServiceMapLink>
</TableLinkFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</Row>
<Row>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.errorsTableLinkText',
'xpack.apm.serviceOverview.instancesLatencyDistributionChartTitle',
{
defaultMessage: 'View errors',
defaultMessage: 'Instances latency distribution',
}
)}
</ErrorOverviewLink>
</TableLinkFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</Row>
<Row>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.averageDurationBySpanTypeChartTitle',
{
defaultMessage: 'Average duration by span type',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.dependenciesTableTitle',
{
defaultMessage: 'Dependencies',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<TableLinkFlexItem>
<ServiceMapLink serviceName={serviceName}>
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.dependenciesTableLinkText',
'xpack.apm.serviceOverview.instancesTableTitle',
{
defaultMessage: 'View service map',
defaultMessage: 'Instances',
}
)}
</ServiceMapLink>
</TableLinkFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</Row>
<Row>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.instancesLatencyDistributionChartTitle',
{
defaultMessage: 'Instances latency distribution',
}
)}
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.instancesTableTitle',
{
defaultMessage: 'Instances',
}
)}
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</Row>
</EuiFlexGroup>
</h2>
</EuiTitle>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</Row>
</EuiFlexGroup>
</ChartsSyncContextProvider>
);
}

View file

@ -4,16 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { render } from '@testing-library/react';
import React, { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { CoreStart } from 'src/core/public';
import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public';
import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext';
import { renderWithTheme } from '../../../utils/testHelpers';
import { ServiceOverview } from './';
const KibanaReactContext = createKibanaReactContext({
usageCollection: { reportUiStats: () => {} },
} as Partial<CoreStart>);
function Wrapper({ children }: { children?: ReactNode }) {
return (
<MemoryRouter>
<MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>
<KibanaReactContext.Provider>
<MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>
</KibanaReactContext.Provider>
</MemoryRouter>
);
}
@ -21,7 +29,7 @@ function Wrapper({ children }: { children?: ReactNode }) {
describe('ServiceOverview', () => {
it('renders', () => {
expect(() =>
render(<ServiceOverview serviceName="test service name" />, {
renderWithTheme(<ServiceOverview serviceName="test service name" />, {
wrapper: Wrapper,
})
).not.toThrowError();

View file

@ -19,7 +19,7 @@ import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform
import CustomPlot from '../CustomPlot';
import { Coordinate } from '../../../../../typings/timeseries';
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
import { useChartsSync } from '../../../../hooks/useChartsSync';
import { useLegacyChartsSync as useChartsSync } from '../../../../hooks/use_charts_sync';
import { Maybe } from '../../../../../typings/common';
interface Props {

View file

@ -6,7 +6,7 @@
import React, { useCallback } from 'react';
import { Coordinate, TimeSeries } from '../../../../../../typings/timeseries';
import { useChartsSync } from '../../../../../hooks/useChartsSync';
import { useLegacyChartsSync as useChartsSync } from '../../../../../hooks/use_charts_sync';
// @ts-expect-error
import CustomPlot from '../../CustomPlot';

View file

@ -26,7 +26,7 @@ import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { ITransactionChartData } from '../../../../selectors/chartSelectors';
import { asDecimal, tpmUnit } from '../../../../../common/utils/formatters';
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
import { ErroneousTransactionsRateChart } from '../ErroneousTransactionsRateChart';
import { ErroneousTransactionsRateChart } from '../erroneous_transactions_rate_chart/legacy';
import { TransactionBreakdown } from '../../TransactionBreakdown';
import {
getResponseTimeTickFormatter,

View file

@ -0,0 +1,34 @@
/*
* 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 { render } from '@testing-library/react';
import React from 'react';
import { ChartContainer } from './chart_container';
describe('ChartContainer', () => {
describe('when isLoading is true', () => {
it('shows loading the indicator', () => {
const component = render(
<ChartContainer height={100} isLoading={true}>
<div>My amazing component</div>
</ChartContainer>
);
expect(component.getByTestId('loading')).toBeInTheDocument();
});
});
describe('when isLoading is false', () => {
it('does not show the loading indicator', () => {
const component = render(
<ChartContainer height={100} isLoading={false}>
<div>My amazing component</div>
</ChartContainer>
);
expect(component.queryByTestId('loading')).not.toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,29 @@
/*
* 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 { EuiLoadingChart } from '@elastic/eui';
import React from 'react';
interface Props {
isLoading: boolean;
height: number;
children: React.ReactNode;
}
export function ChartContainer({ isLoading, children, height }: Props) {
return (
<div
style={{
height,
display: isLoading ? 'flex' : 'block',
alignItems: 'center',
justifyContent: 'center',
}}
>
{isLoading && <EuiLoadingChart data-test-subj="loading" size={'xl'} />}
{children}
</div>
);
}

View file

@ -0,0 +1,80 @@
/*
* 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 { i18n } from '@kbn/i18n';
import React from 'react';
import { useParams } from 'react-router-dom';
import { asPercent } from '../../../../../common/utils/formatters';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher';
import { useTheme } from '../../../../hooks/useTheme';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { callApmApi } from '../../../../services/rest/createCallApmApi';
import { LineChart } from '../line_chart';
function yLabelFormat(y?: number | null) {
return asPercent(y || 0, 1);
}
function yTickFormat(y?: number | null) {
return i18n.translate('xpack.apm.chart.averagePercentLabel', {
defaultMessage: '{y} (avg.)',
values: { y: yLabelFormat(y) },
});
}
export function ErroneousTransactionsRateChart() {
const theme = useTheme();
const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams, uiFilters } = useUrlParams();
const { start, end, transactionType, transactionName } = urlParams;
const { data, status } = useFetcher(() => {
if (serviceName && start && end) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/transaction_groups/error_rate',
params: {
path: {
serviceName,
},
query: {
start,
end,
transactionType,
transactionName,
uiFilters: JSON.stringify(uiFilters),
},
},
});
}
}, [serviceName, start, end, uiFilters, transactionType, transactionName]);
const errorRates = data?.transactionErrorRate || [];
return (
<LineChart
id="errorRate"
isLoading={
(status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING) &&
!data
}
timeseries={[
{
data: errorRates,
type: 'linemark',
color: theme.eui.euiColorVis7,
hideLegend: true,
title: i18n.translate('xpack.apm.chart.currentPeriodLabel', {
defaultMessage: 'Current period',
}),
},
]}
yLabelFormat={yLabelFormat}
yTickFormat={yTickFormat}
/>
);
}

View file

@ -10,7 +10,7 @@ import { max } from 'lodash';
import React, { useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { asPercent } from '../../../../../common/utils/formatters';
import { useChartsSync } from '../../../../hooks/useChartsSync';
import { useLegacyChartsSync as useChartsSync } from '../../../../hooks/use_charts_sync';
import { useFetcher } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { callApmApi } from '../../../../services/rest/createCallApmApi';
@ -21,6 +21,12 @@ const tickFormatY = (y?: number | null) => {
return asPercent(y || 0, 1);
};
/**
* "Legacy" version of this chart using react-vis charts. See index.tsx for the
* Elastic Charts version.
*
* This will be removed with #70290.
*/
export function ErroneousTransactionsRateChart() {
const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams, uiFilters } = useUrlParams();

View file

@ -0,0 +1,39 @@
/*
* 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 { onBrushEnd } from './helper';
import { History } from 'history';
describe('Chart helper', () => {
describe('onBrushEnd', () => {
const history = ({
push: jest.fn(),
location: {
search: '',
},
} as unknown) as History;
it("doesn't push a new history when x is not defined", () => {
onBrushEnd({ x: undefined, history });
expect(history.push).not.toBeCalled();
});
it('pushes a new history with time range converted to ISO', () => {
onBrushEnd({ x: [1593409448167, 1593415727797], history });
expect(history.push).toBeCalledWith({
search:
'rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z',
});
});
it('pushes a new history keeping current search', () => {
history.location.search = '?foo=bar';
onBrushEnd({ x: [1593409448167, 1593415727797], history });
expect(history.push).toBeCalledWith({
search:
'foo=bar&rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z',
});
});
});
});

View file

@ -0,0 +1,35 @@
/*
* 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 { XYBrushArea } from '@elastic/charts';
import { History } from 'history';
import { fromQuery, toQuery } from '../../Links/url_helpers';
export const onBrushEnd = ({
x,
history,
}: {
x: XYBrushArea['x'];
history: History;
}) => {
if (x) {
const start = x[0];
const end = x[1];
const currentSearch = toQuery(history.location.search);
const nextSearch = {
rangeFrom: new Date(start).toISOString(),
rangeTo: new Date(end).toISOString(),
};
history.push({
...history.location,
search: fromQuery({
...currentSearch,
...nextSearch,
}),
});
}
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment-timezone';
import { getDomainTZ, getTimeTicksTZ } from '../timezone';
import { getDomainTZ, getTimeTicksTZ } from './timezone';
describe('Timezone helper', () => {
let originalTimezone: moment.MomentZone | null;

View file

@ -0,0 +1,140 @@
/*
* 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 {
Axis,
Chart,
LegendItemListener,
LineSeries,
niceTimeFormatter,
Placement,
Position,
ScaleType,
Settings,
SettingsSpec,
} from '@elastic/charts';
import moment from 'moment';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { TimeSeries } from '../../../../../typings/timeseries';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useChartsSync } from '../../../../hooks/use_charts_sync';
import { unit } from '../../../../style/variables';
import { ChartContainer } from '../chart_container';
import { onBrushEnd } from '../helper/helper';
interface Props {
id: string;
isLoading: boolean;
onToggleLegend?: LegendItemListener;
timeseries: TimeSeries[];
/**
* Formatter for y-axis tick values
*/
yLabelFormat: (y: number) => string;
/**
* Formatter for legend and tooltip values
*/
yTickFormat: (y: number) => string;
}
const XY_HEIGHT = unit * 16;
export function LineChart({
id,
isLoading,
onToggleLegend,
timeseries,
yLabelFormat,
yTickFormat,
}: Props) {
const history = useHistory();
const chartRef = React.createRef<Chart>();
const { event, setEvent } = useChartsSync();
const { urlParams } = useUrlParams();
const { start, end } = urlParams;
useEffect(() => {
if (event.chartId !== id && chartRef.current) {
chartRef.current.dispatchExternalPointerEvent(event);
}
}, [event, chartRef, id]);
const min = moment.utc(start).valueOf();
const max = moment.utc(end).valueOf();
const xFormatter = niceTimeFormatter([min, max]);
const chartTheme: SettingsSpec['theme'] = {
lineSeriesStyle: {
point: { visible: false },
line: { strokeWidth: 2 },
},
};
const isEmpty = timeseries
.map((serie) => serie.data)
.flat()
.every(
({ y }: { x?: number | null; y?: number | null }) =>
y === null || y === undefined
);
return (
<ChartContainer isLoading={isLoading} height={XY_HEIGHT}>
<Chart ref={chartRef} id={id}>
<Settings
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
theme={chartTheme}
onPointerUpdate={(currEvent: any) => {
setEvent(currEvent);
}}
externalPointerEvents={{
tooltip: { visible: true, placement: Placement.Bottom },
}}
showLegend
showLegendExtra
legendPosition={Position.Bottom}
xDomain={{ min, max }}
onLegendItemClick={(legend) => {
if (onToggleLegend) {
onToggleLegend(legend);
}
}}
/>
<Axis
id="x-axis"
position={Position.Bottom}
showOverlappingTicks
tickFormat={xFormatter}
/>
<Axis
id="y-axis"
ticks={3}
position={Position.Left}
tickFormat={yTickFormat}
labelFormat={yLabelFormat}
showGridLines
/>
{timeseries.map((serie) => {
return (
<LineSeries
key={serie.title}
id={serie.title}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
data={isEmpty ? [] : serie.data}
color={serie.color}
/>
);
})}
</Chart>
</ChartContainer>
);
}

View file

@ -10,14 +10,18 @@ import { fromQuery, toQuery } from '../components/shared/Links/url_helpers';
import { useFetcher } from '../hooks/useFetcher';
import { useUrlParams } from '../hooks/useUrlParams';
const ChartsSyncContext = React.createContext<{
export const LegacyChartsSyncContext = React.createContext<{
hoverX: number | null;
onHover: (hoverX: number) => void;
onMouseLeave: () => void;
onSelectionEnd: (range: { start: number; end: number }) => void;
} | null>(null);
function ChartsSyncContextProvider({ children }: { children: ReactNode }) {
export function LegacyChartsSyncContextProvider({
children,
}: {
children: ReactNode;
}) {
const history = useHistory();
const [time, setTime] = useState<number | null>(null);
const { serviceName } = useParams<{ serviceName?: string }>();
@ -79,7 +83,25 @@ function ChartsSyncContextProvider({ children }: { children: ReactNode }) {
return { ...hoverXHandlers };
}, [history, time, data.annotations]);
return <ChartsSyncContext.Provider value={value} children={children} />;
return <LegacyChartsSyncContext.Provider value={value} children={children} />;
}
export { ChartsSyncContext, ChartsSyncContextProvider };
export const ChartsSyncContext = React.createContext<{
event: any;
setEvent: Function;
} | null>(null);
export function ChartsSyncContextProvider({
children,
}: {
children: ReactNode;
}) {
const [event, setEvent] = useState({});
return (
<ChartsSyncContext.Provider
value={{ event, setEvent }}
children={children}
/>
);
}

View file

@ -5,7 +5,10 @@
*/
import { useContext } from 'react';
import { ChartsSyncContext } from '../context/ChartsSyncContext';
import {
ChartsSyncContext,
LegacyChartsSyncContext,
} from '../context/charts_sync_context';
export function useChartsSync() {
const context = useContext(ChartsSyncContext);
@ -16,3 +19,13 @@ export function useChartsSync() {
return context;
}
export function useLegacyChartsSync() {
const context = useContext(LegacyChartsSyncContext);
if (!context) {
throw new Error('Missing ChartsSync context provider');
}
return context;
}