[7.x] [APM] Replace ui/kfetch with core.http (#47635) (#48211)

* [APM] Replace ui/kfetch with core.http

Closes #46548.

* Remove kfetch mocks in tests

* Expose HttpFetchError from src/core/public/index

* Make HttpFetchError public

* Simplify tests for ServiceOverview
This commit is contained in:
Dario Gieselaar 2019-10-15 15:25:06 +02:00 committed by GitHub
parent 1a3c8bf814
commit cf7f2aa8c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 882 additions and 701 deletions

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpFetchError](./kibana-plugin-public.httpfetcherror.md) &gt; [(constructor)](./kibana-plugin-public.httpfetcherror._constructor_.md)
## HttpFetchError.(constructor)
Constructs a new instance of the `HttpFetchError` class
<b>Signature:</b>
```typescript
constructor(message: string, request: Request, response?: Response | undefined, body?: any);
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| message | <code>string</code> | |
| request | <code>Request</code> | |
| response | <code>Response &#124; undefined</code> | |
| body | <code>any</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpFetchError](./kibana-plugin-public.httpfetcherror.md) &gt; [body](./kibana-plugin-public.httpfetcherror.body.md)
## HttpFetchError.body property
<b>Signature:</b>
```typescript
readonly body?: any;
```

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpFetchError](./kibana-plugin-public.httpfetcherror.md)
## HttpFetchError class
<b>Signature:</b>
```typescript
export declare class HttpFetchError extends Error
```
## Constructors
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(message, request, response, body)](./kibana-plugin-public.httpfetcherror._constructor_.md) | | Constructs a new instance of the <code>HttpFetchError</code> class |
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [body](./kibana-plugin-public.httpfetcherror.body.md) | | <code>any</code> | |
| [request](./kibana-plugin-public.httpfetcherror.request.md) | | <code>Request</code> | |
| [response](./kibana-plugin-public.httpfetcherror.response.md) | | <code>Response &#124; undefined</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpFetchError](./kibana-plugin-public.httpfetcherror.md) &gt; [request](./kibana-plugin-public.httpfetcherror.request.md)
## HttpFetchError.request property
<b>Signature:</b>
```typescript
readonly request: Request;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpFetchError](./kibana-plugin-public.httpfetcherror.md) &gt; [response](./kibana-plugin-public.httpfetcherror.response.md)
## HttpFetchError.response property
<b>Signature:</b>
```typescript
readonly response?: Response | undefined;
```

View file

@ -14,6 +14,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| Class | Description |
| --- | --- |
| [HttpFetchError](./kibana-plugin-public.httpfetcherror.md) | |
| [HttpInterceptController](./kibana-plugin-public.httpinterceptcontroller.md) | |
| [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. |
| [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) | This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md)<!-- -->.<!-- -->It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. |

View file

