[ML] APM Latency Correlations: Fix empty state (#109813)

- Correctly renders the empty chart state when no data is available.
- Hides the "Click drag to select" and trace samples message when the chart shows an empty state to avoid redundant info.
- Adds jest unit tests that would fail with the previously visible loading indicators.
- Fix a bug with cancelling search strategies.
This commit is contained in:
Walter Rafelsberger 2021-08-27 13:08:50 +02:00 committed by GitHub
parent d43e9f586b
commit 54a45bba65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 621 additions and 300 deletions

View file

@ -52,3 +52,12 @@ export interface AsyncSearchProviderProgress {
loadedFieldValuePairs: number;
loadedHistograms: number;
}
export interface SearchServiceRawResponse {
ccsWarning: boolean;
log: string[];
overallHistogram?: HistogramItem[];
percentileThresholdValue?: number;
took: number;
values: SearchServiceValue[];
}

View file

@ -33,8 +33,7 @@ export function CorrelationsEmptyStatePrompt() {
id="xpack.apm.correlations.noCorrelationsTextLine1"
defaultMessage="Correlations will only be identified if they have significant impact."
/>
</p>
<p>
<br />
<FormattedMessage
id="xpack.apm.correlations.noCorrelationsTextLine2"
defaultMessage="Try selecting another time range or remove any added filter."

View file

@ -41,7 +41,6 @@ import { CorrelationsLog } from './correlations_log';
import { CorrelationsEmptyStatePrompt } from './empty_state_prompt';
import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning';
import { CorrelationsProgressControls } from './progress_controls';
import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types';
import type { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types';
import { Summary } from '../../shared/Summary';
import { asPercent } from '../../../../common/utils/formatters';
@ -70,16 +69,6 @@ export function FailedTransactionsCorrelations({
const inspectEnabled = uiSettings.get<boolean>(enableInspectEsQueries);
const searchServicePrams: SearchServiceParams = {
environment,
kuery,
serviceName,
transactionName,
transactionType,
start,
end,
};
const result = useFailedTransactionsCorrelationsFetcher();
const {
@ -93,26 +82,30 @@ export function FailedTransactionsCorrelations({
} = result;
const startFetchHandler = useCallback(() => {
startFetch(searchServicePrams);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [environment, serviceName, kuery, start, end]);
startFetch({
environment,
kuery,
serviceName,
transactionName,
transactionType,
start,
end,
});
}, [
startFetch,
environment,
serviceName,
transactionName,
transactionType,
kuery,
start,
end,
]);
// start fetching on load
// we want this effect to execute exactly once after the component mounts
useEffect(() => {
if (isRunning) {
cancelFetch();
}
startFetchHandler();
return () => {
// cancel any running async partial request when unmounting the component
// we want this effect to execute exactly once after the component mounts
cancelFetch();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startFetchHandler]);
return cancelFetch;
}, [cancelFetch, startFetchHandler]);
const [
selectedSignificantTerm,

View file

@ -0,0 +1,131 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { render, screen, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React, { ReactNode } from 'react';
import { of } from 'rxjs';
import { __IntlProvider as IntlProvider } from '@kbn/i18n/react';
import { CoreStart } from 'kibana/public';
import { merge } from 'lodash';
import { dataPluginMock } from 'src/plugins/data/public/mocks';
import type { IKibanaSearchResponse } from 'src/plugins/data/public';
import { EuiThemeProvider } from 'src/plugins/kibana_react/common';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
import type { SearchServiceRawResponse } from '../../../../common/search_strategies/correlations/types';
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context';
import {
mockApmPluginContextValue,
MockApmPluginContextWrapper,
} from '../../../context/apm_plugin/mock_apm_plugin_context';
import { fromQuery } from '../../shared/Links/url_helpers';
import { LatencyCorrelations } from './latency_correlations';
function Wrapper({
children,
dataSearchResponse,
}: {
children?: ReactNode;
dataSearchResponse: IKibanaSearchResponse<SearchServiceRawResponse>;
}) {
const mockDataSearch = jest.fn(() => of(dataSearchResponse));
const dataPluginMockStart = dataPluginMock.createStartContract();
const KibanaReactContext = createKibanaReactContext({
data: {
...dataPluginMockStart,
search: {
...dataPluginMockStart.search,
search: mockDataSearch,
},
},
usageCollection: { reportUiCounter: () => {} },
} as Partial<CoreStart>);
const httpGet = jest.fn();
const history = createMemoryHistory();
jest.spyOn(history, 'push');
jest.spyOn(history, 'replace');
history.replace({
pathname: '/services/the-service-name/transactions/view',
search: fromQuery({ transactionName: 'the-transaction-name' }),
});
const mockPluginContext = (merge({}, mockApmPluginContextValue, {
core: { http: { get: httpGet } },
}) as unknown) as ApmPluginContextValue;
return (
<IntlProvider locale="en">
<EuiThemeProvider darkMode={false}>
<KibanaReactContext.Provider>
<MockApmPluginContextWrapper
history={history}
value={mockPluginContext}
>
<MockUrlParamsContextProvider
params={{
rangeFrom: 'now-15m',
rangeTo: 'now',
start: 'mystart',
end: 'myend',
}}
>
{children}
</MockUrlParamsContextProvider>
</MockApmPluginContextWrapper>
</KibanaReactContext.Provider>
</EuiThemeProvider>
</IntlProvider>
);
}
describe('correlations', () => {
describe('LatencyCorrelations', () => {
it('shows loading indicator when the service is running and returned no results yet', async () => {
render(
<Wrapper
dataSearchResponse={{
isRunning: true,
rawResponse: { ccsWarning: false, took: 1234, values: [], log: [] },
}}
>
<LatencyCorrelations onFilter={jest.fn()} />
</Wrapper>
);
await waitFor(() => {
expect(screen.getByTestId('apmCorrelationsChart')).toBeInTheDocument();
expect(screen.getByTestId('loading')).toBeInTheDocument();
});
});
it("doesn't show loading indicator when the service isn't running", async () => {
render(
<Wrapper
dataSearchResponse={{
isRunning: false,
rawResponse: { ccsWarning: false, took: 1234, values: [], log: [] },
}}
>
<LatencyCorrelations onFilter={jest.fn()} />
</Wrapper>
);
await waitFor(() => {
expect(screen.getByTestId('apmCorrelationsChart')).toBeInTheDocument();
expect(screen.queryByTestId('loading')).toBeNull(); // it doesn't exist
});
});
});
});

View file

@ -61,7 +61,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) {
const {
query: { kuery, environment, rangeFrom, rangeTo },
} = useApmParams('/services/:serviceName');
} = useApmParams('/services/:serviceName/transactions/view');
const { urlParams } = useUrlParams();
@ -95,25 +95,21 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) {
end,
percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [environment, serviceName, kuery, start, end]);
}, [
startFetch,
environment,
serviceName,
transactionName,
transactionType,
kuery,
start,
end,
]);
// start fetching on load
// we want this effect to execute exactly once after the component mounts
useEffect(() => {
if (isRunning) {
cancelFetch();
}
startFetchHandler();
return () => {
// cancel any running async partial request when unmounting the component
// we want this effect to execute exactly once after the component mounts
cancelFetch();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startFetchHandler]);
return cancelFetch;
}, [cancelFetch, startFetchHandler]);
useEffect(() => {
if (isErrorMessage(error)) {

View file

@ -1,22 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getFormattedSelection } from './index';
describe('transaction_details/distribution', () => {
describe('getFormattedSelection', () => {
it('displays only one unit if from and to share the same unit', () => {
expect(getFormattedSelection([10000, 100000])).toEqual('10 - 100 ms');
});
it('displays two units when from and to have different units', () => {
expect(getFormattedSelection([100000, 1000000000])).toEqual(
'100 ms - 17 min'
);
});
});
});

View file

@ -0,0 +1,151 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { render, screen, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React, { ReactNode } from 'react';
import { of } from 'rxjs';
import { CoreStart } from 'kibana/public';
import { merge } from 'lodash';
import { dataPluginMock } from 'src/plugins/data/public/mocks';
import type { IKibanaSearchResponse } from 'src/plugins/data/public';
import { EuiThemeProvider } from 'src/plugins/kibana_react/common';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
import type { SearchServiceRawResponse } from '../../../../../common/search_strategies/correlations/types';
import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider';
import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context';
import {
mockApmPluginContextValue,
MockApmPluginContextWrapper,
} from '../../../../context/apm_plugin/mock_apm_plugin_context';
import { fromQuery } from '../../../shared/Links/url_helpers';
import { getFormattedSelection, TransactionDistribution } from './index';
function Wrapper({
children,
dataSearchResponse,
}: {
children?: ReactNode;
dataSearchResponse: IKibanaSearchResponse<SearchServiceRawResponse>;
}) {
const mockDataSearch = jest.fn(() => of(dataSearchResponse));
const dataPluginMockStart = dataPluginMock.createStartContract();
const KibanaReactContext = createKibanaReactContext({
data: {
...dataPluginMockStart,
search: {
...dataPluginMockStart.search,
search: mockDataSearch,
},
},
usageCollection: { reportUiCounter: () => {} },
} as Partial<CoreStart>);
const httpGet = jest.fn();
const history = createMemoryHistory();
jest.spyOn(history, 'push');
jest.spyOn(history, 'replace');
history.replace({
pathname: '/services/the-service-name/transactions/view',
search: fromQuery({ transactionName: 'the-transaction-name' }),
});
const mockPluginContext = (merge({}, mockApmPluginContextValue, {
core: { http: { get: httpGet } },
}) as unknown) as ApmPluginContextValue;
return (
<EuiThemeProvider darkMode={false}>
<KibanaReactContext.Provider>
<MockApmPluginContextWrapper
history={history}
value={mockPluginContext}
>
<MockUrlParamsContextProvider
params={{
rangeFrom: 'now-15m',
rangeTo: 'now',
start: 'mystart',
end: 'myend',
}}
>
{children}
</MockUrlParamsContextProvider>
</MockApmPluginContextWrapper>
</KibanaReactContext.Provider>
</EuiThemeProvider>
);
}
describe('transaction_details/distribution', () => {
describe('getFormattedSelection', () => {
it('displays only one unit if from and to share the same unit', () => {
expect(getFormattedSelection([10000, 100000])).toEqual('10 - 100 ms');
});
it('displays two units when from and to have different units', () => {
expect(getFormattedSelection([100000, 1000000000])).toEqual(
'100 ms - 17 min'
);
});
});
describe('TransactionDistribution', () => {
it('shows loading indicator when the service is running and returned no results yet', async () => {
const onHasData = jest.fn();
render(
<Wrapper
dataSearchResponse={{
isRunning: true,
rawResponse: { ccsWarning: false, took: 1234, values: [], log: [] },
}}
>
<TransactionDistribution
onChartSelection={jest.fn()}
onClearSelection={jest.fn()}
onHasData={onHasData}
/>
</Wrapper>
);
await waitFor(() => {
expect(screen.getByTestId('apmCorrelationsChart')).toBeInTheDocument();
expect(screen.getByTestId('loading')).toBeInTheDocument();
expect(onHasData).toHaveBeenLastCalledWith(false);
});
});
it("doesn't show loading indicator when the service isn't running", async () => {
const onHasData = jest.fn();
render(
<Wrapper
dataSearchResponse={{
isRunning: false,
rawResponse: { ccsWarning: false, took: 1234, values: [], log: [] },
}}
>
<TransactionDistribution
onChartSelection={jest.fn()}
onClearSelection={jest.fn()}
onHasData={onHasData}
/>
</Wrapper>
);
await waitFor(() => {
expect(screen.getByTestId('apmCorrelationsChart')).toBeInTheDocument();
expect(screen.queryByTestId('loading')).toBeNull(); // it doesn't exist
expect(onHasData).toHaveBeenLastCalledWith(false);
});
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useEffect } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { BrushEndListener, XYBrushArea } from '@elastic/charts';
import {
EuiBadge,
@ -21,7 +21,10 @@ import { getDurationFormatter } from '../../../../../common/utils/formatters';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useTransactionDistributionFetcher } from '../../../../hooks/use_transaction_distribution_fetcher';
import { TransactionDistributionChart } from '../../../shared/charts/transaction_distribution_chart';
import {
OnHasData,
TransactionDistributionChart,
} from '../../../shared/charts/transaction_distribution_chart';
import { useUiTracker } from '../../../../../../observability/public';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
@ -47,10 +50,11 @@ export function getFormattedSelection(selection: Selection): string {
}`;
}
interface Props {
interface TransactionDistributionProps {
markerCurrentTransaction?: number;
onChartSelection: BrushEndListener;
onClearSelection: () => void;
onHasData: OnHasData;
selection?: Selection;
}
@ -58,8 +62,9 @@ export function TransactionDistribution({
markerCurrentTransaction,
onChartSelection,
onClearSelection,
onHasData,
selection,
}: Props) {
}: TransactionDistributionProps) {
const {
core: { notifications },
} = useApmPluginContext();
@ -68,7 +73,7 @@ export function TransactionDistribution({
const {
query: { kuery, environment, rangeFrom, rangeTo },
} = useApmParams('/services/:serviceName');
} = useApmParams('/services/:serviceName/transactions/view');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
@ -76,6 +81,16 @@ export function TransactionDistribution({
const { transactionName } = urlParams;
const [showSelection, setShowSelection] = useState(false);
const onTransactionDistributionHasData: OnHasData = useCallback(
(hasData) => {
setShowSelection(hasData);
onHasData(hasData);
},
[onHasData]
);
const emptySelectionText = i18n.translate(
'xpack.apm.transactionDetails.emptySelectionText',
{
@ -93,17 +108,12 @@ export function TransactionDistribution({
const {
error,
percentileThresholdValue,
isRunning,
startFetch,
cancelFetch,
transactionDistribution,
} = useTransactionDistributionFetcher();
useEffect(() => {
if (isRunning) {
cancelFetch();
}
const startFetchHandler = useCallback(() => {
startFetch({
environment,
kuery,
@ -114,14 +124,21 @@ export function TransactionDistribution({
end,
percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD,
});
}, [
startFetch,
environment,
serviceName,
transactionName,
transactionType,
kuery,
start,
end,
]);
return () => {
// cancel any running async partial request when unmounting the component
// we want this effect to execute exactly once after the component mounts
cancelFetch();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [environment, serviceName, kuery, start, end]);
useEffect(() => {
startFetchHandler();
return cancelFetch;
}, [cancelFetch, startFetchHandler]);
useEffect(() => {
if (isErrorMessage(error)) {
@ -166,7 +183,7 @@ export function TransactionDistribution({
</h5>
</EuiTitle>
</EuiFlexItem>
{!selection && (
{showSelection && !selection && (
<EuiFlexItem>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="xs">
<EuiFlexItem
@ -184,7 +201,7 @@ export function TransactionDistribution({
</EuiFlexGroup>
</EuiFlexItem>
)}
{selection && (
{showSelection && selection && (
<EuiFlexItem grow={false}>
<EuiBadge
iconType="cross"
@ -217,6 +234,7 @@ export function TransactionDistribution({
markerValue={percentileThresholdValue ?? 0}
overallHistogram={transactionDistribution}
onChartSelection={onTrackedChartSelection}
onHasData={onTransactionDistributionHasData}
selection={selection}
/>
</div>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import React, { useState } from 'react';
import { EuiSpacer } from '@elastic/eui';
@ -34,11 +34,17 @@ function TraceSamplesTab({
status: waterfallStatus,
} = useWaterfallFetcher();
const [
transactionDistributionHasData,
setTransactionDistributionHasData,
] = useState(false);
return (
<>
<TransactionDistribution
onChartSelection={selectSampleFromChartSelection}
onClearSelection={clearChartSelection}
onHasData={setTransactionDistributionHasData}
selection={
sampleRangeFrom !== undefined && sampleRangeTo !== undefined
? [sampleRangeFrom, sampleRangeTo]
@ -49,15 +55,19 @@ function TraceSamplesTab({
}
/>
<EuiSpacer size="s" />
{transactionDistributionHasData && (
<>
<EuiSpacer size="s" />
<WaterfallWithSummary
urlParams={urlParams}
waterfall={waterfall}
isLoading={waterfallStatus === FETCH_STATUS.LOADING}
exceedsMax={exceedsMax}
traceSamples={traceSamples}
/>
<WaterfallWithSummary
urlParams={urlParams}
waterfall={waterfall}
isLoading={waterfallStatus === FETCH_STATUS.LOADING}
exceedsMax={exceedsMax}
traceSamples={traceSamples}
/>
</>
)}
</>
);
}

View file

@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
interface Props {
export interface ChartContainerProps {
hasData: boolean;
status: FETCH_STATUS;
height: number;
@ -24,7 +24,7 @@ export function ChartContainer({
status,
hasData,
id,
}: Props) {
}: ChartContainerProps) {
if (!hasData && status === FETCH_STATUS.LOADING) {
return <LoadingChartPlaceholder height={height} />;
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import {
AnnotationDomainType,
AreaSeries,
@ -35,7 +35,14 @@ import { HistogramItem } from '../../../../../common/search_strategies/correlati
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { useTheme } from '../../../../hooks/use_theme';
import { ChartContainer } from '../chart_container';
import { ChartContainer, ChartContainerProps } from '../chart_container';
export type TransactionDistributionChartLoadingState = Pick<
ChartContainerProps,
'hasData' | 'status'
>;
export type OnHasData = (hasData: boolean) => void;
interface TransactionDistributionChartProps {
field?: string;
@ -46,6 +53,7 @@ interface TransactionDistributionChartProps {
markerPercentile: number;
overallHistogram?: HistogramItem[];
onChartSelection?: BrushEndListener;
onHasData?: OnHasData;
selection?: [number, number];
}
@ -103,6 +111,7 @@ export function TransactionDistributionChart({
markerPercentile,
overallHistogram,
onChartSelection,
onHasData,
selection,
}: TransactionDistributionChartProps) {
const chartTheme = useChartTheme();
@ -154,6 +163,24 @@ export function TransactionDistributionChart({
]
: undefined;
const chartLoadingState: TransactionDistributionChartLoadingState = useMemo(
() => ({
hasData:
Array.isArray(patchedOverallHistogram) &&
patchedOverallHistogram.length > 0,
status: Array.isArray(patchedOverallHistogram)
? FETCH_STATUS.SUCCESS
: FETCH_STATUS.LOADING,
}),
[patchedOverallHistogram]
);
useEffect(() => {
if (onHasData) {
onHasData(chartLoadingState.hasData);
}
}, [chartLoadingState, onHasData]);
return (
<div
data-test-subj="apmCorrelationsChart"
@ -161,15 +188,8 @@ export function TransactionDistributionChart({
>
<ChartContainer
height={250}
hasData={
Array.isArray(patchedOverallHistogram) &&
patchedOverallHistogram.length > 0
}
status={
Array.isArray(patchedOverallHistogram)
? FETCH_STATUS.SUCCESS
: FETCH_STATUS.LOADING
}
hasData={chartLoadingState.hasData}
status={chartLoadingState.status}
>
<Chart>
<Settings
@ -282,7 +302,8 @@ export function TransactionDistributionChart({
fieldName !== undefined &&
fieldValue !== undefined && (
<AreaSeries
id={`apmTransactionDistributionChartAreaSeries${fieldName}${fieldValue}`}
// id is used as the label for the legend
id={`${fieldName}:${fieldValue}`}
xScaleType={ScaleType.Log}
yScaleType={ScaleType.Log}
data={histogram}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import type { Subscription } from 'rxjs';
import {
IKibanaSearchRequest,
@ -72,62 +72,65 @@ export const useFailedTransactionsCorrelationsFetcher = () => {
}));
}
const startFetch = (params: SearchServiceParams) => {
setFetchState((prevState) => ({
...prevState,
error: undefined,
isComplete: false,
}));
searchSubscription$.current?.unsubscribe();
abortCtrl.current.abort();
abortCtrl.current = new AbortController();
const startFetch = useCallback(
(params: SearchServiceParams) => {
setFetchState((prevState) => ({
...prevState,
error: undefined,
isComplete: false,
}));
searchSubscription$.current?.unsubscribe();
abortCtrl.current.abort();
abortCtrl.current = new AbortController();
const req = { params };
const req = { params };
// Submit the search request using the `data.search` service.
searchSubscription$.current = data.search
.search<IKibanaSearchRequest, IKibanaSearchResponse<RawResponse>>(req, {
strategy: FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY,
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (res: IKibanaSearchResponse<RawResponse>) => {
setResponse(res);
if (isCompleteResponse(res)) {
searchSubscription$.current?.unsubscribe();
// Submit the search request using the `data.search` service.
searchSubscription$.current = data.search
.search<IKibanaSearchRequest, IKibanaSearchResponse<RawResponse>>(req, {
strategy: FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY,
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (res: IKibanaSearchResponse<RawResponse>) => {
setResponse(res);
if (isCompleteResponse(res)) {
searchSubscription$.current?.unsubscribe();
setFetchState((prevState) => ({
...prevState,
isRunnning: false,
isComplete: true,
}));
} else if (isErrorResponse(res)) {
searchSubscription$.current?.unsubscribe();
setFetchState((prevState) => ({
...prevState,
error: (res as unknown) as Error,
isRunning: false,
}));
}
},
error: (error: Error) => {
setFetchState((prevState) => ({
...prevState,
isRunnning: false,
isComplete: true,
error,
isRunning: false,
}));
} else if (isErrorResponse(res)) {
searchSubscription$.current?.unsubscribe();
setFetchState((prevState) => ({
...prevState,
error: (res as unknown) as Error,
setIsRunning: false,
}));
}
},
error: (error: Error) => {
setFetchState((prevState) => ({
...prevState,
error,
setIsRunning: false,
}));
},
});
};
},
});
},
[data.search, setFetchState]
);
const cancelFetch = () => {
const cancelFetch = useCallback(() => {
searchSubscription$.current?.unsubscribe();
searchSubscription$.current = undefined;
abortCtrl.current.abort();
setFetchState((prevState) => ({
...prevState,
setIsRunning: false,
isRunning: false,
}));
};
}, [setFetchState]);
return {
...fetchState,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import type { Subscription } from 'rxjs';
import {
IKibanaSearchRequest,
@ -14,31 +14,21 @@ import {
isErrorResponse,
} from '../../../../../src/plugins/data/public';
import type {
HistogramItem,
SearchServiceParams,
SearchServiceValue,
SearchServiceRawResponse,
} from '../../common/search_strategies/correlations/types';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { ApmPluginStartDeps } from '../plugin';
interface RawResponse {
percentileThresholdValue?: number;
took: number;
values: SearchServiceValue[];
overallHistogram: HistogramItem[];
log: string[];
ccsWarning: boolean;
}
interface TransactionDistributionFetcherState {
error?: Error;
isComplete: boolean;
isRunning: boolean;
loaded: number;
ccsWarning: RawResponse['ccsWarning'];
log: RawResponse['log'];
transactionDistribution?: RawResponse['overallHistogram'];
percentileThresholdValue?: RawResponse['percentileThresholdValue'];
ccsWarning: SearchServiceRawResponse['ccsWarning'];
log: SearchServiceRawResponse['log'];
transactionDistribution?: SearchServiceRawResponse['overallHistogram'];
percentileThresholdValue?: SearchServiceRawResponse['percentileThresholdValue'];
timeTook?: number;
total: number;
}
@ -63,7 +53,9 @@ export function useTransactionDistributionFetcher() {
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef<Subscription>();
function setResponse(response: IKibanaSearchResponse<RawResponse>) {
function setResponse(
response: IKibanaSearchResponse<SearchServiceRawResponse>
) {
setFetchState((prevState) => ({
...prevState,
isRunning: response.isRunning || false,
@ -83,71 +75,81 @@ export function useTransactionDistributionFetcher() {
response.rawResponse?.percentileThresholdValue,
}
: {}),
// if loading is done but didn't return any data for the overall histogram,
// set it to an empty array so the consuming chart component knows loading is done.
...(!response.isRunning &&
response.rawResponse?.overallHistogram === undefined
? { transactionDistribution: [] }
: {}),
}));
}
const startFetch = (
params: Omit<SearchServiceParams, 'analyzeCorrelations'>
) => {
setFetchState((prevState) => ({
...prevState,
error: undefined,
isComplete: false,
}));
searchSubscription$.current?.unsubscribe();
abortCtrl.current.abort();
abortCtrl.current = new AbortController();
const startFetch = useCallback(
(params: Omit<SearchServiceParams, 'analyzeCorrelations'>) => {
setFetchState((prevState) => ({
...prevState,
error: undefined,
isComplete: false,
}));
searchSubscription$.current?.unsubscribe();
abortCtrl.current.abort();
abortCtrl.current = new AbortController();
const searchServiceParams: SearchServiceParams = {
...params,
analyzeCorrelations: false,
};
const req = { params: searchServiceParams };
const searchServiceParams: SearchServiceParams = {
...params,
analyzeCorrelations: false,
};
const req = { params: searchServiceParams };
// Submit the search request using the `data.search` service.
searchSubscription$.current = data.search
.search<IKibanaSearchRequest, IKibanaSearchResponse<RawResponse>>(req, {
strategy: 'apmCorrelationsSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (res: IKibanaSearchResponse<RawResponse>) => {
setResponse(res);
if (isCompleteResponse(res)) {
searchSubscription$.current?.unsubscribe();
// Submit the search request using the `data.search` service.
searchSubscription$.current = data.search
.search<
IKibanaSearchRequest,
IKibanaSearchResponse<SearchServiceRawResponse>
>(req, {
strategy: 'apmCorrelationsSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (res: IKibanaSearchResponse<SearchServiceRawResponse>) => {
setResponse(res);
if (isCompleteResponse(res)) {
searchSubscription$.current?.unsubscribe();
setFetchState((prevState) => ({
...prevState,
isRunnning: false,
isComplete: true,
}));
} else if (isErrorResponse(res)) {
searchSubscription$.current?.unsubscribe();
setFetchState((prevState) => ({
...prevState,
error: (res as unknown) as Error,
isRunning: false,
}));
}
},
error: (error: Error) => {
setFetchState((prevState) => ({
...prevState,
isRunnning: false,
isComplete: true,
error,
isRunning: false,
}));
} else if (isErrorResponse(res)) {
searchSubscription$.current?.unsubscribe();
setFetchState((prevState) => ({
...prevState,
error: (res as unknown) as Error,
setIsRunning: false,
}));
}
},
error: (error: Error) => {
setFetchState((prevState) => ({
...prevState,
error,
setIsRunning: false,
}));
},
});
};
},
});
},
[data.search, setFetchState]
);
const cancelFetch = () => {
const cancelFetch = useCallback(() => {
searchSubscription$.current?.unsubscribe();
searchSubscription$.current = undefined;
abortCtrl.current.abort();
setFetchState((prevState) => ({
...prevState,
setIsRunning: false,
isRunning: false,
}));
};
}, [setFetchState]);
return {
...fetchState,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import type { Subscription } from 'rxjs';
import {
IKibanaSearchRequest,
@ -14,32 +14,22 @@ import {
isErrorResponse,
} from '../../../../../src/plugins/data/public';
import type {
HistogramItem,
SearchServiceParams,
SearchServiceValue,
SearchServiceRawResponse,
} from '../../common/search_strategies/correlations/types';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { ApmPluginStartDeps } from '../plugin';
interface RawResponse {
percentileThresholdValue?: number;
took: number;
values: SearchServiceValue[];
overallHistogram: HistogramItem[];
log: string[];
ccsWarning: boolean;
}
interface TransactionLatencyCorrelationsFetcherState {
error?: Error;
isComplete: boolean;
isRunning: boolean;
loaded: number;
ccsWarning: RawResponse['ccsWarning'];
histograms: RawResponse['values'];
log: RawResponse['log'];
overallHistogram?: RawResponse['overallHistogram'];
percentileThresholdValue?: RawResponse['percentileThresholdValue'];
ccsWarning: SearchServiceRawResponse['ccsWarning'];
histograms: SearchServiceRawResponse['values'];
log: SearchServiceRawResponse['log'];
overallHistogram?: SearchServiceRawResponse['overallHistogram'];
percentileThresholdValue?: SearchServiceRawResponse['percentileThresholdValue'];
timeTook?: number;
total: number;
}
@ -65,7 +55,9 @@ export const useTransactionLatencyCorrelationsFetcher = () => {
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef<Subscription>();
function setResponse(response: IKibanaSearchResponse<RawResponse>) {
function setResponse(
response: IKibanaSearchResponse<SearchServiceRawResponse>
) {
setFetchState((prevState) => ({
...prevState,
isRunning: response.isRunning || false,
@ -85,71 +77,86 @@ export const useTransactionLatencyCorrelationsFetcher = () => {
response.rawResponse?.percentileThresholdValue,
}
: {}),
// if loading is done but didn't return any data for the overall histogram,
// set it to an empty array so the consuming chart component knows loading is done.
...(!response.isRunning &&
response.rawResponse?.overallHistogram === undefined
? { overallHistogram: [] }
: {}),
}));
}
const startFetch = (
params: Omit<SearchServiceParams, 'analyzeCorrelations'>
) => {
setFetchState((prevState) => ({
...prevState,
error: undefined,
isComplete: false,
}));
searchSubscription$.current?.unsubscribe();
abortCtrl.current.abort();
abortCtrl.current = new AbortController();
const startFetch = useCallback(
(params: Omit<SearchServiceParams, 'analyzeCorrelations'>) => {
setFetchState((prevState) => ({
...prevState,
error: undefined,
isComplete: false,
}));
searchSubscription$.current?.unsubscribe();
abortCtrl.current.abort();
abortCtrl.current = new AbortController();
const searchServiceParams: SearchServiceParams = {
...params,
analyzeCorrelations: true,
};
const req = { params: searchServiceParams };
const searchServiceParams: SearchServiceParams = {
...params,
analyzeCorrelations: true,
};
const req = { params: searchServiceParams };
// Submit the search request using the `data.search` service.
searchSubscription$.current = data.search
.search<IKibanaSearchRequest, IKibanaSearchResponse<RawResponse>>(req, {
strategy: 'apmCorrelationsSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (res: IKibanaSearchResponse<RawResponse>) => {
setResponse(res);
if (isCompleteResponse(res)) {
searchSubscription$.current?.unsubscribe();
// Submit the search request using the `data.search` service.
searchSubscription$.current = data.search
.search<
IKibanaSearchRequest,
IKibanaSearchResponse<SearchServiceRawResponse>
>(req, {
strategy: 'apmCorrelationsSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (res: IKibanaSearchResponse<SearchServiceRawResponse>) => {
setResponse(res);
if (isCompleteResponse(res)) {
searchSubscription$.current?.unsubscribe();
setFetchState((prevState) => ({
...prevState,
isRunnning: false,
isComplete: true,
}));
} else if (isErrorResponse(res)) {
searchSubscription$.current?.unsubscribe();
setFetchState((prevState) => ({
...prevState,
error: (res as unknown) as Error,
isRunning: false,
}));
}
},
error: (error: Error) => {
setFetchState((prevState) => ({
...prevState,
isRunnning: false,
isComplete: true,
error,
isRunning: false,
}));
} else if (isErrorResponse(res)) {
searchSubscription$.current?.unsubscribe();
setFetchState((prevState) => ({
...prevState,
error: (res as unknown) as Error,
setIsRunning: false,
}));
}
},
error: (error: Error) => {
setFetchState((prevState) => ({
...prevState,
error,
setIsRunning: false,
}));
},
});
};
},
});
},
[data.search, setFetchState]
);
const cancelFetch = () => {
const cancelFetch = useCallback(() => {
searchSubscription$.current?.unsubscribe();
searchSubscription$.current = undefined;
abortCtrl.current.abort();
setFetchState((prevState) => ({
...prevState,
setIsRunning: false,
// If we didn't receive data for the overall histogram yet
// set it to an empty array to indicate loading stopped.
...(prevState.overallHistogram === undefined
? { overallHistogram: [] }
: {}),
isRunning: false,
}));
};
}, [setFetchState]);
return {
...fetchState,

View file

@ -16,6 +16,7 @@ import {
import type {
SearchServiceParams,
SearchServiceRawResponse,
SearchServiceValue,
} from '../../../../common/search_strategies/correlations/types';
@ -100,20 +101,22 @@ export const apmCorrelationsSearchStrategyProvider = (
const took = Date.now() - started;
const rawResponse: SearchServiceRawResponse = {
ccsWarning,
log,
took,
values,
percentileThresholdValue,
overallHistogram,
};
return of({
id,
loaded,
total,
isRunning,
isPartial: isRunning,
rawResponse: {
ccsWarning,
log,
took,
values,
percentileThresholdValue,
overallHistogram,
},
rawResponse,
});
},
cancel: async (id, options, deps) => {