mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
e2f0f94a49
commit
2f504a05a7
20 changed files with 622 additions and 202 deletions
|
@ -48,7 +48,9 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) {
|
|||
})}
|
||||
</ServiceOverviewLink>
|
||||
),
|
||||
render: () => <ServiceOverview serviceName={serviceName} />,
|
||||
render: () => (
|
||||
<ServiceOverview agentName={agentName} serviceName={serviceName} />
|
||||
),
|
||||
name: 'overview',
|
||||
};
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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();
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue