);
+
function Wrapper({ children }: { children?: ReactNode }) {
return (
- {children}
+
+ {children}
+
);
}
@@ -21,7 +29,7 @@ function Wrapper({ children }: { children?: ReactNode }) {
describe('ServiceOverview', () => {
it('renders', () => {
expect(() =>
- render(, {
+ renderWithTheme(, {
wrapper: Wrapper,
})
).not.toThrowError();
diff --git a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx
index 270ebd1c0830d..2f63a77132be9 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx
@@ -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 {
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
index 09e6b0e43945f..2e4b51af00d6b 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
@@ -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';
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
index 0b741447f6fec..b3c0c3b6de857 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
@@ -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,
diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx
new file mode 100644
index 0000000000000..409cb69575ca9
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx
@@ -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(
+
+ My amazing component
+
+ );
+
+ expect(component.getByTestId('loading')).toBeInTheDocument();
+ });
+ });
+
+ describe('when isLoading is false', () => {
+ it('does not show the loading indicator', () => {
+ const component = render(
+
+ My amazing component
+
+ );
+
+ expect(component.queryByTestId('loading')).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
new file mode 100644
index 0000000000000..a6f579308597f
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
@@ -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 (
+
+ {isLoading && }
+ {children}
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/index.tsx
new file mode 100644
index 0000000000000..e08e8cec44a56
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/index.tsx
@@ -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 (
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx
similarity index 93%
rename from x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx
rename to x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx
index 8aec4184f924d..29102f606414f 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx
@@ -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();
diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts
new file mode 100644
index 0000000000000..585eef546e754
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts
@@ -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',
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts
new file mode 100644
index 0000000000000..a9c1337feac99
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts
@@ -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,
+ }),
+ });
+ }
+};
diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.test.ts
similarity index 97%
rename from x-pack/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts
rename to x-pack/plugins/apm/public/components/shared/charts/helper/timezone.test.ts
index 0a6daf47b3ca6..3997448d17385 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts
+++ b/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.test.ts
@@ -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;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx
new file mode 100644
index 0000000000000..3f2a08ecb7641
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx
@@ -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();
+ 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 (
+
+
+ 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);
+ }
+ }}
+ />
+
+
+
+ {timeseries.map((serie) => {
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx b/x-pack/plugins/apm/public/context/charts_sync_context.tsx
similarity index 79%
rename from x-pack/plugins/apm/public/context/ChartsSyncContext.tsx
rename to x-pack/plugins/apm/public/context/charts_sync_context.tsx
index 7df35bc443226..6f69ae097828b 100644
--- a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx
+++ b/x-pack/plugins/apm/public/context/charts_sync_context.tsx
@@ -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(null);
const { serviceName } = useParams<{ serviceName?: string }>();
@@ -79,7 +83,25 @@ function ChartsSyncContextProvider({ children }: { children: ReactNode }) {
return { ...hoverXHandlers };
}, [history, time, data.annotations]);
- return ;
+ return ;
}
-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 (
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/hooks/useChartsSync.tsx b/x-pack/plugins/apm/public/hooks/use_charts_sync.tsx
similarity index 61%
rename from x-pack/plugins/apm/public/hooks/useChartsSync.tsx
rename to x-pack/plugins/apm/public/hooks/use_charts_sync.tsx
index 0416d2c0a7f18..52c7e4c1e3a31 100644
--- a/x-pack/plugins/apm/public/hooks/useChartsSync.tsx
+++ b/x-pack/plugins/apm/public/hooks/use_charts_sync.tsx
@@ -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;
+}