@ -17,6 +17,7 @@
* under the License.
*/
/** @public */
export class HttpFetchError extends Error {
constructor(
message: string,

View file

@ -110,6 +110,7 @@ export {
HttpFetchQuery,
HttpErrorResponse,
HttpErrorRequest,
HttpFetchError,
HttpInterceptor,
HttpResponse,
HttpHandler,

View file

@ -425,8 +425,6 @@ export interface HttpErrorRequest {
export interface HttpErrorResponse {
// (undocumented)
body?: HttpBody;
// Warning: (ae-forgotten-export) The symbol "HttpFetchError" needs to be exported by the entry point index.d.ts
//
// (undocumented)
error: Error | HttpFetchError;
// (undocumented)
@ -435,6 +433,17 @@ export interface HttpErrorResponse {
response?: Response;
}
// @public (undocumented)
export class HttpFetchError extends Error {
constructor(message: string, request: Request, response?: Response | undefined, body?: any);
// (undocumented)
readonly body?: any;
// (undocumented)
readonly request: Request;
// (undocumented)
readonly response?: Response | undefined;
}
// @public (undocumented)
export interface HttpFetchOptions extends HttpRequestInit {
// (undocumented)

View file

@ -10,8 +10,6 @@ import React from 'react';
import { mockMoment } from '../../../../utils/testHelpers';
import { DetailView } from './index';
jest.mock('ui/kfetch');
describe('DetailView', () => {
beforeEach(() => {
// Avoid timezone issues

View file

@ -27,7 +27,6 @@ import { ErrorDistribution } from './Distribution';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { useTrackPageview } from '../../../../../infra/public';
import { callApmApi } from '../../../services/rest/callApmApi';
const Titles = styled.div`
margin-bottom: ${px(units.plus)};
@ -63,43 +62,49 @@ export function ErrorGroupDetails() {
const { urlParams, uiFilters } = useUrlParams();
const { serviceName, start, end, errorGroupId } = urlParams;
const { data: errorGroupData } = useFetcher(() => {
if (serviceName && start && end && errorGroupId) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/{groupId}',
params: {
path: {
serviceName,
groupId: errorGroupId
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
const { data: errorGroupData } = useFetcher(
callApmApi => {
if (serviceName && start && end && errorGroupId) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/{groupId}',
params: {
path: {
serviceName,
groupId: errorGroupId
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
}
}
}
});
}
}, [serviceName, start, end, errorGroupId, uiFilters]);
});
}
},
[serviceName, start, end, errorGroupId, uiFilters]
);
const { data: errorDistributionData } = useFetcher(() => {
if (serviceName && start && end && errorGroupId) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName
},
query: {
start,
end,
groupId: errorGroupId,
uiFilters: JSON.stringify(uiFilters)
const { data: errorDistributionData } = useFetcher(
callApmApi => {
if (serviceName && start && end && errorGroupId) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName
},
query: {
start,
end,
groupId: errorGroupId,
uiFilters: JSON.stringify(uiFilters)
}
}
}
});
}
}, [serviceName, start, end, errorGroupId, uiFilters]);
});
}
},
[serviceName, start, end, errorGroupId, uiFilters]
);
useTrackPageview({ app: 'apm', path: 'error_group_details' });
useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 });

View file

@ -20,50 +20,55 @@ import { useUrlParams } from '../../../hooks/useUrlParams';
import { useTrackPageview } from '../../../../../infra/public';
import { PROJECTION } from '../../../../common/projections/typings';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { callApmApi } from '../../../services/rest/callApmApi';
const ErrorGroupOverview: React.SFC = () => {
const { urlParams, uiFilters } = useUrlParams();
const { serviceName, start, end, sortField, sortDirection } = urlParams;
const { data: errorDistributionData } = useFetcher(() => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
const { data: errorDistributionData } = useFetcher(
callApmApi => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
}
}
}
});
}
}, [serviceName, start, end, uiFilters]);
});
}
},
[serviceName, start, end, uiFilters]
);
const { data: errorGroupListData } = useFetcher(() => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors',
params: {
path: {
serviceName
},
query: {
start,
end,
sortField,
sortDirection,
uiFilters: JSON.stringify(uiFilters)
const { data: errorGroupListData } = useFetcher(
callApmApi => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors',
params: {
path: {
serviceName
},
query: {
start,
end,
sortField,
sortDirection,
uiFilters: JSON.stringify(uiFilters)
}
}
}
});
}
}, [serviceName, start, end, sortField, sortDirection, uiFilters]);
});
}
},
[serviceName, start, end, sortField, sortDirection, uiFilters]
);
useTrackPageview({
app: 'apm',

View file

@ -8,7 +8,6 @@ import { shallow } from 'enzyme';
import React from 'react';
import { Home } from '../Home';
jest.mock('ui/kfetch');
jest.mock('ui/index_patterns');
jest.mock('ui/new_platform');

View file

@ -10,7 +10,6 @@ import { MemoryRouter } from 'react-router-dom';
import { UpdateBreadcrumbs } from '../UpdateBreadcrumbs';
import * as kibanaCore from '../../../../../../observability/public/context/kibana_core';
jest.mock('ui/kfetch');
jest.mock('ui/index_patterns');
jest.mock('ui/new_platform');

View file

@ -10,7 +10,7 @@ import { startMLJob } from '../../../../../services/rest/ml';
import { IUrlParams } from '../../../../../context/UrlParamsContext/types';
import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink';
import { MachineLearningFlyoutView } from './view';
import { KibanaCoreContext } from '../../../../../../../observability/public/context/kibana_core';
import { KibanaCoreContext } from '../../../../../../../observability/public';
interface Props {
isOpen: boolean;
@ -36,11 +36,12 @@ export class MachineLearningFlyout extends Component<Props, State> {
}) => {
this.setState({ isCreatingJob: true });
try {
const { http } = this.context;
const { serviceName } = this.props.urlParams;
if (!serviceName) {
throw new Error('Service name is required to create this ML job');
}
const res = await startMLJob({ serviceName, transactionType });
const res = await startMLJob({ http, serviceName, transactionType });
const didSucceed = res.datafeeds[0].success && res.jobs[0].success;
if (!didSucceed) {
throw new Error('Creating ML job failed');

View file

@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState, useEffect } from 'react';
import { isEmpty } from 'lodash';
import { useKibanaCore } from '../../../../../../../observability/public';
import { FETCH_STATUS, useFetcher } from '../../../../../hooks/useFetcher';
import { getHasMLJob } from '../../../../../services/rest/ml';
import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink';
@ -49,14 +50,18 @@ export function MachineLearningFlyoutView({
const [selectedTransactionType, setSelectedTransactionType] = useState<
string | undefined
>(undefined);
const { http } = useKibanaCore();
const { data: hasMLJob = false, status } = useFetcher(() => {
if (serviceName && selectedTransactionType) {
return getHasMLJob({
serviceName,
transactionType: selectedTransactionType
transactionType: selectedTransactionType,
http
});
}
}, [serviceName, selectedTransactionType]);
}, [serviceName, selectedTransactionType, http]);
// update selectedTransactionType when list of transaction types has loaded
useEffect(() => {

View file

@ -13,8 +13,6 @@ import * as rest from '../../../../../services/rest/watcher';
import { createErrorGroupWatch } from '../createErrorGroupWatch';
import { esResponse } from './esResponse';
jest.mock('ui/kfetch');
// disable html escaping since this is also disabled in watcher\s mustache implementation
mustache.escape = value => value;

View file

@ -8,7 +8,6 @@ import React from 'react';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { useFetcher } from '../../../hooks/useFetcher';
import { callApmApi } from '../../../services/rest/callApmApi';
import { Cytoscape } from './Cytoscape';
import { Controls } from './Controls';
@ -41,14 +40,17 @@ export function ServiceMap({ serviceName }: ServiceMapProps) {
urlParams: { start, end }
} = useUrlParams();
const { data } = useFetcher(async () => {
if (start && end) {
return callApmApi({
pathname: '/api/apm/service-map',
params: { query: { start, end } }
});
}
}, [start, end]);
const { data } = useFetcher(
callApmApi => {
if (start && end) {
return callApmApi({
pathname: '/api/apm/service-map',
params: { query: { start, end } }
});
}
},
[start, end]
);
const elements = Array.isArray(data) ? data : [];

View file

@ -25,7 +25,6 @@ import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts';
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
import { MetricsChart } from '../../shared/charts/MetricsChart';
import { useFetcher, FETCH_STATUS } from '../../../hooks/useFetcher';
import { callApmApi } from '../../../services/rest/callApmApi';
import { truncate, px, unit } from '../../../style/variables';
const INITIAL_DATA = {
@ -46,20 +45,20 @@ export function ServiceNodeMetrics() {
const { data } = useServiceMetricCharts(urlParams, agentName);
const { start, end } = urlParams;
const {
data: { host, containerId } = INITIAL_DATA,
status
} = useFetcher(() => {
if (serviceName && serviceNodeName) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/node/{serviceNodeName}/metadata',
params: {
path: { serviceName, serviceNodeName }
}
});
}
}, [serviceName, serviceNodeName]);
const { data: { host, containerId } = INITIAL_DATA, status } = useFetcher(
callApmApi => {
if (serviceName && serviceNodeName) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/node/{serviceNodeName}/metadata',
params: {
path: { serviceName, serviceNodeName }
}
});
}
},
[serviceName, serviceNodeName]
);
const isLoading = status === FETCH_STATUS.LOADING;

View file

@ -12,7 +12,6 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { ManagedTable, ITableColumn } from '../../shared/ManagedTable';
import { useFetcher } from '../../../hooks/useFetcher';
import { callApmApi } from '../../../services/rest/callApmApi';
import {
asDynamicBytes,
asInteger,
@ -46,24 +45,27 @@ const ServiceNodeOverview = () => {
[serviceName]
);
const { data: items } = useFetcher(() => {
if (!serviceName || !start || !end) {
return;
}
return callApmApi({
pathname: '/api/apm/services/{serviceName}/serviceNodes',
params: {
path: {
serviceName
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
}
const { data: items = [] } = useFetcher(
callApmApi => {
if (!serviceName || !start || !end) {
return undefined;
}
});
}, [serviceName, start, end, uiFilters]);
return callApmApi({
pathname: '/api/apm/services/{serviceName}/serviceNodes',
params: {
path: {
serviceName
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
}
}
});
},
[serviceName, start, end, uiFilters]
);
if (!serviceName) {
return null;
@ -134,7 +136,7 @@ const ServiceNodeOverview = () => {
noItemsMessage={i18n.translate('xpack.apm.jvmsTable.noJvmsLabel', {
defaultMessage: 'No JVMs were found'
})}
items={items || []}
items={items}
columns={columns}
initialPageSize={INITIAL_PAGE_SIZE}
initialSortField={INITIAL_SORT_FIELD}

View file

@ -7,15 +7,13 @@
import React from 'react';
import { render, wait, waitForElement } from 'react-testing-library';
import 'react-testing-library/cleanup-after-each';
import * as callApmApi from '../../../../services/rest/callApmApi';
import { ServiceOverview } from '..';
import * as urlParamsHooks from '../../../../hooks/useUrlParams';
import * as kibanaCore from '../../../../../../observability/public/context/kibana_core';
import { LegacyCoreStart } from 'src/core/public';
import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters';
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
jest.mock('ui/kfetch');
import { SessionStorageMock } from '../../../../services/__test__/SessionStorageMock';
function renderServiceOverview() {
return render(<ServiceOverview />);
@ -25,13 +23,24 @@ const coreMock = ({
http: {
basePath: {
prepend: (path: string) => `/basepath${path}`
}
},
get: jest.fn()
},
notifications: { toasts: { addWarning: () => {} } }
} as unknown) as LegacyCoreStart;
notifications: {
toasts: {
addWarning: () => {}
}
}
} as unknown) as LegacyCoreStart & {
http: { get: jest.Mock<any, any> };
};
describe('Service Overview -> View', () => {
beforeEach(() => {
// @ts-ignore
global.sessionStorage = new SessionStorageMock();
spyOn(kibanaCore, 'useKibanaCore').and.returnValue(coreMock);
// mock urlParams
spyOn(urlParamsHooks, 'useUrlParams').and.returnValue({
urlParams: {
@ -39,7 +48,6 @@ describe('Service Overview -> View', () => {
end: 'myEnd'
}
});
spyOn(kibanaCore, 'useKibanaCore').and.returnValue(coreMock);
jest.spyOn(useLocalUIFilters, 'useLocalUIFilters').mockReturnValue({
filters: [],
@ -65,53 +73,49 @@ describe('Service Overview -> View', () => {
it('should render services, when list is not empty', async () => {
// mock rest requests
const dataFetchingSpy = jest
.spyOn(callApmApi, 'callApmApi')
.mockResolvedValue({
hasLegacyData: false,
hasHistoricalData: true,
items: [
{
serviceName: 'My Python Service',
agentName: 'python',
transactionsPerMinute: 100,
errorsPerMinute: 200,
avgResponseTime: 300,
environments: ['test', 'dev']
},
{
serviceName: 'My Go Service',
agentName: 'go',
transactionsPerMinute: 400,
errorsPerMinute: 500,
avgResponseTime: 600,
environments: []
}
]
});
coreMock.http.get.mockResolvedValueOnce({
hasLegacyData: false,
hasHistoricalData: true,
items: [
{
serviceName: 'My Python Service',
agentName: 'python',
transactionsPerMinute: 100,
errorsPerMinute: 200,
avgResponseTime: 300,
environments: ['test', 'dev']
},
{
serviceName: 'My Go Service',
agentName: 'go',
transactionsPerMinute: 400,
errorsPerMinute: 500,
avgResponseTime: 600,
environments: []
}
]
});
const { container, getByText } = renderServiceOverview();
// wait for requests to be made
await wait(() => expect(dataFetchingSpy).toHaveBeenCalledTimes(1));
await wait(() => expect(coreMock.http.get).toHaveBeenCalledTimes(1));
await waitForElement(() => getByText('My Python Service'));
expect(container.querySelectorAll('.euiTableRow')).toMatchSnapshot();
});
it('should render getting started message, when list is empty and no historical data is found', async () => {
const dataFetchingSpy = jest
.spyOn(callApmApi, 'callApmApi')
.mockResolvedValue({
hasLegacyData: false,
hasHistoricalData: false,
items: []
});
coreMock.http.get.mockResolvedValueOnce({
hasLegacyData: false,
hasHistoricalData: false,
items: []
});
const { container, getByText } = renderServiceOverview();
// wait for requests to be made
await wait(() => expect(dataFetchingSpy).toHaveBeenCalledTimes(1));
await wait(() => expect(coreMock.http.get).toHaveBeenCalledTimes(1));
// wait for elements to be rendered
await waitForElement(() =>
@ -124,18 +128,16 @@ describe('Service Overview -> View', () => {
});
it('should render empty message, when list is empty and historical data is found', async () => {
const dataFetchingSpy = jest
.spyOn(callApmApi, 'callApmApi')
.mockResolvedValue({
hasLegacyData: false,
hasHistoricalData: true,
items: []
});
coreMock.http.get.mockResolvedValueOnce({
hasLegacyData: false,
hasHistoricalData: true,
items: []
});
const { container, getByText } = renderServiceOverview();
// wait for requests to be made
await wait(() => expect(dataFetchingSpy).toHaveBeenCalledTimes(1));
await wait(() => expect(coreMock.http.get).toHaveBeenCalledTimes(1));
await waitForElement(() => getByText('No services found'));
expect(container.querySelectorAll('.euiTableRow')).toMatchSnapshot();
@ -149,18 +151,16 @@ describe('Service Overview -> View', () => {
'addWarning'
);
const dataFetchingSpy = jest
.spyOn(callApmApi, 'callApmApi')
.mockResolvedValue({
hasLegacyData: true,
hasHistoricalData: true,
items: []
});
coreMock.http.get.mockResolvedValueOnce({
hasLegacyData: true,
hasHistoricalData: true,
items: []
});
renderServiceOverview();
// wait for requests to be made
await wait(() => expect(dataFetchingSpy).toHaveBeenCalledTimes(1));
await wait(() => expect(coreMock.http.get).toHaveBeenCalledTimes(1));
expect(addWarning).toHaveBeenLastCalledWith(
expect.objectContaining({
@ -177,18 +177,16 @@ describe('Service Overview -> View', () => {
coreMock.notifications.toasts,
'addWarning'
);
const dataFetchingSpy = jest
.spyOn(callApmApi, 'callApmApi')
.mockResolvedValue({
hasLegacyData: false,
hasHistoricalData: true,
items: []
});
coreMock.http.get.mockResolvedValueOnce({
hasLegacyData: false,
hasHistoricalData: true,
items: []
});
renderServiceOverview();
// wait for requests to be made
await wait(() => expect(dataFetchingSpy).toHaveBeenCalledTimes(1));
await wait(() => expect(coreMock.http.get).toHaveBeenCalledTimes(1));
expect(addWarning).not.toHaveBeenCalled();
});

View file

@ -17,7 +17,6 @@ import { useTrackPageview } from '../../../../../infra/public';
import { useKibanaCore } from '../../../../../observability/public';
import { PROJECTION } from '../../../../common/projections/typings';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { callApmApi } from '../../../services/rest/callApmApi';
const initalData = {
items: [],
@ -33,16 +32,19 @@ export function ServiceOverview() {
urlParams: { start, end },
uiFilters
} = useUrlParams();
const { data = initalData, status } = useFetcher(() => {
if (start && end) {
return callApmApi({
pathname: '/api/apm/services',
params: {
query: { start, end, uiFilters: JSON.stringify(uiFilters) }
}
});
}
}, [start, end, uiFilters]);
const { data = initalData, status } = useFetcher(
callApmApi => {
if (start && end) {
return callApmApi({
pathname: '/api/apm/services',
params: {
query: { start, end, uiFilters: JSON.stringify(uiFilters) }
}
});
}
},
[start, end, uiFilters]
);
useEffect(() => {
if (data.hasLegacyData && !hasDisplayedToast) {

View file

@ -8,10 +8,11 @@ import React, { useState } from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import { NotificationsStart } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import { useCallApmApi } from '../../../../../hooks/useCallApmApi';
import { Config } from '../index';
import { callApmApi } from '../../../../../services/rest/callApmApi';
import { getOptionLabel } from '../../../../../../common/agent_configuration_constants';
import { useKibanaCore } from '../../../../../../../observability/public';
import { APMClient } from '../../../../../services/rest/createCallApmApi';
interface Props {
onDeleted: () => void;
@ -24,6 +25,8 @@ export function DeleteButton({ onDeleted, selectedConfig }: Props) {
notifications: { toasts }
} = useKibanaCore();
const callApmApi = useCallApmApi();
return (
<EuiButtonEmpty
color="danger"
@ -31,7 +34,7 @@ export function DeleteButton({ onDeleted, selectedConfig }: Props) {
iconSide="right"
onClick={async () => {
setIsDeleting(true);
await deleteConfig(selectedConfig, toasts);
await deleteConfig(callApmApi, selectedConfig, toasts);
setIsDeleting(false);
onDeleted();
}}
@ -45,6 +48,7 @@ export function DeleteButton({ onDeleted, selectedConfig }: Props) {
}
async function deleteConfig(
callApmApi: APMClient,
selectedConfig: Config,
toasts: NotificationsStart['toasts']
) {

View file

@ -9,7 +9,6 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { SelectWithPlaceholder } from '../../../../shared/SelectWithPlaceholder';
import { useFetcher } from '../../../../../hooks/useFetcher';
import { callApmApi } from '../../../../../services/rest/callApmApi';
import {
getOptionLabel,
omitAllOption
@ -36,7 +35,7 @@ export function ServiceSection({
setEnvironment
}: Props) {
const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher(
() => {
callApmApi => {
if (!isReadOnly) {
return callApmApi({
pathname: '/api/apm/settings/agent-configuration/services',
@ -48,7 +47,7 @@ export function ServiceSection({
{ preservePreviousData: false }
);
const { data: environments = [], status: environmentStatus } = useFetcher(
() => {
callApmApi => {
if (!isReadOnly && serviceName) {
return callApmApi({
pathname: '/api/apm/settings/agent-configuration/environments',

View file

@ -23,8 +23,8 @@ import { idx } from '@kbn/elastic-idx';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { isRight } from 'fp-ts/lib/Either';
import { useCallApmApi } from '../../../../../hooks/useCallApmApi';
import { transactionSampleRateRt } from '../../../../../../common/runtime_types/transaction_sample_rate_rt';
import { callApmApi } from '../../../../../services/rest/callApmApi';
import { Config } from '../index';
import { SettingsSection } from './SettingsSection';
import { ServiceSection } from './ServiceSection';
@ -60,6 +60,8 @@ export function AddEditFlyout({
} = useKibanaCore();
const [isSaving, setIsSaving] = useState(false);
const callApmApiFromHook = useCallApmApi();
// config conditions (service)
const [serviceName, setServiceName] = useState<string>(
selectedConfig ? selectedConfig.service.name || ALL_OPTION_VALUE : ''
@ -69,9 +71,9 @@ export function AddEditFlyout({
);
const { data: { agentName } = { agentName: undefined } } = useFetcher(
() => {
callApmApi => {
if (serviceName === ALL_OPTION_VALUE) {
return { agentName: undefined };
return Promise.resolve({ agentName: undefined });
}
if (serviceName) {
@ -127,6 +129,7 @@ export function AddEditFlyout({
setIsSaving(true);
await saveConfig({
callApmApi: callApmApiFromHook,
serviceName,
environment,
sampleRate,

View file

@ -6,13 +6,13 @@
import { i18n } from '@kbn/i18n';
import { NotificationsStart } from 'kibana/public';
import { APMClient } from '../../../../../services/rest/createCallApmApi';
import { trackEvent } from '../../../../../../../infra/public/hooks/use_track_metric';
import { isRumAgentName } from '../../../../../../common/agent_name';
import {
getOptionLabel,
omitAllOption
} from '../../../../../../common/agent_configuration_constants';
import { callApmApi } from '../../../../../services/rest/callApmApi';
interface Settings {
transaction_sample_rate: number;
@ -21,6 +21,7 @@ interface Settings {
}
export async function saveConfig({
callApmApi,
serviceName,
environment,
sampleRate,
@ -30,6 +31,7 @@ export async function saveConfig({
agentName,
toasts
}: {
callApmApi: APMClient;
serviceName: string;
environment: string;
sampleRate: string;

View file

@ -18,7 +18,6 @@ import {
import { isEmpty } from 'lodash';
import { useFetcher } from '../../../../hooks/useFetcher';
import { AgentConfigurationListAPIResponse } from '../../../../../server/lib/settings/agent_configuration/list_configurations';
import { callApmApi } from '../../../../services/rest/callApmApi';
import { HomeLink } from '../../../shared/Links/apm/HomeLink';
import { AgentConfigurationList } from './AgentConfigurationList';
import { useTrackPageview } from '../../../../../../infra/public';
@ -28,7 +27,8 @@ export type Config = AgentConfigurationListAPIResponse[0];
export function AgentConfigurations() {
const { data = [], status, refetch } = useFetcher(
() => callApmApi({ pathname: `/api/apm/settings/agent-configuration` }),
callApmApi =>
callApmApi({ pathname: `/api/apm/settings/agent-configuration` }),
[],
{ preservePreviousData: false }
);

View file

@ -12,25 +12,27 @@ import { useUrlParams } from '../../../hooks/useUrlParams';
import { useTrackPageview } from '../../../../../infra/public';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { PROJECTION } from '../../../../common/projections/typings';
import { callApmApi } from '../../../services/rest/callApmApi';
export function TraceOverview() {
const { urlParams, uiFilters } = useUrlParams();
const { start, end } = urlParams;
const { status, data = [] } = useFetcher(() => {
if (start && end) {
return callApmApi({
pathname: '/api/apm/traces',
params: {
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
const { status, data = [] } = useFetcher(
callApmApi => {
if (start && end) {
return callApmApi({
pathname: '/api/apm/traces',
params: {
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
}
}
}
});
}
}, [start, end, uiFilters]);
});
}
},
[start, end, uiFilters]
);
useTrackPageview({ app: 'apm', path: 'traces_overview' });
useTrackPageview({ app: 'apm', path: 'traces_overview', delay: 15000 });

View file

@ -28,8 +28,6 @@ import { LegacyCoreStart } from 'kibana/public';
jest.spyOn(history, 'push');
jest.spyOn(history, 'replace');
jest.mock('ui/kfetch');
const coreMock = ({
notifications: { toasts: { addWarning: () => {} } }
} as unknown) as LegacyCoreStart;

View file

@ -15,6 +15,7 @@ import {
import { Location } from 'history';
import { first } from 'lodash';
import React, { useMemo } from 'react';
import { useKibanaCore } from '../../../../../observability/public';
import { useTransactionList } from '../../../hooks/useTransactionList';
import { useTransactionCharts } from '../../../hooks/useTransactionCharts';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
@ -85,11 +86,13 @@ export function TransactionOverview() {
status: transactionListStatus
} = useTransactionList(urlParams);
const { http } = useKibanaCore();
const { data: hasMLJob = false } = useFetcher(() => {
if (serviceName && transactionType) {
return getHasMLJob({ serviceName, transactionType });
return getHasMLJob({ serviceName, transactionType, http });
}
}, [serviceName, transactionType]);
}, [http, serviceName, transactionType]);
const localFiltersConfig: React.ComponentProps<
typeof LocalUIFilters

View file

@ -16,7 +16,6 @@ import {
ENVIRONMENT_ALL,
ENVIRONMENT_NOT_DEFINED
} from '../../../../common/environment_filter_values';
import { callApmApi } from '../../../services/rest/callApmApi';
function updateEnvironmentUrl(
location: ReturnType<typeof useLocation>,
@ -79,20 +78,23 @@ export const EnvironmentFilter: React.FC = () => {
const { start, end, serviceName } = urlParams;
const { environment } = uiFilters;
const { data: environments = [], status = 'loading' } = useFetcher(() => {
if (start && end) {
return callApmApi({
pathname: '/api/apm/ui_filters/environments',
params: {
query: {
start,
end,
serviceName
const { data: environments = [], status = 'loading' } = useFetcher(
callApmApi => {
if (start && end) {
return callApmApi({
pathname: '/api/apm/ui_filters/environments',
params: {
query: {
start,
end,
serviceName
}
}
}
});
}
}, [start, end, serviceName]);
});
}
},
[start, end, serviceName]
);
return (
<EuiSelect

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { uniqueId, startsWith } from 'lodash';
import { EuiCallOut } from '@elastic/eui';
import styled from 'styled-components';
@ -25,8 +25,10 @@ import { history } from '../../../utils/history';
import { useMatchedRoutes } from '../../../hooks/useMatchedRoutes';
import { RouteName } from '../../app/Main/route_config/route_names';
import { useKibanaCore } from '../../../../../observability/public';
import { getAPMIndexPattern } from '../../../services/rest/savedObjects';
import { ISavedObject } from '../../../services/rest/savedObjects';
import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public';
import { FETCH_STATUS } from '../../../hooks/useFetcher';
import { useAPMIndexPattern } from '../../../hooks/useAPMIndexPattern';
const Container = styled.div`
margin-bottom: 10px;
@ -36,9 +38,7 @@ const getAutocompleteProvider = (language: string) =>
npStart.plugins.data.autocomplete.getProvider(language);
interface State {
indexPattern: StaticIndexPattern | null;
suggestions: AutocompleteSuggestion[];
isLoadingIndexPattern: boolean;
isLoadingSuggestions: boolean;
}
@ -50,13 +50,9 @@ function convertKueryToEsQuery(
return toElasticsearchQuery(ast, indexPattern);
}
async function getAPMIndexPatternForKuery(): Promise<
StaticIndexPattern | undefined
> {
const apmIndexPattern = await getAPMIndexPattern();
if (!apmIndexPattern) {
return;
}
function getAPMIndexPatternForKuery(
apmIndexPattern: ISavedObject
): StaticIndexPattern | undefined {
return getFromSavedObject(apmIndexPattern);
}
@ -89,9 +85,7 @@ function getSuggestions(
export function KueryBar() {
const core = useKibanaCore();
const [state, setState] = useState<State>({
indexPattern: null,
suggestions: [],
isLoadingIndexPattern: true,
isLoadingSuggestions: false
});
const { urlParams } = useUrlParams();
@ -101,8 +95,19 @@ export function KueryBar() {
const apmIndexPatternTitle = core.injectedMetadata.getInjectedVar(
'apmIndexPatternTitle'
);
const indexPatternMissing =
!state.isLoadingIndexPattern && !state.indexPattern;
const {
apmIndexPattern,
status: apmIndexPatternStatus
} = useAPMIndexPattern();
const indexPattern =
apmIndexPatternStatus === FETCH_STATUS.SUCCESS
? getAPMIndexPatternForKuery(apmIndexPattern)
: null;
const indexPatternMissing = status === FETCH_STATUS.SUCCESS && !indexPattern;
let currentRequestCheck;
const exampleMap: { [key: string]: string } = {
@ -116,36 +121,8 @@ export function KueryBar() {
matchedRoutes.map(({ name }) => exampleMap[name]).find(Boolean) ||
'transaction.duration.us > 300000 AND http.response.status_code >= 400';
useEffect(() => {
let didCancel = false;
async function loadIndexPattern() {
setState(value => ({ ...value, isLoadingIndexPattern: true }));
const indexPattern = await getAPMIndexPatternForKuery();
if (didCancel) {
return;
}
if (!indexPattern) {
setState(value => ({ ...value, isLoadingIndexPattern: false }));
} else {
setState(value => ({
...value,
indexPattern,
isLoadingIndexPattern: false
}));
}
}
loadIndexPattern();
return () => {
didCancel = true;
};
}, []);
async function onChange(inputValue: string, selectionStart: number) {
const { indexPattern } = state;
if (indexPattern === null) {
if (indexPattern == null) {
return;
}
@ -177,9 +154,7 @@ export function KueryBar() {
}
function onSubmit(inputValue: string) {
const { indexPattern } = state;
if (indexPattern === null) {
if (indexPattern == null) {
return;
}

View file

@ -32,7 +32,7 @@ interface Props {
export function DiscoverLink({ query = {}, ...rest }: Props) {
const core = useKibanaCore();
const apmIndexPattern = useAPMIndexPattern();
const { apmIndexPattern } = useAPMIndexPattern();
const location = useLocation();
if (!apmIndexPattern.id) {

View file

@ -10,8 +10,6 @@ import React from 'react';
import { APMError } from '../../../../../../typings/es_schemas/ui/APMError';
import { DiscoverErrorLink } from '../DiscoverErrorLink';
jest.mock('ui/kfetch');
describe('DiscoverErrorLink without kuery', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {

View file

@ -10,8 +10,6 @@ import React from 'react';
import { APMError } from '../../../../../../typings/es_schemas/ui/APMError';
import { DiscoverErrorLink } from '../DiscoverErrorLink';
jest.mock('ui/kfetch');
describe('DiscoverErrorLink without kuery', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {

View file

@ -9,115 +9,120 @@ import React from 'react';
import { APMError } from '../../../../../../typings/es_schemas/ui/APMError';
import { Span } from '../../../../../../typings/es_schemas/ui/Span';
import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction';
import * as savedObjects from '../../../../../services/rest/savedObjects';
import { getRenderedHref } from '../../../../../utils/testHelpers';
import { DiscoverErrorLink } from '../DiscoverErrorLink';
import { DiscoverSpanLink } from '../DiscoverSpanLink';
import { DiscoverTransactionLink } from '../DiscoverTransactionLink';
import * as kibanaCore from '../../../../../../../observability/public/context/kibana_core';
import * as useAPMIndexPattern from '../../../../../hooks/useAPMIndexPattern';
import { LegacyCoreStart } from 'src/core/public';
import { FETCH_STATUS } from '../../../../../hooks/useFetcher';
jest.mock('ui/kfetch');
jest.spyOn(useAPMIndexPattern, 'useAPMIndexPattern').mockReturnValue({
status: FETCH_STATUS.SUCCESS,
apmIndexPattern: { id: 'apm-index-pattern-id' }
} as any);
jest
.spyOn(savedObjects, 'getAPMIndexPattern')
.mockResolvedValue({ id: 'apm-index-pattern-id' } as any);
describe('DiscoverLinks', () => {
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => null);
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => null);
const coreMock = ({
http: {
basePath: {
prepend: (path: string) => `/basepath${path}`
const coreMock = ({
http: {
notifications: {
toasts: {}
},
basePath: {
prepend: (path: string) => `/basepath${path}`
}
}
}
} as unknown) as LegacyCoreStart;
} as unknown) as LegacyCoreStart;
jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock);
});
spyOn(kibanaCore, 'useKibanaCore').and.returnValue(coreMock);
});
afterAll(() => {
jest.restoreAllMocks();
});
afterAll(() => {
jest.restoreAllMocks();
});
test('DiscoverTransactionLink should produce the correct URL', async () => {
const transaction = {
transaction: {
id: '8b60bd32ecc6e150'
},
trace: {
id: '8b60bd32ecc6e1506735a8b6cfcf175c'
}
} as Transaction;
it('produces the correct URL for a transaction', async () => {
const transaction = {
transaction: {
id: '8b60bd32ecc6e150'
},
trace: {
id: '8b60bd32ecc6e1506735a8b6cfcf175c'
}
} as Transaction;
const href = await getRenderedHref(
() => <DiscoverTransactionLink transaction={transaction} />,
{
const href = await getRenderedHref(
() => <DiscoverTransactionLink transaction={transaction} />,
{
search: '?rangeFrom=now/w&rangeTo=now'
} as Location
);
expect(href).toEqual(
`/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'processor.event:"transaction" AND transaction.id:"8b60bd32ecc6e150" AND trace.id:"8b60bd32ecc6e1506735a8b6cfcf175c"'))`
);
});
it('produces the correct URL for a span', async () => {
const span = {
span: {
id: 'test-span-id'
}
} as Span;
const href = await getRenderedHref(() => <DiscoverSpanLink span={span} />, {
search: '?rangeFrom=now/w&rangeTo=now'
} as Location
);
} as Location);
expect(href).toEqual(
`/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'processor.event:"transaction" AND transaction.id:"8b60bd32ecc6e150" AND trace.id:"8b60bd32ecc6e1506735a8b6cfcf175c"'))`
);
});
test('DiscoverSpanLink should produce the correct URL', async () => {
const span = {
span: {
id: 'test-span-id'
}
} as Span;
const href = await getRenderedHref(() => <DiscoverSpanLink span={span} />, {
search: '?rangeFrom=now/w&rangeTo=now'
} as Location);
expect(href).toEqual(
`/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'span.id:"test-span-id"'))`
);
});
test('DiscoverErrorLink should produce the correct URL', async () => {
const error = {
service: {
name: 'service-name'
},
error: {
grouping_key: 'grouping-key'
}
} as APMError;
const href = await getRenderedHref(
() => <DiscoverErrorLink error={error} />,
{
search: '?rangeFrom=now/w&rangeTo=now'
} as Location
);
expect(href).toEqual(
`/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'service.name:"service-name" AND error.grouping_key:"grouping-key"'),sort:('@timestamp':desc))`
);
});
test('DiscoverErrorLink should include optional kuery string in URL', async () => {
const error = {
service: {
name: 'service-name'
},
error: {
grouping_key: 'grouping-key'
}
} as APMError;
const href = await getRenderedHref(
() => <DiscoverErrorLink error={error} kuery="some:kuery-string" />,
{
search: '?rangeFrom=now/w&rangeTo=now'
} as Location
);
expect(href).toEqual(
`/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'service.name:"service-name" AND error.grouping_key:"grouping-key" AND some:kuery-string'),sort:('@timestamp':desc))`
);
expect(href).toEqual(
`/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'span.id:"test-span-id"'))`
);
});
test('DiscoverErrorLink should produce the correct URL', async () => {
const error = {
service: {
name: 'service-name'
},
error: {
grouping_key: 'grouping-key'
}
} as APMError;
const href = await getRenderedHref(
() => <DiscoverErrorLink error={error} />,
{
search: '?rangeFrom=now/w&rangeTo=now'
} as Location
);
expect(href).toEqual(
`/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'service.name:"service-name" AND error.grouping_key:"grouping-key"'),sort:('@timestamp':desc))`
);
});
test('DiscoverErrorLink should include optional kuery string in URL', async () => {
const error = {
service: {
name: 'service-name'
},
error: {
grouping_key: 'grouping-key'
}
} as APMError;
const href = await getRenderedHref(
() => <DiscoverErrorLink error={error} kuery="some:kuery-string" />,
{
search: '?rangeFrom=now/w&rangeTo=now'
} as Location
);
expect(href).toEqual(
`/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'service.name:"service-name" AND error.grouping_key:"grouping-key" AND some:kuery-string'),sort:('@timestamp':desc))`
);
});
});

View file

@ -14,8 +14,6 @@ import {
} from '../DiscoverTransactionLink';
import mockTransaction from './mockTransaction.json';
jest.mock('ui/kfetch');
describe('DiscoverTransactionLink component', () => {
it('should render with data', () => {
const transaction: Transaction = mockTransaction;

View file

@ -10,8 +10,6 @@ import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction
import configureStore from '../../../../../store/config/configureStore';
import { getDiscoverQuery } from '../DiscoverTransactionLink';
jest.mock('ui/kfetch');
function getMockTransaction() {
return {
transaction: {

View file

@ -12,8 +12,6 @@ import * as savedObjects from '../../../../services/rest/savedObjects';
import * as kibanaCore from '../../../../../../observability/public/context/kibana_core';
import { LegacyCoreStart } from 'src/core/public';
jest.mock('ui/kfetch');
const coreMock = ({
http: {
basePath: {

View file

@ -14,8 +14,7 @@ import * as apmIndexPatternHooks from '../../../../hooks/useAPMIndexPattern';
import * as kibanaCore from '../../../../../../observability/public/context/kibana_core';
import { ISavedObject } from '../../../../services/rest/savedObjects';
import { LegacyCoreStart } from 'src/core/public';
jest.mock('ui/kfetch');
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
const renderTransaction = async (transaction: Record<string, any>) => {
const rendered = render(
@ -37,9 +36,10 @@ describe('TransactionActionMenu component', () => {
}
} as unknown) as LegacyCoreStart;
jest
.spyOn(apmIndexPatternHooks, 'useAPMIndexPattern')
.mockReturnValue({ id: 'foo' } as ISavedObject);
jest.spyOn(apmIndexPatternHooks, 'useAPMIndexPattern').mockReturnValue({
apmIndexPattern: { id: 'foo' } as ISavedObject,
status: FETCH_STATUS.SUCCESS
});
jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock);
});

View file

@ -7,6 +7,7 @@ import React from 'react';
import { FETCH_STATUS, useFetcher } from '../../hooks/useFetcher';
import { loadLicense, LicenseApiResponse } from '../../services/rest/xpack';
import { InvalidLicenseNotification } from './InvalidLicenseNotification';
import { useKibanaCore } from '../../../../observability/public';
const initialLicense: LicenseApiResponse = {
features: {},
@ -17,7 +18,11 @@ const initialLicense: LicenseApiResponse = {
export const LicenseContext = React.createContext(initialLicense);
export const LicenseProvider: React.FC = ({ children }) => {
const { data = initialLicense, status } = useFetcher(() => loadLicense(), []);
const { http } = useKibanaCore();
const { data = initialLicense, status } = useFetcher(
() => loadLicense(http),
[http]
);
const hasValidLicense = data.license.is_active;
// if license is invalid show an error message

View file

@ -4,25 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState } from 'react';
import {
getAPMIndexPattern,
ISavedObject
} from '../services/rest/savedObjects';
import { useFetcher } from './useFetcher';
export function useAPMIndexPattern() {
const [pattern, setPattern] = useState({} as ISavedObject);
const { data: apmIndexPattern = {} as ISavedObject, status } = useFetcher(
getAPMIndexPattern,
[]
);
async function fetchPattern() {
const indexPattern = await getAPMIndexPattern();
if (indexPattern) {
setPattern(indexPattern);
}
}
useEffect(() => {
fetchPattern();
}, []);
return pattern;
return { apmIndexPattern, status };
}

View file

@ -5,24 +5,26 @@
*/
import { useFetcher } from './useFetcher';
import { callApmApi } from '../services/rest/callApmApi';
import { useUrlParams } from './useUrlParams';
export function useAgentName() {
const { urlParams } = useUrlParams();
const { start, end, serviceName } = urlParams;
const { data: agentName, error, status } = useFetcher(() => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/agent_name',
params: {
path: { serviceName },
query: { start, end }
}
}).then(res => res.agentName);
}
}, [serviceName, start, end]);
const { data: agentName, error, status } = useFetcher(
callApmApi => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/agent_name',
params: {
path: { serviceName },
query: { start, end }
}
}).then(res => res.agentName);
}
},
[serviceName, start, end]
);
return {
agentName,

View file

@ -6,7 +6,6 @@
import { useFetcher } from './useFetcher';
import { useUrlParams } from './useUrlParams';
import { callApmApi } from '../services/rest/callApmApi';
export function useAvgDurationByCountry() {
const {
@ -14,22 +13,25 @@ export function useAvgDurationByCountry() {
uiFilters
} = useUrlParams();
const { data = [], error, status } = useFetcher(() => {
if (serviceName && start && end) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_country',
params: {
path: { serviceName },
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
const { data = [], error, status } = useFetcher(
callApmApi => {
if (serviceName && start && end) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_country',
params: {
path: { serviceName },
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
}
}
}
});
}
}, [serviceName, start, end, uiFilters]);
});
}
},
[serviceName, start, end, uiFilters]
);
return {
data,

View file

@ -0,0 +1,17 @@
/*
* 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 { useMemo } from 'react';
import { useKibanaCore } from '../../../observability/public';
import { callApi, FetchOptions } from '../services/rest/callApi';
export function useCallApi() {
const { http } = useKibanaCore();
return useMemo(() => {
return <T = void>(options: FetchOptions) => callApi<T>(http, options);
}, [http]);
}

View file

@ -0,0 +1,17 @@
/*
* 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 { useMemo } from 'react';
import { useKibanaCore } from '../../../observability/public';
import { createCallApmApi } from '../services/rest/createCallApmApi';
export function useCallApmApi() {
const { http } = useKibanaCore();
return useMemo(() => {
return createCallApmApi(http);
}, [http]);
}

View file

@ -7,10 +7,12 @@
import React, { useContext, useEffect, useState, useMemo } from 'react';
import { idx } from '@kbn/elastic-idx';
import { i18n } from '@kbn/i18n';
import { HttpFetchError } from 'src/core/public';
import { LoadingIndicatorContext } from '../context/LoadingIndicatorContext';
import { useComponentId } from './useComponentId';
import { KFetchError } from '../../../../../../src/legacy/ui/public/kfetch/kfetch_error';
import { useKibanaCore } from '../../../observability/public';
import { APMClient } from '../services/rest/createCallApmApi';
import { useCallApmApi } from './useCallApmApi';
export enum FETCH_STATUS {
LOADING = 'loading',
@ -20,43 +22,35 @@ export enum FETCH_STATUS {
}
interface Result<Data> {
data: Data;
data?: Data;
status: FETCH_STATUS;
error?: Error;
}
export function useFetcher<TState>(
fn: () => Promise<TState> | TState | undefined,
fnDeps: any[],
options?: {
preservePreviousData?: boolean;
}
): Result<TState> & { refetch: () => void };
// fetcher functions can return undefined OR a promise. Previously we had a more simple type
// but it led to issues when using object destructuring with default values
type InferResponseType<TReturn> = Exclude<TReturn, undefined> extends Promise<
infer TResponseType
>
? TResponseType
: unknown;
// To avoid infinite rescursion when infering the type of `TState` `initialState` must be given if `prevResult` is consumed
export function useFetcher<TState>(
fn: (prevResult: Result<TState>) => Promise<TState> | TState | undefined,
export function useFetcher<TReturn>(
fn: (callApmApi: APMClient) => TReturn,
fnDeps: any[],
options: {
preservePreviousData?: boolean;
initialState: TState;
}
): Result<TState> & { refetch: () => void };
export function useFetcher(
fn: Function,
fnDeps: any[],
options: {
preservePreviousData?: boolean;
initialState?: unknown;
} = {}
) {
): Result<InferResponseType<TReturn>> & { refetch: () => void } {
const { notifications } = useKibanaCore();
const { preservePreviousData = true } = options;
const id = useComponentId();
const callApmApi = useCallApmApi();
const { dispatchStatus } = useContext(LoadingIndicatorContext);
const [result, setResult] = useState<Result<unknown>>({
data: options.initialState,
const [result, setResult] = useState<Result<InferResponseType<TReturn>>>({
data: undefined,
status: FETCH_STATUS.PENDING
});
const [counter, setCounter] = useState(0);
@ -65,7 +59,7 @@ export function useFetcher(
let didCancel = false;
async function doFetch() {
const promise = fn(result);
const promise = fn(callApmApi);
// if `fn` doesn't return a promise it is a signal that data fetching was not initiated.
// This can happen if the data fetching is conditional (based on certain inputs).
// In these cases it is not desirable to invoke the global loading spinner, or change the status to success
@ -89,10 +83,10 @@ export function useFetcher(
data,
status: FETCH_STATUS.SUCCESS,
error: undefined
});
} as Result<InferResponseType<TReturn>>);
}
} catch (e) {
const err = e as KFetchError;
const err = e as HttpFetchError;
if (!didCancel) {
notifications.toasts.addWarning({
title: i18n.translate('xpack.apm.fetcher.error.title', {
@ -105,14 +99,14 @@ export function useFetcher(
defaultMessage: `Error`
})}
</h5>
{idx(err.res, r => r.statusText)} ({idx(err.res, r => r.status)}
)
{idx(err.response, r => r.statusText)} (
{idx(err.response, r => r.status)})
<h5>
{i18n.translate('xpack.apm.fetcher.error.url', {
defaultMessage: `URL`
})}
</h5>
{idx(err.res, r => r.url)}
{idx(err.response, r => r.url)}
</div>
)
});

View file

@ -6,7 +6,6 @@
import { omit } from 'lodash';
import { useFetcher } from './useFetcher';
import { callApi } from '../services/rest/callApi';
import { LocalUIFiltersAPIResponse } from '../../server/lib/ui_filters/local_ui_filters';
import { useUrlParams } from './useUrlParams';
import {
@ -18,6 +17,7 @@ import { toQuery, fromQuery } from '../components/shared/Links/url_helpers';
import { removeUndefinedProps } from '../context/UrlParamsContext/helpers';
import { PROJECTION } from '../../common/projections/typings';
import { pickKeys } from '../utils/pickKeys';
import { useCallApi } from './useCallApi';
const getInitialData = (
filterNames: LocalUIFilterName[]
@ -38,6 +38,7 @@ export function useLocalUIFilters({
params?: Record<string, string | number | boolean | undefined>;
}) {
const { uiFilters, urlParams } = useUrlParams();
const callApi = useCallApi();
const values = pickKeys(uiFilters, ...filterNames);
@ -75,7 +76,15 @@ export function useLocalUIFilters({
...params
}
});
}, [uiFilters, urlParams, params, filterNames, projection]);
}, [
callApi,
projection,
uiFilters,
urlParams.start,
urlParams.end,
filterNames,
params
]);
const filters = data.map(filter => ({
...filter,

View file

@ -8,7 +8,6 @@ import { MetricsChartsByAgentAPIResponse } from '../../server/lib/metrics/get_me
import { IUrlParams } from '../context/UrlParamsContext/types';
import { useUiFilters } from '../context/UrlParamsContext';
import { useFetcher } from './useFetcher';
import { callApmApi } from '../services/rest/callApmApi';
const INITIAL_DATA: MetricsChartsByAgentAPIResponse = {
charts: []
@ -20,22 +19,25 @@ export function useServiceMetricCharts(
) {
const { serviceName, start, end } = urlParams;
const uiFilters = useUiFilters(urlParams);
const { data = INITIAL_DATA, error, status } = useFetcher(() => {
if (serviceName && start && end && agentName) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/metrics/charts',
params: {
path: { serviceName },
query: {
start,
end,
agentName,
uiFilters: JSON.stringify(uiFilters)
const { data = INITIAL_DATA, error, status } = useFetcher(
callApmApi => {
if (serviceName && start && end && agentName) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/metrics/charts',
params: {
path: { serviceName },
query: {
start,
end,
agentName,
uiFilters: JSON.stringify(uiFilters)
}
}
}
});
}
}, [serviceName, start, end, agentName, uiFilters]);
});
}
},
[serviceName, start, end, agentName, uiFilters]
);
return {
data,

View file

@ -6,23 +6,25 @@
import { IUrlParams } from '../context/UrlParamsContext/types';
import { useFetcher } from './useFetcher';
import { callApmApi } from '../services/rest/callApmApi';
const INITIAL_DATA = { transactionTypes: [] };
export function useServiceTransactionTypes(urlParams: IUrlParams) {
const { serviceName, start, end } = urlParams;
const { data = INITIAL_DATA } = useFetcher(() => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/transaction_types',
params: {
path: { serviceName },
query: { start, end }
}
});
}
}, [serviceName, start, end]);
const { data = INITIAL_DATA } = useFetcher(
callApmApi => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/transaction_types',
params: {
path: { serviceName },
query: { start, end }
}
});
}
},
[serviceName, start, end]
);
return data.transactionTypes;
}

View file

@ -6,7 +6,6 @@
import { useFetcher } from './useFetcher';
import { useUrlParams } from './useUrlParams';
import { callApmApi } from '../services/rest/callApmApi';
export function useTransactionBreakdown() {
const {
@ -14,28 +13,27 @@ export function useTransactionBreakdown() {
uiFilters
} = useUrlParams();
const {
data = { kpis: [], timeseries: [] },
error,
status
} = useFetcher(() => {
if (serviceName && start && end && transactionType) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/transaction_groups/breakdown',
params: {
path: { serviceName },
query: {
start,
end,
transactionName,
transactionType,
uiFilters: JSON.stringify(uiFilters)
const { data = { kpis: [], timeseries: [] }, error, status } = useFetcher(
callApmApi => {
if (serviceName && start && end && transactionType) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/transaction_groups/breakdown',
params: {
path: { serviceName },
query: {
start,
end,
transactionName,
transactionType,
uiFilters: JSON.stringify(uiFilters)
}
}
}
});
}
}, [serviceName, start, end, transactionType, transactionName, uiFilters]);
});
}
},
[serviceName, start, end, transactionType, transactionName, uiFilters]
);
return {
data,

View file

@ -8,7 +8,6 @@ import { useMemo } from 'react';
import { getTransactionCharts } from '../selectors/chartSelectors';
import { useFetcher } from './useFetcher';
import { useUrlParams } from './useUrlParams';
import { callApmApi } from '../services/rest/callApmApi';
export function useTransactionCharts() {
const {
@ -16,23 +15,26 @@ export function useTransactionCharts() {
uiFilters
} = useUrlParams();
const { data, error, status } = useFetcher(() => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/transaction_groups/charts',
params: {
path: { serviceName },
query: {
start,
end,
transactionType,
transactionName,
uiFilters: JSON.stringify(uiFilters)
const { data, error, status } = useFetcher(
callApmApi => {
if (serviceName && start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/transaction_groups/charts',
params: {
path: { serviceName },
query: {
start,
end,
transactionType,
transactionName,
uiFilters: JSON.stringify(uiFilters)
}
}
}
});
}
}, [serviceName, start, end, transactionName, transactionType, uiFilters]);
});
}
},
[serviceName, start, end, transactionName, transactionType, uiFilters]
);
const memoizedData = useMemo(
() => getTransactionCharts({ transactionType }, data),

View file

@ -7,7 +7,6 @@
import { IUrlParams } from '../context/UrlParamsContext/types';
import { useFetcher } from './useFetcher';
import { useUiFilters } from '../context/UrlParamsContext';
import { callApmApi } from '../services/rest/callApmApi';
import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution';
const INITIAL_DATA = {
@ -28,30 +27,42 @@ export function useTransactionDistribution(urlParams: IUrlParams) {
} = urlParams;
const uiFilters = useUiFilters(urlParams);
const { data = INITIAL_DATA, status, error } = useFetcher(() => {
if (serviceName && start && end && transactionType && transactionName) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/transaction_groups/distribution',
params: {
path: {
serviceName
},
query: {
start,
end,
transactionType,
transactionName,
transactionId,
traceId,
uiFilters: JSON.stringify(uiFilters)
const { data = INITIAL_DATA, status, error } = useFetcher(
callApmApi => {
if (serviceName && start && end && transactionType && transactionName) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/transaction_groups/distribution',
params: {
path: {
serviceName
},
query: {
start,
end,
transactionType,
transactionName,
transactionId,
traceId,
uiFilters: JSON.stringify(uiFilters)
}
}
}
});
}
// the histogram should not be refetched if the transactionId or traceId changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [serviceName, start, end, transactionType, transactionName, uiFilters]);
});
}
// the histogram should not be refetched if the transactionId or traceId changes
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
serviceName,
start,
end,
transactionType,
transactionName,
transactionId,
traceId,
uiFilters
]
);
return { data, status, error };
}

View file

@ -9,7 +9,6 @@ import { IUrlParams } from '../context/UrlParamsContext/types';
import { useUiFilters } from '../context/UrlParamsContext';
import { useFetcher } from './useFetcher';
import { TransactionGroupListAPIResponse } from '../../server/lib/transaction_groups';
import { callApmApi } from '../services/rest/callApmApi';
const getRelativeImpact = (
impact: number,
@ -43,22 +42,25 @@ function getWithRelativeImpact(items: TransactionGroupListAPIResponse) {
export function useTransactionList(urlParams: IUrlParams) {
const { serviceName, transactionType, start, end } = urlParams;
const uiFilters = useUiFilters(urlParams);
const { data = [], error, status } = useFetcher(() => {
if (serviceName && start && end && transactionType) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/transaction_groups',
params: {
path: { serviceName },
query: {
start,
end,
transactionType,
uiFilters: JSON.stringify(uiFilters)
const { data = [], error, status } = useFetcher(
callApmApi => {
if (serviceName && start && end && transactionType) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/transaction_groups',
params: {
path: { serviceName },
query: {
start,
end,
transactionType,
uiFilters: JSON.stringify(uiFilters)
}
}
}
});
}
}, [serviceName, start, end, transactionType, uiFilters]);
});
}
},
[serviceName, start, end, transactionType, uiFilters]
);
const memoizedData = useMemo(() => getWithRelativeImpact(data), [data]);
return {

View file

@ -7,7 +7,6 @@
import { useMemo } from 'react';
import { IUrlParams } from '../context/UrlParamsContext/types';
import { useFetcher } from './useFetcher';
import { callApmApi } from '../services/rest/callApmApi';
import { getWaterfall } from '../components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
const INITIAL_DATA = {
@ -18,20 +17,23 @@ const INITIAL_DATA = {
export function useWaterfall(urlParams: IUrlParams) {
const { traceId, start, end, transactionId } = urlParams;
const { data = INITIAL_DATA, status, error } = useFetcher(() => {
if (traceId && start && end) {
return callApmApi({
pathname: '/api/apm/traces/{traceId}',
params: {
path: { traceId },
query: {
start,
end
const { data = INITIAL_DATA, status, error } = useFetcher(
callApmApi => {
if (traceId && start && end) {
return callApmApi({
pathname: '/api/apm/traces/{traceId}',
params: {
path: { traceId },
query: {
start,
end
}
}
}
});
}
}, [traceId, start, end]);
});
}
},
[traceId, start, end]
);
const waterfall = useMemo(() => getWaterfall(data, transactionId), [
data,

View file

@ -4,26 +4,31 @@
* you may not use this file except in compliance with the Elastic License.
*/
import * as kfetchModule from 'ui/kfetch';
import { mockNow } from '../../utils/testHelpers';
import { clearCache, callApi } from '../rest/callApi';
import { SessionStorageMock } from './SessionStorageMock';
import { HttpServiceBase } from 'kibana/public';
jest.mock('ui/kfetch');
type HttpMock = HttpServiceBase & {
get: jest.SpyInstance<HttpServiceBase['get']>;
};
describe('callApi', () => {
let kfetchSpy: jest.SpyInstance;
let http: HttpMock;
beforeEach(() => {
kfetchSpy = jest.spyOn(kfetchModule, 'kfetch').mockResolvedValue({
my_key: 'hello world'
});
http = ({
get: jest.fn().mockReturnValue({
my_key: 'hello_world'
})
} as unknown) as HttpMock;
// @ts-ignore
global.sessionStorage = new SessionStorageMock();
});
afterEach(() => {
kfetchSpy.mockClear();
http.get.mockClear();
clearCache();
});
@ -33,32 +38,17 @@ describe('callApi', () => {
});
it('should add debug param for APM endpoints', async () => {
await callApi({ pathname: `/api/apm/status/server` });
await callApi(http, { pathname: `/api/apm/status/server` });
expect(kfetchSpy).toHaveBeenCalledWith(
{ pathname: '/api/apm/status/server', query: { _debug: true } },
undefined
);
expect(http.get).toHaveBeenCalledWith('/api/apm/status/server', {
query: { _debug: true }
});
});
it('should not add debug param for non-APM endpoints', async () => {
await callApi({ pathname: `/api/kibana` });
await callApi(http, { pathname: `/api/kibana` });
expect(kfetchSpy).toHaveBeenCalledWith(
{ pathname: '/api/kibana' },
undefined
);
});
});
describe('prependBasePath', () => {
it('should be passed on to kFetch', async () => {
await callApi({ pathname: `/api/kibana` }, { prependBasePath: false });
expect(kfetchSpy).toHaveBeenCalledWith(
{ pathname: '/api/kibana' },
{ prependBasePath: false }
);
expect(http.get).toHaveBeenCalledWith('/api/kibana', {});
});
});
@ -74,98 +64,98 @@ describe('callApi', () => {
describe('when the call does not contain start/end params', () => {
it('should not return cached response for identical calls', async () => {
await callApi({ pathname: `/api/kibana`, query: { foo: 'bar' } });
await callApi({ pathname: `/api/kibana`, query: { foo: 'bar' } });
await callApi({ pathname: `/api/kibana`, query: { foo: 'bar' } });
await callApi(http, { pathname: `/api/kibana`, query: { foo: 'bar' } });
await callApi(http, { pathname: `/api/kibana`, query: { foo: 'bar' } });
await callApi(http, { pathname: `/api/kibana`, query: { foo: 'bar' } });
expect(kfetchSpy).toHaveBeenCalledTimes(3);
expect(http.get).toHaveBeenCalledTimes(3);
});
});
describe('when the call contains start/end params', () => {
it('should return cached response for identical calls', async () => {
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { start: '2010', end: '2011' }
});
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { start: '2010', end: '2011' }
});
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { start: '2010', end: '2011' }
});
expect(kfetchSpy).toHaveBeenCalledTimes(1);
expect(http.get).toHaveBeenCalledTimes(1);
});
it('should not return cached response for subsequent calls if arguments change', async () => {
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { start: '2010', end: '2011', foo: 'bar1' }
});
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { start: '2010', end: '2011', foo: 'bar2' }
});
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { start: '2010', end: '2011', foo: 'bar3' }
});
expect(kfetchSpy).toHaveBeenCalledTimes(3);
expect(http.get).toHaveBeenCalledTimes(3);
});
it('should not return cached response if `end` is a future timestamp', async () => {
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { end: '2030' }
});
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { end: '2030' }
});
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { end: '2030' }
});
expect(kfetchSpy).toHaveBeenCalledTimes(3);
expect(http.get).toHaveBeenCalledTimes(3);
});
it('should return cached response if calls contain `end` param in the past', async () => {
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { start: '2009', end: '2010' }
});
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { start: '2009', end: '2010' }
});
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { start: '2009', end: '2010' }
});
expect(kfetchSpy).toHaveBeenCalledTimes(1);
expect(http.get).toHaveBeenCalledTimes(1);
});
it('should return cached response even if order of properties change', async () => {
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { end: '2010', start: '2009' }
});
await callApi({
await callApi(http, {
pathname: `/api/kibana`,
query: { start: '2009', end: '2010' }
});
await callApi({
await callApi(http, {
query: { start: '2009', end: '2010' },
pathname: `/api/kibana`
});
expect(kfetchSpy).toHaveBeenCalledTimes(1);
expect(http.get).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -5,15 +5,19 @@
*/
import * as callApiExports from '../rest/callApi';
import { callApmApi } from '../rest/callApmApi';
jest.mock('ui/kfetch');
import { createCallApmApi, APMClient } from '../rest/createCallApmApi';
import { HttpServiceBase } from 'kibana/public';
const callApi = jest
.spyOn(callApiExports, 'callApi')
.mockImplementation(() => Promise.resolve(null));
describe('callApmApi', () => {
let callApmApi: APMClient;
beforeEach(() => {
callApmApi = createCallApmApi({} as HttpServiceBase);
});
afterEach(() => {
callApi.mockClear();
});
@ -30,6 +34,7 @@ describe('callApmApi', () => {
} as never);
expect(callApi).toHaveBeenCalledWith(
{},
expect.objectContaining({
pathname: '/api/apm/foo/to/bar'
})
@ -48,6 +53,7 @@ describe('callApmApi', () => {
} as never);
expect(callApi).toHaveBeenCalledWith(
{},
expect.objectContaining({
pathname: '/api/apm',
query: {
@ -71,6 +77,7 @@ describe('callApmApi', () => {
} as never);
expect(callApi).toHaveBeenCalledWith(
{},
expect.objectContaining({
pathname: '/api/apm',
method: 'POST',

View file

@ -4,14 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { FetchOptions } from 'apollo-link-http';
import { isString, startsWith } from 'lodash';
import LRU from 'lru-cache';
import hash from 'object-hash';
import { kfetch, KFetchOptions } from 'ui/kfetch';
import { KFetchKibanaOptions } from 'ui/kfetch/kfetch';
import { HttpServiceBase, HttpFetchOptions } from 'kibana/public';
function fetchOptionsWithDebug(fetchOptions: KFetchOptions) {
export type FetchOptions = HttpFetchOptions & {
pathname: string;
forceCache?: boolean;
method?: string;
};
function fetchOptionsWithDebug(fetchOptions: FetchOptions) {
const debugEnabled =
sessionStorage.getItem('apm_debug') === 'true' &&
startsWith(fetchOptions.pathname, '/api/apm');
@ -35,9 +39,11 @@ export function clearCache() {
cache.reset();
}
export type CallApi = typeof callApi;
export async function callApi<T = void>(
fetchOptions: KFetchOptions & { forceCache?: boolean },
options?: KFetchKibanaOptions
http: HttpServiceBase,
fetchOptions: FetchOptions
): Promise<T> {
const cacheKey = getCacheKey(fetchOptions);
const cacheResponse = cache.get(cacheKey);
@ -45,8 +51,18 @@ export async function callApi<T = void>(
return cacheResponse;
}
const combinedFetchOptions = fetchOptionsWithDebug(fetchOptions);
const res = await kfetch(combinedFetchOptions, options);
const { pathname, method = 'get', ...options } = fetchOptionsWithDebug(
fetchOptions
);
const lowercaseMethod = method.toLowerCase() as
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch';
const res = await http[lowercaseMethod](pathname, options);
if (isCachable(fetchOptions)) {
cache.set(cacheKey, res);
@ -57,7 +73,7 @@ export async function callApi<T = void>(
// only cache items that has a time range with `start` and `end` params,
// and where `end` is not a timestamp in the future
function isCachable(fetchOptions: KFetchOptions & { forceCache?: boolean }) {
function isCachable(fetchOptions: FetchOptions) {
if (fetchOptions.forceCache) {
return true;
}

View file

@ -1,27 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { callApi } from './callApi';
import { APMAPI } from '../../../server/routes/create_apm_api';
import { Client } from '../../../server/routes/typings';
export const callApmApi: Client<APMAPI['_S']> = (options => {
const { pathname, params = {}, ...opts } = options;
const path = (params.path || {}) as Record<string, any>;
const body = params.body ? { body: JSON.stringify(params.body) } : undefined;
const query = params.query ? { query: params.query } : undefined;
const formattedPathname = Object.keys(path).reduce((acc, paramName) => {
return acc.replace(`{${paramName}}`, path[paramName]);
}, pathname);
return callApi({
...opts,
pathname: formattedPathname,
...body,
...query
}) as any;
}) as Client<APMAPI['_S']>;

View file

@ -0,0 +1,40 @@
/*
* 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 { HttpServiceBase } from 'kibana/public';
import { callApi, FetchOptions } from './callApi';
import { APMAPI } from '../../../server/routes/create_apm_api';
import { Client } from '../../../server/routes/typings';
export type APMClient = Client<APMAPI['_S']>;
export type APMClientOptions = Omit<FetchOptions, 'query' | 'body'> & {
params?: {
body?: any;
query?: any;
path?: any;
};
};
export const createCallApmApi = (http: HttpServiceBase) =>
((options: APMClientOptions) => {
const { pathname, params = {}, ...opts } = options;
const path = (params.path || {}) as Record<string, any>;
const body = params.body
? { body: JSON.stringify(params.body) }
: undefined;
const query = params.query ? { query: params.query } : undefined;
const formattedPathname = Object.keys(path).reduce((acc, paramName) => {
return acc.replace(`{${paramName}}`, path[paramName]);
}, pathname);
return callApi(http, {
...opts,
pathname: formattedPathname,
...body,
...query
});
}) as APMClient;

View file

@ -6,6 +6,7 @@
import { npStart } from 'ui/new_platform';
import { ESFilter } from 'elasticsearch';
import { HttpServiceBase } from 'kibana/public';
import {
PROCESSOR_EVENT,
SERVICE_NAME,
@ -35,10 +36,12 @@ const { core } = npStart;
export async function startMLJob({
serviceName,
transactionType
transactionType,
http
}: {
serviceName: string;
transactionType: string;
http: HttpServiceBase;
}) {
const indexPatternName = core.injectedMetadata.getInjectedVar(
'apmTransactionIndices'
@ -50,7 +53,7 @@ export async function startMLJob({
{ term: { [TRANSACTION_TYPE]: transactionType } }
];
groups.push(transactionType.toLowerCase());
return callApi<StartedMLJobApiResponse>({
return callApi<StartedMLJobApiResponse>(http, {
method: 'POST',
pathname: `/api/ml/modules/setup/apm_transaction`,
body: JSON.stringify({
@ -77,13 +80,15 @@ export interface MLJobApiResponse {
export async function getHasMLJob({
serviceName,
transactionType
transactionType,
http
}: {
serviceName: string;
transactionType: string;
http: HttpServiceBase;
}) {
try {
await callApi<MLJobApiResponse>({
await callApi<MLJobApiResponse>(http, {
method: 'GET',
pathname: `/api/ml/anomaly_detectors/${getMlJobId(
serviceName,

View file

@ -5,7 +5,7 @@
*/
import { memoize } from 'lodash';
import { callApmApi } from './callApmApi';
import { APMClient } from './createCallApmApi';
export interface ISavedObject {
attributes: {
@ -16,7 +16,7 @@ export interface ISavedObject {
type: string;
}
export const getAPMIndexPattern = memoize(async () => {
export const getAPMIndexPattern = memoize(async (callApmApi: APMClient) => {
try {
return await callApmApi({
pathname: '/api/apm/index_pattern'

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpServiceBase } from 'kibana/public';
import { StringMap } from '../../../typings/common';
import { callApi } from './callApi';
@ -38,8 +39,8 @@ export interface LicenseApiResponse {
};
}
export async function loadLicense() {
return callApi<LicenseApiResponse>({
export async function loadLicense(http: HttpServiceBase) {
return callApi<LicenseApiResponse>(http, {
pathname: `/api/xpack/v1/info`
});
}

View file

@ -97,7 +97,8 @@ export const agentConfigurationAgentNameRoute = createRoute(() => ({
handler: async (req, { query }) => {
const setup = await setupRequest(req);
const { serviceName } = query;
return await getAgentNameByService({ serviceName, setup });
const agentName = await getAgentNameByService({ serviceName, setup });
return agentName;
}
}));

View file

@ -7,8 +7,8 @@
import t from 'io-ts';
import { Request, ResponseToolkit } from 'hapi';
import { InternalCoreSetup } from 'src/core/server';
import { KFetchOptions } from 'ui/kfetch';
import { PickByValue, Optional } from 'utility-types';
import { FetchOptions } from '../../public/services/rest/callApi';
export interface Params {
query?: t.HasProps;
@ -105,7 +105,7 @@ type GetParams<TParams extends Params> = Exclude<
export type Client<TRouteState> = <
TPath extends keyof TRouteState & string,
TMethod extends keyof TRouteState[TPath],
TMethod extends keyof TRouteState[TPath] & string,
TRouteDescription extends TRouteState[TPath][TMethod],
TParams extends TRouteDescription extends { params: Params }
? TRouteDescription['params']
@ -114,7 +114,7 @@ export type Client<TRouteState> = <
? TRouteDescription['ret']
: undefined
>(
options: Omit<KFetchOptions, 'query' | 'body' | 'pathname' | 'method'> & {
options: Omit<FetchOptions, 'query' | 'body' | 'pathname' | 'method'> & {
forceCache?: boolean;
pathname: TPath;
} & (TMethod extends 'GET' ? { method?: TMethod } : { method: TMethod }) &