From 2f8edb5d7f239a65a537bed39c463796d4b13094 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 2 Nov 2020 10:24:03 -0600 Subject: [PATCH] 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. --- .../app/ServiceDetails/ServiceDetailTabs.tsx | 4 +- .../components/app/ServiceMetrics/index.tsx | 2 +- .../app/ServiceNodeMetrics/index.tsx | 2 +- .../app/TransactionDetails/index.tsx | 2 +- .../app/TransactionOverview/index.tsx | 2 +- .../components/app/service_overview/index.tsx | 372 +++++++++--------- .../service_overview.test.tsx | 14 +- .../shared/charts/MetricsChart/index.tsx | 2 +- .../TransactionLineChart/index.tsx | 2 +- .../shared/charts/TransactionCharts/index.tsx | 2 +- .../shared/charts/chart_container.test.tsx | 34 ++ .../shared/charts/chart_container.tsx | 29 ++ .../index.tsx | 80 ++++ .../legacy.tsx} | 8 +- .../shared/charts/helper/helper.test.ts | 39 ++ .../components/shared/charts/helper/helper.ts | 35 ++ .../helper/{__test__ => }/timezone.test.ts | 2 +- .../shared/charts/line_chart/index.tsx | 140 +++++++ ...yncContext.tsx => charts_sync_context.tsx} | 30 +- ...{useChartsSync.tsx => use_charts_sync.tsx} | 15 +- 20 files changed, 618 insertions(+), 198 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/index.tsx rename x-pack/plugins/apm/public/components/shared/charts/{ErroneousTransactionsRateChart/index.tsx => erroneous_transactions_rate_chart/legacy.tsx} (93%) create mode 100644 x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts create mode 100644 x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts rename x-pack/plugins/apm/public/components/shared/charts/helper/{__test__ => }/timezone.test.ts (97%) create mode 100644 x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx rename x-pack/plugins/apm/public/context/{ChartsSyncContext.tsx => charts_sync_context.tsx} (79%) rename x-pack/plugins/apm/public/hooks/{useChartsSync.tsx => use_charts_sync.tsx} (61%) diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index d51e4a2dd3d7c..625a8e73debc9 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -48,7 +48,9 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { })} ), - render: () => , + render: () => ( + + ), name: 'overview', }; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx index 2fb500f3c9916..042752ef62f53 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -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'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx index 84a1920d17fa8..566585c67e212 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx @@ -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'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index b79186a90cd1d..efdd7b1f34221 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -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'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 003df632d11b3..5444d2d521f37 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -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'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 81f23b6427508..342152b572f1e 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -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 ( - - - - - Search bar - - - Comparison picker - - - Date picker - - - - - - -

- {i18n.translate('xpack.apm.serviceOverview.latencyChartTitle', { - defaultMessage: 'Latency', - })} -

-
-
-
- - - - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.trafficChartTitle', - { - defaultMessage: 'Traffic', - } - )} -

-
-
-
- - - - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableTitle', - { - defaultMessage: 'Transactions', - } - )} -

-
-
- - + + + + + + Search bar + + + Comparison picker + + + Date picker + + + + + + +

+ {i18n.translate('xpack.apm.serviceOverview.latencyChartTitle', { + defaultMessage: 'Latency', + })} +

+
+
+
+ + + + + +

{i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableLinkText', + 'xpack.apm.serviceOverview.trafficChartTitle', { - defaultMessage: 'View transactions', + defaultMessage: 'Traffic', } )} - - - - - - - - - - - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.errorRateChartTitle', - { - defaultMessage: 'Error rate', - } - )} -

-
-
-
- - - - - -

+

+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableTitle', + { + defaultMessage: 'Transactions', + } + )} +

+
+
+ + {i18n.translate( - 'xpack.apm.serviceOverview.errorsTableTitle', + 'xpack.apm.serviceOverview.transactionsTableLinkText', { - defaultMessage: 'Errors', + defaultMessage: 'View transactions', } )} -

-
-
- - - {i18n.translate( - 'xpack.apm.serviceOverview.errorsTableLinkText', - { - defaultMessage: 'View errors', - } - )} - - -
-
-
-
-
- - - - - - + + + + + + + + + + {!isRumAgentName(agentName) && ( + +

{i18n.translate( - 'xpack.apm.serviceOverview.averageDurationBySpanTypeChartTitle', + 'xpack.apm.serviceOverview.errorRateChartTitle', { - defaultMessage: 'Average duration by span type', + defaultMessage: 'Error rate', } )}

-
-
- - - - - - - -

+ + + + )} + + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.errorsTableTitle', + { + defaultMessage: 'Errors', + } + )} +

+
+
+ + {i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableTitle', + 'xpack.apm.serviceOverview.errorsTableLinkText', { - defaultMessage: 'Dependencies', + defaultMessage: 'View errors', } )} -

-
-
- - + + +
+
+
+
+ + + + + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.averageDurationBySpanTypeChartTitle', + { + defaultMessage: 'Average duration by span type', + } + )} +

+
+
+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableTitle', + { + defaultMessage: 'Dependencies', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableLinkText', + { + defaultMessage: 'View service map', + } + )} + + +
+
+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.instancesLatencyDistributionChartTitle', + { + defaultMessage: 'Instances latency distribution', + } + )} +

+
+
+
+ + + +

{i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableLinkText', + 'xpack.apm.serviceOverview.instancesTableTitle', { - defaultMessage: 'View service map', + defaultMessage: 'Instances', } )} - - - - - - - - - - - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.instancesLatencyDistributionChartTitle', - { - defaultMessage: 'Instances latency distribution', - } - )} -

-
-
-
- - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.instancesTableTitle', - { - defaultMessage: 'Instances', - } - )} -

-
-
-
-
-
- +

+
+
+
+
+
+ + ); } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 4e2063930a9c9..210d353303854 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -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); + 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 59a96bbf98b3e..c26bf07d14e33 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; +}